Flutter: how to validate fields dynamically created
Introduction
This is going to be a very specific article. Some days ago a user asked me about how he could implement a form validation with fields added dynamically, so I wrote an example and now I’d like to show you how I implemented it.
The screen contains:
- A list of pairs of fields “Name” and “Age” created dynamically (the first three pairs are created programmatically).
- A button for each pair to remove them from the list.
- A button to add more of these pairs of fields.
- A submit button.
Part 1 - Project setup
1 — Create a new flutter project:
flutter create your_project_name
2 — Edit the file “pubspec.yaml” and add the frideos packages:
dependencies:
flutter:
sdk: flutter
frideos: ^0.6.1
3 — Delete the content of the main.dart
file and write this:
import ‘package:flutter/material.dart’;import ‘dynamic_fields.dart’;void main() => runApp(MyApp());class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: ‘Flutter Demo’,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: DynamicFieldsPage(),
);
}
}
This will be the entry point of the example and the DynamicFieldsPage
the widget that contains the main widgets.
The example has two more files:
bloc.dart
: the file of the BLoC class. In this case, to keep the things simple, it is declared a global instance of this class.dynamic_field.dart
: this contains theDynamicFieldsPage
class (it is a StatefulWidget), theFieldsWidget
(Stateless) and theDynamicFieldsWidget
(Stateless).
Part 2 - BLoC class
Properties
To handle all the fields, it is declared a StreamedList
of StreamedValue<String>
for each one of the two types of fields, name, and age. This is necessary to make it possible adding new fields associated with a stream and handle their validation (see the checkForm
method).
final nameFields = StreamedList<StreamedValue<String>>(initialData: []);final ageFields = StreamedList<StreamedValue<String>>(initialData: []);
The property isFormValid
(aStramedValue<bool>
) is used to stream the current state of the form. This will be used to drive a StreamBuilder
widget in the DynamicFieldsWidget
(explained in the next step) to enable or disable the submit button.
final isFormValid = StreamedValue<bool>();
Constructor
In the constructor, both the StreamedList
are initialized with three StreamedValue<String>
each one, so that the app starts with three pairs of name/age fields filled with some values.
Then, to the onChange
method of every StreamedValue
is assigned the checkForm
method in order to trigger the checking of all the fields every time a single field changes.
DynamicFieldsBloc() {
print(‘ — — — -DynamicFields BLOC — — — — ‘); // Adding the initial three pairs of fields to the screen
nameFields.addAll([
StreamedValue<String>(initialData: ‘Name AA’),
StreamedValue<String>(initialData: ‘Name BB’),
StreamedValue<String>(initialData: ‘Name CC’)
]); ageFields.addAll([
StreamedValue<String>(initialData: ‘11’),
StreamedValue<String>(initialData: ‘22’),
StreamedValue<String>(initialData: ‘33’)
]); // Set the method to call every time the stream emits a new event
for (var item in nameFields.value) {
item.onChange(checkForm);
} for (var item in ageFields.value) {
item.onChange(checkForm);
}
}
checkForm
The checkForm
method is called every time a field changes and detects if there are fields not valid. In this example, for the name fields, it is just checked if the string is not empty, otherwise, an error is sent to the stream. Instead, for the age fields is checked if the value is not null and it is between 1 and 130.
The boolean variables isValidFieldsTypeName
and isValidFieldsTypeAge
are set to true by default: the corresponding boolean is set to false when a field of that type (name or age) is not valid.
In the last lines, is checked if both these boolean are true: this means that all fields are valid. In this case, isFormValid
(a StreamedType<bool>
) is set to true, a true event is sent to stream triggering the StreamBuilder
to rebuild the widget with the new value, enabling the submit button. Otherwise, if at least one field is not valid, the condition of the if
is not met and isFormValid
is set to false (the submit button will be disabled).
newFields
This method is used to add a new pair of name/age fields to the screen. In the first two lines, a new StreamedValue<String>
is added to the nameFields and ageFields StreamedList
and to itsonChange
method is assigned the checkForm
method like seen in the constructor. Finally, thecheckForm
method is called to detect the new fields that, having an empty value, will cause the disabling of the submit button.
void newFields() { nameFields.addElement(StreamedValue<String>();
ageFields.addElement(StreamedValue<String>();
nameFields.value.last.onChange(checkForm);
ageFields.value.last.onChange(checkForm); // This is used to force the checking of the form so that, adding
// the new fields, it can reveal them as empty and sets the form
// to not valid.
checkForm(null);
}
Part 3 — UI
DynamicFieldsPage
In the build
method of the related State class to this StatefulWidget, is returned a Scaffold
with just a simple AppBar
and the DynamicFieldsWidget
in its body. In the dispose
method of this widget is called the dispose
method of the bloc instance to close all the streams opened.
class DynamicFieldsPage extends StatefulWidget {
@override_DynamicFieldsPageState createState() =>
DynamicFieldsPageState();
}class _DynamicFieldsPageState extends State<DynamicFieldsPage> {
@override
void dispose() {
bloc.dispose();
super.dispose();
} @override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: const Text(‘Dynamic fields validation’),
),
body: DynamicFieldsWidget(),
),
);
}
}
DynamicFieldsWidget
This is the widget that contains the fields. In the build
method it returns a ListView
so that it is possible to add an indefinite number of fields. Inside the ListView there is a ValueBuilder
widget that will rebuild the widgets in its builder every time the stream emits a new event. Inside the builder, there is a Column
widget and its children are built by the _buildFields
method.
Then there is a Row
widget used for the buttons: one for adding new fields and another to submit the form.
_buildFields(int length)
This method takes as a parameter the length of the list of name fields and builds the list of fields.
In the first part the list of TextEditingController
are cleared:
nameFieldsController.clear();
ageFieldsController.clear();
Then with a for
loop is added a controller for every field. This will be used to set the initial text of the field. The values of the names and ages are taken from the StreamedValue
of type String, declared in the BLoC class.
for (int i = 0; i < length; i++) {
final name = bloc.nameFields.value[i].value;
final age = bloc.ageFields.value[i].value; nameFieldsController.add(TextEditingController(text: name));
ageFieldsController.add(TextEditingController(text: age));
}
Finally, it is generated a list of FieldsWidget
:
return List<Widget>.generate(
length,
(i) => FieldsWidget(
index: i,
nameController: nameFieldsController[i],
ageController: ageFieldsController[i],
),
);
FieldsWidget
This widget takes as a parameter an index and two TextEditingController
, and builds every pair of Name/Age fields, associated with a button to delete them.
The index is used to pass to the StreamBuilder
of each TextField
the relatedStreamedValue
.
StreamBuilder<String>(
initialData: ‘ ‘,
stream: bloc.nameFields.value[index].outStream,
builder: (context, snapshot) {
return Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
),
child: TextField(
controller: nameController,
style: const TextStyle(
fontSize: 14,
color: Colors.black,
),
decoration: InputDecoration(
labelText: ‘Name:’,
hintText: ‘Insert a name…’,
errorText: snapshot.error,
),
onChanged: bloc.nameFields.value[index].inStream,
),
),
],
);
}),
Let’s analyze this snippet in its most important parts:
Stream:
In this case, using the index parameter, it is passed to the stream parameter of the StreamBuilder
the single element of the StreamedList
nameFields (a list of StreamedValue<String>
) to build the current name field.
stream: bloc.nameFields.value[index].outStream,
- nameFields is a
StreamedList
ofStreamedValue<String>
- nameFields.value is the the
List
itself, and nameFields.value[index] is a singleStreamedValue<String>
. - outStream is the getter of the stream of the single
StreamedValue<String>
onChanged:
To the onChanged parameter, it is passed the inStream
setter of the same StreamedValue<String>
to send to stream every change in this field.
onChanged: bloc.nameFields.value[index].inStream,
Conclusion
This is just a method to validate dynamically created fields with streams and the BLoC pattern in a pretty simple way. As always, for any suggestion, advice or other, feel free to comment. See you to the next article:-) In the meanwhile, you can find the source code of this example in this GitHub repository.