You are working with Clean Architecture in a Flutter project.
Layer Responsibilities
┌─────────────────────────────────────────┐
│ PRESENTATION │
│ Modules, Screens, Controllers (Cubits) │
│ Only knows: Domain entities + Usecases │
├─────────────────────────────────────────┤
│ DOMAIN (pure Dart, no Flutter) │
│ Entities, Repository interfaces, │
│ Usecases │
│ Depends on: nothing │
├─────────────────────────────────────────┤
│ DATA │
│ Models, Datasources, Repo impls │
│ Depends on: Domain interfaces + Dio │
└─────────────────────────────────────────┘
Full Feature Scaffold
When argument is scaffold or no argument, create all layers for <feature_name>:
Domain Layer
entity (domain/entities/.dart):
import 'package:equatable/equatable.dart';
class <Feature> extends Equatable {
final String id;
final String name;
// Add all business-relevant fields (no JSON, no framework types)
const <Feature>({
required this.id,
required this.name,
});
@override
List<Object?> get props => [id, name];
<Feature> copyWith({String? id, String? name}) => <Feature>(
id: id ?? this.id,
name: name ?? this.name,
);
}
repository interface (domain/repositories/_repository.dart):
import '../entities/<feature>.dart';
abstract class <Feature>Repository {
Future<List<<Feature>>> getAll();
Future<<Feature>> getById(String id);
Future<void> create(<Feature> entity);
Future<void> update(<Feature> entity);
Future<void> delete(String id);
}
usecases (domain/usecases//):
Each usecase = one file, one responsibility:
// get_<feature>s_usecase.dart
class Get<Feature>sUsecase {
final <Feature>Repository _repository;
Get<Feature>sUsecase(this._repository);
Future<List<<Feature>>> call() => _repository.getAll();
}
// create_<feature>_usecase.dart
class Create<Feature>Usecase {
final <Feature>Repository _repository;
Create<Feature>Usecase(this._repository);
Future<void> call(<Feature> entity) => _repository.create(entity);
}
Data Layer
model (data/models/_model.dart):
import '../../domain/entities/<feature>.dart';
class <Feature>Model {
final String id;
final String name;
const <Feature>Model({required this.id, required this.name});
factory <Feature>Model.fromJson(Map<String, dynamic> json) =>
<Feature>Model(
id: json['id'] as String,
name: json['name'] as String,
);
Map<String, dynamic> toJson() => {'id': id, 'name': name};
<Feature> toEntity() => <Feature>(id: id, name: name);
static <Feature>Model fromEntity(<Feature> entity) =>
<Feature>Model(id: entity.id, name: entity.name);
}
datasource (data/datasources/_datasource.dart):
import 'package:dio/dio.dart';
import '../models/<feature>_model.dart';
class <Feature>Datasource {
final Dio _dio;
<Feature>Datasource(this._dio);
Future<List<<Feature>Model>> getAll() async {
final response = await _dio.get('/<feature>s');
return (response.data as List)
.map((e) => <Feature>Model.fromJson(e as Map<String, dynamic>))
.toList();
}
Future<<Feature>Model> getById(String id) async {
final response = await _dio.get('/<feature>s/$id');
return <Feature>Model.fromJson(response.data as Map<String, dynamic>);
}
Future<void> create(<Feature>Model model) async {
await _dio.post('/<feature>s', data: model.toJson());
}
Future<void> update(<Feature>Model model) async {
await _dio.put('/<feature>s/${model.id}', data: model.toJson());
}
Future<void> delete(String id) async {
await _dio.delete('/<feature>s/$id');
}
}
repository impl (data/repositories/_repository_impl.dart):
import '../../domain/entities/<feature>.dart';
import '../../domain/repositories/<feature>_repository.dart';
import '../datasources/<feature>_datasource.dart';
import '../models/<feature>_model.dart';
class <Feature>RepositoryImpl implements <Feature>Repository {
final <Feature>Datasource _dataSource;
<Feature>RepositoryImpl(this._dataSource);
@override
Future<List<<Feature>>> getAll() async {
final models = await _dataSource.getAll();
return models.map((m) => m.toEntity()).toList();
}
@override
Future<<Feature>> getById(String id) async {
final model = await _dataSource.getById(id);
return model.toEntity();
}
@override
Future<void> create(<Feature> entity) =>
_dataSource.create(<Feature>Model.fromEntity(entity));
@override
Future<void> update(<Feature> entity) =>
_dataSource.update(<Feature>Model.fromEntity(entity));
@override
Future<void> delete(String id) => _dataSource.delete(id);
}
When argument is audit
Read the existing code for <feature_name> and check:
- Domain isolation: Do entities import anything from Flutter/Dio/external? (They shouldn't)
- Repository pattern: Does the datasource return models? Does the repo impl map to entities?
- Usecase single responsibility: Does each usecase do exactly one thing?
- Dependency direction: Does data layer depend on domain? (Yes, correct) Does domain depend on data? (No, wrong)
- Model ↔ Entity separation: Are API models separate from domain entities?
- Exception handling: Are data-layer exceptions caught and re-thrown as domain exceptions?
Report findings and fix any violations.
Architecture Rules
- Domain layer has zero external dependencies — pure Dart, no Flutter, no Dio
- Entities use Equatable — value equality for BLoC state comparisons
- Models handle JSON — entities never have
fromJson/toJson - Repository interface in domain — implementation in data
- Usecases are thin — orchestrate repo calls, no business logic in datasources
- One usecase = one operation — don't create
MenuUsecaseswith 5 methods - Datasources return models — repositories return entities
- Exceptions: datasource throws HTTP exceptions, repository catches and rethrows domain exceptions
Instructions
- Read existing domain/data files before creating new ones
- Check
core/lib/injections.dartto see what's already registered - Only create layers that don't exist yet
- After scaffolding, run
/flutter-dito register new dependencies - After DI, run
/flutter-stateto create the Cubit