How to simplify your life when coding with Flutter

How to simplify your life when coding with Flutter

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 🥰👨‍💻