Step 10 of 15
Repository pattern, Clean Architecture layers, dependency rule, Use Cases
Repository pattern และ Clean Architecture — แยกชั้น presentation/domain/data
A repository is the single source of truth for one type of data. It decides where data comes from — a local database, a remote API, or an in-memory cache. The rest of the app talks only to the repository and never touches the data sources directly.
Diagram: Clean Architecture layers. The dependency rule points inward — outer layers depend on inner layers, never the reverse.
Loading diagram...
| Layer | Contains | Depends on |
|---|---|---|
| Presentation | Composables, ViewModel | Domain |
| Domain | Models, repository interfaces, use cases | nothing (pure Kotlin) |
| Data | Repository implementation, API client, Room DAO | Domain |
The key rule: dependencies point inward. The Data layer knows about Domain, but Domain knows nothing about Data. This keeps business logic testable and free of Android.
data class Task(
val id: Int,
val title: String,
val done: Boolean = false
)
Define an interface in the domain layer. This is the contract the rest of the app uses:
interface TaskRepository {
suspend fun getAll(): List<Task>
suspend fun add(title: String): Task
suspend fun markDone(id: Int)
}
The implementation hides the data sources. Here it uses an in-memory list, but you can swap in Room or a remote API without changing callers:
class InMemoryTaskRepository : TaskRepository {
private val tasks = mutableListOf<Task>()
private var nextId = 1
override suspend fun getAll(): List<Task> = tasks.toList()
override suspend fun add(title: String): Task {
val task = Task(id = nextId++, title = title)
tasks.add(task)
return task
}
override suspend fun markDone(id: Int) {
val index = tasks.indexOfFirst { it.id == id }
if (index != -1) {
tasks[index] = tasks[index].copy(done = true)
}
}
}
A use case is a small class that does one business action. It keeps the ViewModel thin and makes rules reusable.
class AddTaskUseCase(private val repository: TaskRepository) {
suspend operator fun invoke(title: String): Task {
require(title.isNotBlank()) { "Title cannot be empty" }
return repository.add(title.trim())
}
}
The operator fun invoke() lets you call the use case like a function: addTask("Buy milk").
class TaskViewModel(
private val addTask: AddTaskUseCase,
private val repository: TaskRepository
) : ViewModel() {
private val _tasks = MutableStateFlow<List<Task>>(emptyList())
val tasks: StateFlow<List<Task>> = _tasks.asStateFlow()
fun load() {
viewModelScope.launch {
_tasks.value = repository.getAll()
}
}
fun add(title: String) {
viewModelScope.launch {
addTask(title)
load()
}
}
}
Define a UserRepository interface with findById(id), an in-memory implementation, and a GetUserUseCase. Write a UserViewModel that loads a user by id into a StateFlow. Add a fake repository in a test and confirm the ViewModel loads the right user.