Flutter Tutorial

Form Validation with ChangeNotifier in Flutter

Change carefully!

Saurabh Pant
Dev Genius
Published in
4 min readJan 13, 2024

--

Well! I know that there exists a TextFormField in Flutter for creating form fields. And so this article is not about raising question on any existing widget’s functionality but to understand how change notifiers work and how can we use them for our use cases.

Let’s suppose we want to create a form with some text fields and a submit button. Once all the fields are validated, our final action button gets enabled and otherwise remain disabled or it may change its color (any property you can consider).

In the above scenario, as the number of fields increases, it becomes quite complex to handle the validation logic and inform the submit button about the validation state of all fields so that it can change its own state. We can even have different type of fields like textfield, checkbox, radio button etc.

In this article, we’ll see how can we use change notifier in such cases for managing states.

Let’s say we’ve few text fields, which have a validation logic and once they’re all become valid, our final button changes its color and shows a pop up on click. Below is the video for our final result.

We’ll be using the strategy where

  • each of our ui field will register itself with the validation notifier
  • every time something changes on any field, it validate itself and updates the validation status with validation notifier
  • validation notifier prepares the final validation status and broadcasts the it to its consumer (yeah! quite similar to how we do it in jetpack compose with flows)
  • the action button is a consumer of the validation notifier and update its state based on the final validation status of all fields.

The above points are depicted in the below flow

Let’s do some code now. First we create our input field which is simply a textfield widget.

  @override
Widget build(BuildContext context) {
return SizedBox(
width: widget.width,
child: TextField(
controller: textController,
onChanged: onTextChanged,
cursorColor: Colors.black,
cursorWidth: 2,
decoration: const InputDecoration(
hintText: 'write here',
hintStyle: TextStyle(
color: Colors.grey,
fontWeight: FontWeight.normal,
fontSize: 16,
),
),
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.normal,
fontSize: 16,
),
textInputAction: TextInputAction.next),
);
}

Now, we validate our field every after 500ms once the user stops typing as follows

  performValidation() {
final value = textController.value.text;
final isValid = value.isNotEmpty;
// accessing the validation notifier
final notifier = context.read<ValidationNotifier>();
// updating the field validation state
notifier.addFieldValidationState(widget.fieldName, isValid);
}

We access the validation notifier by reading it via context

    // accessing the validation notifier 
final notifier = context.read<ValidationNotifier>();
// updating the field validation state
notifier.addFieldValidationState(widget.fieldName, isValid);

And this perform validation is done in the ontextChanged function callback as

onTextChanged(value) {
// debouncing logic of 500ms
_textDebounce.run(() {
performValidation();
});
}

Now we create our validation notifier which extends ChangeNotifier as follows

class ValidationNotifier extends ChangeNotifier {
...
}

It was mentioned earlier that every field register itself with validation notifier and then keep updating its status.

class ValidationNotifier extends ChangeNotifier {

final List<FieldData> _fieldData = [];

// function to register and update validation status for each field
addFieldValidationState(String fieldKey, bool isValid) {
if(!_fieldData.any((element) => element.field == fieldKey)) {
_fieldData.add(FieldData(field: fieldKey, isValid: isValid));
return;
}
final index = _fieldData.indexWhere((item) => item.field == fieldKey);
if (index > -1) {
_fieldData[index] = FieldData(field: fieldKey, isValid: isValid);
}
// notifying the consumers
notifyListeners();
}
}

Now we’re maintaining a single variable for our action button which either tells if any field is invalid or not as follows

class ValidationNotifier extends ChangeNotifier {
...
// single variable for our action button
bool get isAnyFieldInvalid => _fieldData.isEmpty || _fieldData.any((element) => !element.isValid);
...
}

Finally we create our form screen which is simply a stateless widget as follows

@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width * 0.5;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
InputField(fieldName: 'field1', width: screenWidth),
InputField(fieldName: 'field2', width: screenWidth),
InputField(fieldName: 'field3', width: screenWidth),
InputField(fieldName: 'field4', width: screenWidth),
const SizedBox(
height: 16,
),
// validation notifier consumer
Consumer<ValidationNotifier>(builder: (context, notifier, child) {
return ElevatedButton(
onPressed: () {
if(!notifier.isAnyFieldInvalid) {
showDialog(
context: context,
builder: (context) => const AlertDialog(
title: Text('Success', textAlign: TextAlign.center),
content: Text(
'All fields validated',
style: TextStyle(
fontSize: 18,
color: Colors.black,
),
),
));
}
},
style: ButtonStyle(
backgroundColor: MaterialStateColor.resolveWith((states) =>
notifier.isAnyFieldInvalid ? Colors.grey : Colors.green)),
child: Text(
'Click me',
style: TextStyle(
fontSize: 16,
color: notifier.isAnyFieldInvalid
? Colors.white
: Colors.black),
));
})
],
);
}

The catch here is that we make our action button ‘Click me’ a consumer of the validation notifier.

backgroundColor: MaterialStateColor.resolveWith((states) =>
// accessing the status field via notifier object
notifier.isAnyFieldInvalid ? Colors.grey : Colors.green)

Bamn! And finally we’re able to update our action button very easily with so many validations and fields. We can even add more custom fields and validations but the flow for updating the action button remains the same.

This is one way I found quite useful in my projects. It is clean and simple. You can try it out too and let me know what do you think.

That is all for now! Stay tuned!

Connect with me (if the content is helpful to you ) on

Until next time…

Cheers!

--

--

App Developer (Native & Flutter) | Mentor | Writer | Youtuber @_zaqua | Traveller | Content Author & Course Instructor @Droidcon | Frontend Lead @MahilaMoney