Agent skill
clean-architecture-altrupets-monorepo
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/clean-architecture-altrupets-monorepo
SKILL.md
ποΈ Skill: Clean Architecture
π Metadata
| Atributo | Valor |
|---|---|
| ID | flutter-clean-architecture |
| Nivel | π΄ Avanzado |
| VersiΓ³n | 1.0.0 |
| Keywords | clean, clean-arch, clean-architecture, bloc, usecase, repository, ddd |
π Keywords para InvocaciΓ³n
Usa cualquiera de estos keywords en tus prompts para invocar este skill:
cleanclean-archclean-architectureblocusecaserepository-patternddd(Domain-Driven Design)@skill:clean-architecture
Ejemplos de Prompts
Crea un mΓ³dulo de autenticaciΓ³n usando clean-architecture
Implementa clean-arch para un sistema de e-commerce con bloc
@skill:clean-architecture - Genera feature de productos con repository-pattern
π DescripciΓ³n
β οΈ IMPORTANTE: Todos los comandos de este skill deben ejecutarse desde la raΓz del proyecto (donde existe el directorio mobile/). El skill incluye verificaciones para asegurar que se estΓ‘ en el directorio correcto antes de ejecutar cualquier comando.
Clean Architecture organiza el cΓ³digo en capas con dependencias unidireccionales, facilitando la independencia del framework, testabilidad y mantenibilidad a largo plazo. Ideal para aplicaciones enterprise y proyectos escalables.
β CuΓ‘ndo Usar Este Skill
- Aplicaciones enterprise grandes y complejas
- Proyectos que requieren mΓ‘xima testabilidad
- Equipos grandes con mΓΊltiples desarrolladores
- Necesitas independencia del framework
- Planeas mantener el proyecto por aΓ±os
- Requieres mΓΊltiples fuentes de datos (API, cachΓ©, etc.)
β CuΓ‘ndo NO Usar Este Skill
- Prototipos rΓ‘pidos o MVPs
- Proyectos pequeΓ±os con lΓ³gica simple
- Equipos sin experiencia en arquitecturas complejas
- Deadlines muy ajustados
ποΈ Estructura del Proyecto
lib/
βββ core/
β βββ error/
β β βββ exceptions.dart # Excepciones personalizadas
β β βββ failures.dart # Failures para manejo de errores
β βββ usecases/
β β βββ usecase.dart # Clase base para UseCases
β βββ network/
β β βββ network_info.dart # VerificaciΓ³n de conectividad
β βββ utils/
β β βββ constants.dart
β β βββ validators.dart
β β βββ input_converter.dart
β βββ theme/
β βββ app_theme.dart
β
βββ features/
β βββ [feature_name]/ # ej: authentication, products
β β
β βββ data/ # πΎ CAPA DE DATOS
β β βββ datasources/
β β β βββ feature_remote_datasource.dart
β β β βββ feature_local_datasource.dart
β β βββ models/
β β β βββ feature_model.dart
β β βββ repositories/
β β βββ feature_repository_impl.dart
β β
β βββ domain/ # π― CAPA DE DOMINIO
β β βββ entities/
β β β βββ feature_entity.dart
β β βββ repositories/
β β β βββ feature_repository.dart
β β βββ usecases/
β β βββ get_feature.dart
β β βββ create_feature.dart
β β βββ update_feature.dart
β β βββ delete_feature.dart
β β
β βββ presentation/ # π¨ CAPA DE PRESENTACIΓN
β βββ bloc/
β β βββ feature_bloc.dart
β β βββ feature_event.dart
β β βββ feature_state.dart
β βββ pages/
β β βββ feature_list_page.dart
β β βββ feature_detail_page.dart
β βββ widgets/
β βββ feature_card.dart
β βββ feature_form.dart
β
βββ injection_container.dart # π Dependency Injection
π§© Capas de la Arquitectura
1. Domain Layer (Capa de Dominio) π―
La capa mΓ‘s interna, contiene la lΓ³gica de negocio pura e independiente del framework.
Entities (Entidades)
// domain/entities/user_entity.dart
import 'package:equatable/equatable.dart';
class UserEntity extends Equatable {
final String id;
final String name;
final String email;
final String? avatar;
final DateTime createdAt;
const UserEntity({
required this.id,
required this.name,
required this.email,
this.avatar,
required this.createdAt,
});
@override
List<Object?> get props => [id, name, email, avatar, createdAt];
}
Repositories (Interfaces)
// domain/repositories/user_repository.dart
import 'package:dartz/dartz.dart';
import '../../core/error/failures.dart';
import '../entities/user_entity.dart';
abstract class UserRepository {
Future<Either<Failure, UserEntity>> getUser(String id);
Future<Either<Failure, List<UserEntity>>> getUsers();
Future<Either<Failure, UserEntity>> createUser(UserEntity user);
Future<Either<Failure, UserEntity>> updateUser(UserEntity user);
Future<Either<Failure, void>> deleteUser(String id);
}
Use Cases (Casos de Uso)
// domain/usecases/get_user.dart
import 'package:dartz/dartz.dart';
import '../../core/error/failures.dart';
import '../../core/usecases/usecase.dart';
import '../entities/user_entity.dart';
import '../repositories/user_repository.dart';
class GetUser implements UseCase<UserEntity, String> {
final UserRepository repository;
GetUser(this.repository);
@override
Future<Either<Failure, UserEntity>> call(String userId) async {
return await repository.getUser(userId);
}
}
// core/usecases/usecase.dart
import 'package:dartz/dartz.dart';
import '../error/failures.dart';
abstract class UseCase<Type, Params> {
Future<Either<Failure, Type>> call(Params params);
}
class NoParams {}
2. Data Layer (Capa de Datos) πΎ
Implementa las interfaces definidas en la capa de dominio.
Models
// data/models/user_model.dart
import '../../domain/entities/user_entity.dart';
class UserModel extends UserEntity {
const UserModel({
required String id,
required String name,
required String email,
String? avatar,
required DateTime createdAt,
}) : super(
id: id,
name: name,
email: email,
avatar: avatar,
createdAt: createdAt,
);
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json['id'],
name: json['name'],
email: json['email'],
avatar: json['avatar'],
createdAt: DateTime.parse(json['createdAt']),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'avatar': avatar,
'createdAt': createdAt.toIso8601String(),
};
}
factory UserModel.fromEntity(UserEntity entity) {
return UserModel(
id: entity.id,
name: entity.name,
email: entity.email,
avatar: entity.avatar,
createdAt: entity.createdAt,
);
}
}
Data Sources
// data/datasources/user_remote_datasource.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../core/error/exceptions.dart';
import '../models/user_model.dart';
abstract class UserRemoteDataSource {
/// Calls the https://api.example.com/users/{id} endpoint.
///
/// Throws a [ServerException] for all error codes.
Future<UserModel> getUser(String id);
Future<List<UserModel>> getUsers();
Future<UserModel> createUser(UserModel user);
Future<UserModel> updateUser(UserModel user);
Future<void> deleteUser(String id);
}
class UserRemoteDataSourceImpl implements UserRemoteDataSource {
final http.Client client;
final String baseUrl;
UserRemoteDataSourceImpl({
required this.client,
required this.baseUrl,
});
@override
Future<UserModel> getUser(String id) async {
final response = await client.get(
Uri.parse('$baseUrl/users/$id'),
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
return UserModel.fromJson(json.decode(response.body));
} else {
throw ServerException();
}
}
@override
Future<List<UserModel>> getUsers() async {
final response = await client.get(
Uri.parse('$baseUrl/users'),
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
final List<dynamic> jsonList = json.decode(response.body);
return jsonList.map((json) => UserModel.fromJson(json)).toList();
} else {
throw ServerException();
}
}
@override
Future<UserModel> createUser(UserModel user) async {
final response = await client.post(
Uri.parse('$baseUrl/users'),
headers: {'Content-Type': 'application/json'},
body: json.encode(user.toJson()),
);
if (response.statusCode == 201) {
return UserModel.fromJson(json.decode(response.body));
} else {
throw ServerException();
}
}
@override
Future<UserModel> updateUser(UserModel user) async {
final response = await client.put(
Uri.parse('$baseUrl/users/${user.id}'),
headers: {'Content-Type': 'application/json'},
body: json.encode(user.toJson()),
);
if (response.statusCode == 200) {
return UserModel.fromJson(json.decode(response.body));
} else {
throw ServerException();
}
}
@override
Future<void> deleteUser(String id) async {
final response = await client.delete(
Uri.parse('$baseUrl/users/$id'),
);
if (response.statusCode != 204) {
throw ServerException();
}
}
}
// data/datasources/user_local_datasource.dart
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../../core/error/exceptions.dart';
import '../models/user_model.dart';
abstract class UserLocalDataSource {
Future<UserModel> getCachedUser(String id);
Future<List<UserModel>> getCachedUsers();
Future<void> cacheUser(UserModel user);
Future<void> cacheUsers(List<UserModel> users);
}
class UserLocalDataSourceImpl implements UserLocalDataSource {
final SharedPreferences sharedPreferences;
static const CACHED_USER = 'CACHED_USER_';
static const CACHED_USERS = 'CACHED_USERS';
UserLocalDataSourceImpl({required this.sharedPreferences});
@override
Future<UserModel> getCachedUser(String id) {
final jsonString = sharedPreferences.getString('$CACHED_USER$id');
if (jsonString != null) {
return Future.value(UserModel.fromJson(json.decode(jsonString)));
} else {
throw CacheException();
}
}
@override
Future<List<UserModel>> getCachedUsers() {
final jsonString = sharedPreferences.getString(CACHED_USERS);
if (jsonString != null) {
final List<dynamic> jsonList = json.decode(jsonString);
return Future.value(
jsonList.map((json) => UserModel.fromJson(json)).toList(),
);
} else {
throw CacheException();
}
}
@override
Future<void> cacheUser(UserModel user) {
return sharedPreferences.setString(
'$CACHED_USER${user.id}',
json.encode(user.toJson()),
);
}
@override
Future<void> cacheUsers(List<UserModel> users) {
return sharedPreferences.setString(
CACHED_USERS,
json.encode(users.map((user) => user.toJson()).toList()),
);
}
}
Repository Implementation
// data/repositories/user_repository_impl.dart
import 'package:dartz/dartz.dart';
import '../../core/error/exceptions.dart';
import '../../core/error/failures.dart';
import '../../core/network/network_info.dart';
import '../../domain/entities/user_entity.dart';
import '../../domain/repositories/user_repository.dart';
import '../datasources/user_local_datasource.dart';
import '../datasources/user_remote_datasource.dart';
import '../models/user_model.dart';
class UserRepositoryImpl implements UserRepository {
final UserRemoteDataSource remoteDataSource;
final UserLocalDataSource localDataSource;
final NetworkInfo networkInfo;
UserRepositoryImpl({
required this.remoteDataSource,
required this.localDataSource,
required this.networkInfo,
});
@override
Future<Either<Failure, UserEntity>> getUser(String id) async {
if (await networkInfo.isConnected) {
try {
final user = await remoteDataSource.getUser(id);
await localDataSource.cacheUser(user);
return Right(user);
} on ServerException {
return Left(ServerFailure());
}
} else {
try {
final user = await localDataSource.getCachedUser(id);
return Right(user);
} on CacheException {
return Left(CacheFailure());
}
}
}
@override
Future<Either<Failure, List<UserEntity>>> getUsers() async {
if (await networkInfo.isConnected) {
try {
final users = await remoteDataSource.getUsers();
await localDataSource.cacheUsers(users);
return Right(users);
} on ServerException {
return Left(ServerFailure());
}
} else {
try {
final users = await localDataSource.getCachedUsers();
return Right(users);
} on CacheException {
return Left(CacheFailure());
}
}
}
@override
Future<Either<Failure, UserEntity>> createUser(UserEntity user) async {
if (await networkInfo.isConnected) {
try {
final userModel = UserModel.fromEntity(user);
final createdUser = await remoteDataSource.createUser(userModel);
return Right(createdUser);
} on ServerException {
return Left(ServerFailure());
}
} else {
return Left(NetworkFailure());
}
}
@override
Future<Either<Failure, UserEntity>> updateUser(UserEntity user) async {
if (await networkInfo.isConnected) {
try {
final userModel = UserModel.fromEntity(user);
final updatedUser = await remoteDataSource.updateUser(userModel);
await localDataSource.cacheUser(updatedUser);
return Right(updatedUser);
} on ServerException {
return Left(ServerFailure());
}
} else {
return Left(NetworkFailure());
}
}
@override
Future<Either<Failure, void>> deleteUser(String id) async {
if (await networkInfo.isConnected) {
try {
await remoteDataSource.deleteUser(id);
return const Right(null);
} on ServerException {
return Left(ServerFailure());
}
} else {
return Left(NetworkFailure());
}
}
}
3. Presentation Layer (Capa de PresentaciΓ³n) π¨
Maneja la UI y la gestiΓ³n de estado usando BLoC.
BLoC Pattern
// presentation/bloc/user_event.dart
import 'package:equatable/equatable.dart';
import '../../domain/entities/user_entity.dart';
abstract class UserEvent extends Equatable {
const UserEvent();
@override
List<Object?> get props => [];
}
class GetUsersEvent extends UserEvent {}
class GetUserEvent extends UserEvent {
final String userId;
const GetUserEvent(this.userId);
@override
List<Object> get props => [userId];
}
class CreateUserEvent extends UserEvent {
final UserEntity user;
const CreateUserEvent(this.user);
@override
List<Object> get props => [user];
}
class UpdateUserEvent extends UserEvent {
final UserEntity user;
const UpdateUserEvent(this.user);
@override
List<Object> get props => [user];
}
class DeleteUserEvent extends UserEvent {
final String userId;
const DeleteUserEvent(this.userId);
@override
List<Object> get props => [userId];
}
// presentation/bloc/user_state.dart
import 'package:equatable/equatable.dart';
import '../../domain/entities/user_entity.dart';
abstract class UserState extends Equatable {
const UserState();
@override
List<Object?> get props => [];
}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UsersLoaded extends UserState {
final List<UserEntity> users;
const UsersLoaded(this.users);
@override
List<Object> get props => [users];
}
class UserLoaded extends UserState {
final UserEntity user;
const UserLoaded(this.user);
@override
List<Object> get props => [user];
}
class UserOperationSuccess extends UserState {
final String message;
const UserOperationSuccess(this.message);
@override
List<Object> get props => [message];
}
class UserError extends UserState {
final String message;
const UserError(this.message);
@override
List<Object> get props => [message];
}
// presentation/bloc/user_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/usecases/get_user.dart';
import '../../domain/usecases/get_users.dart';
import '../../domain/usecases/create_user.dart';
import '../../domain/usecases/update_user.dart';
import '../../domain/usecases/delete_user.dart';
import 'user_event.dart';
import 'user_state.dart';
class UserBloc extends Bloc<UserEvent, UserState> {
final GetUser getUser;
final GetUsers getUsers;
final CreateUser createUser;
final UpdateUser updateUser;
final DeleteUser deleteUser;
UserBloc({
required this.getUser,
required this.getUsers,
required this.createUser,
required this.updateUser,
required this.deleteUser,
}) : super(UserInitial()) {
on<GetUsersEvent>(_onGetUsers);
on<GetUserEvent>(_onGetUser);
on<CreateUserEvent>(_onCreateUser);
on<UpdateUserEvent>(_onUpdateUser);
on<DeleteUserEvent>(_onDeleteUser);
}
Future<void> _onGetUsers(
GetUsersEvent event,
Emitter<UserState> emit,
) async {
emit(UserLoading());
final result = await getUsers(NoParams());
result.fold(
(failure) => emit(UserError(_mapFailureToMessage(failure))),
(users) => emit(UsersLoaded(users)),
);
}
Future<void> _onGetUser(
GetUserEvent event,
Emitter<UserState> emit,
) async {
emit(UserLoading());
final result = await getUser(event.userId);
result.fold(
(failure) => emit(UserError(_mapFailureToMessage(failure))),
(user) => emit(UserLoaded(user)),
);
}
Future<void> _onCreateUser(
CreateUserEvent event,
Emitter<UserState> emit,
) async {
emit(UserLoading());
final result = await createUser(event.user);
result.fold(
(failure) => emit(UserError(_mapFailureToMessage(failure))),
(_) => emit(const UserOperationSuccess('Usuario creado exitosamente')),
);
}
Future<void> _onUpdateUser(
UpdateUserEvent event,
Emitter<UserState> emit,
) async {
emit(UserLoading());
final result = await updateUser(event.user);
result.fold(
(failure) => emit(UserError(_mapFailureToMessage(failure))),
(_) => emit(const UserOperationSuccess('Usuario actualizado exitosamente')),
);
}
Future<void> _onDeleteUser(
DeleteUserEvent event,
Emitter<UserState> emit,
) async {
emit(UserLoading());
final result = await deleteUser(event.userId);
result.fold(
(failure) => emit(UserError(_mapFailureToMessage(failure))),
(_) => emit(const UserOperationSuccess('Usuario eliminado exitosamente')),
);
}
String _mapFailureToMessage(Failure failure) {
switch (failure.runtimeType) {
case ServerFailure:
return 'Error del servidor. Por favor intenta de nuevo.';
case CacheFailure:
return 'Error al cargar datos locales.';
case NetworkFailure:
return 'Sin conexiΓ³n a internet.';
default:
return 'Error inesperado. Por favor intenta de nuevo.';
}
}
}
Pages
// presentation/pages/user_list_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/user_bloc.dart';
import '../bloc/user_event.dart';
import '../bloc/user_state.dart';
import '../widgets/user_card.dart';
class UserListPage extends StatelessWidget {
const UserListPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Usuarios'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
context.read<UserBloc>().add(GetUsersEvent());
},
),
],
),
body: BlocBuilder<UserBloc, UserState>(
builder: (context, state) {
if (state is UserInitial) {
// Cargar usuarios al iniciar
context.read<UserBloc>().add(GetUsersEvent());
return const Center(child: CircularProgressIndicator());
} else if (state is UserLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is UsersLoaded) {
if (state.users.isEmpty) {
return const Center(
child: Text('No hay usuarios disponibles'),
);
}
return ListView.builder(
itemCount: state.users.length,
itemBuilder: (context, index) {
return UserCard(user: state.users[index]);
},
);
} else if (state is UserError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(state.message),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
context.read<UserBloc>().add(GetUsersEvent());
},
child: const Text('Reintentar'),
),
],
),
);
}
return Container();
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Navegar a pΓ‘gina de creaciΓ³n
},
child: const Icon(Icons.add),
),
);
}
}
π Dependency Injection
// injection_container.dart
import 'package:get_it/get_it.dart';
import 'package:http/http.dart' as http;
import 'package:internet_connection_checker/internet_connection_checker.dart';
import 'package:shared_preferences/shared_preferences.dart';
final sl = GetIt.instance;
Future<void> init() async {
//! Features - User
// Bloc
sl.registerFactory(
() => UserBloc(
getUser: sl(),
getUsers: sl(),
createUser: sl(),
updateUser: sl(),
deleteUser: sl(),
),
);
// Use cases
sl.registerLazySingleton(() => GetUser(sl()));
sl.registerLazySingleton(() => GetUsers(sl()));
sl.registerLazySingleton(() => CreateUser(sl()));
sl.registerLazySingleton(() => UpdateUser(sl()));
sl.registerLazySingleton(() => DeleteUser(sl()));
// Repository
sl.registerLazySingleton<UserRepository>(
() => UserRepositoryImpl(
remoteDataSource: sl(),
localDataSource: sl(),
networkInfo: sl(),
),
);
// Data sources
sl.registerLazySingleton<UserRemoteDataSource>(
() => UserRemoteDataSourceImpl(
client: sl(),
baseUrl: 'https://api.example.com',
),
);
sl.registerLazySingleton<UserLocalDataSource>(
() => UserLocalDataSourceImpl(sharedPreferences: sl()),
);
//! Core
sl.registerLazySingleton<NetworkInfo>(
() => NetworkInfoImpl(sl()),
);
//! External
final sharedPreferences = await SharedPreferences.getInstance();
sl.registerLazySingleton(() => sharedPreferences);
sl.registerLazySingleton(() => http.Client());
sl.registerLazySingleton(() => InternetConnectionChecker());
}
π¦ Dependencias Recomendadas
dependencies:
flutter:
sdk: flutter
# State Management
flutter_bloc: ^8.1.3
# Dependency Injection
get_it: ^7.6.4
# Functional Programming
dartz: ^0.10.1
# Remote
http: ^1.1.0
# Local Storage
shared_preferences: ^2.2.2
hive: ^2.2.3
hive_flutter: ^1.1.0
# Network
internet_connection_checker: ^1.0.0+1
# Utilities
equatable: ^2.0.5
dev_dependencies:
flutter_test:
sdk: flutter
mockito: ^5.4.4
mocktail: ^1.0.1
bloc_test: ^9.1.5
hive_generator: ^2.0.1
build_runner: ^2.4.6
π Flujo de Datos
ββββββββββββββββ
β User β
β Interaction β
ββββββββ¬ββββββββ
β
βΌ
ββββββββββββββββ
β Presentation β
β (BLoC) β
ββββββββ¬ββββββββ
β
βΌ
ββββββββββββββββ
β Domain β
β (UseCase) β
ββββββββ¬ββββββββ
β
βΌ
ββββββββββββββββ
β Data β
β (Repository) β
ββββββββ¬ββββββββ
β
βββββββββββββββ¬ββββββββββββββ
βΌ βΌ βΌ
ββββββββββ ββββββββββ ββββββββββ
β Remote β β Local β βNetwork β
β API β β Cache β β Info β
ββββββββββ ββββββββββ ββββββββββ
π§ͺ Testing
Ver documentaciΓ³n completa de testing en cada archivo del skill.
β Principios SOLID
- Single Responsibility: Cada clase tiene una ΓΊnica responsabilidad
- Open/Closed: Abierto para extensiΓ³n, cerrado para modificaciΓ³n
- Liskov Substitution: Las implementaciones pueden sustituir a las abstracciones
- Interface Segregation: Interfaces especΓficas en lugar de generales
- Dependency Inversion: Dependencia de abstracciones, no de implementaciones
π Recursos Adicionales
Γltima actualizaciΓ³n: Diciembre 2025 VersiΓ³n: 1.0.0
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?