Flutter Clean Architecture Basics

Apps live for years. Features grow, servers change, designs get refreshed, and teams rotate. Clean architecture arranges code in layers so each change touches as little as possible. The rule is simple: important business ideas sit in the center, and details such as screens, databases, and web services sit around the outside.

The Onion Picture


  ┌─────────────────────────────────────────┐
  │  Presentation  (widgets, ViewModels)    │
  │  ┌───────────────────────────────────┐  │
  │  │  Data  (API, database, repo impl) │  │
  │  │  ┌─────────────────────────────┐  │  │
  │  │  │  Domain                     │  │  │
  │  │  │  entities, use cases,       │  │  │
  │  │  │  repository contracts       │  │  │
  │  │  └─────────────────────────────┘  │  │
  │  └───────────────────────────────────┘  │
  └─────────────────────────────────────────┘
        Dependencies point INWARD only

Imagine a castle. The throne room in the middle holds the rules of the kingdom and stays the same for centuries. Walls, gates, and roads around it change often. Guards never rewrite royal law because a road moved.

The Dependency Rule

Code in an inner layer never imports code from an outer layer. The domain layer knows nothing about Flutter, JSON, or Dio. The data layer knows the domain. The presentation layer knows the domain. This single rule keeps the center stable.

The Three Layers

LayerContainsDepends On
DomainEntities, use cases, repository interfacesPure Dart only
DataAPI calls, database code, repository implementations, JSON modelsDomain
PresentationWidgets, screens, ViewModels or BlocsDomain

Folder Layout


  lib/features/orders/
  ├── domain/
  │   ├── entities/order.dart
  │   ├── repositories/order_repository.dart
  │   └── usecases/get_orders.dart
  ├── data/
  │   ├── models/order_model.dart
  │   ├── datasources/order_api.dart
  │   └── repositories/order_repository_impl.dart
  └── presentation/
      ├── view_models/order_view_model.dart
      └── screens/order_screen.dart

Domain Layer: Entity

An entity holds the core facts of the business with no JSON or UI details.


class Order {
  final int id;
  final double total;
  final bool delivered;

  Order({required this.id, required this.total, required this.delivered});
}

Domain Layer: Repository Contract


abstract class OrderRepository {
  Future<List<Order>> getOrders();
}

Domain Layer: Use Case

A use case represents one action the app performs, such as "get recent orders" or "cancel an order". It gives each business rule a clear name and home.


class GetOrders {
  final OrderRepository repository;
  GetOrders(this.repository);

  Future<List<Order>> call() async {
    final orders = await repository.getOrders();
    orders.sort((a, b) => b.id.compareTo(a.id));
    return orders;
  }
}

The call method lets code run the use case like a function: getOrders().

Data Layer: Model and Implementation

A model extends or converts to the entity and adds JSON knowledge.


class OrderModel extends Order {
  OrderModel({required super.id, required super.total, required super.delivered});

  factory OrderModel.fromJson(Map<String, dynamic> json) {
    return OrderModel(
      id: json['id'],
      total: (json['total'] as num).toDouble(),
      delivered: json['delivered'],
    );
  }
}

class OrderRepositoryImpl implements OrderRepository {
  final OrderApi api;
  OrderRepositoryImpl(this.api);

  @override
  Future<List<Order>> getOrders() async {
    final items = await api.fetchOrders();
    return items.map((j) => OrderModel.fromJson(j)).toList();
  }
}

Presentation Layer


class OrderViewModel extends ChangeNotifier {
  final GetOrders getOrders;
  List<Order> orders = [];

  OrderViewModel(this.getOrders);

  Future<void> load() async {
    orders = await getOrders();
    notifyListeners();
  }
}

  Screen → ViewModel → UseCase → Repository (interface)
                                      ▲
                                      │ implements
                                 RepositoryImpl → API

What You Gain

  • Replace the REST API with GraphQL by editing only the data layer.
  • Test business rules with plain Dart tests that need no Flutter at all.
  • Let several developers work on separate layers without conflicts.
  • Understand a feature by reading its folders in order.

What It Costs

More folders and more small classes mean more typing. A tiny app with three screens does not need three layers. Choose the structure that matches the size of the project, and grow into it. Start with a simple feature-based folder layout and add domain use cases when real business rules appear.

Practice Ideas

Convert the notes or task example from earlier topics into the three-layer structure. Move JSON code into a model, add one use case that sorts the results, and check that the domain folder imports no Flutter or Dio code.

Growing Into the Structure

Few teams adopt every layer on the first day. A sensible path starts with a feature-based folder layout and a repository. When a screen begins to contain business rules, such as discount calculations or eligibility checks, those rules move into use cases. When a second data source appears, the repository interface already waits to absorb it. Each step solves a real pain, and the architecture grows from need rather than from fashion.

Mapping Between Layers

Each layer speaks its own language. The data layer talks JSON, the domain layer talks business objects, and the presentation layer talks display text. Small mapper functions convert between them. The extra code buys independence: a server change cannot ripple into widgets, and a design change cannot disturb business rules.