Why Flutter Architecture Matters from Day One
Most Flutter tutorial apps use a single file with setState() — fine for demos, catastrophic for production. When clients come to me to rescue poorly-structured Flutter projects, the most common symptoms are:
setState() causing full-screen rebuildsThe solution is Clean Architecture — a layered approach that separates concerns so strictly that you can swap your backend, state manager, or UI framework without touching business logic.
The 3-Layer Flutter Clean Architecture
┌────────────────────────────────────┐
│ Presentation Layer │
│ (BLoC + Widgets + Pages) │
├────────────────────────────────────┤
│ Domain Layer │
│ (Use Cases + Entities + Repos) │
├────────────────────────────────────┤
│ Data Layer │
│ (APIs + Local DB + DTOs) │
└────────────────────────────────────┘Each layer only communicates downward — the Presentation layer calls Domain, Domain calls Data, never the reverse.
Project Directory Structure
lib/
├── core/
│ ├── error/ # Failures & Exceptions
│ ├── network/ # Dio HTTP client setup
│ └── usecases/ # Base UseCase abstract class
├── features/
│ └── orders/
│ ├── data/
│ │ ├── datasources/ # Remote API & Local Hive datasources
│ │ ├── models/ # DTO models with fromJson/toJson
│ │ └── repositories/ # Repository implementations
│ ├── domain/
│ │ ├── entities/ # Pure business objects (no JSON)
│ │ ├── repositories/ # Abstract repository interfaces
│ │ └── usecases/ # GetOrders, CreateOrder, etc.
│ └── presentation/
│ ├── bloc/ # OrdersBloc, OrdersState, OrdersEvent
│ ├── pages/ # OrdersPage, OrderDetailPage
│ └── widgets/ # OrderCard, OrderStatusChip1. Domain Layer: Entities & Use Cases
Entities are pure Dart classes with no Flutter or JSON dependencies:
// domain/entities/order.dart
class Order {
final String id;
final String customerId;
final List<OrderItem> items;
final OrderStatus status;
final DateTime createdAt;
const Order({
required this.id,
required this.customerId,
required this.items,
required this.status,
required this.createdAt,
});
}Use Cases encapsulate a single business operation:
// domain/usecases/get_orders.dart
class GetOrders implements UseCase<List<Order>, GetOrdersParams> {
final OrderRepository repository;
GetOrders(this.repository);
@override
Future<Either<Failure, List<Order>>> call(GetOrdersParams params) {
return repository.getOrders(customerId: params.customerId);
}
}2. Data Layer: Repository Implementation & DTOs
The Repository Implementation bridges Domain contracts with real API calls:
// data/repositories/order_repository_impl.dart
class OrderRepositoryImpl implements OrderRepository {
final OrderRemoteDataSource remoteDataSource;
final OrderLocalDataSource localDataSource;
final NetworkInfo networkInfo;
@override
Future<Either<Failure, List<Order>>> getOrders({required String customerId}) async {
if (await networkInfo.isConnected) {
try {
final remoteOrders = await remoteDataSource.getOrders(customerId);
await localDataSource.cacheOrders(remoteOrders);
return Right(remoteOrders.map((dto) => dto.toEntity()).toList());
} on ServerException {
return Left(ServerFailure());
}
} else {
final cachedOrders = await localDataSource.getCachedOrders(customerId);
return Right(cachedOrders.map((dto) => dto.toEntity()).toList());
}
}
}3. Presentation Layer: BLoC State Management
// presentation/bloc/orders_bloc.dart
class OrdersBloc extends Bloc<OrdersEvent, OrdersState> {
final GetOrders getOrders;
OrdersBloc({required this.getOrders}) : super(OrdersInitial()) {
on<FetchOrders>(_onFetchOrders);
}
Future<void> _onFetchOrders(FetchOrders event, Emitter<OrdersState> emit) async {
emit(OrdersLoading());
final result = await getOrders(GetOrdersParams(customerId: event.customerId));
result.fold(
(failure) => emit(OrdersError(message: failure.message)),
(orders) => emit(OrdersLoaded(orders: orders)),
);
}
}4. Dependency Injection with get_it
Wire everything together using get_it for testable, decoupled dependency injection:
// core/injection_container.dart
final sl = GetIt.instance;
Future<void> initDependencies() async {
// BLoCs
sl.registerFactory(() => OrdersBloc(getOrders: sl()));
// Use Cases
sl.registerLazySingleton(() => GetOrders(sl()));
// Repositories
sl.registerLazySingleton<OrderRepository>(
() => OrderRepositoryImpl(
remoteDataSource: sl(),
localDataSource: sl(),
networkInfo: sl(),
),
);
// Data Sources
sl.registerLazySingleton<OrderRemoteDataSource>(
() => OrderRemoteDataSourceImpl(client: sl()),
);
}When to Use This Architecture
| App Scale | Recommended Architecture |
|---|---|
| Side project / prototype | Simple setState() or Provider |
| 3–10 features, team of 1–2 | Provider or Riverpod with service layer |
| Enterprise / team / long-term | Clean Architecture + BLoC (this guide) |
Need a Flutter Architect?
Setting up Flutter Clean Architecture correctly from the start saves thousands of hours of refactoring later. If you need an experienced Flutter Solution Architect based in Kerala, India (available worldwide remotely) to architect your mobile application, [start a conversation today](/contact).