Realistic Forms in Flutter, Part 1

What we’ll cover in this tutorial:

<div class="side-note">

Note: you can check out the video for this post here.

</div>

The code for this post can be found here

Extra credit: familiarize yourself with some of the Material Design style controls we can use in Flutter.

Form and GlobalKey

Let’s start coding a simple form, using a MaterialApp widget.

Forms are an essential feature for almost any mobile app. There are two major types of forms, one is for something like sign up or some onboarding flow and another is more of a utility, such as a settings or an update profile screen.

Our form will require some kind of state to be maintained, therefore, using a StatefulWidget. State in Flutter is nothing more than a bunch of variables that are owned and updated by a given widget. If you’re not familiar with StatefulWidgets, I recommend reading up on it before continuing.

Flutter offers a widget called “Form”, much like an HTML form. All this is is a container for a bunch of form widgets. The two major ways we use a form is to one, validate its form fields and two, save the form.

When we want to perform actions like this, say, when we click a “Save” button for example, we need to refer to our form using some unique identifier. This is much like an HTML, where a "div" can have an “id” attribute. All of this is done by using Flutter’s “GlobalKey” class. GlobalKey lets us generate a unique, app-wide ID that we can associate with our form.

// home_material.dart
import 'package:flutter/material.dart';

class HomeMaterial extends StatefulWidget {
  @override
  _HomeMaterialState createState() => _HomeMaterialState();
}

class _HomeMaterialState extends State <homematerial>{
  final _formKey = GlobalKey<formstate>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Profile')),
      body: Container(
        child: Builder(
          builder: (context) => Form(
              key: _formKey,
              child: Container(),
            )
          )
        )
      );
  }
}</formstate></homematerial>`

Defining Our User Model

A simple model here defines a user profile that we'll update in our form. The benefit of this is it keeps our form simple, where it only needs to refer to a simple User instance. Later, we can easily convert this to JSON when we want to submit the form to a server (not covered in this tutorial).

// models/user.dart

class User {
  static const String PassionCooking = 'cooking';
  static const String PassionHiking = 'hiking';
  static const String PassionTraveling = 'traveling';

  String firstName = '';
  String lastName = '';
  Map <string, bool="">passions = {
    PassionCooking: false,
    PassionHiking: false,
    PassionTraveling: false
  };
  bool newsletter = false;

  save() {
    print('saving user using a web service');
  }
}</string,> `

Our Form Fields

The next concept is form fields. The main piece of documentation we’ll be referencing is called Flutter’s “Input Widgets”.

Flutter offers two general categories of form fields, one on the Material Design style and one in the Cupertino style. We’ll cover both in this tutorial but the Material Design form fields in more depth, since they are more customizable when developing custom UI.

Let's add a Material Design based text field, using TextFormField:

// home_material.dart
...

TextFormField(
    decoration: InputDecoration(labelText: 'First name'),
),

...`

Now, because the Material Design text field widget supports validation (note that some don't), we can implement a 'validator', which will be triggered when we validate our form (in a later step in this tutorial):

// home_material.dart
...

TextFormField(
    decoration: InputDecoration(labelText: 'First name'),
    validator: (value) {
      if (value.isEmpty) {
          return 'Please enter your first name.';
      }
    },
),

...`

Finally, we need to update our state whenever the form is saved. This way, we can submit an up to date user to our server when we want (not covered in this tutorial):

// home_material.dart
...

TextFormField(
    decoration: InputDecoration(labelText: 'First name'),
    validator: (value) {
      if (value.isEmpty) {
          return 'Please enter your first name.';
      }
    },
    onSaved: (val) => setState(() => _user.firstName = val),
),

...`

Switches and Checkboxes

Implementing a switch and checkbox is pretty straightforward. Note that on iOS, it's not typically the norm to use checkboxes, you only use switches.

In our first example, we do not only implement a Switch widget, we implment a SwitchListTile. What this does is give us a "list tile" that nicely supports a text label on the left and right aligns our switch to the right:

// home_material.dart
...

SwitchListTile(
  title: const Text('Monthly Newsletter'),
  value: _user.newsletter,
  onChanged: (bool val) =>
      setState(() => _user.newsletter = val)
)

...`

We provide a boolean value for the "value" parameter here.

For our checkbox, it's pretty similar. We use a CheckboxListTile, which offers up the same presentation of the widget:

// home_material.dart
...

CheckboxListTile(
  title: const Text('Cooking'),
  value: _user.passions[User.PassionCooking],
  onChanged: (val) {
    setState(() =>
        _user.passions[User.PassionCooking] = val);
  }
)

...`

Validating and Saving our Form

In our final step, we render a RaisedButton Material Design widget, which triggers the following:

  1. Validate our form
  2. If it's valid, trigger all 'onSaved' functions in each of our widgets
  3. Save our user model by submitting the values to a web service API (mocked here)
// home_material.dart
...

RaisedButton(
  onPressed: () {
    final form = _formKey.currentState;
    if (form.validate()) {
      form.save();
      _user.save();
      _showDialog(context);
    }
  },
  child: Text('Save'),
),

...

_showDialog(BuildContext context) {
  Scaffold.of(context)
      .showSnackBar(SnackBar(content: Text('Submitting form')));
}

...`

Complete Code

// home_material.dart
import 'package:flutter/material.dart';
import '../models/user.dart';

class HomeMaterial extends StatefulWidget {
  @override
  _HomeMaterialState createState() => _HomeMaterialState();
}

class _HomeMaterialState extends State <homematerial>{
  final _formKey = GlobalKey<formstate>();
  final _user = User();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text('Profile')),
        body: Container(
            padding:
                const EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0),
            child: Builder(
                builder: (context) => Form(
                    key: _formKey,
                    child: Column(
                        crossAxisAlignment: CrossAxisAlignment.stretch,
                        children: [
                          TextFormField(
                            decoration:
                                InputDecoration(labelText: 'First name'),
                            validator: (value) {
                              if (value.isEmpty) {
                                return 'Please enter your first name';
                              }
                            },
                            onSaved: (val) =>
                                setState(() => _user.firstName = val),
                          ),
                          TextFormField(
                              decoration:
                                  InputDecoration(labelText: 'Last name'),
                              validator: (value) {
                                if (value.isEmpty) {
                                  return 'Please enter your last name.';
                                }
                              },
                              onSaved: (val) =>
                                  setState(() => _user.lastName = val)),
                          Container(
                            padding: const EdgeInsets.fromLTRB(0, 50, 0, 20),
                            child: Text('Subscribe'),
                          ),
                          SwitchListTile(
                              title: const Text('Monthly Newsletter'),
                              value: _user.newsletter,
                              onChanged: (bool val) =>
                                  setState(() => _user.newsletter = val)),
                          Container(
                            padding: const EdgeInsets.fromLTRB(0, 50, 0, 20),
                            child: Text('Interests'),
                          ),
                          CheckboxListTile(
                              title: const Text('Cooking'),
                              value: _user.passions[User.PassionCooking],
                              onChanged: (val) {
                                setState(() =>
                                    _user.passions[User.PassionCooking] = val);
                              }),
                          CheckboxListTile(
                              title: const Text('Traveling'),
                              value: _user.passions[User.PassionTraveling],
                              onChanged: (val) {
                                setState(() => _user
                                    .passions[User.PassionTraveling] = val);
                              }),
                          CheckboxListTile(
                              title: const Text('Hiking'),
                              value: _user.passions[User.PassionHiking],
                              onChanged: (val) {
                                setState(() =>
                                    _user.passions[User.PassionHiking] = val);
                              }),
                          Container(
                              padding: const EdgeInsets.symmetric(
                                  vertical: 16.0, horizontal: 16.0),
                              child: RaisedButton(
                                  onPressed: () {
                                    final form = _formKey.currentState;
                                    if (form.validate()) {
                                      form.save();
                                      _user.save();
                                      _showDialog(context);
                                    }
                                  },
                                  child: Text('Save'))),
                        ])))));
  }

  _showDialog(BuildContext context) {
    Scaffold.of(context)
        .showSnackBar(SnackBar(content: Text('Submitting form')));
  }
}</formstate></homematerial>`

A Note About "Multi-Platform" Friendly UX

There's a glaring problem with this form example, it's not friendly to iOS users. If you’re serious about writing multi platform mobile apps, it’s important to understand what both Android and iOS users are used to. Now, we have an Apple style form in the source code posted, but unless we have strict requirements to show an Apple style form if the OS is iOS, we're going to want to render a form that's more OS neutral.

In order to show a nice looking form that's not overly Material Design like but also not overly Apple Flat Design (CocoaTouch) like, we're going to want to customize our Material Design based form.

Summary

Forms are a critical skill for any mobile developer. Learning how to present a form that's easy to use and familiar to any user, especially based on the OS they use, is a great skill to have. In the next part of this tutorial, we'll be customizing our form so that it looks much more "multi platform" friendly.

Happy Fluttering! Nick

Resources

The code for this post can be found here.