Flutter tips that will simplify your life when developing an app
When writing widgets with Flutter, you keep doing the same thing again and again … After developing for over a year mobile applications with Flutter, I started to implement collections of widgets that I reuse everywhere in one or more applications. It goes from a simple custom RaisedButton to a custom Scaffold/Appbar and even up to a custom State. I compiled a list of what is most useful to do to save a lot of time while developing an application with Flutter.
Custom widgets to replace buttons
The first very useful thing to do is to create custom widgets for all the kinds of buttons you will use. Let’s say that you want to use rounded buttons everywhere in your application with font size set to 14px. The code would look like this:
import 'package:flutter/material.dart';
class MyOutlinedButton extends StatelessWidget {
final Function onPressed;
final String text;
final Color color;
const MyOutlinedButton({Key key, this.onPressed, this.text, this.color})
: super(key: key);
@override
Widget build(BuildContext context) {
Color _color = color ?? Theme.of(context).accentColor;
return SizedBox(
height: 32.0,
child: FlatButton(
shape: OutlineInputBorder(borderSide: BorderSide(color: _color)),
child: Text(text, style: TextStyle(fontSize: 14.0, color: _color)),
onPressed: onPressed));
}
}
class MyRaisedButton extends StatelessWidget {
final Function onPressed;
final String text;
final Color color;
const MyRaisedButton({Key key, this.onPressed, this.text, this.color})
: super(key: key);
@override
Widget build(BuildContext context) {
Color _color = color ?? Theme.of(context).accentColor;
return SizedBox(
height: 32.0,
child: RaisedButton(
shape: OutlineInputBorder(borderSide: BorderSide(color: _color)),
color: _color,
child: Text(text,
style: TextStyle(fontSize: 14.0, color: Colors.white)),
onPressed: onPressed));
}
}
So now when you want to use one of your buttons, just write:
MyOutlinedButton(
text: "MyButton",
onPressed: () => print("^^"),
),
MyRaisedButton(
text: "MyButton",
onPressed: () => print("^^"),
color: Colors.blue
)
Custom App bar
When you develop an app with lots of different views, you’re probably writing over and over the same code for the App bar … and that’s a big waste of time. So instead, why not first write a Custom App bar? It would look like that:
import 'package:flutter/material.dart';
class MyAppBar extends AppBar {
final String titleString;
final Color color;
MyAppBar({this.titleString, this.color});
@override
Color get backgroundColor => color ?? super.backgroundColor;
@override
Widget get title => Text(titleString, style: TextStyle(color: Colors.white, fontSize: 18.0));
@override
IconThemeData get iconTheme => IconThemeData(color: Colors.white);
So now instead of writing a complete AppBar, just write:
MyAppBar(titleString: "MySuperTitle")
Custom TextFormField
Coding a TextFormField can often be very time-consuming. And even more so if you want to make sure that the next field is focused automatically, that when the user arrives on the last field, the form is validated, etc…. So here is a suggestion that should save you a lot of time if you use text fields in your application often enough.
import 'package:flutter/material.dart';
class MyTextFormField extends StatefulWidget {
final TextEditingController controller;
final Function completeAction;
final InputDecoration decoration;
final FocusNode focusNode;
final FocusNode nextFocusNode;
final bool numberKeyboard;
final bool email;
final bool autoFocus;
final bool obscureText;
final TextInputAction textInputAction;
final Function validator;
const MyTextFormField({Key key, this.controller, this.completeAction, this.decoration, this.focusNode, this.nextFocusNode, this.numberKeyboard, this.email, this.autoFocus, this.obscureText, this.textInputAction, this.validator}) : super(key: key);
@override
State<StatefulWidget> createState() => _MyTextFormFieldState();
}
class _MyTextFormFieldState extends State<MyTextFormField> {
@override
Widget build(BuildContext context) => TextFormField(
controller: widget.controller,
focusNode: widget.focusNode,
autofocus: widget.autoFocus,
decoration: widget.decoration,
textInputAction: widget.textInputAction ?? widget.nextFocusNode == null ? null : TextInputAction.next,
onFieldSubmitted: widget.completeAction ?? (e) {
if (widget.nextFocusNode != null) {
widget.focusNode.unfocus();
FocusScope.of(context).requestFocus(widget.nextFocusNode);
}
},
keyboardType: widget.numberKeyboard
? TextInputType.numberWithOptions(decimal: false)
: widget.email ? TextInputType.emailAddress : TextInputType.text,
validator: widget.validator,
obscureText: widget.obscureText,
);
}
So now, when you want to code a form, you just have to write that down:
class ExampleForm extends StatefulWidget {
@override
_ExampleFormState createState() => _ExampleFormState();
}
class _ExampleFormState extends State<ExampleForm> {
final TextEditingController firstController = TextEditingController();
final TextEditingController secondController = TextEditingController();
final TextEditingController thirdController = TextEditingController();
final FocusNode firstFocusNode = FocusNode();
final FocusNode secondFocusNode = FocusNode();
final FocusNode thirdFocusNode = FocusNode();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(50.0),
child: Column(
children: <Widget>[
MyTextFormField(
controller: firstController,
focusNode: firstFocusNode,
nextFocusNode: secondFocusNode,
email: true,
),
MyTextFormField(
controller: secondController,
focusNode: secondFocusNode,
nextFocusNode: thirdFocusNode,
obscureText: true,
),
MyTextFormField(
controller: thirdController,
focusNode: thirdFocusNode,
numberKeyboard: true,
completeAction: () => print("Form completed"),
),
],
),
),
);
}
}
There is a way to go even further to simplify life but explaining it would deserve a second article…
Custom Dialogs
Displaying dialogues with Flutter is horribly and unnecessarily complicated. So I came to define a class called DialogModel to which I pass the necessary arguments to display the type of dialog of my choice. Then I coded a CustomDialog that accepts a DialogModel as an argument. After all, displaying dialogues has become a child’s play.
First, we need to write the DialogModel:
typedef FunctionWithContext = Function(BuildContext context);
class DialogModel {
String text;
String title;
String actionText;
String backText;
Function backAction;
FunctionWithContext action;
TextAlign textAlign;
MainAxisAlignment actionsMainAxisAlignment;
CrossAxisAlignment dialogCrossAxisAlignment;
DialogModel({this.text, this.title, this.actionText, this.action, this.backAction, this.backText, this.textAlign, this.actionsMainAxisAlignment});
DialogModel.alert(this.title, {this.text, this.backText, this.actionText, this.action}) {
actionsMainAxisAlignment = MainAxisAlignment.spaceBetween;
textAlign = TextAlign.left;
dialogCrossAxisAlignment = CrossAxisAlignment.start;
}
}
This model does not allow complex dialogs to be displayed but is more than sufficient for relatively simple dialogs.
Then we need to write the MyCustomDialog. It’s a little more complicated but not so much!
import 'package:flutter/material.dart';
class MyCustomDialog extends StatefulWidget {
final DialogModel model;
const MyCustomDialog({Key key, @required this.model}) : super(key: key);
/// Use that to show a BasicDialog from the given context.
static Future showBasicDialog(BuildContext context, model) {
return showDialog(
context: context,
builder: (context) => MyCustomDialog(model: model)
);
}
@override
State<StatefulWidget> createState() => _MyCustomDialogState();
}
class _MyCustomDialogState extends State<MyCustomDialog> {
DialogModel get model => widget.model;
// Pop the current view if there is no back action set
void back() {
if (model.backAction != null) {
return model.backAction();
}
Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
return Align(
alignment: FractionalOffset.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Container(
constraints: BoxConstraints(maxWidth: 600.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.5),
color: Colors.white,
),
margin: const EdgeInsets.symmetric(horizontal: 30.0),
padding: const EdgeInsets.symmetric(horizontal: 25.0, vertical: 20.0),
child: Material(
color: Colors.white,
child: Column(
crossAxisAlignment: model.dialogCrossAxisAlignment ?? CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
title,
text,
actions,
],
))
)
]
)
);
}
Widget get title => model.title == null ? SizedBox() : Text(
model.title,
style: TextStyle(fontSize: 18.0),
textAlign: model.textAlign ?? TextAlign.center
);
Widget get text => model.text == null ? SizedBox() : Padding(
padding: EdgeInsets.only(top: 10.0),
child: Text(
model.text,
style: TextStyle(fontSize: 16.0),
textAlign: model.textAlign ?? TextAlign.center
)
);
Widget get actions => Padding(
padding: const EdgeInsets.only(top: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
backButton,
model.action == null ? null : SizedBox(width: 20.0),
actionButton()
]..removeWhere((e) => e == null), // We don't want null values in the widget list
)
);
Widget get backButton => MyFlatButton(
text: model.backText ?? "Retour",
onPressed: back,
);
Widget actionButton() {
// We don't want to show anything if there's no special action specified
if (model.action != null)
return MyRaisedButton(
text: model.actionText,
onPressed: () => model.action(context)
);
return null;
}
}
So now as each time, you want to display a dialog you just have to write this down:
void showAlertDialog() {
MyCustomDialog.showBasicDialog(
context,
DialogModel.alert("MyDialogTitle",
text: "MyDialogText",
backText: "MyBackText",
actionText: "MyActionText", action: (dialogContext) {
print("Do something");
Navigator.pop(dialogContext);
}));
}
void showDialog() {
// No action button if not specified
MyCustomDialog.showBasicDialog(
context,
DialogModel(
title: "MyDialogTitle",
text: "MyDialogText",
backText: "MyBackText",
backAction: (dialogContext) {
print("Do something when user press My")
Navigator.pop(dialogContext);
}
)
);
}
And last but not least, a Custom State
Often when you code a StatefulWidget, you often have to access the Theme, TextTheme, or even Localization. So here is a piece of code that should also save you time.
abstract class CustomState<T extends StatefulWidget> extends State<T> {
ThemeData get theme => Theme.of(context);
TextTheme get textTheme => theme.textTheme;
MobileTranslations get translation => MobileTranslations.of(context);
}
And here is an example of how to use it.
abstract class CustomState<T extends StatefulWidget> extends State<T> {
ThemeData get theme => Theme.of(context);
TextTheme get textTheme => theme.textTheme;
Translations get translation => Translations.of(context);
}
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends CustomState<MyWidget> {
@override
Widget build(BuildContext context) {
print(textTheme);
print(translation);
print(theme);
return Container();
}
}
Thank you for reading to the end.
I hope you enjoyed it.
🔗 Social Media / Let's Connect 🔗 ==> Github | Twitter | Youtube | WhatsApp | LinkedIn | Patreon | Facebook.
Join the Flutter Dev Community 👨💻👨💻 ==> Facebook | Telegram | WhatsApp | Signal.
Subscribe to my Telegram channel | Youtube channel | and also to hashnode newsletter in the input box above 👆👆. Thanks
Happy Fluttering 🥰👨💻