skills/flutter-clean-arch/SKILL.md
Generate Flutter applications using Clean Architecture with feature-first structure, Riverpod state management, Dio + Retrofit for networking, and fpdart error handling. Use this skill when creating Flutter apps, implementing features with clean architecture patterns, setting up Riverpod providers, handling data with Either type for functional error handling, making HTTP requests with type-safe API clients, or structuring projects with domain/data/presentation layers. Triggers include "Flutter app", "clean architecture", "Riverpod", "feature-first", "state management", "API client", "Retrofit", "Dio", "REST API", or requests to build Flutter features with separation of concerns.
npx skillsauth add duckyman-ai/agent-skills flutter-clean-archInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
Architecture: Clean Architecture (Feature-First)
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):
@riverpod SomeType someType(SomeTypeRef ref) { ... }Riverpod 3.x+ (Current):
@riverpod 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 Person|abstract class Person| | Union type (multiple constructors) |class Result|sealed class Result|Freezed 2.x (Legacy) - Single Constructor:
@freezed class Person with _$Person { const factory Person({ required String firstName, required String lastName, }) = _Person; }Freezed 3.x+ (Current) - Single Constructor:
@freezed abstract class Person with _$Person { const factory Person({ required String firstName, required String lastName, }) = _Person; }Freezed 2.x (Legacy) - Union Type:
@freezed class Result with _$Result { const factory Result.success(String data) = Success; const factory Result.error(String message) = Error; }Freezed 3.x+ (Current) - Union Type:
@freezed 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
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
// Entity
@freezed
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);
}
// Model with JSON serialization
@freezed
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
@RestApi()
abstract class UserApiService {
factory UserApiService(Dio dio) = _UserApiService;
@GET('/users/{id}')
Future<UserModel> getUser(@Path('id') String id);
}
// Repository Implementation
class UserRepositoryImpl implements UserRepository {
final UserApiService apiService;
@override
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));
}
}
}
// Provider
@riverpod
UserApiService userApiService(Ref ref) {
return UserApiService(ref.watch(dioProvider));
}
@riverpod
UserRepositoryImpl userRepository(Ref ref) {
return UserRepositoryImpl(ref.watch(userApiServiceProvider));
}
@riverpod
class UserNotifier extends _$UserNotifier {
@override
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 {
@override
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'),
),
);
}
}
# Generate all files
dart run build_runner build --delete-conflicting-outputs
# Watch mode
dart run build_runner watch --delete-conflicting-outputs
DO:
sealed keyword for immutable data classesRef typeDON'T:
XxxRef types in new code| 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 |
Primary Libraries (used in this skill):
sealed modifier)Ref typedevelopment
Generate conventional git commit messages following Angular convention format. Use this skill when creating commits, writing commit messages, or reviewing git history. Triggers include "git commit", "commit message", "changelog", or requests to version control changes.
tools
Use when work should span one or more detached tasks but still behave like one job with a single owner context. TaskFlow is the durable flow substrate under authoring layers like Lobster, ACPX, plugins, or plain code. Keep conditional logic in the caller; use TaskFlow for flow identity, child-task linkage, waiting state, revision-checked mutations, and user-facing emergence.
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------