Clean Architecture in Flutter for Large-Scale Applications
As a Flutter application grows, putting networking, business rules, persistence, and UI logic into the same feature quickly becomes difficult to maintain.
Clean Architecture helps separate responsibilities so that business logic remains independent from frameworks and external services.
The Core Idea
A practical Flutter structure is:
Presentation
|
v
Domain
|
v
Data
|
v
External Services
The dependency direction should point toward the domain:
UI -> BLoC -> Use Case -> Repository Interface
^
|
Repository Implementation
|
API / Database
Project Structure
For a large application, feature-first organization works well:
lib/
core/
error/
network/
storage/
utils/
features/
authentication/
data/
domain/
presentation/
products/
data/
domain/
presentation/
orders/
data/
domain/
presentation/
Domain Layer
The domain layer contains business concepts and rules.
Example entity:
class User {
final String id;
final String name;
final String email;
const User({
required this.id,
required this.name,
required this.email,
});
}
A use case represents one business action:
class GetUser {
final UserRepository repository;
GetUser(this.repository);
Future<User> call(String id) {
return repository.getUser(id);
}
}
The domain layer should not depend on Flutter widgets, HTTP clients, or database implementations.
Repository Interfaces
Define contracts in the domain layer:
abstract class UserRepository {
Future<User> getUser(String id);
}
The domain knows what it needs, but not how the data is obtained.
Data Layer
The data layer implements the domain contract.
class UserRepositoryImpl implements UserRepository {
final UserRemoteDataSource remote;
UserRepositoryImpl(this.remote);
@override
Future<User> getUser(String id) async {
final model = await remote.getUser(id);
return model.toEntity();
}
}
This keeps infrastructure details outside the business layer.
Data Sources
class UserRemoteDataSource {
final ApiClient client;
UserRemoteDataSource(this.client);
Future<UserModel> getUser(String id) async {
final json = await client.get('/users/$id');
return UserModel.fromJson(json);
}
}
A local data source can implement the same idea:
class UserLocalDataSource {
Future<UserModel?> getUser(String id) async {
// Read from local database.
return null;
}
}
Presentation Layer
The UI should communicate with application state rather than directly calling APIs.
For example:
User taps screen
|
v
BLoC event
|
v
Use Case
|
v
Repository
|
v
Data Source
|
v
API
This makes UI components easier to test.
Dependency Injection
Dependencies should be assembled at the application boundary.
Conceptually:
final apiClient = ApiClient();
final remote = UserRemoteDataSource(apiClient);
final repository = UserRepositoryImpl(remote);
final getUser = GetUser(repository);
A dependency injection package can automate this for larger projects.
Error Handling
Avoid leaking low-level exceptions throughout the application.
Create application-level failures:
sealed class Failure {
const Failure();
}
class NetworkFailure extends Failure {
const NetworkFailure();
}
class UnauthorizedFailure extends Failure {
const UnauthorizedFailure();
}
class ServerFailure extends Failure {
const ServerFailure();
}
The presentation layer can then decide how to display each failure.
When Clean Architecture Helps
It is particularly valuable when you have:
- multiple teams
- many features
- complex business rules
- multiple data sources
- offline support
- long application lifetimes
- extensive automated testing
For a tiny application, full Clean Architecture may add unnecessary ceremony.
Testing Strategy
Test each layer independently:
Entity / Use Case
↓
Repository
↓
Data Source
↓
BLoC
↓
Widget
The domain layer can usually be tested without Flutter bindings.
Common Mistakes
Too many abstractions
Do not create interfaces simply because a tutorial says every class needs one.
Create abstractions where they provide real flexibility or testability.
Business logic inside widgets
Avoid:
onPressed: () async {
final response = await http.get(...);
// business logic
}
Move this into a use case or application service.
Repository doing everything
A repository should coordinate data access, not become a giant business-logic class.
Conclusion
Clean Architecture is most useful when it helps a large application remain understandable as requirements grow.
The goal is not to create the maximum number of folders. The goal is to isolate business rules from UI and infrastructure so that each part can evolve independently.
Useful Links
SDK Flutter: https://github.com/v-modal/vmodal_sdk_flutter
SDK Android: https://github.com/v-modal/vmodal_sdk_android
Discord: https://discord.gg/K72z28KUx
Top comments (0)