BiDev
Posted on Jun 11
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
**
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 to handle errors explicitly.
Originally published on bidev.site
Top comments (0)
Subscribe
For further actions, you may consider blocking this person and/or reporting abuse
