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
| Layer | Contains | Depends On |
|---|---|---|
| Domain | Entities, use cases, repository interfaces | Pure Dart only |
| Data | API calls, database code, repository implementations, JSON models | Domain |
| Presentation | Widgets, screens, ViewModels or Blocs | Domain |
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.
