DEV Community

Abin S Chandran
Abin S Chandran

Posted on Originally published at abinschandran.in

Flutter App Architecture Guide: Clean Architecture with BLoC & Repository Pattern (2025)

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:

  • Business logic mixed with UI widgets
  • No clear data layer or API abstraction
  • Impossible to write unit tests
  • setState() causing full-screen rebuilds
  • Shared state spaghetti across widgets

The 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)         │
└────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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, OrderStatusChip
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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.


🏛️ About the Author & Original Publication

This architectural guide was originally published on abinschandran.in.

Abin S Chandran is a Senior Freelance Software Developer & Solution Architect serving clients in Kochi & Infopark, Kerala, and worldwide. He specializes in high-velocity Next.js 15 SaaS platforms, 60fps Flutter mobile applications, sub-10ms Node.js enterprise APIs, and production AI/RAG integrations.

👉 Planning a custom software project or SaaS MVP? Hire Abin or Request an Architecture Consultation ↗

Top comments (0)