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




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});

   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;

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


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);



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'],


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;


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



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.


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.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


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