Skip to content

Latest commit

 

History

History
126 lines (95 loc) · 3.73 KB

10-repository-pattern.md

File metadata and controls

126 lines (95 loc) · 3.73 KB

Repository Pattern

What Is a Repository?

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.

flowchart TD
    UI["Presentation<br/>(UI, ViewModel)"]
    Domain["Domain<br/>(Models, Use Cases)"]
    Data["Data<br/>(Repository, API, DB)"]
    UI --> Domain
    Data --> Domain
    Data -. "implements" .-> Repo["Repository interface (domain)"]
Loading

Clean Architecture Layers

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.

The Model

data class Task(
    val id: Int,
    val title: String,
    val done: Boolean = false
)

The Repository Interface (Domain)

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 Repository Implementation (Data)

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)
        }
    }
}

Use Cases

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").

Use It in a ViewModel

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()
        }
    }
}

Why This Matters

  • Testable: test the ViewModel with a fake repository — no network or database needed.
  • Swappable: change Room to an API without touching the UI.
  • Readable: each class has one job.

Practice Exercise

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.