Flutter Form Validation Using Flutter Bloc with Freezed in Domain Driven Design Architecture
Tags: flutter, flutterbloc, flutterformvalidation, blocpattern
Published: 2023-12-06 14:24:35

## Introduction
In Flutter development, managing form validation is a crucial aspect of building robust applications. When combined with the Flutter Bloc pattern and the Freezed package, it becomes a powerful trio that helps organize, validate, and handle form-related logic efficiently. In this article, we’ll explore how to integrate Flutter Bloc, Freezed annotation, and Domain-Driven Design (DDD) principles to create a clean and maintainable form validation solution in a Flutter application.

## NB:
Before diving into the article, I apologize for its length. However, there is no other way to fully explain this topic. At the end of the article, I provide a GitHub repository for reference. Let’s dive in!

## Understanding the Components
### Flutter Bloc
Flutter Bloc is a state management library that helps developers separate the business logic from the UI layer. It follows the BLoC (Business Logic Component) pattern, where the logic is encapsulated in separate classes and managed by a central authority. This separation enhances code maintainability, testability, and scalability.

### Freezed Annotation
Freezed is a code generation library for Dart and Flutter that simplifies the process of creating immutable classes. It utilizes code generation to create boilerplate code for you, reducing redundancy and ensuring immutability, which is crucial for maintaining a predictable state in your application.

### Domain-Driven Design (DDD)
DDD is a software design approach that focuses on aligning software design with the business domain. It emphasizes clear separation of concerns, modularization, and a common language between technical and non-technical team members. In the context of form validation, DDD helps organize the validation logic within the domain layer, making it more cohesive and maintainable.

## Setting Up the Project
To get started, create a new Flutter project and add the required dependencies to your pubspec.yaml file:

```yaml
dependencies:
  dartz: ^0.10.1
  flutter:
    sdk: flutter
  flutter_bloc: ^8.1.3
  freezed_annotation: ^2.4.1

dev_dependencies:
  build_runner: ^2.4.6
  flutter_lints: ^2.0.0
  flutter_test:
    sdk: flutter
  freezed: ^2.4.5
```
Run `flutter pub get` to install the dependencies.

## Defining the Domain
In DDD, the domain layer holds the business logic of your application. Create a `domain` directory in your project, and within it, define a file named `value_failure.dart`:

```dart
// domain/value_failure.dart

import 'package:freezed_annotation/freezed_annotation.dart';

part 'value_failure.freezed.dart';

@freezed
class ValueFailure<T> with _$ValueFailure<T> {
  const factory ValueFailure.empty({
    required T failedValue,
  }) = _Empty<T>;
  const factory ValueFailure.invalid({
    required T failedValue,
  }) = _Invalid<T>;
}
```

This `value_failure.dart` file defines the possible validation failures using the freezed annotation.

Within the `domain` directory define another file named `value_errors.dart`:

```dart
// domain/value_errors.dart

import 'value_failure.dart';

class UnexpectedValueError extends Error {
  final ValueFailure valueFailure;
  UnexpectedValueError(this.valueFailure);

  @override
  String toString() {
    const errorMessage =
        'Encountered a ValueFailure at an unrecoverable point. Terminating.';
    return Error.safeToString('$errorMessage Failure was: $valueFailure');
  }
}
```

This `value_errors.dart` file defines the errors that occur during validation.

Within the `domain` directory define another file named `value_object.dart`:

```dart
// domain/value_object.dart

import 'package:dartz/dartz.dart';
import 'value_errors.dart';
import 'value_failure.dart';

abstract class ValueObject<T> {
  const ValueObject();
  Either<ValueFailure<T>, T> get value;

  bool isValid() => value.isRight();
  bool isInvalid() => value.isLeft();

  T getOrCrash() => value.fold((f) => throw UnexpectedValueError(f), id);

  Either<ValueFailure<dynamic>, Unit> get failureOrUnit {
    return value.fold((l) => left(l), (_) => right(unit));
  }

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;

    return other is ValueObject<T> && other.value == value;
  }

  @override
  int get hashCode => value.hashCode;

  @override
  String toString() => 'ValueObject(value: $value)';
}
```

This `value_object.dart` file establishes the base model for validating values.

Within the `domain` directory define another file named `value_validators.dart`:

```dart
// domain/value_validators.dart

import 'package:dartz/dartz.dart';
import 'value_failure.dart';

Either<ValueFailure<String>, String> validateStringNotEmpty(String input) {
  if (input.isNotEmpty) {
    return right(input);
  }
  return left(ValueFailure.empty(failedValue: input));
}

Either<ValueFailure<String>, String> validateEmailAddress(String input) {
  const emailRegex =
      r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';
  if (RegExp(emailRegex).hasMatch(input)) {
    return right(input);
  }
  return left(ValueFailure.invalid(failedValue: input));
}

Either<ValueFailure<String>, String> validatePassWord(String input) {
  const passwordRegex =
      r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{6,}$';
  if (RegExp(passwordRegex).hasMatch(input)) {
    return right(input);
  }
  return left(ValueFailure.invalid(failedValue: input));
}
```

This `value_validators.dart` file defines functions for validating value objects.

Within the `domain` directory define another file named `value_objects.dart`:

```dart
// domain/value_objects.dart

import 'package:dartz/dartz.dart';
import 'value_failure.dart';
import 'value_object.dart';
import 'value_validators.dart';

class EmailAddress extends ValueObject<String> {
  @override
  final Either<ValueFailure<String>, String> value;
  factory EmailAddress(String input) {
    return EmailAddress._(
      validateStringNotEmpty(input).flatMap(
        validateEmailAddress,
      ),
    );
  }
  const EmailAddress._(this.value);
}

class Password extends ValueObject<String> {
  @override
  final Either<ValueFailure<String>, String> value;
  factory Password(String input) {
    return Password._(
      validateStringNotEmpty(input).flatMap(
        validatePassWord,
      ),
    );
  }
  const Password._(this.value);
}
```

This `value_objects.dart` file defines models that inherit the base model `ValueObject` for validating values.

### Creating credentials model
We are planning to validate a sign-in form, so create a directory named `auth` within the `domain` directory.

Within the `auth` directory define file named `credentials.dart`:

```dart
// domain/auth/credentials.dart

import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../value_failure.dart';
import '../value_objects.dart';

part 'credentials.freezed.dart';

@freezed
class Credentials with _$Credentials {
  const Credentials._();

  const factory Credentials({
    required EmailAddress emailAddress,
    required Password password,
  }) = _Credentials;

  factory Credentials.empty() {
    return Credentials(
      emailAddress: EmailAddress(''),
      password: Password(''),
    );
  }

  Option<ValueFailure<dynamic>> get failureOption => emailAddress.failureOrUnit
      .andThen(password.failureOrUnit)
      .fold((f) => some(f), (_) => none());
}
```

This `credentials.dart` file defines the model for managing credentials within the state, using the freezed annotation.

Within the `auth` directory define another file named `auth_failure.dart`:

```dart
import 'package:freezed_annotation/freezed_annotation.dart';

part 'auth_failure.freezed.dart';

@freezed
class AuthFailure with _$AuthFailure {
  const factory AuthFailure.validationFailure() = _ValidationFailure;
}
```

The file `auth_failure.dart` utilizes the freezed annotation to define potential failures that may occur during the authentication process.

## Creating the SigninForm Bloc
Now, create a `bloc` that will handle the form-related logic. In the `lib` directory, create a folder named `application`, the `application` layer holds all the bloc’s. Within the `application` folder create a sub folder named `signin_form` and within it define a file named `signin_form_state.dart`:

```dart
// application/signin_form/signin_form_state.dart

part of 'signin_form_bloc.dart';

@freezed
class SigninFormState with _$SigninFormState {
  const factory SigninFormState({
    required bool isSubmitting,
    required bool hidePassword,
    required bool showValidationError,
    required Credentials credentials,
    required Option<Either<AuthFailure, Unit>> failureOrSuccessOption,
  }) = _SigninFormState;

  factory SigninFormState.initial() {
    return SigninFormState(
      isSubmitting: false,
      hidePassword: true,
      showValidationError: false,
      credentials: Credentials.empty(),
      failureOrSuccessOption: none(),
    );
  }
}
```

Within the `signin_form` folder define another file named `signin_form_event.dart`:

```dart
part of 'signin_form_bloc.dart';

@freezed
class SigninFormEvent with _$SigninFormEvent {
  const factory SigninFormEvent.emailAddressChanged({
    required String emailAddress,
  }) = _EmailChanged;
  const factory SigninFormEvent.passwordChanged({
    required String password,
  }) = _PasswordChanged;
  const factory SigninFormEvent.obscureTextChanged() = _ObscureTextChanged;
  const factory SigninFormEvent.signinButtonPressed() = _SigninButtonPressed;
}
```

Within the `signin_form` folder define another file named `signin_form_bloc.dart`:

```dart
import 'package:dartz/dartz.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../domain/auth/auth_failure.dart';
import '../../domain/auth/credentials.dart';
import '../../domain/core/value_objects.dart';

part 'signin_form_event.dart';
part 'signin_form_state.dart';
part 'signin_form_bloc.freezed.dart';

class SigninFormBloc extends Bloc<SigninFormEvent, SigninFormState> {
  SigninFormBloc() : super(SigninFormState.initial()) {
    on<SigninFormEvent>((event, emit) async {
      await event.map(
        emailAddressChanged: (e) async => emit(state.copyWith(
          credentials: state.credentials.copyWith(
            emailAddress: EmailAddress(e.emailAddress),
          ),
          failureOrSuccessOption: none(),
        )),
        passwordChanged: (e) async => emit(state.copyWith(
          credentials: state.credentials.copyWith(
            password: Password(e.password),
          ),
          failureOrSuccessOption: none(),
        )),
        obscureTextChanged: (_) async => emit(state.copyWith(
          hidePassword: !state.hidePassword,
        )),
        signinButtonPressed: (_) async {
          emit(state.copyWith(
            isSubmitting: true,
            failureOrSuccessOption: none(),
          ));

          await Future.delayed(const Duration(seconds: 1));

          if (state.credentials.failureOption.isNone()) {
            return emit(state.copyWith(
              isSubmitting: false,
              failureOrSuccessOption: some(right(unit)),
            ));
          }

          emit(state.copyWith(
            isSubmitting: false,
            showValidationError: true,
            failureOrSuccessOption: some(left(
              const AuthFailure.validationFailure(),
            )),
          ));
        },
      );
    });
  }
}
```

This bloc contains the form state and methods to handle changes in email and password fields. The `signinButtonPressed` event performs the validation logic and updates the state accordingly.

### Now simply run the generator
Use the [watch] flag to watch the files’ system for edits and rebuild as necessary.

```bash
flutter pub run build_runner watch --delete-conflicting-outputs
```

if you want the generator to run one time and exit, use

```bash
flutter pub run build_runner build
```

## Integrating the SigninFormBloc with the UI
Now inside the `lib` directory create a folder named `presentation`, the `presentation` layer holds all the UI elements.

Create the following files in the `presentation` directory for the UI implementation:

![form validation UI sample screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hziq4dlxg7lun0u8p18j.jpg)

#### 1. presentation/screens/signin_screen.dart

```dart
// presentation/screens/signin_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../application/signin_form/signin_form_bloc.dart';
import 'widgets/signin_form.dart';

class SigninScreen extends StatelessWidget {
  const SigninScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => SigninFormBloc(),
      child: BlocListener<SigninFormBloc, SigninFormState>(
        listenWhen: (p, c) =>
            p.failureOrSuccessOption != c.failureOrSuccessOption,
        listener: (context, state) {
          state.failureOrSuccessOption.fold(
            () => null,
            (either) => either.fold(
              (f) {
                // Handle failure
              },
              (_) {
                // Handle success
              },
            ),
          );
        },
        child: Scaffold(
          appBar: AppBar(
            title: const Text('SIGN IN'),
          ),
          body: Center(
            child: SizedBox(
              height: 450,
              width: MediaQuery.of(context).size.width - 32,
              child: const Card(
                child: Padding(
                  padding: EdgeInsets.all(16),
                  child: SigninForm(),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}
```

#### 2. presentation/screens/widgets/signin_form.dart

```dart
// presentation/screens/widgets/signin_form.dart

import 'package:flutter/material.dart';
import 'email_input_field.dart';
import 'password_input_field.dart';
import 'submit_button.dart';

class SigninForm extends StatelessWidget {
  const SigninForm({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(
          'Sign in',
          style: Theme.of(context).textTheme.headlineLarge?.copyWith(
                fontWeight: FontWeight.bold,
                color: Colors.deepPurple,
              ),
        ),
        const EmailInputField(),
        const PasswordInputField(),
        const SubmitButton()
      ],
    );
  }
}
```

#### 3. presentation/screens/widgets/email_input_field.dart

```dart
// presentation/screens/widgets/email_input_field.dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../application/signin_form/signin_form_bloc.dart';

class EmailInputField extends StatelessWidget {
  const EmailInputField({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    final controller = TextEditingController();
    return BlocConsumer<SigninFormBloc, SigninFormState>(
      listenWhen: (p, c) => p.showValidationError != c.showValidationError,
      listener: (context, state) {
        controller.text =
            state.credentials.emailAddress.value.getOrElse(() => '');
      },
      builder: (context, state) {
        return TextFormField(
          controller: controller,
          autovalidateMode: state.showValidationError
              ? AutovalidateMode.always
              : AutovalidateMode.disabled,
          decoration: InputDecoration(
            border: OutlineInputBorder(
              borderSide: const BorderSide(color: Colors.deepPurple),
              borderRadius: BorderRadius.circular(10),
            ),
            hintText: 'Type email address',
            labelText: 'Email address',
            prefixIcon: const Icon(
              Icons.email,
              color: Colors.deepPurple,
            ),
          ),
          onChanged: (email) => context
              .read<SigninFormBloc>()
              .add(SigninFormEvent.emailAddressChanged(emailAddress: email)),
          validator: (_) => context
              .read<SigninFormBloc>()
              .state
              .credentials
              .emailAddress
              .value
              .fold(
                (l) => l.map(
                  empty: (_) => "Email address can't be empty.",
                  invalid: (_) => "Invalid email address.",
                ),
                (_) => null,
              ),
        );
      },
    );
  }
}
```

#### 4. presentation/screens/widgets/password_input_field.dart

```dart
// presentation/screens/widgets/password_input_field.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../application/signin_form/signin_form_bloc.dart';

class PasswordInputField extends StatelessWidget {
  const PasswordInputField({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    final controller = TextEditingController();
    return BlocConsumer<SigninFormBloc, SigninFormState>(
      listenWhen: (p, c) => p.showValidationError != c.showValidationError,
      listener: (context, state) {
        controller.text = state.credentials.password.value.getOrElse(() => '');
      },
      builder: (context, state) {
        return TextFormField(
          controller: controller,
          autovalidateMode: state.showValidationError
              ? AutovalidateMode.always
              : AutovalidateMode.disabled,
          decoration: InputDecoration(
            errorMaxLines: 3,
            border: OutlineInputBorder(
              borderSide: const BorderSide(color: Colors.deepPurple),
              borderRadius: BorderRadius.circular(10),
            ),
            hintText: 'Type password',
            labelText: 'Password',
            prefixIcon: const Icon(
              Icons.lock,
              color: Colors.deepPurple,
            ),
            suffixIcon: IconButton(
              onPressed: () => context
                  .read<SigninFormBloc>()
                  .add(const SigninFormEvent.obscureTextChanged()),
              icon: Icon(
                state.hidePassword
                    ? CupertinoIcons.eye
                    : CupertinoIcons.eye_slash,
                color: Colors.deepPurple,
              ),
            ),
          ),
          obscureText: state.hidePassword,
          onChanged: (password) => context
              .read<SigninFormBloc>()
              .add(SigninFormEvent.passwordChanged(password: password)),
          validator: (_) => context
              .read<SigninFormBloc>()
              .state
              .credentials
              .password
              .value
              .fold(
                (l) => l.map(
                  empty: (_) => "Password can't be empty",
                  invalid: (_) =>
                      'Password must be minimum six characters, at least one uppercase letter, one lowercase letter, one number and one special character',
                ),
                (r) => null,
              ),
        );
      },
    );
  }
}
```

#### 5. presentation/screens/widgets/submit_button.dart

```dart
// presentation/screens/widgets/submit_button.dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../application/signin_form/signin_form_bloc.dart';

class SubmitButton extends StatelessWidget {
  const SubmitButton({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<SigninFormBloc, SigninFormState>(
      builder: (context, state) {
        return SizedBox(
          width: double.infinity,
          height: 50,
          child: ElevatedButton(
            style: ButtonStyle(
              backgroundColor: const MaterialStatePropertyAll<Color>(
                Colors.deepPurple,
              ),
              shape: MaterialStatePropertyAll<RoundedRectangleBorder>(
                RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
              ),
            ),
            onPressed: () => context
                .read<SigninFormBloc>()
                .add(const SigninFormEvent.signinButtonPressed()),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                state.isSubmitting
                    ? const SizedBox(
                        height: 16,
                        width: 16,
                        child: CircularProgressIndicator(
                          color: Colors.white,
                        ),
                      )
                    : const Icon(
                        Icons.login,
                        color: Colors.white,
                      ),
                Text(
                  state.isSubmitting ? 'Signing in...' : 'Sign in',
                  style: Theme.of(context).textTheme.bodyLarge?.copyWith(
                        color: Colors.white,
                        fontWeight: FontWeight.bold,
                      ),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}
```

We use the `BlocProvider` to provide the `SigninFormBloc` to the widget tree and `BlocConsumer` to listen to and rebuild the UI based on state changes.

## Conclusion
In this article, we explored how to leverage Flutter Bloc and Freezed annotation to implement form validation in a Flutter application following Domain-Driven Design principles. This approach enhances code organization, readability, and maintainability. As your application grows, the separation of concerns provided by these tools will become even more valuable, allowing you to easily extend and modify your form validation logic. Consider exploring additional features, such as dependency injection and testing, to further enhance the robustness of your Flutter application.

> Here’s a link to a sample project repository for a more concrete demonstration of the topic discussed in this article: [Github Repo ➡️](https://github.com/vishnucprasad/flutter_form_validation)