Saat aplikasi tumbuh, ViewModel sering tergoda untuk langsung memanggil Retrofit dan Room secara bersamaan — cache lokal lebih cepat, network adalah kebenaran terbaru. Tanpa abstraksi, logika sinkronisasi menyebar ke banyak ViewModel, sulit diuji, dan duplikasi terjadi. Repository Pattern memecahkan masalah ini dengan menyatukan akses data di satu kelas, sehingga ViewModel hanya melihat satu titik masuk. Bab ini menjelaskan Repository, pola single source of truth, dan lapisan Use Case untuk membungkus niat bisnis.

Repository sebagai Abstraksi Sumber Data #

Repository adalah satu-satunya tempat yang tahu sumber data mana yang dipakai. ViewModel memanggil repo.muatTugas() tanpa peduli apakah data datang dari network, database, atau cache memori. Repository yang memutuskan strategi: cache-first, network-first, atau keduanya.

kotlin Kontrak Repository
interface TugasRepository {
    suspend fun muatSemua(): List<Tugas>
    suspend fun tambah(tugas: Tugas)
}

class TugasRepositoryImpl(
    private val api: TugasApi,
    private val dao: TugasDao
) : TugasRepository {
    override suspend fun muatSemua(): List<Tugas> {
        val cached = dao.all().first()
        if (cached.isNotEmpty()) return cached  // cache-first
        val remote = api.fetch()
        dao.replaceAll(remote)
        return remote
    }
}

Dengan interface, kita dapat menyuntikkan implementasi mock saat testing ViewModel. Pola ini juga memudahkan migrasi sumber data — bila satu hari kita mengganti Retrofit dengan Ktor, hanya Repository yang berubah.

Single Source of Truth #

Pola single source of truth (SSOT) berarti hanya ada satu sumber data yang dianggap valid pada satu waktu — biasanya cache lokal (Room atau DataStore). Network dipakai untuk memperbarui cache, lalu UI membaca dari cache. Strategi ini disebut network-bound resource atau offline-first.

StrategiUrutanCocok untuk
Cache-firstBaca cache → bila kosong, fetch network → simpan cacheData relatif statis (katalog, pengaturan)
Network-firstFetch network → simpan cache → bila gagal, baca cacheData real-time (chat, stok)
BothKirim cache dulu untuk tampil cepat, lalu update dari networkDaftar panjang dengan refresh

Use Case Layer #

Untuk aplikasi kompleks, logika bisnis yang melibatkan beberapa Repository sebaiknya dipisahkan ke Use Case. Satu Use Case = satu niat bisnis. TambahTugasUseCase memvalidasi input, generate ID, dan memanggil Repository. ViewModel cukup memanggil useCase(judul).

kotlin Use Case dengan operator fun invoke()
class TambahTugasUseCase(
    private val repo: TugasRepository,
    private val notifier: NotificationService
) {
    operator fun invoke(judul: String): Tugas {
        require(judul.isNotBlank()) { "Judul tidak boleh kosong" }
        val tugas = Tugas(id = generateId(), judul = judul, selesai = false)
        repo.tambah(tugas)
        notifier.kirim("Tugas baru: $judul")
        return tugas
    }
}

operator fun invoke() membuat Use Case dapat dipanggil seperti function: tambahUseCase("Belajar Kotlin"). Konvensi ini populer di arsitektur Android modern karena membuat ViewModel sangat ringkas.

Simulasi Repository + Use Case #

Cuplikan berikut memodelkan Repository yang menyatukan TugasApi (network mock) dan TugasDao (cache lokal). Use Case membungkus logika tambah tugas:

kotlin
// repository_pattern_android.kt
// Mendemonstrasikan Repository Pattern dengan abstraksi sumber data
// (network, cache lokal), serta Use Case layer yang membungkus logika bisnis.

// Model data domain
data class Tugas(val id: Int, val judul: String, val selesai: Boolean)

// Sumber data: network (mock)
class TugasApi {
    private val remote = mutableListOf(
        Tugas(1, "Belanja bulanan", false),
        Tugas(2, "Bayar tagihan listrik", false)
    )
    fun fetch(): List<Tugas> = remote.toList()
    fun push(tugas: Tugas) { remote.add(tugas) }
}

// Sumber data: lokal (mock)
class TugasDao {
    private val cache = mutableListOf<Tugas>()
    fun all(): List<Tugas> = cache.toList()
    fun insert(tugas: Tugas) { cache.add(tugas) }
    fun replaceAll(items: List<Tugas>) { cache.clear(); cache.addAll(items) }
}

// Repository menyatukan kedua sumber, menerapkan strategi cache-then-network
class TugasRepository(
    private val api: TugasApi,
    private val dao: TugasDao
) {
    // Single source of truth: lokal cache selalu dianggap valid
    fun muatSemua(): List<Tugas> {
        val cached = dao.all()
        if (cached.isEmpty()) {
            val dariNetwork = api.fetch()
            dao.replaceAll(dariNetwork)
            return dao.all()
        }
        return cached
    }

    fun tambah(tugas: Tugas) {
        api.push(tugas)
        dao.insert(tugas)
    }
}

// Use case layer: satu kelas = satu niat bisnis
class TambahTugasUseCase(private val repo: TugasRepository) {
    private var nextId = 1000  // counter deterministik untuk demo

    operator fun invoke(judul: String): Tugas {
        require(judul.isNotBlank()) { "Judul tidak boleh kosong" }
        val tugas = Tugas(id = nextId++, judul = judul, selesai = false)
        repo.tambah(tugas)
        return tugas
    }
}

class MuatTugasUseCase(private val repo: TugasRepository) {
    operator fun invoke(): List<Tugas> = repo.muatSemua()
}

fun main() {
    val api = TugasApi()
    val dao = TugasDao()
    val repo = TugasRepository(api, dao)
    val muatUseCase = MuatTugasUseCase(repo)
    val tambahUseCase = TambahTugasUseCase(repo)

    println("=== Muat awal (cache kosong, ambil dari network) ===")
    val awal = muatUseCase()
    awal.forEach { println("  $it") }

    println("\n=== Tambah tugas baru via UseCase ===")
    val baru = tambahUseCase("Belajar Kotlin")
    println("  Ditambahkan: $baru")

    println("\n=== Muat ulang (dari cache lokal) ===")
    muatUseCase().forEach { println("  $it") }
}

Output yang Dihasilkan:

text
=== Muat awal (cache kosong, ambil dari network) ===
  Tugas(id=1, judul=Belanja bulanan, selesai=false)
  Tugas(id=2, judul=Bayar tagihan listrik, selesai=false)

=== Tambah tugas baru via UseCase ===
  Ditambahkan: Tugas(id=1000, judul=Belajar Kotlin, selesai=false)

=== Muat ulang (dari cache lokal) ===
  Tugas(id=1, judul=Belanja bulanan, selesai=false)
  Tugas(id=2, judul=Bayar tagihan listrik, selesai=false)
  Tugas(id=1000, judul=Belajar Kotlin, selesai=false)

Perhatikan bahwa muat awal mendapati cache kosong, sehingga Repository mengambil dari network dan menyimpan ke cache. Muat ulang langsung membaca cache — tidak ada panggilan network kedua. Inilah inti cache-first + SSOT.

Referensi Lanjutan #

  • Guide to app architecture — panduan resmi arsitektur Android yang menjelaskan pola Repository di layer data
  • Jetpack — rujukan Jetpack sebagai dasar pustaka pendukung pola Repository

Selanjutnya #

Repository sering berhubungan dengan database lokal. Bab berikutnya membahas Room — library ORM Android yang memetakan tabel SQLite ke class Kotlin dengan verifikasi SQL saat kompilasi.