¿Por qué Clean Architecture?

MVVM resuelve la separación entre UI y lógica de presentación. Pero en apps medianas o grandes, el Repository empieza a acumular lógica de negocio compleja, transformaciones, reglas de validación. Clean Architecture agrega una capa más que evita eso.

El objetivo es que la lógica de negocio no dependa de Android. Las reglas de tu app deben poder testearse en una JVM pura, sin emulador, sin mocks de Context.

Las tres capas

// Dirección de dependencias — las flechas apuntan hacia adentro:
//
//  Presentation  →  Domain  ←  Data
//  (ViewModel)      (puro Kotlin)   (Repository impl, Room, Retrofit)
//
// Domain no sabe que existe Android.
// Data no sabe que existe la UI.
// Presentation solo habla con Domain.
  • Presentation: ViewModels, Fragments, Activities. Depende de Domain.
  • Domain: modelos de negocio, interfaces de Repository, Use Cases. Sin dependencias externas. Puro Kotlin.
  • Data: implementaciones de los Repositories, Room, Retrofit, DataStore. Implementa las interfaces de Domain.

Domain layer — el corazón de la app

// domain/model/Producto.kt — modelo de negocio puro
data class Producto(
    val id: Int,
    val nombre: String,
    val precio: Double,
    val stock: Int,
    val categoria: Categoria
) {
    // Lógica de negocio vive en el modelo
    val disponible: Boolean get() = stock > 0
    val precioConIva: Double get() = precio * 1.21
}

// domain/repository/ProductoRepository.kt — interfaz, no implementación
// Domain define el contrato; Data lo implementa
interface ProductoRepository {
    fun getProductos(): Flow<List<Producto>>
    suspend fun getProducto(id: Int): Producto?
    suspend fun guardar(producto: Producto)
    suspend fun eliminar(id: Int)
}

La inversión de dependenciasDomain define la interfaz ProductoRepository. La capa Data tiene la clase ProductoRepositoryImpl que la implementa. Así, si mañana cambiás de Room a una API REST, solo cambiás la implementación — Domain y Presentation no se enteran.

Use Cases (Interactors)

Un Use Case encapsula una operación de negocio específica. Cada uno hace una sola cosa. Viven en Domain y no tienen dependencias de Android:

// domain/usecase/GetProductosDisponiblesUseCase.kt
class GetProductosDisponiblesUseCase(
    private val repository: ProductoRepository
) {
    // operator fun invoke permite llamarlo como función: useCase()
    operator fun invoke(): Flow<List<Producto>> {
        return repository.getProductos()
            .map { productos -> productos.filter { it.disponible } }
            .map { productos -> productos.sortedBy { it.nombre } }
    }
}

// domain/usecase/ComprarProductoUseCase.kt
class ComprarProductoUseCase(
    private val repository: ProductoRepository,
    private val carritoRepository: CarritoRepository
) {
    suspend operator fun invoke(productoId: Int, cantidad: Int): Result<Unit> {
        val producto = repository.getProducto(productoId)
            ?: return Result.failure(Exception("Producto no encontrado"))

        if (producto.stock < cantidad)
            return Result.failure(Exception("Stock insuficiente"))

        carritoRepository.agregar(productoId, cantidad)
        return Result.success(Unit)
    }
}

// En el ViewModel — limpio, sin lógica de negocio:
class ProductosViewModel(
    private val getProductosDisponibles: GetProductosDisponiblesUseCase,
    private val comprarProducto: ComprarProductoUseCase
) : ViewModel() {

    val productos = getProductosDisponibles()
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())

    fun comprar(productoId: Int, cantidad: Int) {
        viewModelScope.launch {
            comprarProducto(productoId, cantidad)
                .onSuccess { _eventos.send(UiEvento.CompraExitosa) }
                .onFailure { _uiState.update { s -> s.copy(error = it.message) } }
        }
    }
}

¿Siempre necesitás Use Cases?No. En pantallas simples con un solo Repository, un Use Case que solo delega es overhead innecesario. Agregá Use Cases cuando la lógica es compleja, cuando combina múltiples repositories, o cuando la misma lógica se usa en múltiples ViewModels.

Data layer — implementaciones concretas

// data/repository/ProductoRepositoryImpl.kt
// Implementa la interfaz de Domain combinando múltiples fuentes
class ProductoRepositoryImpl(
    private val dao: ProductoDao,           // Room (local)
    private val apiService: ProductoApi,    // Retrofit (remoto)
    private val networkMonitor: NetworkMonitor
) : ProductoRepository {

    override fun getProductos(): Flow<List<Producto>> {
        // Estrategia: base local como fuente de verdad, red para sincronizar
        return dao.getAll()
            .map { entities -> entities.map { it.toDomain() } }
    }

    override suspend fun guardar(producto: Producto) {
        dao.insertar(producto.toEntity())
        if (networkMonitor.isConnected) {
            runCatching { apiService.sincronizar(producto.toDto()) }
        }
    }
}

Mappers entre capas

Cada capa tiene sus propios modelos. Los mappers convierten entre ellos. Así una entidad de Room nunca llega hasta la UI:

// data/mapper/ProductoMapper.kt
// Extensiones de conversión entre capas

// Entity (Room) → Domain
fun ProductoEntity.toDomain() = Producto(
    id = id,
    nombre = nombre,
    precio = precio,
    stock = stock,
    categoria = Categoria.valueOf(categoria)
)

// Domain → Entity (Room)
fun Producto.toEntity() = ProductoEntity(
    id = id,
    nombre = nombre,
    precio = precio,
    stock = stock,
    categoria = categoria.name
)

// DTO (Retrofit JSON) → Domain
fun ProductoDto.toDomain() = Producto(
    id = id,
    nombre = name,          // nombres distintos entre API y dominio
    precio = price,
    stock = stockCount,
    categoria = Categoria.fromApiValue(categoryId)
)

Estructura de paquetes recomendada

app/
└── src/main/kotlin/ar/pensa/miapp/
    ├── presentation/
    │   ├── productos/
    │   │   ├── ProductosFragment.kt
    │   │   ├── ProductosViewModel.kt
    │   │   ├── ProductosUiState.kt
    │   │   └── ProductosAdapter.kt
    │   └── detalle/
    │       ├── DetalleFragment.kt
    │       └── DetalleViewModel.kt
    ├── domain/
    │   ├── model/
    │   │   ├── Producto.kt
    │   │   └── Categoria.kt
    │   ├── repository/
    │   │   └── ProductoRepository.kt    ← interfaz
    │   └── usecase/
    │       ├── GetProductosDisponiblesUseCase.kt
    │       └── ComprarProductoUseCase.kt
    └── data/
        ├── local/
        │   ├── ProductoDao.kt
        │   ├── ProductoEntity.kt
        │   └── AppDatabase.kt
        ├── remote/
        │   ├── ProductoApi.kt
        │   └── ProductoDto.kt
        ├── repository/
        │   └── ProductoRepositoryImpl.kt
        └── mapper/
            └── ProductoMapper.kt