Flutter Clean Architecture Guide 2026 – Complete Folder Structure

Bilal Fali··2 min read
Flutter Clean Architecture Guide 2026 – Complete Folder Structure

Flutter Clean Architecture is the industry standard for building scalable, testable, and maintainable mobile apps. This guide walks you through a production-ready implementation for 2026.

What Is Clean Architecture?

Three concentric layers where dependencies point inward only:

  1. Presentation — Widgets, pages, state management (Bloc/GetX)
  2. Domain — Pure Dart: entities, use cases, repository interfaces
  3. Data — Repository implementations, models, remote/local data sources

Recommended Folder Structure

lib/
├── core/
│   ├── errors/
│   │   ├── exceptions.dart
│   │   └── failures.dart
│   └── usecases/usecase.dart
│
└── features/
    └── auth/
        ├── data/
        │   ├── datasources/auth_remote_datasource.dart
        │   ├── models/user_model.dart
        │   └── repositories/auth_repository_impl.dart
        ├── domain/
        │   ├── entities/user_entity.dart
        │   ├── repositories/auth_repository.dart
        │   └── usecases/login_usecase.dart
        └── presentation/
            ├── bloc/
            │   ├── auth_bloc.dart
            │   ├── auth_event.dart
            │   └── auth_state.dart
            └── pages/login_page.dart

Domain Layer — The Core

// entity — pure Dart, no packages
class UserEntity {
  final String id;
  final String email;
  final String displayName;
  const UserEntity({required this.id, required this.email, required this.displayName});
}

// repository interface
abstract class AuthRepository {
  Future<Either<Failure, UserEntity>> login({
    required String email,
    required String password,
  });
  Future<Either<Failure, void>> logout();
}

// use case — single responsibility
class LoginUseCase {
  final AuthRepository _repo;
  const LoginUseCase(this._repo);

  Future<Either<Failure, UserEntity>> call(LoginParams params) =>
      _repo.login(email: params.email, password: params.password);
}

Data Layer — Repository Implementation

class AuthRepositoryImpl implements AuthRepository {
  final AuthRemoteDataSource _remote;
  AuthRepositoryImpl(this._remote);

  @override
  Future<Either<Failure, UserEntity>> login({
    required String email,
    required String password,
  }) async {
    try {
      final model = await _remote.login(email: email, password: password);
      return Right(model);
    } on ServerException catch (e) {
      return Left(ServerFailure(e.message));
    } on NetworkException {
      return Left(const NetworkFailure('No internet connection'));
    }
  }
}

Presentation Layer — Bloc

class AuthBloc extends Bloc<AuthEvent, AuthState> {
  final LoginUseCase _loginUseCase;

  AuthBloc({required LoginUseCase loginUseCase})
      : _loginUseCase = loginUseCase,
        super(AuthInitial()) {
    on<LoginRequested>(_onLoginRequested);
  }

  Future<void> _onLoginRequested(
    LoginRequested event,
    Emitter<AuthState> emit,
  ) async {
    emit(AuthLoading());
    final result = await _loginUseCase(
      LoginParams(email: event.email, password: event.password),
    );
    result.fold(
      (failure) => emit(AuthError(failure.message)),
      (user)    => emit(AuthAuthenticated(user)),
    );
  }
}

Dependency Injection with GetIt

final sl = GetIt.instance;

Future<void> init() async {
  sl.registerFactory(() => AuthBloc(loginUseCase: sl()));
  sl.registerLazySingleton(() => LoginUseCase(sl()));
  sl.registerLazySingleton<AuthRepository>(
    () => AuthRepositoryImpl(sl()),
  );
  sl.registerLazySingleton<AuthRemoteDataSource>(
    () => AuthRemoteDataSourceImpl(firebaseAuth: sl()),
  );
  sl.registerLazySingleton(() => FirebaseAuth.instance);
}

Key Rules

Entities have no packages (pure Dart classes only). Models extend entities and add fromJson/toJson. Use cases have one call() method. Blocs live in the presentation layer only. Always use Either<Failure, T> to handle errors explicitly.

Share

Comments

Comments

Leave a comment

0/2000

Comments appear after review.