flutter-clean-arch
Flutter Clean Architecture Skill
Generate Flutter applications following Clean Architecture principles with feature-first organization, Riverpod for state management, and functional error handling using fpdart.
Includes Dio + Retrofit for type-safe REST API calls.
Core Principles
Architecture: Clean Architecture (Feature-First)
- Domain layer: Pure business logic, no dependencies
- Data layer: Data sources, repositories implementation, data models
- Presentation layer: UI, state management, view models
Dependency Rule: Presentation → Domain ← Data (Domain has no external dependencies)
State Management: Riverpod 3.0+ with code generation
Note: Riverpod 3.0+ & Freezed 3.0+ Required
Riverpod 3.0+: The
XxxReftypes (likeDioRef,UserRepositoryRef, etc.) have been removed in favor of a unifiedReftype.Riverpod 2.x (Legacy):
SomeType someType(SomeTypeRef ref) { ... }Riverpod 3.x+ (Current):
SomeType someType(Ref ref) { ... }Freezed 3.0+: Two major breaking changes from v2:
1. Required
sealed/abstractKeywordAll classes using factory constructors now require either
sealedorabstractkeyword.
Class Type Freezed 2.x (Legacy) Freezed 3.x+ (Current) Single constructor class Personabstract class PersonUnion type (multiple constructors) class Resultsealed class ResultFreezed 2.x (Legacy) - Single Constructor:
class Person with _$Person { const factory Person({ required String firstName, required String lastName, }) = _Person; }Freezed 3.x+ (Current) - Single Constructor:
abstract class Person with _$Person { const factory Person({ required String firstName, required String lastName, }) = _Person; }Freezed 2.x (Legacy) - Union Type:
class Result with _$Result { const factory Result.success(String data) = Success; const factory Result.error(String message) = Error; }Freezed 3.x+ (Current) - Union Type:
sealed class Result with _$Result { const factory Result.success(String data) = Success; const factory Result.error(String message) = Error; }2. Pattern Matching (
.map/.whenRemoved)Freezed 3.x no longer generates
.map/.whenextensions. Use Dart 3's native pattern matching instead.Freezed 2.x (Legacy) - Using
.map:final model = Model.first('42'); final res = model.map( first: (value) => 'first ${value.a}', second: (value) => 'second ${value.b} ${value.c}', );Freezed 3.x+ (Current) - Using
switchexpression:final model = Model.first('42'); final res = switch (model) { First(:final a) => 'first $a', Second(:final b, :final c) => 'second $b $c', };Required versions: This skill requires Riverpod 3.0+ and Freezed 3.0+. Check your version with
flutter pub deps | grep riverpod.
Error Handling: fpdart's Either<Failure, T> for functional error handling
Networking: Dio + Retrofit for type-safe REST API calls
Project Structure
lib/
├── core/
│ ├── constants/
│ │ ├── api_constants.dart
│ ├── errors/
│ │ ├── failures.dart
│ │ └── network_exceptions.dart
│ ├── network/
│ │ ├── dio_provider.dart
│ │ └── interceptors/
│ │ ├── auth_interceptor.dart
│ │ ├── logging_interceptor.dart
│ │ └── error_interceptor.dart
│ ├── storage/
│ ├── services/
│ ├── router/
│ │ └── app_router.dart
│ └── utils/
├── shared/
├── features/
│ └── [feature_name]/
│ ├── data/
│ │ ├── models/
│ │ │ └── [entity]_model.dart
│ │ ├── datasources/
│ │ │ └── [feature]_api_service.dart
│ │ └── repositories/
│ │ └── [feature]_repository_impl.dart
│ ├── domain/
│ │ ├── entities/
│ │ ├── repositories/
│ │ │ └── [feature]_repository.dart
│ │ └── usecases/
│ │ └── [action]_usecase.dart
│ └── presentation/
│ ├── providers/
│ │ └── [feature]_provider.dart
│ ├── screens/
│ │ └── [feature]_screen.dart
│ └── widgets/
│ └── [feature]_widget.dart
└── main.dart
Quick Start
1. Domain Layer (Entities, Repository Interfaces, UseCases)
// Entity
sealed class User with _$User {
const factory User({
required String id,
required String name,
required String email,
}) = _User;
}
// Repository Interface
abstract class UserRepository {
Future<Either<Failure, User>> getUser(String id);
}
// UseCase
class GetUser {
final UserRepository repository;
GetUser(this.repository);
Future<Either<Failure, User>> call(String id) => repository.getUser(id);
}
2. Data Layer (Models, API Service, Repository Implementation)
// Model with JSON serialization
sealed class UserModel with _$UserModel {
const UserModel._();
const factory UserModel({
required String id,
required String name,
required String email,
}) = _UserModel;
factory UserModel.fromJson(Map<String, dynamic> json) => _$UserModelFromJson(json);
User toEntity() => User(id: id, name: name, email: email);
}
// Retrofit API Service
()
abstract class UserApiService {
factory UserApiService(Dio dio) = _UserApiService;
('/users/{id}')
Future<UserModel> getUser(('id') String id);
}
// Repository Implementation
class UserRepositoryImpl implements UserRepository {
final UserApiService apiService;
Future<Either<Failure, User>> getUser(String id) async {
try {
final userModel = await apiService.getUser(id);
return Right(userModel.toEntity());
} on DioException catch (e) {
return Left(Failure.network(NetworkExceptions.fromDioError(e).message));
}
}
}
3. Presentation Layer (Providers, Screens)
// Provider
UserApiService userApiService(Ref ref) {
return UserApiService(ref.watch(dioProvider));
}
UserRepositoryImpl userRepository(Ref ref) {
return UserRepositoryImpl(ref.watch(userApiServiceProvider));
}
class UserNotifier extends _$UserNotifier {
FutureOr<User?> build() => null;
Future<void> fetchUser(String id) async {
state = const AsyncLoading();
final result = await ref.read(userRepositoryProvider).getUser(id);
state = result.fold(
(failure) => AsyncError(failure, StackTrace.current),
(user) => AsyncData(user),
);
}
}
// Screen
class UserScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final userState = ref.watch(userNotifierProvider);
return Scaffold(
body: userState.when(
data: (user) => Text('Hello ${user?.name}'),
loading: () => const CircularProgressIndicator(),
error: (e, _) => Text('Error: $e'),
),
);
}
}
Code Generation
# Generate all files
dart run build_runner build --delete-conflicting-outputs
# Watch mode
dart run build_runner watch --delete-conflicting-outputs
Best Practices
DO:
- Keep domain entities pure (no external dependencies)
- Use freezed with
sealedkeyword for immutable data classes - Handle all error cases with Either<Failure, T>
- Use riverpod_generator with unified
Reftype - Separate models (data) from entities (domain)
- Place business logic in use cases, not in widgets
- Use Retrofit for type-safe API calls
- Handle DioException in repositories with NetworkExceptions
- Use interceptors for cross-cutting concerns (auth, logging)
DON'T:
- Import Flutter/HTTP libraries in domain layer
- Mix presentation logic with business logic
- Use try-catch directly in widgets when using Either
- Create god objects or god providers
- Skip the repository pattern
- Use legacy
XxxReftypes in new code
Common Issues
| Issue | Solution |
|---|---|
| Build runner conflicts | dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs |
| Provider not found | Ensure generated files are imported and run build_runner |
| Either not unwrapping | Use fold(), match(), or getOrElse() to extract values |
XxxRef not found |
Use unified Ref type instead (Riverpod 3.x+) |
sealed keyword error |
Upgrade to Dart 3.3+ and Freezed 3.0+ |
.map / .when not found |
Freezed 3.0+ removed these methods. Use Dart 3 switch expression pattern matching instead |
Knowledge References
Primary Libraries (used in this skill):
- Flutter 3.19+: Latest framework features
- Dart 3.3+: Language features (patterns, records,
sealedmodifier) - Riverpod 3.0+: State management with unified
Reftype - Dio 5.9+: HTTP client with interceptors
- Retrofit 4.9+: Type-safe REST API code generation
- freezed 3.0+: Immutable data classes with code generation
- json_serializable 6.x: JSON serialization
- go_router 14.x+: Declarative routing
- fpdart: Functional error handling with Either type
References
- quick_start.md - Step-by-step feature creation workflow
- data_layer.md - Models, Retrofit API services, Repositories
- presentation_layer.md - Providers, Screens, Widgets patterns
- network_setup.md - Dio provider, Interceptors, Network exceptions
- error_handling.md - Either patterns, Failure types, Error strategies
- retrofit_patterns.md - Complete Retrofit API request patterns