Anotasi @Composable di atas sebuah fungsi Kotlin menandai bahwa fungsi tersebut berpartisipasi dalam ekosistem Compose: dapat dipanggil hanya dari composable lain, dapat melewatkan state ke runtime, dan dapat dijadwalkan untuk recomposition. Pada chapter ini kita melihat composable sebagai fungsi murni, memahami apa yang memicu recomposition, dan belajar aturan stabilitas parameter yang menentukan apakah Compose akan skip render ulang.
Composable sebagai Fungsi Murni #
Fungsi composable idealnya adalah fungsi murni dari inputnya: output (UI yang dirender) hanya bergantung pada parameter, bukan pada variabel global atau side effect. Sifat murni ini memungkinkan Compose runtime menjadwalkan recomposition cerdas — bila input tidak berubah, runtime dapat melewati pemanggilan.
@Composable
fun Salam(nama: String) {
Text("Halo, $nama")
}Saat nama berubah, Compose memanggil ulang Salam. Saat nama sama, runtime dapat mengembalikan hasil render sebelumnya. Inilah mengapa composable dianjurkan stateless — semua data masuk lewat parameter, semua event keluar lewat callback.
Mekanisme Recomposition #
Recomposition adalah proses Compose memanggil ulang fungsi composable untuk menghasilkan pohon UI baru. Pemicunya selalu sama: state yang dibaca di dalam composable berubah. Compose runtime memantau state melalui State<T> dan MutableState<T>; setiap pembacaan .value dicatat sebagai dependency.
| Pemicu recomposition | Yang terjadi |
|---|---|
mutableStateOf diubah | Composable yang membaca state tersebut dipanggil ulang |
| Parameter berubah | Composable dengan parameter baru dipanggil ulang |
| State tidak dibaca | Tidak ada recomposition walau state berubah |
| Input sama persis | Compose dapat skip (smart recomposition) |
Simulasi Recomposition #
Cuplikan berikut memodelkan Compose runtime sebagai counter yang mencatat setiap pemanggilan composable. Saat state berubah, kita memanggil ulang layarPengguna untuk melihat urutan recomposition:
// composable_functions.kt
// Mendemonstrasikan sifat fungsi @Composable sebagai fungsi murni dari
// input-nya. Recomposition dipicu ketika input berubah — di sini disimulasikan
// dengan mencatat setiap pemanggilan ke composable dalam buffer.
import kotlin.properties.Delegates
class RecompositionCounter {
var count: Int by Delegates.observable(0) { _, _, new ->
println(" [Compose] recompose #$new")
}
}
class ComposeScope(val counter: RecompositionCounter, val tag: String) {
fun invoke(input: Any?) {
counter.count = counter.count + 1
println("$tag(input=$input)")
}
}
class ComposeRuntime {
private val counter = RecompositionCounter()
private val scopes = mutableMapOf<String, ComposeScope>()
fun composable(tag: String, input: Any?) {
val scope = scopes.getOrPut(tag) { ComposeScope(counter, tag) }
scope.invoke(input)
}
fun reset() { counter.count = 0 }
fun totalRecompositions(): Int = counter.count
}
// Fungsi yang menerima "state" dan memanggil composable
fun layarPengguna(runtime: ComposeRuntime, nama: String, umur: Int) {
runtime.composable("Header", nama)
runtime.composable("ProfilCard", "$nama:$umur")
// Bila nama/umur berubah, hanya composable yang input-nya berubah yang akan
// direcompose ulang di Compose asli — di sini kita catat semua pemanggilan.
}
fun main() {
val runtime = ComposeRuntime()
println("Render #1: nama=Budi, umur=25")
layarPengguna(runtime, "Budi", 25)
println("Render #2: nama=Budi, umur=26 (umur berubah)")
layarPengguna(runtime, "Budi", 26)
println("Render #3: nama=Siti, umur=26 (nama berubah)")
layarPengguna(runtime, "Siti", 26)
println("Render #4: input sama persis — Compose dapat skip")
layarPengguna(runtime, "Siti", 26)
println("Total recompose tercatat: ${runtime.totalRecompositions()}")
}Output yang Dihasilkan:
Render #1: nama=Budi, umur=25
[Compose] recompose #1
Header(input=Budi)
[Compose] recompose #2
ProfilCard(input=Budi:25)
Render #2: nama=Budi, umur=26 (umur berubah)
[Compose] recompose #3
Header(input=Budi)
[Compose] recompose #4
ProfilCard(input=Budi:26)
Render #3: nama=Siti, umur=26 (nama berubah)
[Compose] recompose #5
Header(input=Siti)
[Compose] recompose #6
ProfilCard(input=Siti:26)
Render #4: input sama persis — Compose dapat skip
[Compose] recompose #7
Header(input=Siti)
[Compose] recompose #8
ProfilCard(input=Siti:26)
Total recompose tercatat: 8Perhatikan bahwa setiap perubahan parameter memicu pemanggilan ulang Header dan ProfilCard. Compose runtime asli melakukan optimasi: bila input tidak berubah sejak pemanggilan terakhir, runtime dapat mengembalikan cache dan menghindari recomposition — inilah yang disebut smart recomposition.
Aturan Stabilitas Parameter #
Compose mengelompokkan parameter menjadi stabil dan tidak stabil. Parameter stabil adalah tipe yang hasil equals-nya konsisten untuk nilai yang sama — String, Int, Boolean, dan semua data class val. Parameter tidak stabil adalah List<T>, Map<K,V>, atau class biasa dengan var. Bila composable menerima parameter tidak stabil, runtime tidak dapat menjamin input sama → skip, sehingga recomposition terjadi lebih sering dari yang seharusnya.
data class StabilUser(val nama: String, val umur: Int) // stabil
class TidakStabilUser { var nama: String = ""; var umur: Int = 0 } // tidak stabilUntuk class yang tidak bisa di-data class (mis. dari library pihak ketiga), kita dapat menandainya stabil dengan anotasi @Immutable atau @Stable. Compose mempercayai anotasi tersebut dan akan mengoptimasi recomposition.
Referensi Lanjutan #
- Jetpack Compose
— rujukan Jetpack Compose untuk anotasi
@Composabledan siklus hidup composable
Selanjutnya #
Setelah memahami mekanisme recomposition, kita perlu tahu cara membaca dan menulis state dengan benar — termasuk pola state hoisting. Chapter State dan Recomposition akan membahas State<T>, mutableStateOf, dan rememberSaveable.