Flutter Form Validation with BLoC

Azhar Ali
11 min readMar 22, 2023

--

Welcome to this article where we’ll explore some fundamental concepts such as understanding the bloc and cubit architecture in Flutter. Additionally, we’ll explore the use of selectors and how mixins can aid in the validation process.

Table of content

What is Bloc and Cubit?

How we can use Selector?

What is Mixin and how we can use it with Form validations?

Implementation

Conclusion

GitHub Link

What are Bloc and Cubit?

In Flutter, the BLoC (Business Logic Component) pattern is a popular state management approach that separates the business logic from the presentation layer. It is a design pattern that helps to manage state and business logic reactively and cleanly.

BLoC

BLoC is a class that is responsible for managing the state of a specific part of an application. It contains business logic, and its primary purpose is to receive events, process them, and emit new states. A BLoC can be used to manage the state of a single widget or a group of widgets, depending on the complexity of the application.

Cubit

Cubit, on the other hand, is a simpler implementation of the BLoC pattern. It is a class that contains business logic and emits new states without events, just like a BLoC. However, it is much simpler and has fewer responsibilities than a BLoC.

Selector

A selector is a function in the Flutter library that takes a part of the application state and returns a derived value. It is used to compute the derived state from the existing state in a reactive way. Selectors are commonly used with the BLoC pattern to provide a simplified interface to the application state.

Implementation

To enable state management, we must first add the dependency to the pubspec.yaml file to obtain all the necessary properties of the bloc.

dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.2

Create a Cubit FormValidator which will generate two files by default one for logic and the other one is state class

FormValidatorCubit.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

part 'form_validator_state.dart';

class FormValidatorCubit extends Cubit<FormValidatorState> {
FormValidatorCubit() : super(const FormValidatorUpdate());

void initForm({
String email = '',
String name = '',
String address = '',
String city = '',
}) {
emit(state.copyWith(
email: email,
name: name,
address: address,
city: city,
));
}

void updateEmail(String? email) {
emit(state.copyWith(email: email));
}

void updatePassword(String? password) {
emit(state.copyWith(password: password));
}

void updateConfirmPassword(String? confirmPassword) {
emit(state.copyWith(confirmPassword: confirmPassword));
}

void updateName(String? name) {
emit(state.copyWith(name: name));
}

void updateAddress(String? address) {
emit(state.copyWith(address: address));
}

void updateCity(String? city) {
emit(state.copyWith(city: city));
}

void updateAutovalidateMode(AutovalidateMode? autovalidateMode) {
emit(state.copyWith(autovalidateMode: autovalidateMode));
}

void toggleObscureText() {
emit(state.copyWith(obscureText: !state.obscureText));
}

void reset() {
emit(const FormValidatorUpdate());
}
}

FormValidatorState.dart

part of 'form_validator_cubit.dart';

@immutable
abstract class FormValidatorState {
final AutovalidateMode autovalidateMode;
final String email;
final String password;
final String confirmPassword;
final String name;
final String address;
final String city;
final bool obscureText;

const FormValidatorState({
this.autovalidateMode = AutovalidateMode.disabled,
this.email = '',
this.password = '',
this.confirmPassword = '',
this.name = '',
this.address = '',
this.city = '',
this.obscureText = true,
});

FormValidatorState copyWith({
AutovalidateMode? autovalidateMode,
String? email,
String? password,
String? confirmPassword,
String? name,
String? address,
String? city,
bool? obscureText,
});
}

class FormValidatorUpdate extends FormValidatorState {

const FormValidatorUpdate({
AutovalidateMode autovalidateMode = AutovalidateMode.disabled,
String email = '',
String password = '',
String confirmPassword = '',
String name = '',
String address = '',
String city = '',
bool obscureText = true,
}) : super(
autovalidateMode: autovalidateMode,
email: email,
password: password,
confirmPassword: confirmPassword,
name: name,
address: address,
city: city,
obscureText: obscureText,
);

@override
FormValidatorUpdate copyWith({
AutovalidateMode? autovalidateMode,
String? email,
String? password,
String? confirmPassword,
String? name,
String? address,
String? city,
bool? obscureText,
}) {
return FormValidatorUpdate(
autovalidateMode: autovalidateMode ?? this.autovalidateMode,
email: email ?? this.email,
password: password ?? this.password,
name: name ?? this.name,
address: address ?? this.address,
city: city ?? this.city,
obscureText: obscureText ?? this.obscureText,
);
}
}

To cater to our specific needs, we will employ the copyWith method as an abstract method of the abstract state class and implement it in all of the child classes.

What is copyWith and how it can be helpful?

In Flutter, the copyWith the method is a commonly used method that is used to create a new instance of a class with updated properties. This method is used in situations where we need to create a new object with some properties of an existing object, but with some changes in the values of those properties.

The copyWith the method is defined in a class as a way to return a new instance of that class with some of the properties modified. The method takes in new values for the properties that need to be updated and returns a new instance of the same class with those properties updated.

One of the main benefits of using the copyWith the method is that it allows us to create new objects that are very similar to the original objects, without having to manually copy over all the properties. This is especially useful when working with immutable objects, where we cannot modify the properties directly.

What is the mixin and how it can be used?

In Flutter, a mixin is a class that contains methods that can be used by other classes without having to be the parent class of those classes. Mixins allow developers to reuse code across multiple class hierarchies without having to create duplicate code or inheritance relationships. They are similar to interfaces in other programming languages but can also contain implementation code. Mixins can be added to a class using the with keyword.

Create a mixin with Validator.dart which will contain all logic of validations.

Validator.dart

mixin Validator {
// Email validation
String? validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Email is required';
} else if (!RegExp(r'^.+@[a-zA-Z]+\.{1}[a-zA-Z]+(\.{0,1}[a-zA-Z]+)$').hasMatch(value)) {
return 'Please enter a valid email';
}
return null;
}

// Password validation
String? validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Password is required';
} else if (value.length < 6) {
return 'Password must be at least 6 characters';
}
return null;
}

// Confirm password validation
String? validateConfirmPassword(String? value, String password) {
if (value == null || value.isEmpty) {
return 'Confirm password is required';
} else if (value != password) {
return 'Confirm password does not match';
}
return null;
}

// Name validation
String? validateName(String? value) {
if (value == null || value.isEmpty) {
return 'Name is required';
}
return null;
}

// Address validation
String? validateAddress(String? value) {
if (value == null || value.isEmpty) {
return 'Address is required';
}
return null;
}

// City validation
String? validateCity(String? value) {
if (value == null || value.isEmpty) {
return 'City is required';
}
return null;
}
}

FormValidationPage.dart

import 'package:flutter/material.dart';
import 'package:flutter_app/bloc/bloc/form_validator/form_validator_cubit.dart';
import 'package:flutter_app/bloc/domain/validator.dart';
import 'package:flutter_app/bloc/pages/edit_form_page.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

// you can see we've added our Validator mixin
// and now we can access all it's method directly
class FormValidationPage extends StatelessWidget with Validator {
FormValidationPage({Key? key}) : super(key: key);

final FormValidatorCubit _formValidatorCubit = FormValidatorCubit();

final _formKey = GlobalKey<FormState>();

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Form Validation'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
BlocSelector<FormValidatorCubit, FormValidatorState, AutovalidateMode>(
bloc: _formValidatorCubit,
selector: (state) => state.autovalidateMode,
builder: (context, AutovalidateMode autovalidateMode) {
return Form(
key: _formKey,
autovalidateMode: autovalidateMode,
child: Column(
children: [
TextFormField(
// mixin method
validator: validateName,
onChanged: _formValidatorCubit.updateName,
decoration: const InputDecoration(
labelText: 'Name',
hintText: 'Enter your name',
prefixIcon: Icon(Icons.lock),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8.0),
TextFormField(
validator: validateEmail,
onChanged: _formValidatorCubit.updateEmail,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
hintText: 'Enter your email',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8.0),
BlocSelector<FormValidatorCubit, FormValidatorState, bool>(
bloc: _formValidatorCubit,
selector: (state) => state.obscureText,
builder: (context, obscureText) {
return Column(
children: [
TextFormField(
validator: validatePassword,
onChanged: _formValidatorCubit.updatePassword,
obscureText: obscureText,
keyboardType: TextInputType.visiblePassword,
decoration: InputDecoration(
labelText: 'Password',
hintText: 'Enter your password',
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(onPressed: _formValidatorCubit.toggleObscureText, icon: const Icon(Icons.remove_red_eye)),
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 8.0),
TextFormField(
validator: (value) =>
validateConfirmPassword(
value,
_formValidatorCubit.state.password,
),
obscureText: obscureText,
keyboardType: TextInputType.visiblePassword,
onChanged: _formValidatorCubit.updateConfirmPassword,
decoration: const InputDecoration(
labelText: 'Confirm password',
hintText: 'Enter your confirm password',
prefixIcon: Icon(Icons.lock),
border: OutlineInputBorder(),
),
),
],
);
},
),
const SizedBox(height: 8.0),
TextFormField(
validator: validateAddress,
onChanged: _formValidatorCubit.updateAddress,
decoration: const InputDecoration(
labelText: 'Address',
hintText: 'Enter your address',
prefixIcon: Icon(Icons.home),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8.0),
TextFormField(
validator: validateCity,
onChanged: _formValidatorCubit.updateAddress,
decoration: const InputDecoration(
labelText: 'City',
hintText: 'Enter your city',
prefixIcon: Icon(Icons.home),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8.0),
],
),
);
},
),
const SizedBox(height: 8.0),
SizedBox(
width: double.infinity,
height: 48.0,
child: ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// Do your job here
} else {
// in case a user has submitted invalid form we'll set
// AutovalidateMode.always which will rebuild the form
// in result we'll start getting error message
_formValidatorCubit.updateAutovalidateMode(AutovalidateMode.always);
}
},
child: const Text('Submit'),
),
),
const SizedBox(height: 8.0),
SizedBox(
width: double.infinity,
height: 48.0,
child: ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => EditFormPage(),
),
);
},
child: const Text('Edit Form'),
),
),
],
),
),
);
}
}

When using a selector, we can enclose the Form widget with it and set the selector type to AutovalidateMode. Although we have the option to use BlocBuilder, it can be costly as it rebuilds the entire form widget every time a TextFormField is changed.

Display error messages for invalid form submissions.

return BlocSelector<FormValidatorCubit, FormValidatorState, AutovalidateMode>(
bloc: _formValidatorCubit,
selector: (state) => state.autovalidateMode,
builder: (context, AutovalidateMode autovalidateMode) {
return Form(
key: _formKey,
autovalidateMode: autovalidateMode,
child: Column(
children: [
// child widgets
],
),
);
});

The BlocSelector will only be constructed if the AutovalidateMode status changes in the state. Otherwise, it will ignore any modifications to the state or its properties, such as email, password, confirm password, and others.

To prevent error messages from appearing upon user interaction at the very start, we will set AutovalidateMode it to disabled at the outset. Once the user submits the form, we will display the validator’s error message to enter valid data. It is possible to remove the selector by setting AutovalidateMode.disabled as the default but it would be good for user experience.

We wanted to implement a show/hide password feature for the password field. The BlocSelector method is an excellent approach for this. We can simply wrap our widget around it and define its type as a boolean. When the eye icon is pressed, we can change the boolean value to enable the show/hide password functionality.

Show hide password functionality

BlocSelector<FormValidatorCubit, FormValidatorState, bool>(
bloc: _formValidatorCubit,
selector: (state) => state.obscureText,
builder: (context, obscureText) {
return Column(
children: [
TextFormField(
validator: validatePassword,
onChanged: _formValidatorCubit.updatePassword,
obscureText: obscureText,
keyboardType: TextInputType.visiblePassword,
decoration: InputDecoration(
labelText: 'Password',
hintText: 'Enter your password',
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(onPressed: _formValidatorCubit.toggleObscureText, icon: const Icon(Icons.remove_red_eye)),
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 8.0),
TextFormField(
validator: (value) => validateConfirmPassword(
value,
_formValidatorCubit.state.password,
),
obscureText: obscureText,
keyboardType: TextInputType.visiblePassword,
onChanged: _formValidatorCubit.updateConfirmPassword,
decoration: const InputDecoration(
labelText: 'Confirm password',
hintText: 'Enter your confirm password',
prefixIcon: Icon(Icons.lock),
border: OutlineInputBorder(),
),
),
],
);
},
)

Update Form Scenario

When working with Form validation, we must consider the scenario of updates. For instance, if a user is creating a profile in an app, we must also provide the functionality to update their profile.

To achieve this, we want to initialize the form values as auto-fill at the outset. In this case, Bloc can be quite useful. We can introduce a function in FormValidatorCubit that initializes the value of the State variables.

class FormValidatorCubit extends Cubit<FormValidatorState> {
FormValidatorCubit() : super(const FormValidatorUpdate());

// we just need to call this function on
// update profile page with default value

void initForm({
String email = '',
String name = '',
String address = '',
String city = '',
}) {
emit(state.copyWith(
email: email,
name: name,
address: address,
city: city,
));
}

// other help functions
// ...
}

EditFormPage.dart

import 'package:flutter/material.dart';
import 'package:flutter_app/bloc/bloc/form_validator/form_validator_cubit.dart';
import 'package:flutter_app/bloc/domain/validator.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class EditFormPage extends StatelessWidget with Validator {
EditFormPage({Key? key}) : super(key: key);

// initalizing value with our default data
final FormValidatorCubit _formValidatorCubit = FormValidatorCubit()..initForm(
name: 'Joseph Kamal',
email: 'josephkamal@gmail.com',
address: '1234 Main Street',
city: 'New York',
);

final _formKey = GlobalKey<FormState>();

@override
Widget build(BuildContext context) {

return Scaffold(
appBar: AppBar(
title: const Text('Update Form'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
BlocSelector<FormValidatorCubit, FormValidatorState, AutovalidateMode>(
bloc: _formValidatorCubit,
selector: (state) => state.autovalidateMode,
builder: (context, AutovalidateMode autovalidateMode) {
return Form(
key: _formKey,
autovalidateMode: autovalidateMode,
child: Column(
children: [
TextFormField(
initialValue: _formValidatorCubit.state.name,
validator: validateName,
onChanged: _formValidatorCubit.updateName,
decoration: const InputDecoration(
labelText: 'Name',
hintText: 'Enter your name',
prefixIcon: Icon(Icons.lock),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8.0),
TextFormField(
initialValue: _formValidatorCubit.state.email,
validator: validateEmail,
onChanged: _formValidatorCubit.updateEmail,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
hintText: 'Enter your email',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8.0),
TextFormField(
initialValue: _formValidatorCubit.state.address,
validator: validateAddress,
onChanged: _formValidatorCubit.updateAddress,
decoration: const InputDecoration(
labelText: 'Address',
hintText: 'Enter your address',
prefixIcon: Icon(Icons.home),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8.0),
TextFormField(
initialValue: _formValidatorCubit.state.city,
validator: validateCity,
onChanged: _formValidatorCubit.updateAddress,
decoration: const InputDecoration(
labelText: 'City',
hintText: 'Enter your city',
prefixIcon: Icon(Icons.home),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8.0),
],
),
);
},
),
const SizedBox(height: 8.0),
SizedBox(
width: double.infinity,
height: 48.0,
child: ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// Do your job here
} else {
//
_formValidatorCubit.updateAutovalidateMode(AutovalidateMode.always);
}
},
child: const Text('Update'),
),
),
],
),
),
);
}
}

Simply add a function call at the beginning of the Edit page. If you’re using a Stateful Widget, you can include it in the initState method as well. The cubit will be initialized with some values that can be used when rendering your UI.

class EditFormPage extends StatelessWidget with Validator {
EditFormPage({Key? key}) : super(key: key);

// initalizing value with our default data
final FormValidatorCubit _formValidatorCubit = FormValidatorCubit()..initForm(
name: 'Joseph Kamal',
email: 'josephkamal@gmail.com',
address: '1234 Main Street',
city: 'New York',
);

final _formKey = GlobalKey<FormState>();

@override
Widget build(BuildContext context) {
return YourWidget();
}

This code snippet will initialize your Cubit with default values. To set the TextFormField’s initial value to a default value, simply use the initialValue parameter. as mentioned below code snippet.


TextFormField(
initialValue: _formValidatorCubit.state.name,
validator: validateName,
onChanged: _formValidatorCubit.updateName,
decoration: const InputDecoration(
labelText: 'Name',
hintText: 'Enter your name',
prefixIcon: Icon(Icons.lock),
border: OutlineInputBorder(),
),
),

// set initial value after getting from cubit state
// which as initialaze in the beginning
initialValue: _formValidatorCubit.state.name,

Conclusion

In conclusion, Bloc and Selector are powerful tools for implementing form validation and updating scenarios in your app. By using Bloc, you can easily manage the state of your form and ensure that it is always in a valid state. Meanwhile, Selector allows you to selectively rebuild only the parts of your widget tree that have changed, which can significantly improve performance and reduce unnecessary rebuilding.

By combining these two approaches, you can create robust and efficient forms that provide a smooth user experience. Whether you are building a simple contact form or a complex multi-step form, Bloc and Selector can help you get the job done with ease.

Remember to always consider your user’s needs and keep your forms simple and intuitive. With the right tools and a thoughtful approach, you can create forms that your users will love and that will help you achieve your app’s goals.

Thank you for taking the time to read this article. If you have any feedback or corrections, please let me know in the comments below. I am always looking for ways to improve my writing and provide better content for my readers.

If you found this article helpful, please show your support by giving it a round of claps. Your feedback and support motivate me to continue creating valuable content.

GitHub Link:

You can check and clone the source code here

Find the source code of the Validation Using Bloc In Flutter:

--

--