How to improve Flutter Forms
Published: 2023-08-05 21:52:22

A Form is how we collect input from users in our apps. Forms are unique points of user interactivity. Examples of where you can find forms include:
- Auth Screens (register or login),
- Checkout Screens (cart contents),
- Item Creation/Updating Screens (like todos), etc.

Most apps and websites have a form at some point. Forms are part of what defines a user's experience on our apps. We should improve forms to give users a good experience.

The forms we build are peculiar to a product or feature. There is no one-fits-all rule for forms. What should be a best practice could have exceptions. What we are saying is, wherever you are building a form, there is room for improvement.

Following are tips that you can apply to improve your forms in [Flutter](https://flutter.dev).

## Table of Contents
[1. Overall Form UI](#1-overall-form-ui)
[2. Form Validation](#2-form-validation)
[3. Password Fields](#3-password-fields)
[4. Other Input Types](#4-other-input-types)
[5. Keyboard Types](#5-keyboard-types)
[6. Autofill](#6-autofill)
[7. Disabling, Loading, and Submitting.](#7-disabling-loading-and-submitting)
[Conclusion](#conclusion)

## 1. Overall Form UI
We can't talk about User Experience without appreciating the User Interface (UI). The UI is the first point of contact with users. It defines the "love at first sight" of our apps.

You want to use/build good UIs that will retain the user. In most cases, you should already have a pre-designed UI (predesigned with Figma or other tools) that you will just implement in Flutter.

[You can implement any UI in Flutter](https://www.freecodecamp.org/news/how-to-implement-any-ui-in-flutter/). The idea is to implement beautiful UIs, whether you created the UI yourself or it was predesigned by someone else. 

The unique combination of box decorations, fonts, colors, icons, sizes, spacings, shadows, etc. all constitute what your form UI looks like. Go for the best UI implementation to give your users a good experience.

Use descriptive labels and placeholders for form fields.

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pxqxg3oyg6qprfedxcun.jpg)
<sub aria-hidden="true"><sup>[Image Source](https://www.pinterest.com/pin/472807660892066219/)</sup></sub>


![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hdmvypv5xvs5kfjnw4d2.png)
<sub aria-hidden="true"><sup>[Image Source](https://dribbble.com/shots/17255320-Subscription-Form)</sup></sub>


![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jhmvypt0l520sp4od6jb.jpg)
<sub aria-hidden="true"><sup>[Image Source](https://bootcamp.uxdesign.cc/dos-and-don-t-for-ui-design-7e5c86c71cac)</sup></sub>


![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kswt8gqf7i5vyc911u2z.jpg)
<sub aria-hidden="true"><sup>[Image Source](https://bootcamp.uxdesign.cc/dos-and-don-t-for-ui-design-7e5c86c71cac)</sup></sub>

## 2. Form Validation

> **Don't trust Users. Don't trust Developers.**

Yes, our code should never trust anybody. "Trust" here is assuming that the user or the developers will do what we expect. The code we write has to enforce boundaries on the data it manipulates.

Many apps use data. This data has to be checked to ensure the smooth flow of the system. 

For example,
- If you are building a registration form, you want to ensure that all emails or usernames are unique. 
- If you are building a booking/reservation system, you want to ensure that users can only book slots for resources in the future.
- If you are building an online exam, you want to be sure the user has answered all questions before submitting. And so on.

Data validation involves checking user data before accepting it into the system. We should do data validation on both client-side and server-side code. 

On client-side code, we don't trust the users. Or rather, we want to help the users to give us the type of data we expect and to respect constraints. 

On server-side code, we don't trust the front-end developers. Or rather we want to be sure that anyone calling our APIs respects the constraints we expect.

Flutter is a client-side tool, so we will focus on client-side data validation. You can do data validation in client code anywhere you take user input in your app (not everything is a form). 

Form validation is checking user-inputted data in a form. Validate forms to ensure data constraints.

Consider validating input fields on user interaction and "onSubmit" of the form. 

Use descriptive messages to explain to the user was is the issue with each field. 


![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nje7do2qfyo5behnwr0i.jpg)
<sub aria-hidden="true"><sup>[Image Source](https://careerfoundry.com/en/blog/ui-design/common-ui-design-mistakes/)</sup></sub>

Also, show validation messages next to each form field rather than showing a generic error at the top or bottom of the form.


![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zfg9dqgpvgskoosvqfeg.jpg)
<sub aria-hidden="true"><sup>[Image Source](https://bootcamp.uxdesign.cc/dos-and-don-t-for-ui-design-7e5c86c71cac)</sup></sub>

To take input text from users in Flutter, use [`TextField`s](https://api.flutter.dev/flutter/material/TextField-class.html). To take input text **while validating the input** in Flutter, use [`TextFormField`s](https://api.flutter.dev/flutter/material/TextFormField-class.html). 

`TextFormField`s accept a [`validator`](https://api.flutter.dev/flutter/widgets/FormField/validator.html) function parameter that should return `null` if the content of the input is okay or the error message as a string if the input's content (what the user filled) has issues. 

If an input is required, the `validator` function should equally return some text explaining that that field is required. 

Asides from _required_, you can check for other properties on input fields like _mininum or maximum length_ of text, _email validity_, _only digits_, or against any _[`RegExp`](https://api.flutter.dev/flutter/dart-core/RegExp-class.html)_ Pattern.

Apply any validation logic you want inside the `validator` function of your input fields. 

For email validation, you can use Regex yourself or use packages like [`email_validator` on pub.dev](https://pub.dev/packages/email_validator).

`TextFormField`s also accept a `maxLength` integer to help you enforce maximum length on input field values (without the validator).

They also accept an `inputFormatters` list of [`TextInputFormatter`s](https://api.flutter.dev/flutter/services/TextInputFormatter-class.html) for more form validation properties.

[`Form`](https://api.flutter.dev/flutter/widgets/Form-class.html) widgets (like every widget in Flutter) accept a `key` parameter. You can use the form's key to validate form fields before submitting form data. 

Flutter `Form`s also accept an [`autovalidateMode`](https://api.flutter.dev/flutter/widgets/FormField/autovalidateMode.html) enum with values of `always`, `disabled`, and `onUserInteraction`.

To reset the validation state of a Flutter `Form`, reassign the value of the form's key and empty the contents of inputs through their [`controller`s](https://api.flutter.dev/flutter/material/TextFormField/controller.html).

{% gist https://gist.github.com/obumnwabude/9768f552db6d391a3dfc5f6ce3e87253 file=simple_form.dart %}

![](https://cdn.obumnwabude.com/gifs/simple-form-demo.gif)

## 3. Password Fields

Passwords are usually in auth-related screens: either registration or login. 

It is good practice to obscure the user's password against preying eyes. However, the user sometimes might want to view the password. We shouldn't deny our users this feature.

As you obscure passwords, please add a button to toggle viewing and obscuring the password. Flutter `TextFormField`s have an `obscureText` boolean property. They also permit you to specify the `obscuringCharacter`.

It is also common practice to double the password fields in registration forms. Doing this helps users to be sure of the password they are creating.

The `controller` of a `TextField` tells its content. We can use the `controller` of a password field in the `validator` of the _confirm password_ field to know whether passwords match.

Still on passwords, you might want users to use complex passwords. Maybe minimum length of 8 characters, mixing alphabets, numbers, and symbols, not using common names, etc. 

You can all such logic in the `validator` function of your password field. You can also add extra UI validation feedback yourself to tell the user the strength of their passwords.

{% gist https://gist.github.com/obumnwabude/9768f552db6d391a3dfc5f6ce3e87253 file=password_form.dart %}

![](https://cdn.obumnwabude.com/gifs/password-form-demo.gif)

## 4. Other Input Types

Text is not the only collectible data. When we want users to select one or more options, we have to use appropriate widgets for those cases.

Use radio buttons when at least one and only one option can be chosen. Use checkboxes when multiple options can be chosen. 

[`DropdownButton`](https://api.flutter.dev/flutter/material/DropdownButton-class.html)s are also usable in forms. Use a dropdown if only one option is to be selected from a list of more than 3 options. If the list is long (more than 10), consider adding "search" functionality for the user to easily locate their choices.

These selector widgets can also be involved in form validation. You might have to add and style extra widgets to show their error messages next to them. Their errors will mainly be "required"-related errors.

In such case, in your submit function, when checking if the form's values are valid with `formKey.currentState!.validate()`, also check if the values of selector fields have been set.

There is a [`DropdownButtonFormField`](https://api.flutter.dev/flutter/material/DropdownButtonFormField-class.html) widget that presents as a dropdown inside a form field. It takes a `decoration` property and a `validator` function, just like `TextField`s. Use it where necessary.

If you want to show a custom widget as a field, you can wrap it in the [`InputDecorator`](https://api.flutter.dev/flutter/material/InputDecorator-class.html) widget and it will appear as you want. 

You can also have fields for Date and Time. Use pickers for these fields. Flutter has the [`showDatePicker`](https://api.flutter.dev/flutter/material/showDatePicker.html) function to open a Date picker. We also have the [`showTimePicker`](https://api.flutter.dev/flutter/material/showTimePicker.html) function.

If you want more customization, there are packages on pub.dev to suit your needs. Or you can code one yourself (it's harder).

You can have a field that will display the selected date/time. But this field will not be "editable". Like the user can't type text into it. On tap of the field, open a picker, and in the callback, set the value of the field to the chosen date.

You can have validators in the field too. You can validate chosen date or time as you wish in the picker's callback or in your validator function.

## 5. Keyboard Types
Still in the world of optimizations, we can open specific keyboard types for each `TextField` in Flutter.

Set the `keyboardType` on the `TextField` to any of the available [`TextInputType`](https://api.flutter.dev/flutter/services/TextInputType-class.html) and you are good to go. The types include `emailAddress`, `phone`, `number`, etc.

You can set a keyboard type for each field to match the demands of that field. 

For example
- If you set the keyboard type to `TextInputType.emailAddress`, the mobile operating system keyboard will have a prominent `@` symbol for emails.
- If you set it to `TextInputType.phone`, the user will see mainly digits on the keyboard.
- If you set it to `TextInputType.multiline`, there will be an enter key, and so on.

`textInputAction` is also a valid setting for text fields in Flutter. It takes any value of the [`TextInputAction`](https://api.flutter.dev/flutter/services/TextInputAction.html) enum like `done`, `search`, `send`, etc.

These will show a corresponding icon or text at the end of the opened keyboard in each mobile operating system. It helps to tell the user what the field does in that context.

## 6. Autofill

Autofill or autocomplete is when browsers or operating systems fill forms in apps or websites on behalf of users. 

If a user opts in for autofill, the system saves user data that has been filled in previous forms. In the next form, the system will help the user fill out the form (autofill).

The user can then edit what the system auto-filled to suit their purpose.

Autofill info could also be gotten from the user's device directly. Like the signed-in email addresses, phone numbers of sim cards, or personal names that the user has set on their phone.

Autofill makes life easy. It makes users spend less time on forms and makes them happy.

We as developers have to make our forms compatible with autofill to help users that opt-in for it. We have to tell the browser or OS what data our form fields are collecting. This will make the system fill the right data into the right field.

`TextField`s in Flutter accept an `autofillHints` property. It takes a list of [`AutoFillHints`](https://api.flutter.dev/flutter/services/AutofillHints-class.html) enums. This enum has values like `email`, `name`, `newPassword`, `password`, `telephoneNumber`, `username`, etc. There are many options. Set the right ones on each field. 

Autofill is also helpful with passwords. Many users use password managers. These managers rely on autofill to know when to fill in passwords in apps or websites for the users. 

When autofill is properly set, these managers can also suggest new passwords for the user's account on your app (which is what you want).

You can wrap related fields (like email and password fields) in an [`AutofillGroup`](https://api.flutter.dev/flutter/widgets/AutofillGroup-class.html) widget. This widget tells the system that the fields are linked. This is a further useful feature for password managers.

Set autofill in your fields. The platform where Flutter is running will collaborate with your form and help the user as you expect.


## 7. Disabling, Loading, and Submitting.
Your form should usually have a button towards its end for submitting all the user-inputted data. This button should call a submit function. The submit function should validate the form before you submit its values.

```dart
// in submit function
if (formKey.currentState!.validate()) {
  // submit the form
} else {
  // toast "resolve all errors"
}
```

If your Flutter code is running on the web, you may want to set the `onFieldSubmitted` property on the `TextFormField`s to the submit function. This will make the form submit when the user presses the enter key while on a desktop browser. Web users are used to this on websites so let's give them the same experience.

Most of the time, submitting your form is an asynchronous process. You would be submitting this form to your backend. You want to take care of the user flow after submission. You can navigate the user to another waiting screen or show a loader to tell the user to wait. 

If errors occur during submission (maybe network error), you want to reinstate the form with the values the user submitted (or just deactivate the loading), so that the user can retry submitting.

You don't want the user to change any of the values of fields while the form is been submitted. You may want to have an `isSubmitting` boolean. Then set the `enabled` property on all form fields to `!_isSubmitting` (that's when not submitting). This will make disable the fields when `_isSubmitting` is `true`.

The submit function should toggle the value of `_isSubmitting`. It will make it `true` before it starts and make it `false` after processing.

```dart
// in submit function
if (formKey.currentState!.validate()) {
  setState(() => _isSubmitting = true);
  try {
    // submit form data 
  } catch (e) {
    // toast the error ("e")
  } finally {
    setState(() => _isSubmitting = false);
  }
} else {
  // toast "resolve all errors"
}
```

You can use [`Snackbar`](https://docs.flutter.dev/cookbook/design/snackbars) or any custom toast in pub.dev to toast error feedback from the backend after submission. 

Supposedly, if the user's submission was successful, your UI should change. The user shouldn't see the form again. Depending on your app, after successful form submission, you could still toast successful (or a useful message), then navigate the user to another screen or close the form.

## Conclusion

Forms are how we collect users' data. Forms should not be difficult for users.

The above are tips to improve your forms in Flutter. You can combine them or use all of them.

Build forms that make life easy for the user. Your objective should be to give your users a nice experience while they are in your app.

Cheers!