• Flutter Times
  • Posts
  • Dartz with Bloc State Management in Flutter: A Clean Architecture Guide

Dartz with Bloc State Management in Flutter: A Clean Architecture Guide

Flutter is an exceptional framework for building modern mobile applications, but scaling complex apps requires structured design patterns. One of the popular ways to achieve scalability and maintainability is through Clean Architecture. Combined with Dartz (a functional programming library) and the Bloc pattern for state management, you can build highly modular and robust Flutter applications.In this article, we will explore how Dartz, Bloc, and Clean Architecture can be integrated to manage state effectively and enforce clean coding principles in your Flutter app development.

Why Dartz?

Dartz is a functional programming package in Dart that introduces functional paradigms such as:

  • Immutability: Avoiding mutable state ensures that the data remains predictable.

  • Monads like Either and Option: These constructs help manage success or failure in a more expressive way without relying on exceptions.

In a nutshell, Dartz provides the tools to handle functional programming concepts that encourage clean, predictable, and testable code.

Why Bloc?

Bloc is a popular state management library in Flutter that separates presentation from business logic. It ensures:

  • Separation of Concerns: Bloc allows developers to keep UI code separate from business logic, promoting modularity.

  • Event-driven architecture: Bloc uses streams to handle UI changes in response to user interactions or data updates, making it easier to follow and debug.

Bloc’s Role in Clean Architecture

In Clean Architecture, Bloc is often used in the Presentation layer. The Bloc component takes care of handling UI events and updating the UI based on state transitions. The Bloc listens to Events from the UI, processes them with the Business Logic, and emits new States back to the UI.

Clean Architecture Overview

Clean Architecture emphasizes dividing your code into layers, each with a specific responsibility. The common layers in Flutter’s Clean Architecture setup are:

  1. Presentation Layer: Responsible for the UI (Bloc and Flutter widgets).

  2. Domain Layer: Contains the core business logic, use cases, and entities.

  3. Data Layer: Handles data sources like APIs or databases, repository implementation.

The key concept in Clean Architecture is the Dependency Rule: dependencies should only point inward, meaning that higher-level layers (Presentation) should depend on lower-level layers (Domain), but not the other way around.

Layer Breakdown:

  1. Entities (Domain Layer): Core business objects and rules that the entire app revolves around.

  2. Use Cases (Domain Layer): Application-specific business rules. For example, "fetching user details."

  3. Repositories (Data Layer): Handles interactions with external data sources, abstracting the data management logic.

  4. UI/Bloc (Presentation Layer): The front-facing code that interacts with users.

Integrating Dartz with Bloc in Clean Architecture

1. Setting up the Domain Layer with Dartz

Dartz comes into play in the Domain Layer, particularly with Use Cases. For instance, instead of using Future<User> to fetch data, you can use Future<Either<Failure, User>> to handle both success and error cases in a functional way.

Example: Creating a Use Case with Dartz

import 'package:dartz/dartz.dart';
import 'package:my_app/core/failures.dart';
import 'package:my_app/features/domain/entities/user.dart';

abstract class UserRepository {
  Future<Either<Failure, User>> getUserDetails();
}

class GetUserDetailsUseCase {
  final UserRepository repository;

  GetUserDetailsUseCase(this.repository);

  Future<Either<Failure, User>> call() async {
    return await repository.getUserDetails();
  }
}

2. Bloc in the Presentation Layer

In the Presentation Layer, Bloc will handle the UI states and trigger the use cases from the Domain Layer. The Bloc listens for Events, calls the Use Cases in response, and emits new States based on the results.

Example: Bloc Handling Dartz Results

import 'package:bloc/bloc.dart';
import 'package:dartz/dartz.dart';
import 'package:my_app/features/domain/usecases/get_user_details_usecase.dart';
import 'package:my_app/features/presentation/bloc/user_event.dart';
import 'package:my_app/features/presentation/bloc/user_state.dart';

class UserBloc extends Bloc<UserEvent, UserState> {
  final GetUserDetailsUseCase getUserDetailsUseCase;

  UserBloc(this.getUserDetailsUseCase) : super(UserInitial());

  @override
  Stream<UserState> mapEventToState(UserEvent event) async* {
    if (event is GetUserDetails) {
      yield UserLoading();
      final failureOrUser = await getUserDetailsUseCase();
      
      yield failureOrUser.fold(
        (failure) => UserError('Failed to fetch user details'),
        (user) => UserLoaded(user),
      );
    }
  }
}

Here, the mapEventToState method listens for the GetUserDetails event, calls the GetUserDetailsUseCase to fetch data, and then folds the Either type to yield either a success (UserLoaded) or failure (UserError) state.

3. Error Handling with Dartz

Using Dartz's Either type also helps manage errors in a clean way. For instance, you can define custom Failure classes that represent different types of errors (e.g., network failure, server failure), making error handling explicit and easy to manage.

Example: Handling Failures

class Failure {}

class NetworkFailure extends Failure {}

class ServerFailure extends Failure {}

In your Bloc, you can handle these errors specifically, providing more meaningful feedback to the UI.

yield failureOrUser.fold(
  (failure) {
    if (failure is NetworkFailure) {
      return UserError('Network issue. Please try again.');
    } else if (failure is ServerFailure) {
      return UserError('Server error. Try again later.');
    }
    return UserError('An unknown error occurred.');
  },
  (user) => UserLoaded(user),
);

Advantages of Using Dartz with Bloc in Clean Architecture

  1. Immutability: By using Dartz, immutability becomes a core part of your code, reducing side effects.

  2. Functional Error Handling: Dartz’s Either and Option types help handle errors in a functional and predictable manner.

  3. Separation of Concerns: Bloc allows for clear separation between business logic and UI, promoting modularity and scalability.

  4. Testability: Clean Architecture inherently promotes testability by isolating concerns and making it easier to test each layer independently.

Conclusion

Combining Dartz with Bloc in Flutter's Clean Architecture offers a powerful, scalable, and maintainable way to build apps. Dartz brings functional programming paradigms into Dart, enabling better error handling and immutability. Bloc ensures that the UI remains separate from business logic, and Clean Architecture ensures that your code remains well-structured and testable. Together, these tools provide a solid foundation for building robust and maintainable Flutter applications.