DEV Community

Cover image for Flutter Clean Architecture Guide 2026 – Complete Folder Structure
BiDev
BiDev

Posted on

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:

  • *Presentation *— Widgets, pages, state management (Bloc/GetX)
  • *Domain *— Pure Dart: entities, use cases, repository interfaces
  • *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
Enter fullscreen mode Exit fullscreen mode

**

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);
}
Enter fullscreen mode Exit fullscreen mode

**

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'));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

**

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)),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

**

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);
}
Enter fullscreen mode Exit fullscreen mode

**

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 to handle errors explicitly.

Originally published on bidev.site

Top comments (0)