Skip to content
Blog

Clean Architecture with Four Layers for Flutter Apps

Implement Clean Architecture in Flutter with four distinct layers: Domain, Data, Presentation, and Application. Scale your Flutter apps without the spaghetti code.

Published on September 15, 2026

AI Assistant

Clean Architecture in Flutter isn’t just about separating files — it’s about creating boundaries that let you change implementations without breaking consumers. The four-layer variant (Domain, Data, Presentation, Application) gives each concern its own rules and dependencies.

The four layers

┌─────────────────────────────────┐
│        Application Layer        │ ← Coordinates repositories, cross-cutting concerns
├─────────────────────────────────┤
│       Presentation Layer        │ ← Widgets + controllers/state management
├─────────────────────────────────┤
│          Domain Layer           │ ← Entities + use cases (pure Dart)
├─────────────────────────────────┤
│          Data Layer             │ ← Repositories + data sources (API, DB, cache)
└─────────────────────────────────┘

Dependency rule: Each layer only depends on layers below it. Domain knows nothing about Data or Presentation.

Domain layer (entities + use cases)

The Domain layer contains business logic with zero Flutter dependencies:

// entities/user.dart
class User {
  final String id;
  final String name;
  final String email;
  const User({required this.id, required this.name, required this.email});
}

// use_cases/get_user.dart
class GetUser {
  final UserRepository _repository;
  GetUser(this._repository);

  Future<User> call(String id) async {
    if (id.isEmpty) throw InvalidArgumentException('User ID required');
    return await _repository.getUserById(id);
  }
}

Use cases are functions wrapped in classes. They orchestrate domain logic without knowing where data comes from.

Data layer (repositories + data sources)

The Data layer implements domain interfaces and handles external data:

// data/repositories/user_repository_impl.dart
class UserRepositoryImpl implements UserRepository {
  final UserRemoteDataSource _remote;
  final UserLocalDataSource _local;
  UserRepositoryImpl(this._remote, this._local);

  @override
  Future<User> getUserById(String id) async {
    try {
      final user = await _remote.fetchUser(id);
      await _local.cacheUser(user);
      return user;
    } catch (e) {
      return await _local.getCachedUser(id);
    }
  }
}

Data sources are abstracted behind interfaces, making testing trivial with mocks.

Presentation layer (widgets + controllers)

The Presentation layer handles UI and state management:

// presentation/providers/user_provider.dart
class UserNotifier extends StateNotifier<AsyncValue<User>> {
  final GetUser _getUser;
  UserNotifier(this._getUser) : super(const AsyncValue.loading());

  Future<void> loadUser(String id) async {
    state = const AsyncValue.loading();
    try {
      final user = await _getUser(id);
      state = AsyncValue.data(user);
    } catch (e) {
      state = AsyncValue.error(e);
    }
  }
}

// presentation/widgets/user_card.dart
class UserCard extends ConsumerWidget {
  final String userId;
  const UserCard({required this.userId});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsync = ref.watch(userProvider(userId));
    return userAsync.when(
      loading: () => CircularProgressIndicator(),
      error: (e, _) => Text('Error: $e'),
      data: (user) => Card(child: Text(user.name)),
    );
  }
}

Application layer (optional coordinator)

The Application layer coordinates multiple repositories for complex workflows:

// application/use_cases/register_user.dart
class RegisterUser {
  final UserRepository _userRepo;
  final AuthRepository _authRepo;
  final EmailService _emailService;

  RegisterUser(this._userRepo, this._authRepo, this._emailService);

  Future<User> call(RegisterUserCommand command) async {
    final user = await _userRepo.createUser(command.toEntity());
    await _authRepo.set_password(user.id, command.password);
    await _emailService.sendWelcome(user.email);
    return user;
  }
}

This layer prevents use cases from knowing about each other while coordinating cross-cutting concerns.

Feature-based folder structure

Organize by feature, not by layer type:

lib/
├── core/                    # Shared utilities, theme, constants
│   ├── theme/
│   └── utils/
├── features/
│   ├── auth/
│   │   ├── domain/
│   │   │   ├── entities/
│   │   │   ├── repositories/
│   │   │   └── use_cases/
│   │   ├── data/
│   │   │   ├── data_sources/
│   │   │   └── repositories/
│   │   └── presentation/
│   │       ├── providers/
│   │       └── widgets/
│   ├── dashboard/
│   │   ├── domain/
│   │   ├── data/
│   │   └── presentation/
│   └── settings/
│       ├── domain/
│       ├── data/
│       └── presentation/
└── main.dart

Each feature is self-contained. You can delete a feature without breaking others.

Dependency injection

Wire everything together with dependency injection:

// injection.dart
final getIt = GetIt.instance;

void setupDependencies() {
  // Data layer
  getIt.registerLazySingleton<UserRemoteDataSource>(
    () => UserRemoteDataSourceImpl(getIt()),
  );
  getIt.registerLazySingleton<UserRepository>(
    () => UserRepositoryImpl(getIt(), getIt()),
  );

  // Domain layer
  getIt.registerLazySingleton(() => GetUser(getIt()));

  // Presentation layer
  getIt.registerFactory(() => UserNotifier(getIt()));
}

Benefits of four-layer Clean Architecture

  • Testability: Mock any layer boundary
  • Replaceability: Swap SQLite for Hive without touching Presentation
  • Scalability: Features don’t interfere with each other
  • Team productivity: Different developers own different layers
  • Business logic isolation: Domain layer is pure Dart, testable without Flutter

Clean Architecture isn’t overkill for small apps, but it pays for itself the moment your app grows beyond a few screens.