September 28, 2026
Jetpack Compose Dasar.
Dasar Jetpack Compose: fungsi composable, state & remember, recomposition, Modifier, Material 3, daftar dengan LazyColumn, dan navigasi.
Jetpack Compose adalah toolkit UI deklaratif untuk Android: UI ditulis sebagai fungsi Kotlin yang menerima state, bukan XML yang dimutasi lewat findViewById. Kalau pernah memakai React, modelnya akan terasa familiar — lihat Cara Menggunakan React
sebagai pembanding. Seluruh UI NotiFly
(riwayat notifikasi, pengaturan, JSON tester) dibangun dengan Compose + Material 3. Prasyarat: Dasar Kotlin
.
1. Setup Singkat
Buat proyek dengan template Empty Activity di Android Studio; Compose sudah terpasang. Versi library Compose diatur lewat BOM sehingga semua artefak saling kompatibel:
// app/build.gradle.kts
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose") // compiler Compose (Kotlin 2.x)
}
dependencies {
val composeBom = platform("androidx.compose:compose-bom:<versi-terbaru>")
implementation(composeBom)
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui-tooling")
implementation("androidx.activity:activity-compose:<versi>")
implementation("androidx.lifecycle:lifecycle-runtime-compose:<versi>")
}
Sejak Kotlin 2.0, compiler Compose ikut rilis Kotlin lewat plugin
org.jetbrains.kotlin.plugin.compose— tidak perlu lagi mencocokkankotlinCompilerExtensionVersionsecara manual. Cek versi BOM terbaru di halaman rilis developer.android.com.
2. Composable Function
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MaterialTheme {
Greeting(name = "NotiFly")
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
Text("Halo, $name", style = MaterialTheme.typography.headlineSmall)
Text("Siap meneruskan notifikasi", color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
@Preview(showBackground = true)
@Composable
private fun GreetingPreview() {
MaterialTheme { Greeting("Preview") }
}
Aturan main composable:
- Diberi anotasi
@Composable, nama diawali huruf besar (seperti komponen), tidak mengembalikan nilai UI — ia memancarkan UI. - Harus idempoten dan bebas efek samping: bisa dipanggil ulang kapan saja, dalam urutan apa pun.
- Konvensi: parameter
modifier: Modifier = Modifiersebagai parameter opsional pertama, diteruskan ke elemen root.
Layout dasar: Column (vertikal), Row (horizontal), Box (tumpuk), Spacer, dan Scaffold untuk kerangka layar (top bar, bottom bar, snackbar, FAB).
3. State, remember, dan Recomposition
UI = f(state). Saat state yang dibaca sebuah composable berubah, Compose memanggil ulang composable itu (recomposition) — hanya bagian yang terdampak.
@Composable
fun KeywordEditor() {
var input by remember { mutableStateOf("") } // bertahan antar-recomposition
var keywords by rememberSaveable { mutableStateOf(listOf<String>()) } // + bertahan saat rotasi
Column(Modifier.padding(16.dp)) {
OutlinedTextField(
value = input,
onValueChange = { input = it },
label = { Text("Kata kunci") },
singleLine = true,
)
Button(
onClick = {
keywords = keywords + input.trim().lowercase()
input = ""
},
enabled = input.isNotBlank(),
) { Text("Tambah") }
keywords.forEach { Text("• $it") }
}
}
| API | Fungsi |
|---|---|
mutableStateOf(x) | state yang diamati Compose |
remember { } | simpan nilai selama composable ada di komposisi |
rememberSaveable { } | seperti remember, plus bertahan saat rotasi/process death |
derivedStateOf { } | nilai turunan yang hanya memicu recomposition bila hasilnya berubah |
LaunchedEffect(key) | jalankan coroutine saat masuk komposisi / key berubah |
collectAsStateWithLifecycle() | ubah StateFlow dari ViewModel menjadi state, sadar lifecycle |
State hoisting: composable yang dapat dipakai ulang sebaiknya stateless — terima value dan onValueChange dari pemanggil. Sumber kebenaran untuk state layar berada di ViewModel (dibahas di Arsitektur Android Modern
).
@Composable
fun HistoryScreen(viewModel: HistoryViewModel = viewModel(factory = ViewModelFactory.Factory)) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
HistoryContent(state = state, onRetry = viewModel::retry) // stateless, mudah di-preview
}
4. Modifier
Modifier adalah rantai dekorasi: ukuran, padding, background, klik, dll. Urutan berpengaruh — setiap modifier membungkus hasil modifier sebelumnya.
@Composable
fun StatusBadge(label: String, container: Color, content: Color, modifier: Modifier = Modifier) {
Text(
text = label,
style = MaterialTheme.typography.labelSmall,
color = content,
modifier = modifier
.clip(RoundedCornerShape(6.dp)) // 1. potong bentuk
.background(container) // 2. warnai area yang sudah dipotong
.padding(horizontal = 8.dp, vertical = 3.dp), // 3. padding di dalam background
)
}
Tukar urutan padding dan background, dan padding akan berada di luar warna. Modifier umum: fillMaxWidth(), size(), weight() (di Row/Column), clickable {}, border(), verticalScroll(rememberScrollState()).
5. Material 3 & Tema
@Composable
fun NotiFlyTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
val context = LocalContext.current
val colors = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ->
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
darkTheme -> darkColorScheme()
else -> lightColorScheme(primary = Color(0xFF0F766E))
}
MaterialTheme(colorScheme = colors, content = content)
}
Gunakan role warna (primary, onPrimary, surfaceVariant, errorContainer, …) alih-alih warna hardcode, agar light/dark mode dan dynamic color (Android 12+) otomatis benar. Material 3 tidak punya role “success”, jadi di NotiFly status SENT memakai sepasang warna hijau tetap yang tetap terbaca di kedua tema.
Komponen yang sering dipakai: Scaffold, TopAppBar, NavigationBar, Card, ListItem, FilterChip, ModalBottomSheet, AlertDialog, SnackbarHost.
6. Daftar dengan LazyColumn
Column + forEach merender semua item sekaligus; untuk daftar panjang pakai LazyColumn yang hanya mengomposisi item yang terlihat.
data class Notif(val id: Long, val title: String, val status: String)
data class DayGroup(val label: String, val items: List<Notif>)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HistoryList(groups: List<DayGroup>, onClick: (Notif) -> Unit, modifier: Modifier = Modifier) {
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
groups.forEach { group ->
stickyHeader(key = "day-${group.label}") {
Text(group.label, style = MaterialTheme.typography.titleSmall,
modifier = Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surface))
}
items(group.items, key = { it.id }) { item ->
ListItem(
headlineContent = { Text(item.title) },
supportingContent = { Text(item.status) },
modifier = Modifier.clickable { onClick(item) }.animateItem(),
)
}
}
}
}
key yang stabil (ID database) penting: posisi scroll dan animasi tetap benar saat item baru masuk di atas. Padanannya: LazyRow, LazyVerticalGrid.
7. Navigasi
Ada tiga pendekatan, pilih sesuai kebutuhan:
| Pendekatan | Kapan |
|---|---|
| Navigation Compose (type-safe route) | Aplikasi dengan back stack: daftar → detail → edit |
Navigation 3 (androidx.navigation3, stabil) | Proyek baru yang ingin back stack sebagai state milik sendiri, layout adaptif |
| Pager / state sederhana | Beberapa tab setara tanpa back stack |
Navigation Compose dengan route bertipe (butuh plugin kotlinx.serialization):
@Serializable object HistoryRoute
@Serializable data class DetailRoute(val id: Long)
@Composable
fun AppNavHost(navController: NavHostController = rememberNavController()) {
NavHost(navController = navController, startDestination = HistoryRoute) {
composable<HistoryRoute> {
HistoryScreen(onOpen = { id -> navController.navigate(DetailRoute(id)) })
}
composable<DetailRoute> { entry ->
val route: DetailRoute = entry.toRoute()
DetailScreen(id = route.id, onBack = { navController.popBackStack() })
}
}
}
NotiFly justru tidak memakai NavHost: empat layar utamanya setara, jadi cukup HorizontalPager yang dikendalikan NavigationBar — bisa di-swipe, tanpa back stack yang perlu dikelola. Detail item dibuka sebagai ModalBottomSheet, bukan layar baru.
Kesalahan Umum
| Kesalahan | Akibat | Perbaikan |
|---|---|---|
mutableStateOf tanpa remember | State ter-reset setiap recomposition | remember { mutableStateOf(...) } |
| Memanggil API / menulis DB langsung di body composable | Terpanggil berkali-kali | Pindahkan ke ViewModel atau LaunchedEffect |
Mengubah MutableList di dalam state | UI tidak ter-update (referensi sama) | Ganti dengan list baru: list = list + x |
LazyColumn tanpa key | Scroll melompat, animasi salah | items(list, key = { it.id }) |
collectAsState() biasa untuk Flow dari ViewModel | Tetap collect saat app di background | collectAsStateWithLifecycle() |
| Urutan modifier asal | Padding/klik/background di area yang salah | Baca rantai dari luar ke dalam |
| Warna hardcode di mana-mana | Dark mode rusak | Pakai MaterialTheme.colorScheme |
LazyColumn di dalam Column yang verticalScroll | Crash: infinite height constraints | Satu container scroll saja; gabungkan item ke LazyColumn |

Hey! I’m Fanny, the software engineer tending to this digital garden. You can read more about me, or subscribe by email.