Flutter Advanced Lesson 96 of 135 3 min read
Clean Architecture in Flutter: Layers, Use Cases and Folder Structure
Clean Architecture in Flutter made simple: presentation, domain and data layers, use cases and repositories, with a folder structure and Bloc example.
On this page
Clean Architecture organises an app into layers, where the inner layers know nothing about the outer ones. Your business rules do not depend on Flutter, on the database or on the API. That makes them easy to test and lets you swap the API, the database or even the state management package without rewriting the core.
It is an architecture, not a state management package. You combine it with a state tool such as Riverpod or Bloc.
The three layers
┌───────────────────────────────────────────┐
│ Presentation widgets + state (Bloc, │
│ Riverpod, ViewModel) │
├───────────────────────────────────────────┤
│ Domain entities, use cases, │ ← pure Dart, no Flutter
│ repository interfaces │
├───────────────────────────────────────────┤
│ Data repository implementations, │
│ API and database sources, │
│ models (JSON) │
└───────────────────────────────────────────┘
Dependencies point inwards: presentation → domain ← data
| Layer | Contains | Knows about |
|---|---|---|
| Domain | Entity classes, UseCase classes, abstract Repository | Nothing else |
| Data | RepositoryImpl, RemoteDataSource, LocalDataSource, JSON Model classes | Domain |
| Presentation | Pages, widgets, Cubits/Notifiers/ViewModels | Domain |
Folder structure (feature first)
lib/
core/ shared helpers, errors, network client
features/
weather/
domain/
entities/weather.dart
repositories/weather_repository.dart (abstract)
usecases/get_weather.dart
data/
models/weather_model.dart (fromJson)
datasources/weather_remote_source.dart
repositories/weather_repository_impl.dart
presentation/
cubit/weather_cubit.dart (or providers/)
pages/weather_page.dart
widgets/
main.dart
The code, layer by layer
Domain: entity, repository contract and use case. Pure Dart.
// domain/entities/weather.dart
class Weather {
const Weather({required this.city, required this.celsius});
final String city;
final double celsius;
}
// domain/repositories/weather_repository.dart
abstract interface class WeatherRepository {
Future<Weather> getWeather(String city);
}
// domain/usecases/get_weather.dart
class GetWeather {
const GetWeather(this.repository);
final WeatherRepository repository;
Future<Weather> call(String city) {
if (city.trim().isEmpty) throw ArgumentError('City is required'); // a business rule
return repository.getWeather(city.trim());
}
}
Data: model, data source and repository implementation.
// data/models/weather_model.dart
class WeatherModel extends Weather {
const WeatherModel({required super.city, required super.celsius});
factory WeatherModel.fromJson(Map<String, dynamic> json) => WeatherModel(
city: json['city'] as String,
celsius: (json['temp'] as num).toDouble(),
);
}
// data/datasources/weather_remote_source.dart
class WeatherRemoteSource {
Future<Map<String, dynamic>> fetch(String city) async {
await Future.delayed(const Duration(milliseconds: 500)); // real code: http.get(...)
return {'city': city, 'temp': 21.5};
}
}
// data/repositories/weather_repository_impl.dart
class WeatherRepositoryImpl implements WeatherRepository {
WeatherRepositoryImpl(this.remote);
final WeatherRemoteSource remote;
@override
Future<Weather> getWeather(String city) async =>
WeatherModel.fromJson(await remote.fetch(city));
}
Presentation: a Cubit that uses the use case.
// presentation/cubit/weather_cubit.dart
sealed class WeatherState {}
class WeatherInitial extends WeatherState {}
class WeatherLoading extends WeatherState {}
class WeatherLoaded extends WeatherState {
WeatherLoaded(this.weather);
final Weather weather;
}
class WeatherError extends WeatherState {
WeatherError(this.message);
final String message;
}
class WeatherCubit extends Cubit<WeatherState> {
WeatherCubit(this.getWeather) : super(WeatherInitial());
final GetWeather getWeather;
Future<void> load(String city) async {
emit(WeatherLoading());
try {
emit(WeatherLoaded(await getWeather(city)));
} catch (e) {
emit(WeatherError(e.toString()));
}
}
}
Wiring it together (with Bloc’s BlocProvider, or with GetIt or Riverpod providers):
BlocProvider(
create: (_) => WeatherCubit(
GetWeather(WeatherRepositoryImpl(WeatherRemoteSource())),
),
child: const WeatherPage(),
);
Why bother?
- Testing: test
GetWeatherwith a fakeWeatherRepository, no network, no Flutter. - Changing your mind: switch from REST to Firebase by writing a new
WeatherRepositoryImpl; nothing else changes. - Teams: everybody knows where every kind of code goes.
When it is too much
A small app with three screens does not need use cases for every action. Start with the simpler layered MVVM structure and add a domain layer when business rules grow. Over-engineering a small app is as bad as under-engineering a big one.
Try it yourself
Write a FakeWeatherRepository that returns fixed data, and a unit test that checks GetWeather('') throws an ArgumentError.