I have a widget that I intend to reuse in various sections of my app.
Widget getTextField({required String hint,required IconData icon, required int minLength,required StringValue callback}){
return CupertinoTextField(
padding: EdgeInsets.all(8),
prefix: Padding(
padding: iconPadding,
child: Icon(icon)),
suffix: Padding(
padding: iconPadding,
child: Icon(_length >= minLength ? Icons.check_circle : Icons.check_circle_outline)),
placeholder: hint,
onChanged: (text){
_length = text.length;
callback(text);
},
);
}
When I tried to reuse a state-full widget I had a problem that if I typed in one textfield the rest had the same text popping in, which means that the same instance of the widget was being used. Using the new keyword did not help as well.
The above approach worked as mentioned here https://stackoverflow.com/a/63424310/8528047
However the icon is not being changed when the boolean value _length becomes true.
How do I make the widget rebuild itself when the boolean condition changes ?
You should create a simple Widget class and pass arguments to that, for example:
class GetTextFieldWidget extends StatefulWidget {
final EdgeInsets iconPadding;
final int minLength;
final ValueChanged onChanged;
final IconData icon;
final String hint;
const GetTextFieldWidget(this.iconPadding, this.minLength, this.onChanged,this.icon, this.hint);
#override
State<StatefulWidget> createState() => _GetTextFieldWidget();
}
class _GetTextFieldWidget extends State<GetTextFieldWidget> {
final int _length = 10;
#override
Widget build(BuildContext context) {
return CupertinoTextField(
padding: const EdgeInsets.all(8),
prefix: Padding(
padding: widget.iconPadding,
child: Icon(widget.icon),
),
suffix: Padding(
padding: widget.iconPadding,
child: Icon(_length >= widget.minLength ? Icons.check_circle : Icons.check_circle_outline)),
placeholder: widget.hint,
onChanged: widget.onChanged,
);
}
}
Related
I have a list of dynamic forms where I need to add and remove form fields between two fields dynamically. I am able to add/remove form fields from the bottom of the list properly.
However, when I try to add a form field in between two form fields the data for the field does not update correctly.
How can I correctly add a field in between the two fields and populate the data correctly?
import 'package:flutter/material.dart';
class DynamicFormWidget extends StatefulWidget {
const DynamicFormWidget({Key? key}) : super(key: key);
#override
State<DynamicFormWidget> createState() => _DynamicFormWidgetState();
}
class _DynamicFormWidgetState extends State<DynamicFormWidget> {
List<String?> names = [null];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Dynamic Forms'),
),
body: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
itemBuilder: (builderContext, index) => Row(
children: [
Flexible(
child: TextFormField(
initialValue: names[index],
onChanged: (name) {
names[index] = name;
debugPrint(names.toString());
},
decoration: InputDecoration(
hintText: 'Enter your name',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8))),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: IconButton(
onPressed: () {
setState(() {
if(index + 1 == names.length){
names.add( null); debugPrint('Added: $names');
} else {
names.insert(index + 1, null); debugPrint('Added [${index+1}]: $names');
}
});
},
color: Colors.green,
iconSize: 32,
icon: const Icon(Icons.add_circle)),
),
Padding(
padding: const EdgeInsets.all(8),
child: IconButton(
onPressed: (index == 0&& names.length == 1)
? null
: () {
setState(() {
names.removeAt(index);
});
debugPrint('Removed [$index]: $names');
},
color: Colors.red,
iconSize: 32,
icon: const Icon(Icons.remove_circle)),
),
],
),
separatorBuilder: (separatorContext, index) => const SizedBox(
height: 16,
),
itemCount: names.length,
),
);
}
}
Basically the problem is that Flutter is confused about who is who in your TextFormField list.
To fix this issue simply add a key to your TextFormField, so that it can be uniquely identified by Flutter:
...
child: TextFormField(
initialValue: names[index],
key: UniqueKey(), // add this line
onChanged: (name) {
...
If you want to learn more about keys and its correct use take a look at this.
The widget AnimatedList solves this problem, it keep track of the widgets as a list would do and uses a build function so it is really easy to sync elements with another list. If you end up having a wide range of forms you can make use of the InheritedWidget to simplify the code.
In this sample i'm making use of the TextEditingController to abstract from the form code part and to initialize with value (the widget inherits from the ChangeNotifier so changing the value will update the text in the form widget), for simplicity it only adds (with the generic text) and removes at an index.
To make every CustomLineForm react the others (as in: disable remove if it only remains one) use a StreamBuilder or a ListModel to notify changes and make each entry evaluate if needs to update instead of rebuilding everything.
class App extends StatelessWidget {
final print_all = ChangeNotifier();
App({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: FormList(print_notifier: print_all),
floatingActionButton: IconButton(
onPressed: print_all.notifyListeners,
icon: Icon(Icons.checklist),
),
),
);
}
}
class FormList extends StatefulWidget {
final ChangeNotifier print_notifier;
FormList({required this.print_notifier, super.key});
#override
_FormList createState() => _FormList();
}
class _FormList extends State<FormList> {
final _controllers = <TextEditingController>[];
final _list_key = GlobalKey<AnimatedListState>();
void print_all() {
for (var controller in _controllers) print(controller.text);
}
#override
void initState() {
super.initState();
widget.print_notifier.addListener(print_all);
_controllers.add(TextEditingController(text: 'Inital entrie'));
}
#override
void dispose() {
widget.print_notifier.removeListener(print_all);
for (var controller in _controllers) controller.dispose();
super.dispose();
}
void _insert(int index) {
final int at = index.clamp(0, _controllers.length - 1);
_controllers.insert(at, TextEditingController(text: 'Insert at $at'));
// AnimatedList will take what is placed in [at] so the controller
// needs to exist before adding the widget
_list_key.currentState!.insertItem(at);
}
void _remove(int index) {
final int at = index.clamp(0, _controllers.length - 1);
// The widget is replacing the original, it is used to animate the
// disposal of the widget, ex: size.y -= delta * amount
_list_key.currentState!.removeItem(at, (_, __) => Container());
_controllers[at].dispose();
_controllers.removeAt(at);
}
#override
Widget build(BuildContext context) {
return AnimatedList(
key: _list_key,
initialItemCount: _controllers.length,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
itemBuilder: (ctx, index, _) {
return CustomLineForm(
index: index,
controler: _controllers[index],
on_insert: _insert,
on_remove: _remove,
);
},
);
}
}
class CustomLineForm extends StatelessWidget {
final int index;
final void Function(int) on_insert;
final void Function(int) on_remove;
final TextEditingController controler;
const CustomLineForm({
super.key,
required this.index,
required this.controler,
required this.on_insert,
required this.on_remove,
});
#override
Widget build(BuildContext context) {
return Row(
children: [
Flexible(
child: TextFormField(
controller: controler,
),
),
IconButton(
icon: Icon(Icons.add_circle),
onPressed: () => on_insert(index),
),
IconButton(
icon: Icon(Icons.remove_circle),
onPressed: () => on_remove(index),
)
],
);
}
}
I'm trying to find a way to implement a functionality in which, in a horizontally scrollable list, there are widgets that I will call P, (which are denoted as P1, P2 and P3 in the diagram) and their children C, (which are denoted as C1, C2 and C3). As the user scrolls the list horizontally, I want C's inside P's to act like sticky headers, until they reach the boundary of their parent.
I'm sorry if the description & diagram is not enough, I will try to clarify anything unclear.
Diagram of the problem
As I'm thinking of a way to implement this, I can't seem to find a plausible solution. Also if there is a package that can help with this issue, I would really appreciate any suggestions.
I am not sure about your picture, but maybe this is do you want?
our tools :
BuildOwners -> to measure size of the widget before rebuild,
NotificationListeners -> to trigger rebuild based on ScrollNotification. i use stateful Widget, but you can tweak it into ValueNotifier and Build the Sticker with ValueListenableBuilder instead.
ListView.Builder -> actually you can replace this with any kind of Scrollable, we only need to listen scroll event.
how its work?
its simple :
we need to know the P dx Offset, check if C offset small than P, then use that value to adjust x Positioned of C in Stack. and clamp it with max value (P.width)
double _calculateStickerXPosition(
{required double px, required double cx, required double cw}) {
if (cx < px) {
return widget.stickerHorizontalPadding + (px - cx).clamp(0.0, cw - (widget.stickerHorizontalPadding*2));
}
return widget.stickerHorizontalPadding;
}
full code :
main.dart :
import 'dart:ui';
import 'package:flutter/material.dart';
import 'scrollable_sticker.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
// i use chrome to test it, so igrone this
scrollBehavior: const MaterialScrollBehavior().copyWith(
dragDevices: {
PointerDeviceKind.mouse,
PointerDeviceKind.touch,
PointerDeviceKind.stylus,
PointerDeviceKind.unknown
},
),
home: const MyWidget(),
);
}
}
class MyWidget extends StatelessWidget {
const MyWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: ScrollableSticker(
children: List.generate(10, (index) => Container(
width: 500,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
border: Border.all(color: Colors.orange)),
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 50.0, horizontal: 50.0),
child: Text(
"P1",
textDirection: TextDirection.ltr,
),
),
)),
stickerBuilder: (index) => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10), color: Colors.red),
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Text(
'C$index',
),
),
)),
),
);
}
}
scrollable_sticker.dart :
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class ScrollableSticker extends StatefulWidget {
final List<Widget> children;
final Widget Function(int index) stickerBuilder;
final double stickerHorizontalPadding;
const ScrollableSticker(
{Key? key,
required this.children,
required this.stickerBuilder,
this.stickerHorizontalPadding = 10.0})
: super(key: key);
#override
State<ScrollableSticker> createState() => _ScrollableStickerState();
}
class _ScrollableStickerState extends State<ScrollableSticker> {
late List<GlobalKey> _keys;
late GlobalKey _parentKey;
#override
void initState() {
super.initState();
_keys = List.generate(widget.children.length, (index) => GlobalKey());
_parentKey = GlobalKey();
}
#override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: (sc) {
setState(() {});
return true;
},
child: ListView.builder(
key: _parentKey,
scrollDirection: Axis.horizontal,
itemCount: widget.children.length,
itemBuilder: (context, index) {
final itemSize = measureWidget(Directionality(
textDirection: TextDirection.ltr, child: widget.children[index]));
final stickerSize = measureWidget(Directionality(
textDirection: TextDirection.ltr,
child: widget.stickerBuilder(index)));
final BuildContext? itemContext = _keys[index].currentContext;
double x = widget.stickerHorizontalPadding;
if (itemContext != null) {
final pcontext = _parentKey.currentContext;
Offset? pOffset;
if (pcontext != null) {
RenderObject? obj = pcontext.findRenderObject();
if (obj != null) {
final prb = obj as RenderBox;
pOffset = prb.localToGlobal(Offset.zero);
}
}
final obj = itemContext.findRenderObject();
if (obj != null) {
final rb = obj as RenderBox;
final cx = rb.localToGlobal(pOffset ?? Offset.zero).dx;
x = _calculateStickerXPosition(
px: pOffset != null ? pOffset.dx : 0.0,
cx: cx,
cw: (itemSize.width - stickerSize.width));
}
}
return SizedBox(
key: _keys[index],
height: itemSize.height,
width: itemSize.width,
child: Stack(
children: [
widget.children[index],
Positioned(
top: itemSize.height / 2,
left: x,
child: FractionalTranslation(
translation: const Offset(0.0, -0.5),
child: widget.stickerBuilder(index)))
],
),
);
},
),
);
}
double _calculateStickerXPosition(
{required double px, required double cx, required double cw}) {
if (cx < px) {
return widget.stickerHorizontalPadding +
(px - cx).clamp(0.0, cw - (widget.stickerHorizontalPadding * 2));
}
return widget.stickerHorizontalPadding;
}
}
Size measureWidget(Widget widget) {
final PipelineOwner pipelineOwner = PipelineOwner();
final MeasurementView rootView = pipelineOwner.rootNode = MeasurementView();
final BuildOwner buildOwner = BuildOwner(focusManager: FocusManager());
final RenderObjectToWidgetElement<RenderBox> element =
RenderObjectToWidgetAdapter<RenderBox>(
container: rootView,
debugShortDescription: '[root]',
child: widget,
).attachToRenderTree(buildOwner);
try {
rootView.scheduleInitialLayout();
pipelineOwner.flushLayout();
return rootView.size;
} finally {
// Clean up.
element.update(RenderObjectToWidgetAdapter<RenderBox>(container: rootView));
buildOwner.finalizeTree();
}
}
class MeasurementView extends RenderBox
with RenderObjectWithChildMixin<RenderBox> {
#override
void performLayout() {
assert(child != null);
child!.layout(const BoxConstraints(), parentUsesSize: true);
size = child!.size;
}
#override
void debugAssertDoesMeetConstraints() => true;
}
you could try to use c padding dynamically
padding: EdgeInsets.only(left: 0.1 * [index], right: 1 * [index])
for example, I hope it helps.
I want to reuse different types of fields in different forms and I have created a separate Widget that returns TextFormField.
Logically, different types of fields have their own validations and other properties, so I have started looking into inheritance and so on to avoid rewriting same chunks of code.
From what I have learnt, Flutter does not encourage inheritance of widgets, so my question is on the best practices of reusing code for various form fields in flutter to remain readability and keep the code clean.
Any tips?
In my experience, I rarely had the need to use other widgets than the original form fields provided by flutter. What I found useful to reuse are validation functions for each fields, since they often have common needs in term of validation.
These are just two basic samples. I pass them to the validator argument of the form field whenever it's needed.
String? validatorForMissingFields(String? input) {
if (input == null || input.isEmpty || input.trim().isEmpty) {
return "Mandatory field";
}
return null;
}
String? validatorForMissingFieldsAndLength(String? input, int length) {
if (input == null || input.isEmpty || input.trim().isEmpty) {
return "Mandatory field";
}
if (input.length != length) {
return 'Not long enough';
}
return null;
}
In any case, instead of extending a basic widget, I prefer to create a new one containing the basic widget with some fixed properties, and others that can be customized. This example does not involve form fields, but I think it can better explain my point.
///if text is not null, icon is ignored
class RectButton extends StatelessWidget {
final Function()? onPressed;
final String? text;
final IconData? icon;
final Color color;
const RectButton({this.text, this.icon, required this.onPressed, Key? key, this.color = mainLightColor}) : super(key: key);
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(12.0),
child: OutlinedButton(
style: ButtonStyle(
side: MaterialStateProperty.all(BorderSide(color: color)),
overlayColor: MaterialStateColor.resolveWith((states) => color.withOpacity(0.5)),
backgroundColor: MaterialStateColor.resolveWith((states) => color.withOpacity(0.3)),
),
onPressed: onPressed,
child: text != null
? Text(
text!,
style: TextStyle(fontWeight: FontWeight.bold, color: color),
)
: Icon(
icon,
color: color,
)),
);
}
}
In order to maintain the same look&feel in all the app, I created a custom button with some 'invisible' widgets above it that allowed me to set some properties without extending a basic widget. The properties I needed to be customized are passed to the constructor.
You can create a class to store only the important things like a label or a controller and then use a wrap widget and a for loop to generate the widgets.
Here's an example:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final List<TextFieldData> _allFieldData = [
TextFieldData(
label: 'field 1',
validator: numberOnlyValidator,
autovalidateMode: AutovalidateMode.onUserInteraction,
),
TextFieldData(
label: 'field 2',
validator: canBeEmptyValidator,
),
TextFieldData(
label: 'field 3',
validator: numberOnlyValidator,
),
];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(16),
child: SingleChildScrollView(
child: Form(
child: Wrap(
runSpacing: 16,
spacing: 16,
children: [
for (var fieldData in _allFieldData)
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 250),
child: TextFormField(
decoration: InputDecoration(label: Text(fieldData.label)),
controller: fieldData.controller,
autovalidateMode: fieldData.autovalidateMode,
validator: fieldData.validator,
),
)
],
),
),
),
),
);
}
}
const String numbersOnlyError = 'Only numbers';
const String requiredFieldError = 'Required field';
RegExp numbersOnlyRegexp = RegExp(r'^[0-9]\d*(,\d+)?$');
String? numberOnlyValidator(String? value) {
if (value == null || value.isEmpty) {
return requiredFieldError;
} else if (!numbersOnlyRegexp.hasMatch(value)) {
return numbersOnlyError;
}
return null;
}
String? canBeEmptyValidator(String? value) {
return null;
}
class TextFieldData {
final String label;
final String? Function(String?)? validator;
final AutovalidateMode autovalidateMode;
TextEditingController controller = TextEditingController();
TextFieldData({
required this.label,
required this.validator,
this.autovalidateMode = AutovalidateMode.disabled,
});
}
And then you can do whatever you want using the .controller of each item inside _allFieldData
Note: I put everything in the same file for simplicity but you would normally have the class and the validators in separate files.
I'm trying to create a Textbutton widget with a disabled property like this:
class AppTextButton extends StatelessWidget {
final String title;
final void Function(BuildContext context) onPress;
final EdgeInsetsGeometry margin;
final EdgeInsetsGeometry padding;
final double borderRadius;
final Color backgroundColor;
final Image? leadingIcon;
final Image? trailingIcon;
final TextStyle? textStyle;
final bool disabled;
AppTextButton(this.title, this.onPress,
{this.margin = const EdgeInsets.all(0),
this.padding = const EdgeInsets.all(12),
this.borderRadius = 0,
this.leadingIcon,
this.trailingIcon,
this.textStyle,
this.disabled = false,
this.backgroundColor = const Color(0xFFFFFFFF)});
#override
Widget build(BuildContext context) {
return Container(
padding: margin,
child: TextButton(
style: ButtonStyle(
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius))),
backgroundColor: MaterialStateProperty.all(backgroundColor)),
child: Row(
children: [
if (this.leadingIcon != null) ...[this.leadingIcon!],
Expanded(
child: Padding(
padding: padding,
child:
Text(title, textAlign: TextAlign.center, style: textStyle),
),
),
if (this.trailingIcon != null) ...[this.trailingIcon!]
],
),
onPressed: () => !disabled ? onPress(context) : null,
),
);
}
}
And in my screen, I declare my formKey and my form as following:
class LoginScreen extends AppBaseScreen {
LoginScreen({Key? key}) : super(key: key);
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
Form(
key: _formKey,
child: Obx(
() => AppTextInput(
"Please input passcode",
_passwordController,
borderRadius: 8,
fillColor: Color(0xFFF6F4F5),
keyboardType: TextInputType.number,
errorMessage: _c.errorLoginConfirm.value,
isObscure: true,
onChange: _onInputChange,
maxLength: 6,
margin: EdgeInsets.only(top: 12, left: 20, right: 20),
validator: (text) {
if (text != null && text.length > 0) {
if (text.length < 6) {
return "Passcode must have at least 6 digits";
}
}
},
),
)),
And I will have a button at the bottom of the screen, which I pass the !_formKey.currentState!.validate() in the disabled field
AppTextButton("Login", _onLogin,
margin: EdgeInsets.fromLTRB(24, 24, 24, 8),
backgroundColor: Color(0xFFFF353C),
disabled: !_formKey.currentState!.validate(),
textStyle: TextStyle(color: Colors.white),
borderRadius: 8),
However, the formKey.currentState is null and throw the following error everytime the screen is opened.
Null check operator used on a null value
What I am doing wrong here? Thank you in advance!
You need to save the form state before passing,
final FormState formState = _formKey.currentState;
formState.save();
onPressed: () {
FocusScope.of(context).requestFocus(FocusNode());
final FormState formState = _formKey.currentState;
if (formState.validate()) {
formState.save();
onPress(context);
}
},
I think the problem is caused because all the widgets are created at the same time, so the _formKey.currentState is still null when the AppTextButton calls it.
You need to create a separate controller to control the state of the button and add it to the validator like this:
validator: (text) {
if (text != null && text.length > 0) {
if (text.length < 6) {
buttonDisableController = true;
return "Passcode must have at least 6 digits";
}
}
buttonDisableController = false;
return null;
},
In your case, you should know how the widgets building process (Assume you have Botton widget and Input widget):
Botton and Input are building initial state. both states are not yet ready to be read and used
Botton and Input are built. States are ready to read.
User interact to Input. Input must call Button to rebuild its state if the value passes the validator
Botton rebuild.
For the process, you should change your code like:
Get and modify the state of Button inside Input
Notify Button to rebuild
There are many ways to handle the state management between widgets. I simply change the AppTextButton into Statefultwidget to achieve it.
...
final _buttonKey = GlobalKey<_AppTextButtonState>();
...
AppTextButton(key: _buttonKey)
...
class AppTextButton extends StatefulWidget {
final bool initDisable;
AppTextButton({
this.initDisable = false,
Key? key,
}) : super(key: key);
#override
_AppTextButtonState createState() => _AppTextButtonState();
}
class _AppTextButtonState extends State<AppTextButton> {
var disable;
#override
void initState() {
disable = widget.initDisable;
super.initState();
}
#override
Widget build(BuildContext context) {
return TextButton(child: Text('Button'), onPressed: disable ? null : () {});
}
void enableButton() {
setState(() {
disable = false;
});
}
void disableButton() {
setState(() {
disable = true;
});
}
}
class LoginScreen extends StatelessWidget {
LoginScreen({Key? key}) : super(key: key);
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: TextFormField(
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (text) {
if (text != null && text.length > 0) {
if (text.length < 6) {
return "Passcode must have at least 6 digits";
}
}
},
onChanged: (v) {
if (_formKey.currentState?.validate() ?? false) {
_buttonKey.currentState?.enableButton();
} else {
_buttonKey.currentState?.disableButton();
}
},
),
);
}
}
I have a widget that presents an error to a user. I want to have it in two variations:
the first would be a page element to show the error right in the view, replacing some portion of content
the second would be presented to a user as a dialog window.
For the second one I want to tweak the layout a little bit and use Dialog as a wrapped. So I created a separate widget, who extends my current one so I can skip adding duplicate fields in class.
class ConnectionErrorDialog extends ConnectionErrorWidget {
ConnectionErrorDialog(
{required String errorText,
required VoidCallback mainButtonOnTap,
String mainButtonText = 'Понятно'})
: super(
errorText: errorText,
mainButtonText: mainButtonText,
mainButtonOnTap: mainButtonOnTap);
#override
Widget build(BuildContext context) {
return Dialog(
elevation: 24.0,
child: Padding(
padding: EdgeInsets.all(20.0),
child: ConnectionErrorWidget(
errorText: errorText,
mainButtonOnTap: mainButtonOnTap,
mainButtonText: mainButtonText,
),
));
}
}
class ConnectionErrorWidget extends StatelessWidget {
ConnectionErrorWidget({
required this.errorText,
required this.mainButtonOnTap,
this.mainButtonText = 'Попробовать снова',
});
final String errorText;
final String mainButtonText;
final VoidCallback mainButtonOnTap;
#override
Widget build(BuildContext context) {
return UserErrorWidget(
errorText: errorText,
mainButtonText: mainButtonText,
mainButtonOnTap: mainButtonOnTap,
showAsDialog: false);
}
}
I want to understand, is it even okay to extend some class and use it in build()? Maybe there's other, better way to achieve the same result?
You can extend widgets like that, but the only benefit is omitting fields duplication. But it looks like you can do something like that:
class ConnectionErrorWidget extends StatelessWidget {
const ConnectionErrorWidget({
#required this.errorText,
#required this.mainButtonOnTap,
this.mainButtonText,
this.showAsDialog = false,
});
final bool showAsDialog;
final String errorText;
final String mainButtonText;
final VoidCallback mainButtonOnTap;
#override
Widget build(BuildContext context) {
if (showAsDialog) {
return Dialog(
elevation: 24.0,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: _buildUserError(),
));
}
return _buildUserError();
}
Widget _buildUserError() {
return UserErrorWidget(
errorText: errorText,
mainButtonText: mainButtonText ?? (showAsDialog ? 'Попробовать снова' : 'Понятно'),
mainButtonOnTap: mainButtonOnTap,
showAsDialog: showAsDialog);
}
}
In that case, besides omitting fields duplication, your benefit is constructor arguments omitting.
I think that there is not much difference between these variants and you can use any variant you like more.