Saltar al contenido principal
A
ArceApps

Clean Architecture: La Guía Definitiva para Android Moderno

calendar_today
Clean Architecture: La Guía Definitiva para Android Moderno

🏛️ 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:

  1. Independientes de Frameworks: Android es un detalle, no el centro de tu arquitectura.
  2. Testables: La lógica de negocio se puede probar sin UI, base de datos o servidor web.
  3. Independientes de la UI: La UI puede cambiar fácilmente sin cambiar el resto del sistema.
  4. 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)

  1. 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.
  2. 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.
  3. 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
Architecture 10 de octubre de 2025

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.

Leer artículo arrow_forward
Arquitectura MVVM en Android: Guía Completa desde Cero
Android 1 de octubre de 2025

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.

Leer artículo arrow_forward
Kotlin Coroutines en Android: Programación Asíncrona Moderna
Android 20 de septiembre de 2025

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.

Leer artículo arrow_forward