Coding Studio

Learn & Grow together.

Clean Architecture in Flutter – A Complete Guide With Code Examples (2025 Edition)

Building scalable Flutter apps becomes challenging as features grow, teams expand, and codebases get complicated. This is where Clean Architecture shines — a proven architecture pattern that keeps your app testable, maintainable, scalable, and independent of frameworks.

In this blog, we’ll break down Clean Architecture for Flutter in simple terms with folder structure, data flow, and real code examples.


What is Clean Architecture?

Proposed by Robert C. Martin (“Uncle Bob”), Clean Architecture divides an app into independent layers, each responsible for a specific job.

The goals are:

  • Independence → UI should not depend on data sources
  • Separation of concerns
  • Easy to test
  • Easy to scale or replace components
  • Business logic stays pure

Clean Architecture Layers in Flutter

Flutter projects commonly follow 3 layers:

lib/
 └── src/
      ├── presentation/   → UI (Widgets, Screens, Controllers)
      ├── domain/         → Business logic (Entities, Repositories, Use Cases)
      └── data/           → API, DB, models, repository implementations

1. Domain Layer (Core Business Logic)

This is the heart of your application.

It contains:

✔ Entities

Plain Dart classes that represent business objects.

✔ Use Cases

Business rules. Each use case does only one thing.

✔ Abstract Repository

Defines what data operations are possible.

Example: Domain Layer

entity – user.dart

class User {
  final int id;
  final String name;

  User({required this.id, required this.name});
}

repository – user_repository.dart

abstract class UserRepository {
  Future<User> getUserById(int id);
}

use case – get_user.dart

class GetUser {
  final UserRepository repository;

  GetUser(this.repository);

  Future<User> call(int id) async {
    return await repository.getUserById(id);
  }
}

The domain layer contains zero Flutter dependencies.
It’s pure Dart → easy to test, reusable, and independent.


2. Data Layer (API + DB + Models)

The data layer communicates with:

  • REST API
  • Local database (Hive/SQLite/Isar)
  • Third-party services

And it implements the domain repository using:

✔ Models (converts JSON to Dart objects)

✔ Remote Data Source

✔ Local Data Source

✔ Repository Implementation

Example: Data Layer

model – user_model.dart

import '../../domain/entities/user.dart';

class UserModel extends User {
  UserModel({required super.id, required super.name});

  factory UserModel.fromJson(Map<String, dynamic> json) {
    return UserModel(
      id: json["id"],
      name: json["name"],
    );
  }
}

remote data source – user_remote_data_source.dart

import 'package:http/http.dart' as http;
import 'dart:convert';

import '../models/user_model.dart';

class UserRemoteDataSource {
  final http.Client client;

  UserRemoteDataSource(this.client);

  Future<UserModel> fetchUser(int id) async {
    final response = await client.get(Uri.parse("https://reqres.in/api/users/$id"));

    final data = jsonDecode(response.body);
    return UserModel.fromJson(data['data']);
  }
}

repository_impl – user_repository_impl.dart

import '../../domain/entities/user.dart';
import '../../domain/repositories/user_repository.dart';
import '../datasources/user_remote_data_source.dart';

class UserRepositoryImpl implements UserRepository {
  final UserRemoteDataSource remoteDataSource;

  UserRepositoryImpl(this.remoteDataSource);

  @override
  Future<User> getUserById(int id) {
    return remoteDataSource.fetchUser(id);
  }
}

3. Presentation Layer (UI + State Management)

This includes:

  • Widgets
  • Screens
  • Providers/Bloc/Riverpod controllers

The UI layer depends on use cases, not the data layer.


Example: Presentation Layer (Using Riverpod)

user_notifier.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../domain/entities/user.dart';
import '../../domain/usecases/get_user.dart';

class UserNotifier extends StateNotifier<AsyncValue<User>> {
  final GetUser getUser;

  UserNotifier(this.getUser) : super(const AsyncLoading());

  Future<void> loadUser(int id) async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => getUser(id));
  }
}

providers.dart

final userRepositoryProvider = Provider(
  (ref) => UserRepositoryImpl(UserRemoteDataSource(http.Client())),
);

final getUserProvider = Provider(
  (ref) => GetUser(ref.read(userRepositoryProvider)),
);

final userNotifierProvider =
    StateNotifierProvider<UserNotifier, AsyncValue<User>>(
  (ref) => UserNotifier(ref.read(getUserProvider)),
);

UI – user_page.dart

class UserPage extends ConsumerWidget {
  const UserPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userState = ref.watch(userNotifierProvider);

    return Scaffold(
      appBar: AppBar(title: const Text("User Details")),
      body: userState.when(
        data: (user) => Center(
          child: Text("Hello, ${user.name}"),
        ),
        error: (e, _) => Center(child: Text("Error: $e")),
        loading: () => const Center(child: CircularProgressIndicator()),
      ),
    );
  }
}

How the Data Flows (Simple Diagram)

 UI (Presentation)
     |
     ▼
Use Case (Domain)
     |
     ▼
Repository Interface (Domain)
     |
     ▼
Repository Impl (Data)
     |
     ▼
Remote / Local Datasource (Data)

Each layer communicates only with the layer below it.


Why Clean Architecture Works Best for Large Flutter Apps

✔ Modular & Scalable

Easy to add new features without breaking existing ones.

✔ Testable

Domain logic is pure Dart → 100% unit test coverage is achievable.

✔ Team Friendly

Different teams can work on domain, data, UI independently.

✔ Replaceable

Want to switch from REST → GraphQL? Only update the data layer.


Best Practices for Clean Architecture in Flutter

  • Use DI (GetIt / Riverpod)
  • Maintain 1 use case = 1 responsibility
  • Keep domain layer pure
  • Avoid putting business logic in UI
  • Keep widgets dumb, view models smart
  • Add folder-by-feature for large apps
  • Use Freezed for models & entities

Conclusion

Clean Architecture helps you build robust, scalable, testable Flutter apps suitable for enterprise-level products. With proper layering and separation of concerns, your code becomes future-proof and easier to extend.

If you’re building a long-term project or working in a team, Clean Architecture is absolutely worth implementing.


Leave a Reply

Your email address will not be published. Required fields are marked *