Soft launch: this site is a work in progress.
Raphael BADA
Raphael BADAApp Developer
Back to articles

Clean Architecture in Flutter: A Practical Guide

How to structure scalable Flutter apps using Clean Architecture principles — with concrete examples of the data, domain, and presentation layers.

Raphael BADA
Feb 13, 202614 min
Clean Architecture in Flutter: A Practical Guide

Introduction

As Flutter apps grow beyond a few screens, the "put everything in one file" approach collapses. State management logic bleeds into UI code, API calls happen directly in widgets, and testing becomes nearly impossible. Clean Architecture solves this by enforcing clear boundaries between your app's layers.

This guide shows you how to implement Clean Architecture in Flutter with practical, copy-pasteable code — not just abstract diagrams.

The Three Layers

Clean Architecture divides your app into three concentric layers, each with strict dependency rules:

  • Domain Layer (innermost) — pure Dart. Contains entities, use cases, and repository interfaces. Has zero dependencies on Flutter, packages, or external services.
  • Data Layer — implements the domain's repository interfaces. Contains API clients, local database access, and data models (DTOs).
  • Presentation Layer (outermost) — Flutter widgets, state management (BLoC/Riverpod), and navigation.
The golden rule: dependencies point inward. The domain layer knows nothing about the data or presentation layers. The data layer knows about the domain layer but not the presentation. The presentation layer knows about both.

Project Structure

Here's a real-world folder structure for a Flutter app with Clean Architecture:

lib/
├── core/
│   ├── error/           # Failure classes, exceptions
│   ├── network/         # Network info, API client config
│   └── usecases/        # Base use case class
├── features/
│   └── articles/
│       ├── domain/
│       │   ├── entities/      # Article entity
│       │   ├── repositories/  # ArticleRepository (abstract)
│       │   └── usecases/      # GetArticles, CreateArticle
│       ├── data/
│       │   ├── models/        # ArticleModel (extends entity)
│       │   ├── datasources/   # Remote & local data sources
│       │   └── repositories/  # ArticleRepositoryImpl
│       └── presentation/
│           ├── bloc/          # ArticleBloc, events, states
│           ├── pages/         # ArticleListPage, ArticleDetailPage
│           └── widgets/       # ArticleCard, ArticleForm
└── injection_container.dart   # Dependency injection setup

Domain Layer: Pure Business Logic

The domain layer is the heart of your app. It contains no Flutter imports:

// domain/entities/article.dart
class Article {
  final String id;
  final String title;
  final String content;
  final DateTime publishedAt;
  
  const Article({
    required this.id,
    required this.title,
    required this.content,
    required this.publishedAt,
  });
}

// domain/repositories/article_repository.dart
abstract class ArticleRepository {
  Future<Either<Failure, List<Article>>> getArticles();
  Future<Either<Failure, Article>> getArticleById(String id);
}

// domain/usecases/get_articles.dart
class GetArticles {
  final ArticleRepository repository;
  GetArticles(this.repository);
  
  Future<Either<Failure, List<Article>>> call() {
    return repository.getArticles();
  }
}

Notice the use of Either<Failure, Success> from the dartz package — this forces explicit error handling without try-catch blocks scattered everywhere.

Data Layer: API & Storage

The data layer implements the domain's abstract repository:

// data/models/article_model.dart
class ArticleModel extends Article {
  const ArticleModel({
    required super.id,
    required super.title,
    required super.content,
    required super.publishedAt,
  });
  
  factory ArticleModel.fromJson(Map<String, dynamic> json) {
    return ArticleModel(
      id: json['id'],
      title: json['title'],
      content: json['content'],
      publishedAt: DateTime.parse(json['published_at']),
    );
  }
}

// data/repositories/article_repository_impl.dart
class ArticleRepositoryImpl implements ArticleRepository {
  final ArticleRemoteDataSource remoteDataSource;
  final ArticleLocalDataSource localDataSource;
  final NetworkInfo networkInfo;
  
  @override
  Future<Either<Failure, List<Article>>> getArticles() async {
    if (await networkInfo.isConnected) {
      try {
        final articles = await remoteDataSource.getArticles();
        localDataSource.cacheArticles(articles);
        return Right(articles);
      } on ServerException {
        return Left(ServerFailure());
      }
    } else {
      final cached = await localDataSource.getCachedArticles();
      return Right(cached);
    }
  }
}

Presentation Layer: BLoC Pattern

The presentation layer uses BLoC to manage state:

// presentation/bloc/article_bloc.dart
class ArticleBloc extends Bloc<ArticleEvent, ArticleState> {
  final GetArticles getArticles;
  
  ArticleBloc({required this.getArticles}) : super(ArticleInitial()) {
    on<LoadArticles>((event, emit) async {
      emit(ArticleLoading());
      final result = await getArticles();
      result.fold(
        (failure) => emit(ArticleError(failure.message)),
        (articles) => emit(ArticleLoaded(articles)),
      );
    });
  }
}

Why This Architecture Makes Testing Easy

Clean Architecture makes every layer independently testable:

  • Domain layer — test use cases with mock repositories. No Flutter, no API, no database needed.
  • Data layer — test repository implementations with mock data sources. Verify caching, error handling, and data transformation.
  • Presentation layer — test BLoCs with mock use cases. Verify state transitions without touching real data.

Conclusion

Clean Architecture in Flutter requires more initial setup than a simple StatefulWidget approach. But for any app that will grow beyond 10-15 screens, the investment pays off exponentially. Your code becomes testable, maintainable, and team-friendly — and swapping out an API client or state management solution becomes a localized change instead of a full rewrite.

#Flutter #Mobile #Dart
Share this article
Written by

Raphael BADA

A developer passionate about Flutter, Laravel, and modern design — sharing hands-on insights through technical articles and practical tutorials.

Contact Me

Have a project in mind? Let's talk.

Send me a message

© 2026 Raphael BADA. All rights reserved.