State adalah jantung Compose: tanpa state, UI tidak akan pernah berubah. State yang dikelola dengan baik membuat UI tetap responsif dan bebas bug; state yang dikelola buruk mengakibatkan recomposition berlebihan, kebocoran memori, atau kehilangan data saat rotasi. Bab ini membahas tiga konsep sentral: State<T> / MutableState<T>, remember, rememberSaveable, serta pola state hoisting yang memisahkan composable stateless dari stateful.
State<T> dan MutableState<T> #
State<T> adalah observable holder yang dapat dibaca di dalam composable. Setiap kali nilainya berubah, Compose menjadwalkan recomposition untuk semua composable yang membacanya. MutableState<T> adalah versi read-write; kita biasanya membuatnya lewat mutableStateOf(initial).
@Composable
fun Counter() {
val count = remember { mutableStateOf(0) }
Button(onClick = { count.value++ }) {
Text("Klik: ${count.value}")
}
}Saat count.value diubah, Compose memanggil ulang Counter untuk menghasilkan UI baru dengan label tombol diperbarui. Tanpa remember, setiap recomposition akan membuat instance MutableState baru — counter tidak akan pernah naik.
remember dan rememberSaveable #
remember mempertahankan nilai selama Activity hidup. Saat Activity direcreate (mis. rotasi), instance remember ikut hilang. Untuk state yang harus bertahan lintas rotasi, kita memakai rememberSaveable:
@Composable
fun FormLogin() {
var email by rememberSaveable { mutableStateOf("") }
var sandi by rememberSaveable { mutableStateOf("") }
TextField(value = email, onValueChange = { email = it })
TextField(value = sandi, onValueChange = { sandi = it }, visualTransformation = PasswordVisualTransformation())
}rememberSaveable otomatis menyimpan ke Bundle dan memulihkan saat Activity direcreate. Untuk tipe kompleks (mis. data class), kita perlu menyediakan Saver atau menggunakan @Parcelize. Tanpa rememberSaveable, pengguna yang memutar layar saat mengetik form akan kehilangan semua input.
Simulasi State Lintas Recreate #
Cuplikan berikut memodelkan MutableState sebagai property delegate dengan listener dan SaveableHolder sebagai Bundle yang menyimpan nilai saat Activity recreate:
// state_recomposition.kt
// Mensimulasikan state hoisting, mutableStateOf, dan rememberSaveable sebagai
// kelas ringkas yang mempertahankan nilai lintas "rotasi layar" (recreate).
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
// State yang memicu listener saat nilai diubah
class MutableState<T>(initial: T) : ReadWriteProperty<Any?, T> {
private var value: T = initial
private val listeners = mutableListOf<(T) -> Unit>()
fun onChange(listener: (T) -> Unit) { listeners.add(listener) }
override fun getValue(thisRef: Any?, property: KProperty<*>): T = value
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
if (this.value != value) {
this.value = value
listeners.forEach { it(value) }
}
}
}
// Simulasi rememberSaveable: nilai disimpan ke "Bundle" dan dipulihkan saat
// "Activity" direcreate
class SaveableHolder {
private val bundle = mutableMapOf<String, Any?>()
fun save(key: String, value: Any?) { bundle[key] = value }
fun <T> restore(key: String, default: T): T {
@Suppress("UNCHECKED_CAST")
return (bundle[key] as? T) ?: default
}
}
// Composable yang state-nya di-hoist ke pemanggil
class CounterState(initial: Int) {
private val state = MutableState(initial)
init { state.onChange { println(" [State] count berubah -> $it") } }
var count by state
fun increment() { count++ }
fun reset() { count = 0 }
}
// Simulasi: Activity recreate (rotasi), state dipulihkan dari SaveableHolder
fun main() {
val saveable = SaveableHolder()
println("=== Render pertama ===")
val state1 = CounterState(0)
state1.increment()
state1.increment()
state1.increment()
println("Sebelum recreate: count=${state1.count}")
saveable.save("count", state1.count)
println("\n=== Activity direcreate (rotasi) ===")
val restored = saveable.restore("count", 0)
val state2 = CounterState(restored)
println("Setelah recreate: count=${state2.count}")
println("\n=== Tanpa SaveableHolder: state hilang ===")
val state3 = CounterState(0)
println("Default baru: count=${state3.count}")
}Output yang Dihasilkan:
=== Render pertama ===
[State] count berubah -> 1
[State] count berubah -> 2
[State] count berubah -> 3
Sebelum recreate: count=3
=== Activity direcreate (rotasi) ===
Setelah recreate: count=3
=== Tanpa SaveableHolder: state hilang ===
Default baru: count=0Pada output, counter yang disimpan ke SaveableHolder sebelum Activity recreate tetap memiliki nilai 3 setelah dipulihkan. Tanpa SaveableHolder, instance baru dimulai dari 0 — inilah mengapa rememberSaveable penting untuk form pengguna.
State Hoisting #
Pola state hoisting memindahkan kepemilikan state dari composable ke pemanggilnya. Composable yang menerima value dan onChange menjadi stateless — dapat dipreview, diuji, dan dipakai ulang dengan mudah.
@Composable
fun NameField(nama: String, onNamaChange: (String) -> Unit) {
TextField(value = nama, onValueChange = onNamaChange)
}Composable stateless tidak peduli darimana state berasal — bisa dari rememberSaveable, ViewModel, atau state global. Pemanggillah yang menentukan. Pemisahan ini juga membuat composable dapat di-reuse di berbagai context tanpa duplikasi logika state.
| Pola | Contoh | Kapan dipakai |
|---|---|---|
| Stateful (internal) | remember { mutableStateOf(0) } | UI sederhana yang state tidak dibagikan |
| Hoisted (stateless) | value, onChange dari parameter | Komponen reusable, form, dialog |
| ViewModel-driven | stateFlow.collectAsState() | State lintas layar atau hidup lama |
Referensi Lanjutan #
- Jetpack Compose — rujukan Jetpack Compose untuk state management dan mekanisme recomposition
Selanjutnya #
State sudah tersimpan dengan benar, namun bagaimana menampilkan daftar panjang atau grid yang efisien? Chapter Compose Layouts akan membahas LazyColumn, LazyRow, dan rantai Modifier.