Clean architecture in Flutter
14 Mar 2024

A quick guide to implement clean architecture in Flutter.

Why Clean Architecture?

Recommended Packages

Also don't forget to add the retrofit_generator and build_runner in your pubspec.yaml.

Folder Structure

blog image

Breakdown

Domain

Entity

what type of data our app will be dealing with. It's a simple abstract class for our models. In this case, it can be something like this.

abstract class AuthEntity extends Equatable {
  final String? token;
  final String? refreshToken;

  const AuthEntity({this.token, this.refreshToken});

   @override
   List<Object?> get props => [token, refreshToken];
}

Use Case

Use cases are basically the business logic of our app. What this feature can do. in our case it'll be log the user in.

class LoginUseCase {
  final AuthRepository _authRepository;

  LoginUseCase(this._authRepository);
  Future<DataState<AuthEntity>> call(String email, String password) {
    return _authRepository.login(email, password);
  }
}

Repository

This is where we define the contract for our data sources. It's an abstract class that will be implemented by the data layer.

abstract class AuthRepository {
  Future<DataState<AuthEntity>> login(String email, String password);
}

Data

Models

This is where we define the models for our app. It's a simple class that extends the entity from the domain layer.

class AuthModel extends AuthEntity {
  final String? token;
  final String? refreshToken;

  const AuthModel({this.token, this.refreshToken});

  factory AuthModel.fromJson(Map<String, dynamic> json) {
    return AuthModel(
      token: json['token'],
      refreshToken: json['refreshToken'],
    );
  }
}

Repository

This is where we implement the repository from the domain layer. It's a class that extends the repository from the domain layer.

class AuthRepositoryImpl implements AuthRepository {
  final AuthDataSource _authDataSource;

  AuthRepositoryImpl(this._authDataSource);

  @override
  Future<DataState<AuthEntity>> login(String email, String password) async {
    try {
      final response = await _authDataSource.login(email, password);
      return DataSuccess(AuthModel.fromJson(response.data));
    } on DioError catch (e) {
      return DataFailed(
        DioError(
          requestOptions: e.requestOptions,
          response: e.response,
          type: e.type,
          error: e.error,
        ),
      );
    }
  }
}

Presentation.

Bloc

Now our bloc implementation will be in the presentation layer. It'll talk to use case from the domain layer and will be responsible for managing the state of the UI.

Core

Data State

An abstract class for data state, the repository will be extending this, so we have a common way to handle the response.


import 'package:dio/dio.dart';

abstract class DataState<T> {
  final T ? data;
  final DioError ? error;

  const DataState({this.data, this.error});
}

class DataSuccess<T> extends DataState<T> {
  const DataSuccess(T data) : super(data: data);
}

class DataFailed<T> extends DataState<T> {
  const DataFailed(DioError error) : super(error: error);
}

Now that we know what each layer does, let's see how they interact with each other.

blog image

Conclusion

This is a simple overview of how to implement clean architecture in Flutter. It's a great way to keep your codebase clean and organized. It's also a great way to make your app more scalable and testable.

References will be added to the Vault soon. `

< Back