agentsclimarketplace

Flutter clean arch

Skill duckyman-ai/agent-skills/skills/flutter-clean-arch

πŸ¦† Ducky's agent skills collection

Install
npx -y skills add duckyman-ai/agent-skills --skill flutter-clean-arch

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Build Flutter apps with Clean Architecture β€” feature-first structure, Riverpod 3.0+ state management, Dio + Retrofit networking, and fpdart functional error handling. Use this skill whenever you are working on a Flutter project that involves creating features, setting up project architecture, building API integrations, managing state, or structuring code with domain/data/presentation layers. This includes tasks like "create a Flutter feature", "set up Riverpod providers", "add an API service", "build a Flutter app", "clean architecture Flutter", "feature-first Flutter", or any Flutter code involving Dio, Retrofit, fpdart Either, or freezed data classes. Also use when scaffolding new Flutter projects, migrating from MVC/MVVM to clean architecture, or adding networking layers to existing Flutter apps.

SKILL.md

10.8 KB, as published. Nobody here has run it

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

Required: Riverpod 3.0+ & Freezed 3.0+ β€” outdated patterns cause compile errors and hallucination loops.

  • Riverpod: use Ref ref (unified), NOT XxxRef ref
  • Freezed: use sealed class for union types, abstract class for single constructors
  • Pattern matching: use Dart 3 switch, NOT .map()/.when()
// Riverpod 3.x+
@riverpod
SomeType someType(Ref ref) { ... }

// Freezed 3.x+ β€” union type
@freezed
sealed class Result with _$Result { ... }

// Freezed 3.x+ β€” pattern matching
final res = switch (model) {
  First(:final a) => 'first $a',
  Second(:final b) => 'second $b',
};

Requires Dart 3.3+, Riverpod 3.0+, Freezed 3.0+. See migration_guide.md for full before/after examples.

Error Handling: fpdart's Either<Failure, T> for functional error handling

Networking: Dio + Retrofit for type-safe REST API calls

Always use the latest stable versions of all libraries. Before adding dependencies, check pub.dev for the current versions of Riverpod, Freezed, Dio, Retrofit, fpdart, and go_router. Never default to outdated major versions (e.g. Riverpod 2.x, Freezed 2.x).

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

Build features in this order: Domain β†’ Data β†’ Presentation.

1. Domain Layer

@freezed
sealed class User with _$User {
  const factory User({required String id, required String name, required String email}) = _User;
}

abstract class UserRepository {
  Future<Either<Failure, User>> getUser(String id);
}

class GetUser {
  final UserRepository repository;
  GetUser(this.repository);
  Future<Either<Failure, User>> call(String id) => repository.getUser(id);
}

2. Data Layer

@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);
}

@RestApi()
abstract class UserApiService {
  factory UserApiService(Dio dio) = _UserApiService;
  @GET('/users/{id}')
  Future<UserModel> getUser(@Path('id') String id);
}

3. Presentation Layer

@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),
    );
  }
}

See quick_start.md for the complete step-by-step workflow with all files.

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 sealed keyword for immutable data classes
  • Handle all error cases with Either<Failure, T>
  • Use riverpod_generator with unified Ref type
  • 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)
  • Validate API response data at the repository boundary
  • Use CachedNetworkImage instead of Image.network for external images
  • Pin API base URLs in config, enforce HTTPS

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 XxxRef types in new code
  • Pass raw external URLs to widgets without validation
  • Allow runtime-configuration of API base URLs from user input

Security

API responses and external content are untrusted input. Validate and sanitize at the data layer boundary to prevent injection and data corruption.

Response validation β€” Always validate API response structure before mapping to models. Retrofit + freezed handle typed deserialization, but wrap calls in try-catch and verify critical fields (IDs, URLs, numeric ranges) in the repository:

@override
Future<Either<Failure, User>> getUser(String id) async {
  try {
    final userModel = await apiService.getUser(id);
    if (userModel.id.isEmpty) {
      return const Left(Failure.validation('Invalid user data'));
    }
    return Right(userModel.toEntity());
  } on DioException catch (e) {
    return Left(Failure.network(NetworkExceptions.fromDioError(e).message));
  }
}

External URLs β€” Never pass raw API URLs directly to Image.network or WebView. Use CachedNetworkImage with error handling, and validate URL schemes:

CachedNetworkImage(
  imageUrl: user.avatarUrl ?? '',
  errorWidget: (_, __, ___) => const Icon(Icons.person),
  httpHeaders: {'Authorization': 'Bearer $token'},
)

Input sanitization β€” Sanitize user inputs before sending to API. Validate email format, trim strings, reject empty IDs, and encode query parameters properly.

Network hardening β€” Pin base URLs in AppConfig (not user-configurable at runtime), enforce HTTPS, use certificate pinning for production, and set conservative timeouts.

Read network_setup.md for the full interceptor setup and data_layer.md for validation patterns at the repository boundary.

Common Issues

IssueSolution
Build runner conflictsdart run build_runner clean && dart run build_runner build --delete-conflicting-outputs
Provider not foundEnsure generated files are imported and run build_runner
Either not unwrappingUse fold(), match(), or getOrElse() to extract values
XxxRef not foundUse unified Ref type instead (Riverpod 3.x+)
sealed keyword errorUpgrade to Dart 3.3+ and Freezed 3.0+
.map / .when not foundFreezed 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, sealed modifier)
  • Riverpod 3.0+: State management with unified Ref type
  • 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

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.