Kotlin Coroutines dan Flow di Android: Panduan Praktis untuk Tim

Cara memakai coroutine dan Flow di aplikasi Android tanpa memory leak atau ANR: suspend function, dispatcher, viewModelScope, cancellation, StateFlow dan SharedFlow, collect yang aman, penanganan error, dan testing dengan runTest.

Dipublikasikan 10 menit baca
Ponsel Android di samping laptop yang menampilkan kode

Setiap kali kami mengaudit aplikasi Android yang sudah agak lama, tiga bug coroutine yang sama hampir selalu muncul. GlobalScope.launch yang masih terus upload padahal pengguna sudah logout. Flow yang di-collect di onCreate dan tetap jalan saat aplikasi ada di background, sampai baterai terkuras. Lalu try/catch di sekitar panggilan network yang diam-diam menelan cancellation, sehingga layar masih di-update padahal sudah ditutup. Bug ini bukan kasus langka. Asalnya dari belajar coroutine sepotong-sepotong lewat jawaban Stack Overflow. Panduan ini berisi aturan yang kami ajarkan supaya satu tim menulis coroutine dengan cara yang sama.

Suspend Function dan Dispatcher

Suspend function bisa berhenti sementara tanpa memblokir thread tempat ia berjalan. Itulah intinya. Saat repository memanggil API Retrofit yang dideklarasikan sebagai suspend, main thread tetap bebas menggambar frame selama request berjalan. Retrofit dan Room sama-sama mendukung suspend function dan mengurus thread-nya sendiri, jadi aman dipanggil dari main thread.

Lain cerita untuk kode blocking buatan Anda sendiri. Membaca file besar, hashing password, atau parsing JSON besar dengan library yang tidak paham coroutine perlu dibungkus withContext. Pakai Dispatchers.IO untuk I/O blocking. Defaultnya mengizinkan sampai 64 thread, atau lebih di perangkat dengan core lebih banyak. Pakai Dispatchers.Default untuk kerja CPU seperti sorting atau resize gambar, dengan jumlah thread sebanyak core. Aturan yang kami pegang: setiap suspend function harus main-safe. Fungsi yang melakukan kerja blocking yang pindah dispatcher sendiri, jadi pemanggilnya tidak perlu memikirkan itu.

Masukkan dispatcher lewat constructor, jangan hardcode Dispatchers.IO di mana-mana. Biayanya cuma satu parameter, dan testing nanti jadi jauh lebih gampang.

Scope dan Structured Concurrency

Setiap coroutine harus punya scope yang umurnya sesuai dengan pekerjaannya. Di Android, Anda jarang perlu membuat scope sendiri.

ScopeHidup sampaiDipakai untuk
viewModelScopeViewModel di-clearMemuat data layar, submit form, kebanyakan kerja yang dipicu UI
lifecycleScopeActivity atau Fragment di-destroyKerja khusus UI seperti animasi, atau memulai repeatOnLifecycle
Application scope yang di-injectProses aplikasi matiKerja yang harus selesai walau pengguna keluar layar, misalnya menyimpan draft
WorkManagerPekerjaan selesai, bahkan setelah restartUpload, sinkronisasi, apa pun yang harus bertahan walau proses dimatikan
GlobalScopeTidak pernah dibatalkanHampir tidak ada, makanya ditandai sebagai delicate API

Structured concurrency artinya parent menunggu semua child-nya, dan membatalkan parent berarti membatalkan semua child. Kalau Anda membuka coroutineScope lalu launch tiga request di dalamnya, fungsi itu baru selesai setelah ketiganya selesai. Kalau satu gagal, dua lainnya ikut dibatalkan. Kalau Anda ingin yang lain tetap jalan saat satu gagal, misalnya tiga widget dashboard yang saling lepas, pakai supervisorScope.

Cancellation Butuh Kerja Sama

Cancellation bekerja dengan melempar CancellationException di titik suspend berikutnya. Ada dua kebiasaan yang merusaknya. Pertama, menangkap Exception atau Throwable di sekitar panggilan suspend tanpa melempar ulang CancellationException. Kedua, memakai runCatching di sekitar panggilan suspend, yang ikut menangkap cancellation. Akibatnya sama: coroutine tetap jalan walaupun scope-nya sudah tidak ada. Tangkap exception yang memang Anda harapkan, seperti IOException atau HttpException, atau lempar ulang CancellationException secara eksplisit. Di loop panjang yang tidak pernah suspend, panggil ensureActive() supaya loop berhenti saat diminta.

Flow, StateFlow, dan SharedFlow

  • Flow itu cold. Tidak ada yang jalan sampai ada yang collect, dan setiap collector dapat eksekusi sendiri. Query Room yang mengembalikan Flow dan fungsi repository cocok di sini.
  • StateFlow itu hot dan selalu punya nilai saat ini. Collector baru langsung dapat nilai terakhir, dan nilai yang sama berturut-turut dilewati. Pakai untuk state layar di ViewModel.
  • SharedFlow itu hot tanpa nilai wajib. Anda yang menentukan berapa nilai lama yang di-replay dan bagaimana buffer-nya. Cocok untuk event broadcast yang didengar beberapa bagian aplikasi.

Untuk mengubah Flow cold dari Room menjadi state layar, pakai stateIn dengan SharingStarted.WhileSubscribed(5000). Jeda lima detik itu menjaga upstream tetap hidup saat layar diputar, lalu menghentikannya kalau pengguna benar-benar pergi. Untuk event sekali jalan seperti menampilkan snackbar atau navigasi, StateFlow akan memutar ulang event itu setelah rotasi. Banyak tim memakai Channel yang diekspos dengan receiveAsFlow untuk kasus ini. Google sekarang menyarankan event seperti ini dijadikan bagian dari UI state lalu dikosongkan setelah ditangani. Lebih repot, tapi event tidak mungkin hilang.

Collect Flow Mengikuti Lifecycle

Memanggil collect di dalam lifecycleScope.launch berarti collect tetap jalan saat aplikasi di background. Kalau sumbernya Room atau update lokasi, ini kerja dan pemakaian baterai yang nyata. Di layar berbasis View, bungkus collect dengan repeatOnLifecycle(Lifecycle.State.STARTED). Collect dimulai saat layar terlihat dan dibatalkan saat layar masuk background. Di Compose, pakai collectAsStateWithLifecycle dari lifecycle-runtime-compose. collectAsState biasa tetap collect tanpa peduli lifecycle.

Menangani Error

  1. 1Di repository, ubah kegagalan yang sudah diperkirakan seperti timeout dan HTTP 4xx menjadi tipe hasil yang dimengerti UI, supaya ViewModel mengolah data, bukan exception.
  2. 2Di rangkaian Flow, pakai operator catch untuk mengirim nilai cadangan atau state error. Operator ini hanya menangkap error dari upstream, jadi taruh setelah operator yang bisa gagal.
  3. 3Pakai retry atau retryWhen dengan backoff untuk panggilan network yang kadang gagal, dan batasi jumlah percobaannya.
  4. 4Pasang CoroutineExceptionHandler hanya di root coroutine yang dimulai dengan launch, untuk logging ke Crashlytics atau Sentry. Handler ini tidak berpengaruh di async atau child coroutine.

Testing dengan runTest

Library kotlinx-coroutines-test menyediakan runTest yang berjalan di virtual time. delay(30_000) selesai seketika, jadi menguji debounce atau retry dengan backoff cuma butuh beberapa milidetik. Ganti main dispatcher dengan Dispatchers.setMain lewat JUnit rule kecil, lalu kirim test dispatcher ke class yang menerima parameter dispatcher. StandardTestDispatcher menahan antrean kerja sampai Anda memajukannya, cocok untuk mengecek state di tengah proses. UnconfinedTestDispatcher langsung menjalankan kerja, cocok supaya test sederhana tetap pendek. Untuk Flow, library Turbine dari Cash App memungkinkan Anda memanggil awaitItem() dan mengecek setiap emisi secara berurutan, termasuk menangkap emisi tambahan yang tidak diharapkan.

Kesalahan yang Masih Sering Kami Temui

  • runBlocking di main thread. UI langsung beku, dan setelah sekitar lima detik tanpa merespons input, Android menampilkan dialog ANR.
  • Launch dari ViewModel ke GlobalScope supaya "pasti selesai". Pakai application scope yang di-inject atau WorkManager.
  • Memanggil SDK blocking di dalam suspend function tanpa withContext. Di code review kelihatannya aman, padahal main thread tetap terblokir.
  • Mengekspos MutableStateFlow dari ViewModel sehingga UI bisa mengubah state langsung. Ekspos StateFlow saja dan simpan versi mutable-nya sebagai private.
  • Memakai flowOn dan mengira itu mengubah tempat collect berjalan. flowOn hanya berpengaruh ke operator di atasnya.

Kalau Anda tidak bisa menyebut scope mana yang akan membatalkan sebuah coroutine, berarti Anda sudah menulis leak yang tinggal menunggu waktu.

Coroutine dan Flow bisa dipelajari dalam beberapa hari, tapi butuh beberapa bulan untuk memakainya dengan benar. Pelatihan Android Kotlin kami menyediakan satu modul penuh untuk topik ini, dengan latihan langsung di layar-layar aplikasi milik tim peserta, supaya aturan di atas benar-benar masuk ke codebase.

Poin penting

  • Buat setiap suspend function main-safe dengan pindah ke Dispatchers.IO atau Default di dalam fungsi yang melakukan kerja blocking.
  • Pakai viewModelScope dan lifecycleScope untuk kerja UI, application scope yang di-inject atau WorkManager untuk kerja yang lebih panjang, dan hindari GlobalScope.
  • Jangan menelan CancellationException dengan catch yang terlalu luas atau runCatching di sekitar panggilan suspend.
  • Ekspos state layar sebagai StateFlow dengan stateIn dan WhileSubscribed(5000), lalu collect dengan repeatOnLifecycle atau collectAsStateWithLifecycle.
  • Uji dengan runTest dan dispatcher yang di-inject, lalu pakai Turbine untuk mengecek urutan emisi Flow.
Bagikan artikel ini

Artikel terkait

Artikel lain tentang software development, AI, cloud, dan infrastruktur.

Smartphone dengan wireframe aplikasi di samping buku catatan dan kalkulator di atas mejaPengembangan Software

10 menit baca

Berapa Biaya Pembuatan Aplikasi Mobile di Indonesia?

Kisaran harga indikatif dalam Rupiah untuk aplikasi mobile sederhana, menengah, dan kompleks, faktor yang benar-benar menentukan harga, Flutter versus dua aplikasi native, tim dan durasi, biaya tahunan setelah rilis, serta cara membandingkan penawaran vendor.

Orang memegang HP Android yang sedang membuka website dengan koneksi selulerPengembangan Web

9 menit baca

Cara Mempercepat Loading Website Bisnis: Panduan Core Web Vitals

Panduan praktis mempercepat website: kenapa kecepatan penting di HP Android kelas menengah dan jaringan seluler Indonesia, arti LCP, INP, dan CLS, beda data lab dan data lapangan, penyebab website lambat, cara memperbaikinya, dan langkah kerjanya.

Mari berdiskusi

Sedang mencari partner untuk pengembangan software?

Ceritakan proyek yang sedang Anda bangun, kebutuhan yang ingin diselesaikan, dan tantangan yang dihadapi. Kami dapat membantu membahas pendekatan teknis, scope, timeline, dan estimasi biaya.

Mulai diskusi