Clean Architecture: La Guía Definitiva para Android Moderno
Índice de contenidos
🏛️ Filosofía: ¿Qué es realmente “Clean”?
Clean Architecture, propuesta por Robert C. Martin (Uncle Bob), no es una plantilla de carpetas; es una filosofía de diseño de software centrada en la independencia.
El objetivo final es crear sistemas que sean:
- Independientes de Frameworks: Android es un detalle, no el centro de tu arquitectura.
- Testables: La lógica de negocio se puede probar sin UI, base de datos o servidor web.
- Independientes de la UI: La UI puede cambiar fácilmente sin cambiar el resto del sistema.
- Independientes de la Base de Datos: Puedes cambiar de Room a Realm o SQLDelight sin tocar la lógica de negocio.
🧅 La Regla de Dependencia (The Dependency Rule)
Esta es la única regla que no puedes romper.
“Las dependencias de código fuente solo pueden apuntar hacia adentro, hacia políticas de nivel superior.”
Imagina la arquitectura como una cebolla:
- Centro (Domain Layer): Entidades y Lógica de Negocio Pura. No sabe nada de Android.
- Capa Intermedia (Data Layer / Adapters): Convierte datos externos al formato que el Dominio necesita.
- Capa Externa (Presentation / Framework): UI, Base de datos, API, Android SDK.
El Dominio nunca importa clases de la capa de Datos o Presentación. Jamás verás import android.* o import retrofit2.* en la capa de Dominio.
🏗️ Implementación Práctica en Android
Vamos a diseccionar cada capa con un ejemplo real: Una app de Noticias.
1. Domain Layer (El Núcleo Sagrado)
Esta capa contiene la “Verdad” de tu aplicación. Es puro Kotlin.
Componentes:
- Entities (Modelos): Objetos de negocio puros.
- Use Cases (Interactors): Reglas de negocio específicas de la aplicación.
- Repository Interfaces: Contratos que la capa de datos debe cumplir.
// Entity: Puro Kotlin, sin anotaciones de JSON o DB
data class NewsArticle(
val id: String,
val title: String,
val publishedAt: LocalDateTime
)
// Repository Interface: El contrato (Dependency Inversion)
interface NewsRepository {
fun getLatestNews(): Flow<Result<List<NewsArticle>>>
}
// Use Case: Orquestador de lógica
class GetLatestNewsUseCase @Inject constructor(
private val repository: NewsRepository
) {
operator fun invoke(): Flow<Result<List<NewsArticle>>> {
return repository.getLatestNews()
.map { result ->
// Regla de negocio: Filtrar noticias futuras (posible error de API)
result.map { list ->
list.filter { it.publishedAt <= LocalDateTime.now() }
}
}
}
}
2. Data Layer (El Adaptador)
Esta capa es el “plugin” que conecta tu Dominio con el mundo exterior.
Componentes:
- Data Models (DTOs): Modelos de API (Retrofit) o DB (Room).
- Mappers: Transforman DTO <-> Entity.
- Repository Implementation: Implementa la interfaz del Dominio.
- Data Sources: Fuentes de datos crudas.
// DTO: Modelo de red con anotaciones específicas
@JsonClass(generateAdapter = true)
data class NetworkNewsArticle(
@Json(name = "article_id") val id: String,
@Json(name = "header") val title: String
)
// Mapper: Extension function para conversión
fun NetworkNewsArticle.toDomain(): NewsArticle {
return NewsArticle(
id = this.id,
title = this.title,
publishedAt = LocalDateTime.now() // Simplificado
)
}
// Repository Impl: Aquí es donde "Android" y librerías viven
class NewsRepositoryImpl @Inject constructor(
private val api: NewsApiService,
private val dao: NewsDao
) : NewsRepository { // Implementa interfaz de Dominio
override fun getLatestNews(): Flow<Result<List<NewsArticle>>> = flow {
// Lógica de caché, red, etc.
val apiResponse = api.fetchNews()
val domainNews = apiResponse.map { it.toDomain() }
emit(Result.success(domainNews))
}
}
3. Presentation Layer (La Cara)
Esta capa se encarga de pintar pixels en la pantalla.
Componentes:
- ViewModel: Mantiene el estado de la UI y ejecuta Use Cases.
- UI (Compose/XML): Observa el estado del ViewModel.
@HiltViewModel
class NewsViewModel @Inject constructor(
private val getLatestNews: GetLatestNewsUseCase // Inyectamos Use Case
) : ViewModel() {
// ViewModel NO conoce el Repository, solo el Use Case
// ViewModel NO conoce Retrofit ni Room
}
🔄 El Flujo de Control vs. Flujo de Dependencias
Aquí es donde muchos se confunden.
- Flujo de Control (Runtime): UI -> ViewModel -> Use Case -> Repository Impl -> API.
- Flujo de Dependencias (Compile time):
- Presentation -> Domain
- Data -> Domain
- Presentation -> Data (Solo para inyección de dependencias en el Root/App module)
Gracias a la Inversión de Dependencias (DIP), aunque el flujo de control va del Use Case al Repository Implementation, la dependencia de código fuente va al revés: RepositoryImpl depende de RepositoryInterface (que está en Dominio).
🧪 Beneficios en Testing
Al tener el Dominio aislado, probar los Use Cases es trivial:
// Test puro de Kotlin, corre en milisegundos en la JVM local
class GetLatestNewsUseCaseTest {
private val fakeRepository = FakeNewsRepository() // Fake en memoria
private val useCase = GetLatestNewsUseCase(fakeRepository)
@Test
fun `should filter future news`() = runTest {
// Arrange
val futureArticle = NewsArticle("1", "Future", LocalDateTime.now().plusDays(1))
fakeRepository.emit(listOf(futureArticle))
// Act
val result = useCase().first()
// Assert
assertTrue(result.getOrThrow().isEmpty())
}
}
⚠️ Errores Comunes (Pitfalls)
- Modelos Anémicos Compartidos: Usar el mismo objeto para DB, API y UI.
- Por qué está mal: Si cambias la API, rompes la UI. Viola la separación de capas.
- Use Cases Pasamanos: Use Cases que solo llaman al repositorio y no hacen nada más.
- Defensa: A veces parece boilerplate, pero protege tu arquitectura para cuando las reglas cambien. Aún así, si realmente no hay lógica, algunos equipos permiten llamar al Repo directo desde el ViewModel (Pragmatic Clean Arch), pero ten cuidado.
- Lógica de Negocio en ViewModel: “Si el usuario es premium, muestra esto”.
- Solución: Mueve esa lógica al Use Case o a la Entidad de Dominio.
🎯 Conclusión
Clean Architecture tiene un costo inicial: más archivos, más mapeo de datos. Pero el retorno de inversión es una base de código que sobrevive al tiempo. Frameworks van y vienen (AsyncTask -> RxJava -> Coroutines -> ?), pero tu lógica de negocio, protegida en el centro de la cebolla, permanece inmutable.
Artículos relacionados
Use Cases en Android: Lógica de Negocio Limpia y Reutilizable
Por qué los Use Cases (Interactors) son el componente secreto de una arquitectura escalable. Cómo diseñarlos, testearlos y reutilizarlos.
Arquitectura MVVM en Android: Guía Completa desde Cero
Domina el patrón Model-View-ViewModel desde los conceptos básicos hasta implementaciones avanzadas con ejemplos prácticos de un juego de Buscaminas para Android.
Kotlin Coroutines en Android: Programación Asíncrona Moderna
Domina las corrutinas de Kotlin para crear aplicaciones Android más eficientes y reactivas, siguiendo las mejores prácticas de arquitectura limpia y MVVM.