Antarmuka statis di Bagian 1 hanya menampilkan data contoh yang ditulis manual. Pada Bagian 2 kita membuat aplikasi yang benar-benar interaktif: pengguna dapat menambah transaksi dan melihat daftar diperbarui secara langsung. Untuk mencapai ini, kita menerapkan dua konsep inti Jetpack Compose: state hoisting (state dipegang ViewModel, UI hanya emit event) dan Compose Navigation (back stack antar layar). Kode contoh tetap menggunakan Kotlin murni yang dapat dijalankan di terminal, dengan pola yang sama persis dengan Compose asli.
State Hoisting #
Pola state hoisting Compose mendorong tiga prinsip:
- State dipegang oleh pemilik tunggal — biasanya
ViewModel. UI hanya membaca dan menampilkan. - Event naik dari UI ke ViewModel — UI tidak mengubah state langsung; UI mengirim event.
- State mengalir turun ke UI secara observable — UI mengamati
StateFlowdan render ulang otomatis saat state berubah.
Pola ini membuat UI mudah diuji (cukup beri state, periksa output) dan ViewModel reusable di banyak layar. Saat state berubah, hanya UI yang mengamati state terkait yang render ulang — bukan seluruh aplikasi.
ViewModel dengan StateFlow #
ViewModel adalah class yang memegang state UI dan bertahan saat konfigurasi berubah (rotasi layar). StateFlow<T> adalah hot flow yang selalu memancarkan nilai terbaru ke observer. Kode berikut menunjukkan implementasi sederhana keduanya dalam Kotlin murni:
// Bagian 2: State management dan Compose Navigation.
// Mendemonstrasikan pola ViewModel dengan StateFlow dan navigasi
// antar layar via back stack. State hoisting: UI mengirim event,
// ViewModel memegang state.
enum class JenisTransaksi { PEMASUKAN, PENGELUARAN }
data class Transaksi(
val id: Long,
val jenis: JenisTransaksi,
val jumlah: Long,
val kategori: String,
val catatan: String
)
data class UiStateDaftar(
val transaksi: List<Transaksi> = emptyList(),
val sedangMemuat: Boolean = false
)
// Simulasi StateFlow<T>: state observable yang dapat dikoleksi.
class StateFlow<T>(initial: T) {
private var nilai: T = initial
private val observer = mutableListOf<(T) -> Unit>()
fun value(): T = nilai
fun update(transform: (T) -> T) {
nilai = transform(nilai)
observer.forEach { it(nilai) }
}
fun collect(block: (T) -> Unit) {
observer.add(block)
block(nilai) // emit nilai terbaru saat mulai mengamati
}
}
sealed class Event {
data class TambahTransaksi(val t: Transaksi) : Event()
object MuatData : Event()
}
class TransaksiViewModel {
val state = StateFlow(UiStateDaftar(sedangMemuat = true))
private var urutan = 0L
fun onEvent(e: Event) {
when (e) {
is Event.MuatData -> {
// Simulasi load dari database
state.update { it.copy(sedangMemuat = false) }
}
is Event.TambahTransaksi -> {
urutan += 1
val baru = e.t.copy(id = urutan)
state.update { ui ->
ui.copy(transaksi = ui.transaksi + baru)
}
}
}
}
}
// Compose Navigation: back stack daftar string route.
class NavController(routeAwal: String) {
private val backStack = mutableListOf(routeAwal)
fun routeSekarang(): String = backStack.last()
fun navigate(route: String) {
backStack.add(route)
println(" -> navigate ke: $route | stack=${backStack}")
}
fun popBack(): Boolean {
if (backStack.size <= 1) return false
backStack.removeAt(backStack.lastIndex)
println(" <- popBack ke: ${backStack.last()} | stack=${backStack}")
return true
}
fun stackSize(): Int = backStack.size
}
fun main() {
val vm = TransaksiViewModel()
val nav = NavController("daftar")
println("=== Initial state ===")
println("route: ${nav.routeSekarang()} | state.sedangMemuat=${vm.state.value().sedangMemuat}")
println()
println("=== UI mengamati state (collect) ===")
var renderCount = 0
vm.state.collect { ui ->
renderCount += 1
println(" Render #$renderCount: ${ui.transaksi.size} transaksi, sedangMemuat=${ui.sedangMemuat}")
}
println()
println("=== Event: MuatData ===")
vm.onEvent(Event.MuatData)
println()
println("=== User tap tombol Tambah -> navigate form ===")
nav.navigate("formTambah")
println()
println("=== User isi form -> event TambahTransaksi ===")
vm.onEvent(Event.TambahTransaksi(
Transaksi(0, JenisTransaksi.PEMASUKAN, 5_000_000, "Gaji", "Gaji Januari")
))
vm.onEvent(Event.TambahTransaksi(
Transaksi(0, JenisTransaksi.PENGELUARAN, 750_000, "Makan", "Makan siang")
))
println()
println("=== User tap Simpan -> popBack ke daftar ===")
nav.popBack()
println()
println("=== State akhir ===")
println("route: ${nav.routeSekarang()} | stack=${nav.stackSize()}")
println("Total transaksi: ${vm.state.value().transaksi.size}")
vm.state.value().transaksi.forEach { t ->
val arah = if (t.jenis == JenisTransaksi.PEMASUKAN) "+" else "-"
println(" [${t.id}] $arah Rp${t.jumlah} ${t.kategori}")
}
}Output yang Dihasilkan:
=== Initial state ===
route: daftar | state.sedangMemuat=true
=== UI mengamati state (collect) ===
Render #1: 0 transaksi, sedangMemuat=true
=== Event: MuatData ===
Render #2: 0 transaksi, sedangMemuat=false
=== User tap tombol Tambah -> navigate form ===
-> navigate ke: formTambah | stack=[daftar, formTambah]
=== User isi form -> event TambahTransaksi ===
Render #3: 1 transaksi, sedangMemuat=false
Render #4: 2 transaksi, sedangMemuat=false
=== User tap Simpan -> popBack ke daftar ===
<- popBack ke: daftar | stack=[daftar]
=== State akhir ===
route: daftar | stack=1
Total transaksi: 2
[1] + Rp5000000 Gaji
[2] - Rp750000 MakanMari kita bedah komponen utama:
class StateFlow<T>(initial: T): implementasi minimalStateFlow. Menyimpan nilai terkini dan daftar observer.update(transform)mengubah nilai lalu memberitahu semua observer.collect(block)mendaftarkan observer dan langsung memancarkan nilai terkini (perilakuStateFlowasli).sealed class Event: mendefinisikan semua event yang dikenal ViewModel.sealedmembuatwhendionEventmenjadi exhaustive — jika event baru ditambah tanpa handler, kompilator memperingatkan.vm.onEvent(Event.TambahTransaksi(...)): UI mengirim event ke ViewModel. ViewModel memutuskan bagaimana state berubah; UI tidak tahu detail implementasi. Inilah inti state hoisting.state.update { ui -> ui.copy(transaksi = ui.transaksi + baru) }: imutable update.copy()membuat instanceUiStateDaftarbaru dengan daftar transaksi yang diperluas. State lama tidak dimutasi; observer menerima referensi baru.NavController(routeAwal: String): simulasi Compose Navigation. Back stack adalahMutableList<String>;navigatepush route baru,popBackpop route terakhir. Compose Navigation asli memakainavController.navigate("route")dannavController.popBackStack().
Compose Navigation #
Compose Navigation memakai konsep route sebagai identifier string. Setiap layar diidentifikasi oleh route unik:
NavHost(navController = navController, startDestination = "daftar") {
composable("daftar") { LayarDaftar(vm, onTambah = { navController.navigate("formTambah") }) }
composable("formTambah") { LayarForm(vm, onSimpan = { vm.onEvent(...); navController.popBack() }) }
}Setiap composable("route") { ... } mendaftarkan layar untuk route tersebut. Saat navController.navigate("formTambah") dipanggil, Compose menambah route ke back stack dan render LayarForm. Saat popBack(), route terakhir dihapus dan layar sebelumnya dirender ulang.
Back Stack dan Lifecycle #
Back stack memungkinkan tombol Back peranti berfungsi: tap Back akan pop route terakhir. Compose Navigation otomatis menangani ini dengan BackHandler. Saat back stack tinggal satu route (start destination), tap Back keluar aplikasi.
Lifecycle layar Compose mengikuti back stack: layar yang tidak terlihat (di bawah top of stack) tetap di-compose tetapi lifecycle-nya STOPPED. Saat pengguna pop kembali, lifecycle kembali STARTED. ViewModel yang dipakai layar tetap bertahan selama back stack masih merujuknya.
ViewModel Survival saat Rotasi #
Salah satu keuntungan utama ViewModel adalah bertahan saat konfigurasi berubah (rotasi layar, ganti bahasa). Saat layar di-rotate, Activity dibuat ulang, tetapi ViewModel yang dipakai layar tersebut tetap hidup. State UI tidak hilang, dan pengguna tidak perlu mengisi form lagi.
Mekanisme ini dicapai dengan viewModel<TransaksiViewModel>() di Compose yang memakai ViewModelStoreOwner. Saat Activity dibuat ulang, ViewModelStore lama dipindahkan ke Activity baru, sehingga ViewModel tetap dipegang.
Referensi Lanjutan #
- Kotlin Multiplatform Mobile getting started — panduan KMP Mobile untuk merancang navigasi dan manajemen state aplikasi Android
Ringkasan Bagian 2 #
State management dan navigasi kini lengkap. TransaksiViewModel memegang state via StateFlow, UI mengamati state dan mengirim event. NavController mengelola back stack dengan navigate dan popBack. State bertahan saat rotasi karena dipegang oleh ViewModel. Pada Bagian 3 kita menghubungkan ViewModel ke database lokal via Room, sehingga transaksi tersimpan persisten dan dapat dimuat kembali saat aplikasi dibuka.