Clean Architecture & the Repository pattern
Clean Architecture splits code into layers with a strict dependency direction:
- Presentation (UI + ViewModels) — what the user sees
- Domain (use cases, business models, repository interfaces) — framework-free, pure Kotlin
- Data (repository implementations, API/DB sources, DTOs) — dealing with the outside world
The dependency rule: arrows always point inward — Data and Presentation depend on Domain; Domain depends on nothing. Dependency inversion makes this possible: the interface lives in domain, the implementation in data.
The Repository pattern hides data sources (network, DB, cache) behind one facade: the ViewModel says "give me the balance" without knowing where it comes from. The repository is exactly where offline-first decisions live.
Each layer keeps its own model: AccountDto (data) → Account (domain) → AccountUiModel (presentation), with mappers between.
// DOMAIN — təmiz Kotlin, Android importu yoxdur
data class Account(val id: String, val balance: BigDecimal)
interface AccountRepository {
suspend fun getAccounts(): List<Account>
fun observeAccounts(): Flow<List<Account>>
}
class GetAccountsUseCase(private val repo: AccountRepository) {
suspend operator fun invoke(): List<Account> =
repo.getAccounts().sortedByDescending { it.balance }
}
// DATA — Retrofit/Room burada yaşayır
class AccountRepositoryImpl @Inject constructor(
private val api: AccountApi,
private val dao: AccountDao
) : AccountRepository {
override suspend fun getAccounts(): List<Account> {
val remote = api.fetchAccounts() // List<AccountDto>
dao.upsertAll(remote.map { it.toEntity() })
return dao.getAll().map { it.toDomain() } // DB — single source of truth
}
override fun observeAccounts() = dao.observeAll().map { list -> list.map { it.toDomain() } }
}The layers in code: interface in domain, implementation in data
Be ready for the "are use cases always necessary?" debate. A balanced, non-dogmatic answer is expected: on a simple CRUD screen, ViewModel → Repository directly is acceptable; use cases earn their keep when (1) logic repeats across ViewModels, (2) the operation is multi-step (a transfer: check limits → compute fees → confirm), (3) domain rules must be testable independently of UI. A banking transfer flow is the ideal use-case example.
🛠 Practice task
Split one feature into three layers: AccountDto (data) → Account (domain) → AccountUiModel (presentation), with mappers between.
- The domain file must contain *no `android.` or Retrofit imports** — verify it.
- Put the repository interface in domain, the implementation in data.
- Write one use case (e.g.
GetAccountsUseCase) and justify in a sentence whether it actually earns its place.
Done when: you have a balanced answer ready for "are use cases always necessary".
📚 Sources and documentation
- Android architecture guide — domain layerofficialdeveloper.android.com
- Data layer guideofficialdeveloper.android.com