Clean Architecture, proposta por Robert C. Martin, e uma abordagem de organizacao de software que prioriza a separacao de responsabilidades e a independencia de frameworks, banco de dados e interfaces externas. Quando aplicada com Kotlin, a Clean Architecture se torna especialmente elegante gracas a recursos como sealed classes, extension functions, data classes e coroutines. Neste guia, vamos implementar Clean Architecture tanto para Android quanto para backend, com exemplos concretos que voce pode aplicar diretamente nos seus projetos.
Principios Fundamentais
A Clean Architecture se baseia na Regra de Dependencia: as camadas internas nao devem conhecer nada sobre as camadas externas. O codigo-fonte so pode apontar para dentro, nunca para fora. Isso significa que a logica de negocio nao depende de frameworks, banco de dados ou APIs externas.
As camadas tipicas sao, de dentro para fora: Domain (entidades e use cases), Data (repositorios e fontes de dados) e Presentation (UI e ViewModels). Cada camada comunica-se com a adjacente atraves de interfaces definidas na camada mais interna.
Estrutura de Modulos
Em projetos maiores, cada camada pode ser um modulo Gradle separado:
// settings.gradle.kts
include(":app") // Presentation + DI
include(":domain") // Entidades + Use Cases + Interfaces
include(":data") // Implementacoes de Repositorios
// domain/build.gradle.kts - Kotlin puro, sem dependencias Android
plugins {
kotlin("jvm")
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}
// data/build.gradle.kts
dependencies {
implementation(project(":domain"))
implementation("io.ktor:ktor-client-core:2.3.7")
implementation("androidx.room:room-ktx:2.6.1")
}
// app/build.gradle.kts
dependencies {
implementation(project(":domain"))
implementation(project(":data"))
}
Camada Domain
A camada Domain e o coracao da aplicacao. Ela contem entidades, use cases e interfaces de repositorio. Nao possui nenhuma dependencia de framework:
// Entidade de dominio
data class Pedido(
val id: Long,
val clienteId: Long,
val itens: List<ItemPedido>,
val status: StatusPedido,
val criadoEm: LocalDateTime
) {
val valorTotal: BigDecimal
get() = itens.fold(BigDecimal.ZERO) { acc, item ->
acc + (item.precoUnitario * BigDecimal(item.quantidade))
}
fun podeSerCancelado(): Boolean {
return status == StatusPedido.PENDENTE ||
status == StatusPedido.CONFIRMADO
}
}
data class ItemPedido(
val produtoId: Long,
val nomeProduto: String,
val quantidade: Int,
val precoUnitario: BigDecimal
)
enum class StatusPedido {
PENDENTE, CONFIRMADO, EM_PREPARO, ENVIADO, ENTREGUE, CANCELADO
}
Interfaces de Repositorio
// Definida no Domain, implementada no Data
interface PedidoRepository {
suspend fun buscarPorId(id: Long): Pedido?
suspend fun buscarPorCliente(clienteId: Long): List<Pedido>
suspend fun salvar(pedido: Pedido): Pedido
suspend fun atualizar(pedido: Pedido): Pedido
fun observarPedidosDoCliente(clienteId: Long): Flow<List<Pedido>>
}
interface ProdutoRepository {
suspend fun buscarPorId(id: Long): Produto?
suspend fun verificarEstoque(produtoId: Long): Int
}
Use Cases
Use cases encapsulam regras de negocio especificas. Cada use case tem uma unica responsabilidade:
class CriarPedidoUseCase(
private val pedidoRepository: PedidoRepository,
private val produtoRepository: ProdutoRepository
) {
suspend operator fun invoke(
clienteId: Long,
itens: List<ItemPedidoRequest>
): Result<Pedido> {
// Validacao de negocio
if (itens.isEmpty()) {
return Result.failure(
NegocioException("Pedido deve ter pelo menos um item")
)
}
// Verificar estoque
for (item in itens) {
val estoque = produtoRepository.verificarEstoque(item.produtoId)
if (estoque < item.quantidade) {
return Result.failure(
NegocioException(
"Estoque insuficiente para produto ${item.produtoId}"
)
)
}
}
// Buscar informacoes dos produtos
val itensCompletos = itens.map { item ->
val produto = produtoRepository.buscarPorId(item.produtoId)
?: return Result.failure(
NegocioException("Produto ${item.produtoId} nao encontrado")
)
ItemPedido(
produtoId = produto.id,
nomeProduto = produto.nome,
quantidade = item.quantidade,
precoUnitario = produto.preco
)
}
val pedido = Pedido(
id = 0,
clienteId = clienteId,
itens = itensCompletos,
status = StatusPedido.PENDENTE,
criadoEm = LocalDateTime.now()
)
val pedidoSalvo = pedidoRepository.salvar(pedido)
return Result.success(pedidoSalvo)
}
}
class CancelarPedidoUseCase(
private val pedidoRepository: PedidoRepository
) {
suspend operator fun invoke(pedidoId: Long): Result<Pedido> {
val pedido = pedidoRepository.buscarPorId(pedidoId)
?: return Result.failure(
NegocioException("Pedido $pedidoId nao encontrado")
)
if (!pedido.podeSerCancelado()) {
return Result.failure(
NegocioException(
"Pedido no status ${pedido.status} nao pode ser cancelado"
)
)
}
val pedidoCancelado = pedido.copy(status = StatusPedido.CANCELADO)
val atualizado = pedidoRepository.atualizar(pedidoCancelado)
return Result.success(atualizado)
}
}
class ObservarPedidosUseCase(
private val pedidoRepository: PedidoRepository
) {
operator fun invoke(clienteId: Long): Flow<List<Pedido>> {
return pedidoRepository.observarPedidosDoCliente(clienteId)
}
}
O uso de operator fun invoke permite chamar o use case como uma funcao: criarPedido(clienteId, itens).
Camada Data
A camada Data implementa as interfaces definidas no Domain:
class PedidoRepositoryImpl(
private val apiService: PedidoApiService,
private val pedidoDao: PedidoDao,
private val mapper: PedidoMapper
) : PedidoRepository {
override suspend fun buscarPorId(id: Long): Pedido? {
// Tenta cache local primeiro
val local = pedidoDao.buscarPorId(id)
if (local != null) {
return mapper.entityToDomain(local)
}
// Busca na API
return try {
val remoto = apiService.buscarPedido(id)
pedidoDao.inserir(mapper.dtoToEntity(remoto))
mapper.dtoToDomain(remoto)
} catch (e: Exception) {
null
}
}
override suspend fun buscarPorCliente(clienteId: Long): List<Pedido> {
return try {
val remotos = apiService.buscarPedidosCliente(clienteId)
val entidades = remotos.map { mapper.dtoToEntity(it) }
pedidoDao.inserirTodos(entidades)
remotos.map { mapper.dtoToDomain(it) }
} catch (e: Exception) {
// Fallback para cache local
pedidoDao.buscarPorCliente(clienteId)
.map { mapper.entityToDomain(it) }
}
}
override suspend fun salvar(pedido: Pedido): Pedido {
val dto = mapper.domainToDto(pedido)
val resposta = apiService.criarPedido(dto)
pedidoDao.inserir(mapper.dtoToEntity(resposta))
return mapper.dtoToDomain(resposta)
}
override suspend fun atualizar(pedido: Pedido): Pedido {
val dto = mapper.domainToDto(pedido)
val resposta = apiService.atualizarPedido(pedido.id, dto)
pedidoDao.atualizar(mapper.dtoToEntity(resposta))
return mapper.dtoToDomain(resposta)
}
override fun observarPedidosDoCliente(clienteId: Long): Flow<List<Pedido>> {
return pedidoDao.observarPorCliente(clienteId)
.map { entidades -> entidades.map { mapper.entityToDomain(it) } }
}
}
Camada Presentation
A camada Presentation contem ViewModels que utilizam os use cases:
class PedidoViewModel(
private val criarPedido: CriarPedidoUseCase,
private val cancelarPedido: CancelarPedidoUseCase,
private val observarPedidos: ObservarPedidosUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<PedidoUiState>(PedidoUiState.Loading)
val uiState: StateFlow<PedidoUiState> = _uiState.asStateFlow()
fun carregarPedidos(clienteId: Long) {
viewModelScope.launch {
observarPedidos(clienteId).collect { pedidos ->
_uiState.value = PedidoUiState.Success(pedidos)
}
}
}
fun realizarPedido(clienteId: Long, itens: List<ItemPedidoRequest>) {
viewModelScope.launch {
_uiState.value = PedidoUiState.Loading
criarPedido(clienteId, itens)
.onSuccess {
_uiState.value = PedidoUiState.PedidoCriado(it)
}
.onFailure {
_uiState.value = PedidoUiState.Error(it.message ?: "Erro")
}
}
}
fun cancelar(pedidoId: Long) {
viewModelScope.launch {
cancelarPedido(pedidoId)
.onFailure {
_uiState.value = PedidoUiState.Error(it.message ?: "Erro")
}
}
}
}
sealed class PedidoUiState {
object Loading : PedidoUiState()
data class Success(val pedidos: List<Pedido>) : PedidoUiState()
data class PedidoCriado(val pedido: Pedido) : PedidoUiState()
data class Error(val mensagem: String) : PedidoUiState()
}
Boas Praticas para Clean Architecture com Kotlin
- Mantenha o Domain puro: sem dependencias de framework, banco de dados ou bibliotecas de terceiros. Apenas Kotlin puro e coroutines.
- Use interfaces para inversao de dependencia: o Domain define contratos; o Data implementa.
- Um use case por acao: cada use case deve resolver exatamente um problema de negocio.
- Use
Resultou sealed classes para retornos: evite lancaar excecoes para fluxos de negocio previstos. - Mapeie entre camadas: cada camada deve ter seus proprios modelos de dados. Nao passe entidades do Room para a UI.
- Injete dependencias: use Koin ou Hilt para conectar as camadas sem acoplamento direto.
- Teste o Domain independentemente: use cases devem ser testados sem emulador, banco de dados ou rede.
Erros Comuns e Armadilhas
- Anemic use cases: use cases que apenas delegam para o repositorio sem adicionar logica sao sinais de que a arquitetura esta sendo aplicada mecanicamente. Simplifique quando nao ha regra de negocio real.
- Domain conhecendo o framework: se o Domain importa classes do Android, Room ou Retrofit, a separacao esta quebrada.
- Mapeamento excessivo: para projetos pequenos, tres modelos para a mesma entidade (Domain, Entity, DTO) pode ser overkill. Avalie a complexidade do projeto.
- Ignorar a testabilidade: se voce nao esta testando os use cases isoladamente, perde o principal beneficio da Clean Architecture.
- Camadas desnecessarias: nem todo projeto precisa de Clean Architecture completa. Para projetos simples, MVVM pode ser suficiente. A arquitetura deve servir ao projeto, nao o contrario.
Conclusao e Proximos Passos
Clean Architecture com Kotlin proporciona uma base solida para projetos que precisam escalar e evoluir ao longo do tempo. A separacao em camadas facilita testes, manutencao e trabalho em equipe. No entanto, aplique-a com pragmatismo: projetos menores podem se beneficiar de abordagens mais simples. Para aprofundar, explore injecao de dependencia com Koin e Hilt, padroes de design complementares e consulte nossos guias sobre testes e MVVM para uma visao completa da arquitetura de aplicacoes Kotlin.