# Clean Architecture
## Introducción
Clean Architecture es un patrón arquitectónico propuesto por Robert C. Martin (Uncle Bob) que busca crear
sistemas que sean:
- **Independientes de frameworks**
- **Testeables**
- **Independientes de la UI**
- **Independientes de la base de datos**
- **Independientes de cualquier agente externo**
## Diagrama de Capas
```mermaid
graph TD
subgraph "Frameworks & Drivers (External)"
UI[UI / Web / DB]
end
subgraph "Interface Adapters"
Controllers[Controllers / Presenters / Gateways]
end
subgraph "Application Business Rules"
UseCases[Use Cases]
end
subgraph "Enterprise Business Rules"
Entities[Entities]
end
UI --> Controllers
Controllers --> UseCases
UseCases --> Entities
style Entities fill:#f9f,stroke:#333,stroke-width:2px
style UseCases fill:#bbf,stroke:#333,stroke-width:2px
style Controllers fill:#dfd,stroke:#333,stroke-width:2px
style UI fill:#eef,stroke:#333,stroke-width:2px
```
## Principios Fundamentales
### 1. La Regla de Dependencia
> Las dependencias del código fuente deben apuntar solo hacia adentro, hacia políticas de más alto nivel.
```dart
// ❌ INCORRECTO: La capa de dominio depende de la infraestructura
class UserRepository {
final HttpClient client; // ¡Dependencia externa!
Future getUser(String id) async {
final response = await client.get('/users/$id');
return User.fromJson(response.data);
}
}
// ✅ CORRECTO: Inversión de dependencias
abstract class UserRepository {
Future getUser(String id);
}
class UserRepositoryImpl implements UserRepository {
final HttpClient client;
UserRepositoryImpl(this.client);
@override
Future getUser(String id) async {
final response = await client.get('/users/$id');
return User.fromJson(response.data);
}
}
```
### 2. Capas de Clean Architecture
#### Capa de Dominio (Entities)
Contiene la lógica de negocio empresarial pura.
```dart
// domain/entities/user.dart
class User {
final String id;
final String name;
final String email;
User({
required this.id,
required this.name,
required this.email,
});
// Lógica de negocio pura
bool get hasValidEmail => email.contains('@') && email.contains('.');
String get displayName => name.isEmpty ? email : name;
}
```
#### Capa de Casos de Uso (Use Cases)
Contiene la lógica de negocio de la aplicación.
```dart
// domain/usecases/get_user.dart
class GetUser {
final UserRepository repository;
GetUser(this.repository);
Future> call(String userId) async {
try {
final user = await repository.getUser(userId);
return Right(user);
} on ServerException {
return Left(ServerFailure());
}
}
}
```
#### Capa de Adaptadores (Data)
Implementa las interfaces definidas en el dominio.
```dart
// data/repositories/user_repository_impl.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 getUser(String id) async {
if (await networkInfo.isConnected) {
final userModel = await remoteDataSource.getUser(id);
await localDataSource.cacheUser(userModel);
return userModel.toEntity();
} else {
final cachedUser = await localDataSource.getCachedUser(id);
return cachedUser.toEntity();
}
}
}
```
#### Capa de Presentación (UI)
Maneja la interfaz de usuario y la lógica de presentación.
```dart
// presentation/bloc/user_bloc.dart
class UserBloc extends Bloc {
final GetUser getUser;
UserBloc({required this.getUser}) : super(UserInitial()) {
on(_onGetUserRequested);
}
Future _onGetUserRequested(
GetUserRequested event,
Emitter emit,
) async {
emit(UserLoading());
final result = await getUser(event.userId);
result.fold(
(failure) => emit(UserError(message: failure.message)),
(user) => emit(UserLoaded(user: user)),
);
}
}
```
## Estructura de Carpetas en Flutter
```
lib/
├── core/
│ ├── error/
│ │ ├── exceptions.dart
│ │ └── failures.dart
│ ├── network/
│ │ └── network_info.dart
│ └── usecases/
│ └── usecase.dart
├── features/
│ └── user/
│ ├── data/
│ │ ├── datasources/
│ │ │ ├── user_local_data_source.dart
│ │ │ └── user_remote_data_source.dart
│ │ ├── models/
│ │ │ └── user_model.dart
│ │ └── repositories/
│ │ └── user_repository_impl.dart
│ ├── domain/
│ │ ├── entities/
│ │ │ └── user.dart
│ │ ├── repositories/
│ │ │ └── user_repository.dart
│ │ └── usecases/
│ │ └── get_user.dart
│ └── presentation/
│ ├── bloc/
│ │ ├── user_bloc.dart
│ │ ├── user_event.dart
│ │ └── user_state.dart
│ ├── pages/
│ │ └── user_page.dart
│ └── widgets/
│ └── user_card.dart
└── main.dart
```
## Beneficios en Flutter
### ✅ Testabilidad Mejorada
```dart
// test/features/user/domain/usecases/get_user_test.dart
void main() {
late GetUser usecase;
late MockUserRepository mockRepository;
setUp(() {
mockRepository = MockUserRepository();
usecase = GetUser(mockRepository);
});
test('should get user from repository', () async {
// Arrange
final tUser = User(id: '1', name: 'Test', email: '[email protected]');
when(mockRepository.getUser('1'))
.thenAnswer((_) async => tUser);
// Act
final result = await usecase('1');
// Assert
expect(result, Right(tUser));
verify(mockRepository.getUser('1'));
verifyNoMoreInteractions(mockRepository);
});
}
```
### ✅ Mantenibilidad
El cambio de un framework (ej: cambiar de dio a http) solo afecta a la capa de datos:
```dart
// Solo necesitas cambiar la implementación del data source
class UserRemoteDataSourceImpl implements UserRemoteDataSource {
// Cambiar de Dio a http package
final http.Client client; // Antes: final Dio dio;
UserRemoteDataSourceImpl(this.client);
@override
Future getUser(String id) async {
final response = await client.get(
Uri.parse('$baseUrl/users/$id'),
);
if (response.statusCode == 200) {
return UserModel.fromJson(json.decode(response.body));
} else {
throw ServerException();
}
}
}
```
### ✅ Escalabilidad
Agregar nuevas features es simple y no afecta el código existente:
```dart
// Nueva feature: Posts
// Solo replica la estructura en features/posts/
features/
├── user/
└── posts/ // Nueva feature
├── data/
├── domain/
└── presentation/
```
## Mejores Prácticas
1. **Mantén las entidades puras**: Sin dependencias de Flutter
2. **Usa interfaces para todo**: Facilita el testing y el cambio de implementaciones
3. **Separa modelos de entidades**: Los modelos manejan serialización, las entidades lógica
de negocio
4. **Inyección de dependencias**: Usa get_it o injectable
5. **Manejo de errores consistente**: Either o Result types
## ⚠️ Errores Comunes y Soluciones
### Error 1: Dependencia Invertida Incorrectamente
**Código problemático:**
```dart
// domain/usecases/get_user.dart
import 'package:http/http.dart'; // ❌ Dominio depende de infraestructura!
class GetUser {
final Client client;
GetUser(this.client);
Future call(String id) async {
final response = await client.get(Uri.parse('/users/$id'));
return User.fromJson(jsonDecode(response.body));
}
}
```
**Síntoma:** Acoplamiento fuerte entre capas, difícil de testear.
**Causa:** La capa de dominio depende directamente de implementaciones de
infraestructura.
**Solución:**
```dart
// domain/repositories/user_repository.dart
abstract class UserRepository {
Future getUser(String id);
}
// domain/usecases/get_user.dart
class GetUser {
final UserRepository repository; // ✅ Depende de abstracción
GetUser(this.repository);
Future> call(String id) async {
try {
final user = await repository.getUser(id);
return Right(user);
} catch (e) {
return Left(ServerFailure());
}
}
}
// data/repositories/user_repository_impl.dart
class UserRepositoryImpl implements UserRepository {
final Client client; // ✅ Solo en capa de datos
// ...
}
```
---
### Error 2: Lógica de Negocio en Presentación
**Código problemático:**
```dart
// presentation/pages/user_page.dart
class UserPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder(
builder: (context, state) {
if (state is UserLoaded) {
final user = state.user;
// ❌ Validación de negocio en UI!
if (user.age < 18 && user.country=='US' ) { return ErrorWidget('Not allowed'); }
return UserCard(user: user); } return LoadingWidget(); }, ); } } ```
**Síntoma:** Lógica de negocio duplicada, difícil de mantener. **Solución:**
```dart // domain/entities/user.dart class User { final int age; final String
country; // ✅ Lógica de negocio en entidad bool get canAccessContent { if
(country=='US' && age < 18) return false; if (country=='UK' && age < 16) return
false; return true; } } // presentation/pages/user_page.dart Widget
build(BuildContext context) { return BlocBuilder(
builder: (context, state) {
if (state is UserLoaded) {
final user = state.user;
// ✅ Solo usa la lógica, no la define
if (!user.canAccessContent) {
return ErrorWidget('Not allowed');
}
return UserCard(user: user);
}
return LoadingWidget();
},
);
}
```
---
### Error 3: Modelos y Entidades Mezclados
**Código problemático:**
```dart
// domain/entities/user.dart
class User {
final String id;
final String name;
// ❌ Serialización en entidad de dominio!
factory User.fromJson(Map json) {
return User(
id: json['id'],
name: json['name'],
);
}
Map toJson() {
return {'id': id, 'name': name};
}
}
```
**Síntoma:** Entidades acopladas al formato de datos externo.
**Solución:**
```dart
// domain/entities/user.dart
class User {
final String id;
final String name;
User({required this.id, required this.name});
// ✅ Solo lógica de negocio
bool get hasValidName => name.isNotEmpty;
}
// data/models/user_model.dart
class UserModel extends User {
UserModel({required String id, required String name})
: super(id: id, name: name);
// ✅ Serialización en modelo de datos
factory UserModel.fromJson(Map json) {
return UserModel(
id: json['user_id'], // Puede mapear nombres diferentes
name: json['full_name'],
);
}
Map toJson() {
return {'user_id': id, 'full_name': name};
}
// ✅ Conversión a entidad
User toEntity() => User(id: id, name: name);
}
```
## Conclusión
Clean Architecture en Flutter permite crear aplicaciones robustas,
testeables y
mantenibles. Aunque requiere más código inicial, los beneficios a largo
plazo superan
ampliamente el costo inicial.
**Recuerda**: La arquitectura debe servir al proyecto, no al revés. Para
proyectos
pequeños, una versión simplificada puede ser suficiente.
## 📖 Recursos Adicionales
### 📄 Artículos Recomendados
- [Clean Architecture in
Flutter](https://resocoder.com/2019/08/27/flutter-tdd-clean-architecture-course-1-explanation-project-structure/)
- Reso Coder
- [Official Flutter Architecture
Guide](https://docs.flutter.dev/development/data-and-backend/state-mgmt/options)
- Flutter Docs
- [Clean Architecture by Uncle
Bob](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
- Robert C. Martin
### 🎥 Videos
- [Flutter Clean Architecture
Course](https://www.youtube.com/watch?v=KjE2IDphA_U) - Reso Coder
- [Flutter Architecture
Blueprints](https://www.youtube.com/watch?v=zI4Rl_S7rw8) - Flutter Team
### 💻 Repositorios de Ejemplo
-
[flutter_tdd_clean_architecture](https://github.com/ResoCoder/flutter-tdd-clean-architecture-course)
- Reso Coder
-
[flutter_architecture_samples](https://github.com/brianegan/flutter_architecture_samples)
- Brian Egan
-
[clean_architecture_flutter](https://github.com/ShadyBoukhary/flutter_clean_architecture)
- Shady Boukhary
### 💬 Comunidades
- [r/FlutterDev](https://reddit.com/r/FlutterDev) - Reddit Flutter
Community
- [Flutter Discord](https://discord.gg/flutter) - Official Flutter
Discord
- [Stack Overflow - flutter
tag](https://stackoverflow.com/questions/tagged/flutter)
### 📚 Libros
- "Clean Architecture" - Robert C. Martin
- "Domain-Driven Design" - Eric Evans
- "Patterns of Enterprise Application Architecture" - Martin Fowler
Este apartado profundiza en los conceptos clave, proporcionando ejemplos prácticos y mejores prácticas para su aplicación en proyectos reales.
Al comprender estos detalles, podrás diseñar soluciones más robustas, mantenibles y escalables en tus aplicaciones Flutter y Dart.