Belajar Mobile Developer - Offline-First & Sync Architecture
Episode 23 of 28

Belajar Mobile Developer - Offline-First & Sync Architecture

Membangun aplikasi yang tetap berguna tanpa sinyal: prinsip offline-first, optimistic updates & outbox pattern, conflict resolution, dan background sync dengan WorkManager untuk Fitku

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

Di episode 22 kita membangun koneksi realtime. Tapi realita di lapangan: pengguna Fitku lari di hutan, gym bawah tanah, atau naik kereta terowongan — jaringan hilang kapan saja. Episode 23 ini membahas offline-first & sync architecture: aplikasi yang tetap berfungsi penuh tanpa koneksi, lalu menyinkronkan perubahan saat jaringan kembali. Ini kebalikan dari pola "online-first" yang crash begitu sinyal hilang.

Mengapa ini penting? Data fitness adalah data pribadi yang dikumpulkan di lapangan (episode 20) — pengguna tidak bisa menunggu sinyal untuk mencatat langkah. Aplikasi yang menyimpan & menyinkronkan dengan benar mengalahkan aplikasi yang menolak beroperasi offline. Mari bangun sync engine untuk Fitku.

Prinsip Offline-First

Pola pikirnya terbalik dari kebanyakan aplikasi:

  • Local-first: aplikasi membaca & menulis dari storage lokal (episode 11) sebagai single source of truth di device.
  • Network sebagai enhancement: server adalah salinan cloud, bukan sumber kebenaran satu-satunya.
  • Sync engine: mekanisme yang mendamaikan perubahan lokal dan remote secara otomatis.
100%

UI tidak pernah berbicara langsung ke server — semua lewat lapisan lokal. Inilah kenapa state management (episode 9) dan arsitektur (episode 8) menjadi krusial.

Optimistic Updates

Saat pengguna menekan "simpan", jangan menunggu server. Optimistic update: tampilkan hasilnya segera, tandai sebagai pending, dan baru sinkronkan di latar belakang.

KotlinOptimistic update + outbox
suspend fun saveWorkout(workout: Workout) {
    // 1. simpan ke lokal dulu, tandai belum tersinkron
    localDb.workouts.insert(workout.copy(syncState = PENDING))
    // 2. tambahkan ke antrean outbox
    syncQueue.enqueue(
        operation = SyncOp.INSERT,
        target = "workout",
        payload = workout
    )
    // 3. UI langsung menampilkan data baru
    emitWorkoutSaved(workout)
}

Kunci dari optimistic update: UI konsisten dengan storage lokal (bukan dengan server). Server menyusul.

Outbox Pattern

Outbox adalah antrean operasi yang belum dikirim. Setiap mutasi ditulis sebagai satu baris di tabel sync_queue, sehingga tidak ada perubahan yang "hilang" meski app ditutup di tengah jalan:

Tabel sync_queue
| id | operation | target    | payload_json          | status   | retries |
|----|-----------|-----------|-----------------------|----------|---------|
| 1  | INSERT    | workout   | {steps: 1200, ...}    | PENDING  | 0       |
| 2  | UPDATE    | profile   | {goalSteps: 15000}    | PENDING  | 0       |

Saat online, sync engine mengirim operasi satu per satu, memperbarui status menjadi SYNCED, lalu menghapusnya.

Conflict Resolution

Masalah klasik: pengguna mengubah data yang sama di dua perangkat (HP + smartwatch) saat offline. Ketika keduanya sinkron, mana yang benar? Strateginya:

StrategiCara kerjaKapan cocok
Last-Write-WinsData dengan timestamp terakhir menangData non-kritis, paling sederhana
Field-level mergeMenggabungkan field yang berbedaProfil (nama di HP, target di watch)
Manual resolutionTanya pengguna saat konflikData penting (target latihan)
CRDTMerge otomatis tanpa serverCounter/langkah, riwayat

Untuk Fitku, steps counter cocok dengan CRDT (langkah hanya bertambah, aman digabung), sedangkan target latihan lebih baik dengan manual resolution agar pengguna memutuskan.

Warning

Last-Write-Wins terdengar mudah tapi berbahaya untuk data yang benar-benar dikerjakan offline: data yang lebih lama tapi lebih "benar" bisa tertimpa. Selalu pertimbangkan: data ini bisa digabung (merge) atau harus dipilih salah satu? Pilih strategi berdasarkan sifat data, bukan kemudahan kode.

Background Sync dengan WorkManager

Sinkronisasi tidak bisa bergantung pada pengguna membuka app. Gunakan background task dengan kondisi yang tepat — di Android memakai WorkManager (episode 14):

KotlinWorkManager sync dengan constraints
val request = OneTimeWorkRequestBuilder<SyncWorker>()
    .setConstraints(
        Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .setRequiresBatteryNotLow(true)
            .build()
    )
    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
    .build()
 
WorkManager.getInstance(context).enqueue(request)

Worker-nya membalik antrean outbox dan mengirim operasi yang tersisa:

KotlinSyncWorker
class SyncWorker(private val ctx: Context) : Worker(ctx) {
    override fun doWork(): Result {
        val ops = syncQueue.getPending()
        if (ops.isEmpty()) return Result.success()
        val ok = api.batchSync(ops.map { it.toJson() })
        return if (ok) {
            syncQueue.markSynced(ops)
            Result.success()
        } else {
            Result.retry() // backoff eksponensial otomatis
        }
    }
}

Di iOS gunakan BGTaskScheduler dengan pola serupa. Jangan pernah memaksa sync dalam thread UI — itu alasan utama app terasa lemot (episode 15).

Delta Sync & Pagination

Menyinkronkan seluruh database setiap kali online sangat boros. Pakai delta sync: server melacak version cursor per perangkat, dan app hanya menarik data yang berubah setelah cursor itu.

Alur delta sync
GET /sync?since_cursor=4821
→ { changes: [...], new_cursor: 4920 }

Cukup satu request ringan, alih-alih men-download semua data pengguna dari nol.

Common Pitfalls Offline-First

  • Menjalankan sync di UI thread: app freeze, dikira hang. Selalu worker (episode 14).
  • Tanpa queue yang durable: perubahan hilang saat app di-kill. Persist outbox di storage, bukan di memory.
  • Konflik diabaikan: pakai LWW untuk semua data = data pengguna bisa hilang diam-diam.
  • Sync seluruh database tiap kali: boros data & baterai. Gunakan delta sync.
  • Tanpa indikator status sync: pengguna tidak tahu datanya "tersimpan di device tapi belum ke cloud". Tampilkan status ringan di UI (misal ikon awan).

Tip

Frasa kunci offline-first: "berfungsi dulu, sinkron kemudian". Mulai dari satu jalur: catat data di lokal, taruh di outbox, sinkron saat online, resolusi konflik untuk data yang bisa bentrok. Begitu pola ini nyaman, tambah background sync & delta sync. Jangan langsung membangun semuanya sekaligus.

Penutup

Pada episode 23 ini, kalian telah membangun fondasi aplikasi yang tangguh:

  • Offline-first: local-first, network sebagai pelengkap, dan sync engine.
  • Optimistic updates + outbox pattern agar tidak ada perubahan yang hilang.
  • Conflict resolution: dari LWW hingga CRDT, dipilih sesuai sifat data.
  • Background sync dengan WorkManager/BGTaskScheduler dan delta sync yang hemat.

Dengan fondasi data yang kokoh, sekarang saatnya membawa Fitku keluar dari layar utama: di episode 24 kita buat Widgets & Wearables — home screen widget dan aplikasi untuk smartwatch. Sampai jumpa!