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 the DynamicFieldsPage class (it is a StatefulWidget), the FieldsWidget (Stateless) and the DynamicFieldsWidget (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 checkFormmethod 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 StreamedListof StreamedValue<String>
  • nameFields.value is the the List itself, and nameFields.value[index] is a single StreamedValue<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.