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.
Related
I have a widget:
class MyWidget extends StatelessWidget {
final Color? color;
final EdgeInsetsGeometry? padding;
final Widget child;
// ... many more properties
const MyWidget({
super.key,
this.color = Colors.blue,
this.padding = const EdgeInsets.all(4),
required this.child,
});
#override
Widget build(BuildContext context) {
return Container(
color: color,
padding: padding,
child: child,
);
}
}
I also have another widget which wraps the above widget in SliverToBoxAdapter like this:
class MySliverWidget extends MyWidget {
MySliverWidget({
super.color,
super.padding,
required super.child,
// ... many more properties
});
#override
Widget build(BuildContext context) {
// Unable to use "child: super".
return SliverToBoxAdapter(child: super);
}
}
But the problem is I am unable to use the parent widget (MyWidget) instance using super inside the child widget (MySliverWidget).
So, the question is how do I access MyWidget instance inside MySliverWidget?
Note:
In MySliverWidget.build() method, I don't want to use child: MyWidget(...) and pass all the parameters to it (which is redundant).
I also don't to simply use SliverToBoxAdapter(child: MyWidget(...)) instead of having a MySliverWidget in the first place.
Maybe like this :
return SliverToBoxAdapter(child: super.build(context));
Is it possible to "merge" two widgets together? I want to build an OptionalWrapper component, that wraps a widget by another widget only if a condition is matched.
The usage should look something like this, I provide a child and if the showWrapper condition is true, this child is put as the child of the wrapper widget.
OptionalWrapper(
showWrapper: !isSmallScreen(context),
wrapper: Padding(
padding: const EdgeInsets.only(top: 16.0),
child: // <-- Here should the child be rendered
),
child: Text("Hello"),
),
of course I could do something like this:
!isSmallScreen(context) ? Padding(
padding: const EdgeInsets.all(16.0),
child: Text("Hello"),
) : Text("Hello"),
But here I declare my Text widget twice which I want to avoid.
The implementation would look like this:
import 'package:flutter/material.dart';
class OptionalWrapper extends StatelessWidget {
final bool showWrapper;
final Widget wrapper;
final Widget child;
const OptionalWrapper({
Key? key,
required this.showWrapper,
required this.wrapper,
required this.child,
}) : super(key: key);
#override
Widget build(BuildContext context) {
if (showWrapper) {
// return the wrapper widget but subject child as child of the wrapper widget
} else {
return child;
}
}
}
You can create builder method like
import 'package:flutter/material.dart';
class OptionalWrapper extends StatelessWidget {
final bool showWrapper;
final Widget Function(Widget) wrapper;
final Widget child;
const OptionalWrapper({
Key? key,
required this.showWrapper,
required this.wrapper,
required this.child,
}) : super(key: key);
#override
Widget build(BuildContext context) {
if (showWrapper) {
return wrapper(child);
} else {
return child;
}
}
}
You can make the padding as EdgeInsects.zero if the condition fails
Padding(
padding: !isSmallScreen(context)?const EdgeInsets.all(16.0): EdgeInsects.zero,
child: Text("Hello"),
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 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,
);
}
}
I have this widget tree
return Container(
color: Colors.blue,
child: Container(
child: Text("Child"),
),
);
Is there a way to remove a parent widget from the tree or conditionally include it?
For example if a state variable such as includeBlueContainer was false, I would like to not render the blue container (but show everything else).
I couldn't achieve an optionally include reusable widget but I've been using this pattern which does achieve what I wanted to achieve. I haven't given this a great amount of thought but I still feel there is a better solution somewhere.
class MyContainer extends StatelessWidget {
final Widget child;
final bool isIncluded;
MyContainer({this.child, this.isIncluded = true});
Widget build(BuildContext context) {
if (!isIncluded) return child;
return Container(
color: Colors.blue,
child: child,
);
}
}
Edit: I made a package out of this: https://pub.dev/packages/conditional_parent_widget
Which you can use like:
import 'package:flutter/widgets.dart';
import 'package:conditional_parent_widget/conditional_parent_widget.dart';
// ...
return ConditionalParentWidget(
condition: includeBlueContainer,
child: Text("Child"),
parentBuilder: (Widget child) => Container(
color: Colors.blue,
child: child,
),
);
Internally it is just:
import 'package:flutter/widgets.dart';
class ConditionalParentWidget extends StatelessWidget {
const ConditionalParentWidget({
Key? key,
required this.condition,
required this.child,
required this.parentBuilder,
}) : super(key: key);
final Widget child;
final bool condition;
final Widget Function(Widget child) parentBuilder;
#override
Widget build(BuildContext context) {
return condition ? this.parentBuilder(this.child) : this.child;
}
}