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:
- Visual Studio Code or Android Studio editor
- Flutter SDK 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:
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:
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:
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:
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:
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:
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:
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 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);
}
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:
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 Rapyd Client Portal now.
Post any thoughts or related questions here in the Rapyd Developer Community.