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.
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.
Use Clean Architecture when:
Skip it when:
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.
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