Clean Architecture, proposta por Robert C. Martin, é uma abordagem de organizacao de software que prioriza a separação de responsabilidades é 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 você pode aplicar diretamente nos seus projetos.
Principios Fundamentais
A Clean Architecture se baseia na Regra de Dependencia: as camadas internas não devem conhecer nada sobre as camadas externas. O código-fonte só pode apontar para dentro, nunca para fora. Isso significa que a lógica de negócio não depende de frameworks, banco de dados ou APIs externas.
As camadas tipicas são, de dentro para fora: Domain (entidades e use cases), Data (repositórios e fontes de dados) e Presentation (UI e ViewModels). Cada camada comunica-se com a adjacente através 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 Repositórios
// domain/build.gradle.kts - Kotlin puro, sem dependências 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 aplicação. Ela contém entidades, use cases e interfaces de repositório. Nao possui nenhuma dependência 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 negócio específicas. Cada use case tem uma única responsabilidade:
class CriarPedidoUseCase(
private val pedidoRepository: PedidoRepository,
private val produtoRepository: ProdutoRepository
) {
suspend operator fun invoke(
clienteId: Long,
itens: List<ItemPedidoRequest>
): Result<Pedido> {
// Validação 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 função: 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 contém 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 Práticas para Clean Architecture com Kotlin
- Mantenha o Domain puro: sem dependências de framework, banco de dados ou bibliotecas de terceiros. Apenas Kotlin puro e coroutines.
- Use interfaces para inversao de dependência: o Domain define contratos; o Data implementa.
- Um use case por acao: cada use case deve resolver exatamente um problema de negócio.
- Use
Resultou sealed classes para retornos: evite lancaar exceções para fluxos de negócio previstos. - Mapeie entre camadas: cada camada deve ter seus próprios modelos de dados. Nao passe entidades do Room para a UI.
- Injete dependências: 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 repositório sem adicionar lógica são sinais de que a arquitetura esta sendo aplicada mecanicamente. Simplifique quando não há regra de negócio real.
- Domain conhecendo o framework: se o Domain importa classes do Android, Room ou Retrofit, a separação 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 você não 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, não o contrario.
Conclusão e Próximos Passos
Clean Architecture com Kotlin proporciona uma base solida para projetos que precisam escalar e evoluir ao longo do tempo. A separação em camadas facilita testes, manutenção e trabalho em equipe. No entanto, aplique-a com pragmatismo: projetos menores podem se beneficiar de abordagens mais simples. Para aprofundar, explore injeção de dependência com Koin e Hilt, padrões de design complementares e consulte nossos guias sobre testes e MVVM para uma visao completa da arquitetura de aplicações Kotlin.