DEV Community

Cover image for Guide to Building, Validating, and Styling Flutter Forms
Kyle Pollock for Rapyd

Posted on • Edited on • Originally published at community.rapyd.net

Guide to Building, Validating, and Styling Flutter Forms

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 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:

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:

flutter create new_flutter_app
Enter fullscreen mode Exit fullscreen mode

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:

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: [],
          ),
        ));
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

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(),
              ],
            ),
          ),
        ));
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

TextFormField(
    validator: (value) {
        if (value == null || value.isEmpty) {
            return 'Please fill this field';
        }
        return null;
    },
),
Enter fullscreen mode Exit fullscreen mode

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:

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

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:

TextFormField(
    inputFormatters: [FilteringTextInputFormatter.digitsOnly],
    keyboardType: TextInputType.number,
),
Enter fullscreen mode Exit fullscreen mode

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:

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',
      ),
),
Enter fullscreen mode Exit fullscreen mode

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 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:

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);
}
Enter fullscreen mode Exit fullscreen mode

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 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

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, 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.

Top comments (0)