Guide to Building, Validating, and Styling Flutter Forms
Tags: webdev, flutter, tutorial, beginners
Published: 2023-06-06 19:28:27

_By Dedan Ndungu_

Collecting information is an essential task in any modern application. Depending on the purpose of your app, you may have to collect all kinds of personally identifiable information (PII), ranging from name and email to phone numbers, mailing addresses, and so on. Accepting payments may require you to use third party APIs if you do not meet the PCI (Payment Card Industry) Security Standard requirements. Securely collecting basic contact information allows you to manage and access your customers' stored information.  This ensures users have a great experience when performing activities like cart checkouts or maintaining social profiles.

[Flutter forms](https://docs.flutter.dev/cookbook/forms) provide a simple and intuitive way to collect such information from users on your Flutter application. You can seamlessly build styled Flutter forms using text fields, radio buttons, drop-down menus, check buttons, and more. In this article, you will learn how to build a basic Flutter form, style it, and validate the user input.

## Implementing Forms in Flutter

In order to build a basic Flutter form in this tutorial, you'll need the following:

- [Visual Studio Code](https://code.visualstudio.com) or [Android Studio](https://developer.android.com/studio) editor
- [Flutter SDK](https://docs.flutter.dev/get-started/install) installed on your machine
- An emulator or real mobile device

### Creating a New Flutter App

To create a new Flutter app, open your terminal pointed to a location of your choice and enter the following command:

```shell
flutter create new_flutter_app
```

This command creates a new Flutter project in your folder. Open the Flutter project using your code editor, then run it. If everything works as expected, you should see the basic counter app displayed on your device's screen.

After that, update the `main.dart` file with the following code, which will form the basis for this tutorial:

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

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Forms',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Flutter Forms'),
        ),
        body: SingleChildScrollView(
          child: Column(
            children: [],
          ),
        ));
  }
}
```

### Building the Form

Flutter provides a `Form` widget that acts as a container for grouping and validating multiple `FormField` widgets. For the form to execute its tasks, you need to assign a `GlobalKey` that will act as a unique identifier for the form. This key also holds the `FormState` that can be used to save, reset, or validate each `FormField`.

The `Form` widget requires each descendant to be wrapped with a `FormField` widget that maintains the state of the form field such that updates and validation errors are reflected in the UI. Luckily, Flutter provides a handy widget, `TextFormField`, that wraps this functionality into a `TextField`, allowing you to easily accept and validate user inputs.

To create a `Form` widget, update the `_MyHomePageState` class as follows:

```dart
class _MyHomePageState extends State<MyHomePage> {
  final _formKey = GlobalKey<FormState>();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Flutter Forms'),
        ),
        body: SingleChildScrollView(
          child: Form(
            key: _formKey,
            child: Column(
              children: [
                TextFormField(),
              ],
            ),
          ),
        ));
  }
}
```

### Validating the Form

To maintain data sanity, it's crucial to check whether the information provided by users is correct and in the right format. With Flutter, minimal code is needed to set up a validation scheme in your form, as the framework's widgets handle most of the work, such as updating or resetting the screen.

The `TextFormField` widget exposes a validator callback function that can be used to validate the user input and display an error when the user tries to submit invalid information. The validator receives the current user input and should return `null` if the input is correct and in the right format; otherwise, it should return an error message to be displayed by the `TextFormField` widget. Below is a simple implementation of the validator function:

```dart
TextFormField(
    validator: (value) {
        if (value == null || value.isEmpty) {
            return 'Please fill this field';
        }
        return null;
    },
),
```

The validator is executed when you call the `validate()` function that is exposed by the `FormState` key. You can do this during form submission as follows:

```dart
    ElevatedButton(
        onPressed: () {
            if (_formKey.currentState!.validate()) {
                // The form is valid
            } else {
                // The form has some validation errors.
                // Do Something...
            }
        },
        child: Text('Submit'))
```

To reduce user input errors, the `TextFormField` widget exposes other arguments that are used to filter inputs, as well as alter the keyboard input type:

```dart
TextFormField(
    inputFormatters: [FilteringTextInputFormatter.digitsOnly],
    keyboardType: TextInputType.number,
),
```

These arguments include the following:

- `inputFormatters` prevent the insertion of characters matching (or not matching) a particular pattern.
- `keyboardType` optimizes the keyboard text input.


### Styling the Form

The form design heavily impacts the user experience. A well-organized and good-looking form can be the difference between retaining users and getting terrible app reviews.

In Flutter, the `TextFormField` widget can be styled to improve the user interface and provide a better user experience. This is accomplished using the `InputDecoration` argument, which allows you to alter properties such as icons, hints, labels, borders, colors, styles, and much more. Below is a sample input decoration:

```dart
TextFormField(
    decoration: InputDecoration(
      enabledBorder: OutlineInputBorder(
      borderSide:BorderSide(color: Colors.blueGrey, width: 2.0)),
      border: OutlineInputBorder(borderSide: BorderSide()),
      fillColor: Colors.white,
      filled: true,
      prefixIcon: Icon(Icons.account_box_outlined),
      suffixIcon: Icon(Icons.check_box_outlined),
      hintText: 'John Doe',
      labelText: 'Name',
      ),
),
```

### Putting the Pieces Together

As discussed above, building a form in Flutter is a simple task. Depending on the requirements of your application, you can easily add validation to the different types of information that you collect.

To put the pieces together, assume you are collecting information to submit to a payment processing backend or application programming interface (API) provider such as [Rapyd](https://rapyd.net) for processing. The backend requires the name, phone number, email address, street address, and city of the application user. To collect this information, update the `_MyHomePageState` class with the following:

```dart
class _MyHomePageState extends State<MyHomePage> {
  final _formKey = GlobalKey<FormState>();
  final _nameController = TextEditingController();
  final _emailController = TextEditingController();
  final _phoneController = TextEditingController();
  final _streetAddressController = TextEditingController();
  final _cityController = TextEditingController();

  @override
  void dispose() {
    _nameController.dispose();
    _emailController.dispose();
    _phoneController.dispose();
    _streetAddressController.dispose();
    _cityController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Flutter Forms'),
        ),
        body: SingleChildScrollView(
          padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
          child: Form(
            key: _formKey,
            child: Column(
              children: [
                TextFormField(
                  controller: _nameController,
                  keyboardType: TextInputType.name,
                  decoration: inputDecoration(
                      prefixIcon: Icon(Icons.account_box_outlined),
                      hintText: 'John Doe',
                      labelText: 'Name'),
                  validator: (value) {
                    if (value == null || value.isEmpty) {
                      return 'Please fill this field';
                    }
                    return null;
                  },
                ),
                SizedBox(
                  height: 10,
                ),
                TextFormField(
                  controller: _emailController,
                  keyboardType: TextInputType.emailAddress,
                  decoration: inputDecoration(
                      prefixIcon: Icon(Icons.email_outlined),
                      hintText: 'johndoe@xyz.com',
                      labelText: 'Email Address'),
                  validator: (value) {
                    if (value == null || value.isEmpty) {
                      return 'Please fill this field';
                    }
                    return null;
                  },
                ),
                SizedBox(
                  height: 10,
                ),
                TextFormField(
                  controller: _phoneController,
                  inputFormatters: [FilteringTextInputFormatter.digitsOnly],
                  keyboardType: TextInputType.number,
                  decoration: inputDecoration(
                      prefixIcon: Icon(Icons.phone_android_outlined),
                      hintText: '12346789',
                      labelText: 'Phone Number'),
                  validator: (value) {
                    if (value == null || value.isEmpty) {
                      return 'Please fill this field';
                    }
                    return null;
                  },
                ),
                SizedBox(
                  height: 10,
                ),
                TextFormField(
                  controller: _streetAddressController,
                  keyboardType: TextInputType.streetAddress,
                  decoration: inputDecoration(
                      prefixIcon: Icon(Icons.streetview_outlined),
                      hintText: '123 State Street',
                      labelText: 'Street Address'),
                  validator: (value) {
                    if (value == null || value.isEmpty) {
                      return 'Please fill this field';
                    }
                    return null;
                  },
                ),
                SizedBox(
                  height: 10,
                ),
                TextFormField(
                  controller: _cityController,
                  keyboardType: TextInputType.text,
                  decoration: inputDecoration(
                      prefixIcon: Icon(Icons.location_city_outlined),
                      hintText: 'London',
                      labelText: 'City'),
                  validator: (value) {
                    if (value == null || value.isEmpty) {
                      return 'Please fill this field';
                    }
                    return null;
                  },
                ),
                SizedBox(
                  height: 20,
                ),
                ElevatedButton(
                    style: ElevatedButton.styleFrom(
                        minimumSize: Size.fromHeight(50)),
                    onPressed: () {
                      if (_formKey.currentState!.validate()) {
                        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                            content: Text(
                                'Hello ${_nameController.text}\nYour details have been submitted and an email sent to ${_emailController.text}.')));
                      } else {
                        // The form has some validation errors.
                        // Do Something...
                      }
                    },
                    child: Text('Submit'))
              ],
            ),
          ),
        ));
  }

  InputDecoration inputDecoration({
    InputBorder? enabledBorder,
    InputBorder? border,
    Color? fillColor,
    bool? filled,
    Widget? prefixIcon,
    String? hintText,
    String? labelText,
  }) =>
      InputDecoration(
          enabledBorder: enabledBorder ??
              OutlineInputBorder(
                  borderSide: BorderSide(color: Colors.blueGrey, width: 2.0)),
          border: border ?? OutlineInputBorder(borderSide: BorderSide()),
          fillColor: fillColor ?? Colors.white,
          filled: filled ?? true,
          prefixIcon: prefixIcon,
          hintText: hintText,
          labelText: labelText);
}
```

The `inputDecoration` function provides a simple and clean implementation to decorate the `TextFormField` widgets without repeating your code in multiple lines.

The variable `_nameController` is of type `TextEditingController()`, which watches changes in the `TextFormField` input and updates its value. You can use the controller to listen to changes in user input as well as fetch their text value. To improve the performance of the app, you need to dispose of this controller using the override `dispose` method.

You can refer to [this GitHub repository](https://github.com/Rapyd-Samples/flutter-forms) for the full code implementation. After running the above code, you should have something similar to this when interacting with the form:

![Flutter form validation](https://i.imgur.com/Tuip4MC.png)

## Conclusion

Collecting user information is a necessity for smooth operation in today's application development. Leveraging Flutter forms makes it easy to collect this data while maintaining a great user experience. Furthermore, you can validate the provided data for correctness before it is submitted for processing.

Submitting correctly formatted and validated data is especially important when processing payments. To accept payments in your application, consider [Rapyd](https://rapyd.net/), a solution with all the tools you need for payments, payouts, and business everywhere.

You can get started in generating a fully PCI-DSS certified hosted page and payment form that can handle personal identifying information for cards. Sign up at https://dashboard.rapyd.net/sign-up now. 

Post any thoughts or related questions in the [Rapyd Developer Community](https://community.rapyd.net/).