← Flutter Reference

What is Clean Architecture in Flutter?

Architecture · Advanced

By Abdelrahman Saed — Senior Mobile Engineer. Last updated: 2026-08-10.

Clean Architecture in Flutter organizes an app into concentric layers — presentation (UI + state), domain (business logic), and data (APIs, databases, caches) — with dependencies always pointing inward. The domain layer knows nothing about Flutter, databases, or network clients; it defines pure business rules that the outer layers implement.

How it works

The dependency rule is the core: dependencies point inward only. The presentation layer depends on the domain layer; the data layer depends on the domain layer; the domain layer depends on nothing.

At iStoria (5M+ users, 50+ modules), every feature follows the same three-layer structure:

lib/features/story/
  domain/
    entities/story.dart          — pure Dart class, no Flutter imports
    repositories/story_repo.dart — abstract interface
  data/
    datasources/story_remote.dart — API client
    datasources/story_local.dart  — Drift database
    repositories/story_repo_impl.dart — concrete implementation
  presentation/
    bloc/story_bloc.dart         — state management
    pages/story_page.dart        — widgets
    widgets/story_card.dart

The repository contract lives in domain; its implementation lives in data:

// domain/repositories/story_repo.dart
abstract class StoryRepository {
  Future<Either<Failure, List<Story>>> fetchStories();
}

// data/repositories/story_repo_impl.dart
class StoryRepositoryImpl implements StoryRepository {
  StoryRepositoryImpl(this._remote, this._local);
  final StoryRemoteDataSource _remote;
  final StoryLocalDataSource _local;

  @override
  Future<Either<Failure, List<Story>>> fetchStories() async {
    try {
      final stories = await _local.cachedStories();   // offline-first read
      unawaited(_remote.refreshInBackground());        // sync, never blocks UI
      return Right(stories);
    } on CacheException catch (e) {
      return Left(CacheFailure(e.message));
    }
  }
}

The presentation layer never sees StoryRemoteDataSource or StoryLocalDataSource — it only knows the abstract StoryRepository. That means you can swap the backend, add caching, or switch from REST to GraphQL without touching the BLoC or the UI.

When to use it

Use Clean Architecture when:

  • Your app will live for years and be maintained by a team — the layering pays for itself in onboarding and refactoring safety.
  • You have multiple data sources (API, local DB, cache, sync engine) that need to be swappable.
  • You want business logic to be unit-testable without Flutter bindings.

Skip it when:

  • The app is small (a few screens, one data source) — the layering overhead isn't worth it.
  • Your team is new to Flutter — the abstraction layers add cognitive load that slows initial development.

Strengths

  • Business logic is framework-independent and unit-testable without widget tests
  • Data sources are swappable — swap REST for GraphQL or add offline-first without touching the UI
  • Consistent structure across features — every module looks the same, so onboarding is fast
  • Scales to large codebases — 50+ modules at iStoria, each independently navigable
  • Failures are values (Either<Failure, T>), not exceptions — error paths are explicit, not hidden

Tradeoffs

  • Significant boilerplate — each feature needs entities, repository contracts, implementations, and mappers
  • Learning curve — teams new to the pattern may over-engineer early features
  • Indirection — tracing a bug requires jumping through 3-4 layers, which feels slow at first

Alternatives

  • Riverpod — Riverpod is a reactive caching and state management framework for Flutter that improves on Provider with compile-time safety, no BuildContext dependency, and auto-dispose.

Comparisons

Related articles

Related case studies

FAQ

Is Clean Architecture overkill for Flutter?

For small apps, yes. For production apps with a team and a multi-year lifespan, no — the layering pays for itself. At iStoria (5M+ users, 50+ modules), Clean Architecture is why four engineers can out-ship larger teams. The key is to apply it at the feature level, not globally — a simple settings screen doesn't need a full three-layer structure.

How does Clean Architecture work with offline-first?

Cleanly: the domain layer defines the repository contract; the data layer implements it with offline-first reads (local database first, background sync). The presentation layer is unaware whether data came from the network or the cache — it just calls the repository and gets an Either<Failure, T>.


Available for hire. Abdelrahman Saed is a Senior Mobile Engineer (Flutter) — open to full-time, fractional, contract, or advisory work. Hire me →

Book a 20-minute call · Download the CV (PDF) · See how I work