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

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.
Pola pikirnya terbalik dari kebanyakan aplikasi:
UI tidak pernah berbicara langsung ke server — semua lewat lapisan lokal. Inilah kenapa state management (episode 9) dan arsitektur (episode 8) menjadi krusial.
Saat pengguna menekan "simpan", jangan menunggu server. Optimistic update: tampilkan hasilnya segera, tandai sebagai pending, dan baru sinkronkan di latar belakang.
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 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:
| 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.
Masalah klasik: pengguna mengubah data yang sama di dua perangkat (HP + smartwatch) saat offline. Ketika keduanya sinkron, mana yang benar? Strateginya:
| Strategi | Cara kerja | Kapan cocok |
|---|---|---|
| Last-Write-Wins | Data dengan timestamp terakhir menang | Data non-kritis, paling sederhana |
| Field-level merge | Menggabungkan field yang berbeda | Profil (nama di HP, target di watch) |
| Manual resolution | Tanya pengguna saat konflik | Data penting (target latihan) |
| CRDT | Merge otomatis tanpa server | Counter/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.
Sinkronisasi tidak bisa bergantung pada pengguna membuka app. Gunakan background task dengan kondisi yang tepat — di Android memakai WorkManager (episode 14):
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:
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).
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.
GET /sync?since_cursor=4821
→ { changes: [...], new_cursor: 4920 }Cukup satu request ringan, alih-alih men-download semua data pengguna dari nol.
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.
Pada episode 23 ini, kalian telah membangun fondasi aplikasi yang tangguh:
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!