Dulu waktu pertama kali bikin UI Android pakai XML layout, rasanya ribet banget. Mau bikin satu halaman sederhana aja harus bolak-balik antara file XML dan Kotlin/Java. Belum lagi masalah constraint di ConstraintLayout yang kadang bikin pusing. Sekarang ada Jetpack Compose, semuanya jadi lebih intuitif kamu tulis UI langsung di Kotlin, tanpa XML, tanpa findViewById, tanpa masalah.
Jetpack Compose itu framework UI modern dari Google yang bikin development Android jauh lebih cepat dan menyenangkan. Kalau kamu masih pakai XML layout atau baru mau mulai belajar Android, artikel ini cocok banget. Kita akan belajar dari nol mulai dari setup project sampai bikin aplikasi yang benar-benar bisa dipakai.
Sebelum masuk ke kode, penting buat paham kenapa Compose itu penting. Google sudah menjadikan Compose sebagai rekomendasi utama untuk membangun UI Android. Artinya, semua library baru dan fitur Android terbaru dibangun dengan Compose-first approach.
Beberapa keuntungan utama Compose:
Untuk mulai, kamu butuh Android Studio versi terbaru (Ladybug atau lebih baru). Buat project baru dan pilih template "Empty Activity" yang sudah pakai Compose.
Pastikan build.gradle.kts (Module level) punya dependency berikut:
dependencies {
// Compose BOM mengatur versi semua library Compose
val composeBom = platform("androidx.compose:compose-bom:2025.04.00")
implementation(composeBom)
// Core Compose
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material:material-icons-extended")
// Activity Compose untuk setContent {}
implementation("androidx.activity:activity-compose:1.10.1")
// Lifecycle & ViewModel
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.9.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.0")
// Navigation
implementation("androidx.navigation:navigation-compose:2.9.0")
// Debug tools
debugImplementation("androidx.compose.ui:ui-tooling")
}
Di bagian buildFeatures, aktifkan Compose:
android {
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.14"
}
}
Di Compose, UI dibangun dari function yang di-annotate dengan @Composable. Setiap function ini mendeskripsikan sebagian dari UI. Konsepnya mirip dengan component di React atau widget di Flutter.
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun Greeting(name: String) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Halo, $name!",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Selamat datang di Jetpack Compose",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Lihat bedanya dengan XML? Tidak ada file XML terpisah, tidak ada inflater, tidak ada findViewById. Semua langsung di Kotlin. Dan yang paling keren kamu bisa preview function ini langsung di Android Studio:
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
MaterialTheme {
Greeting("Developer")
}
}
Kalau kamu tertarik mempelajari lebih lanjut tentang pengembangan mobile, bisa cek juga perbandingan Flutter vs React Native yang sudah saya tulis sebelumnya.
State itu konsep paling penting di Compose. Setiap kali data berubah, UI otomatis ter-update. Ini yang disebut "reactive" atau "declarative" programming.
import androidx.compose.runtime.*
import androidx.compose.material3.*
@Composable
fun Counter() {
// remember = simpan state saat recomposition
// mutableStateOf = buat state yang bisa diubah
var count by remember { mutableStateOf(0) }
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Klik: $count kali",
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = { count-- }) {
Text("-1")
}
Button(onClick = { count = 0 }) {
Text("Reset")
}
Button(onClick = { count++ }) {
Text("+1")
}
}
}
}
Ada beberapa tipe state di Compose:
remember { mutableStateOf() } state lokal yang survive recomposition, tapi hilang saat configuration change (rotate layar)rememberSaveable { mutableStateOf() } survive configuration change juga. Pakai ini untuk data yang pentingViewModel + StateFlow state yang survive bahkan saat activity di-recreate. Cocok untuk data dari API atau databaseUntuk state management yang lebih kompleks, kamu bisa menggunakan pola ViewModel:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
data class TaskListUiState(
val tasks: List<String> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null
)
class TaskViewModel : ViewModel() {
private val _uiState = MutableStateFlow(TaskListUiState())
val uiState: StateFlow<TaskListUiState> = _uiState.asStateFlow()
fun addTask(task: String) {
_uiState.value = _uiState.value.copy(
tasks = _uiState.value.tasks + task
)
}
fun removeTask(index: Int) {
_uiState.value = _uiState.value.copy(
tasks = _uiState.value.tasks.toMutableList().apply {
removeAt(index)
}
)
}
}
Lalu di composable, collect state-nya:
@Composable
fun TaskScreen(viewModel: TaskViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsState()
var newTask by remember { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) {
OutlinedTextField(
value = newTask,
onValueChange = { newTask = it },
label = { Text("Tambah tugas baru") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = {
if (newTask.isNotBlank()) {
viewModel.addTask(newTask)
newTask = ""
}
},
modifier = Modifier.fillMaxWidth()
) {
Text("Tambah")
}
Spacer(modifier = Modifier.height(16.dp))
LazyColumn {
itemsIndexed(uiState.tasks) { index, task ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = task, modifier = Modifier.weight(1f))
IconButton(onClick = { viewModel.removeTask(index) }) {
Icon(
Icons.Default.Delete,
contentDescription = "Hapus"
)
}
}
}
}
}
}
}
Kalau di XML kamu pakai RecyclerView dengan adapter dan ViewHolder yang ribet, di Compose cukup pakai LazyColumn. Jauh lebih simpel:
data class Article(val title: String, val excerpt: String, val date: String)
@Composable
fun ArticleList(articles: List<Article>) {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(
items = articles,
key = { it.title } // stable key untuk animasi yang smooth
) { article ->
Card(
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = article.title,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = article.excerpt,
style = MaterialTheme.typography.bodyMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = article.date,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
Perhatikan key parameter di items. Ini penting untuk performa Compose pakai key untuk tahu item mana yang berubah saat list di-update, sehingga tidak perlu render ulang semua item.
Hampir semua aplikasi punya lebih dari satu halaman. Di Compose, navigasi ditangani oleh NavHost:
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
sealed class Screen(val route: String) {
object Home : Screen("home")
object Detail : Screen("detail/{itemId}") {
fun createRoute(itemId: String) = "detail/$itemId"
}
object Profile : Screen("profile")
}
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.Home.route
) {
composable(Screen.Home.route) {
HomeScreen(
onArticleClick = { id ->
navController.navigate(Screen.Detail.createRoute(id))
},
onProfileClick = {
navController.navigate(Screen.Profile.route)
}
)
}
composable(Screen.Detail.route) { backStackEntry ->
val itemId = backStackEntry.arguments?.getString("itemId") ?: ""
DetailScreen(
itemId = itemId,
onBack = { navController.popBackStack() }
)
}
composable(Screen.Profile.route) {
ProfileScreen(
onBack = { navController.popBackStack() }
)
}
}
}
Setiap screen adalah composable function yang menerima callback untuk navigasi. Pola ini bikin kode tetap clean dan testable setiap screen tidak perlu tahu tentang navigation library, cukup terima callback.
Compose punya integrasi native dengan Material Design 3 (Material You). Kamu bisa bikin UI yang cantik dengan sedikit kode:
@Composable
fun ArticleCard(
title: String,
description: String,
category: String,
onClick: () -> Unit
) {
Card(
onClick = onClick,
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
)
) {
Column(modifier = Modifier.padding(16.dp)) {
// Category chip
SuggestionChip(
onClick = { },
label = { Text(category, style = MaterialTheme.typography.labelSmall) }
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = title,
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
MaterialTheme otomatis menyesuaikan warna berdasarkan wallpaper device pengguna (Dynamic Color). Fitur ini hanya tersedia di Android 12+, tapi Compose punya fallback color scheme untuk device yang lebih lama.
Di Compose, ada beberapa function khusus untuk "side effects" operasi yang terjadi di luar siklus rendering:
@Composable
fun UserProfile(userId: String) {
var user by remember { mutableStateOf<User?>(null) }
var isLoading by remember { mutableStateOf(true) }
// LaunchedEffect jalankan coroutine saat composable pertama muncul
LaunchedEffect(userId) {
isLoading = true
user = userRepository.fetchUser(userId) // suspend function
isLoading = false
}
when {
isLoading -> CircularProgressIndicator()
user != null -> UserInfoCard(user!!)
else -> Text("User tidak ditemukan")
}
}
@Composable
fun AnalyticsScreen(screenName: String) {
// DisposableEffect jalankan saat masuk, cleanup saat keluar
DisposableEffect(screenName) {
analytics.trackScreenView(screenName)
onDispose {
analytics.trackScreenExit(screenName)
}
}
}
Penting untuk diingat: jangan panggil API atau operasi berat langsung di dalam composable function. Selalu gunakan LaunchedEffect atau ViewModel untuk operasi async.
Setelah cukup lama pakai Compose di project production, ini beberapa tips yang saya pelajari:
key di LazyColumn/LazyRow tanpa key, Compose bisa salah mengira item yang berubah dan render ulang semuanya. Performa langsung turun drastis di list besarremember atau bikin function di ViewModelderivedStateOf untuk computed state yang depends pada beberapa state lain. Ini mencegah unnecessary recompositionComposeTestRule jangan cuma rely pada manual testing. Compose punya testing framework yang powerfulModifier dengan benar urutan modifier itu penting. Modifier.padding(16.dp).background(Color.Red) beda hasilnya dengan Modifier.background(Color.Red).padding(16.dp)Untuk project yang lebih besar, pertimbangkan juga arsitektur yang proper. Kamu bisa baca tentang state management patterns yang applicable juga di Android Compose, meskipun artikel itu fokus ke Flutter.
Jetpack Compose itu game-changer untuk Android development. Dari yang dulu bikin UI harus bolak-balik XML dan Kotlin, sekarang semuanya satu tempat. Declarative approach-nya bikin kode lebih mudah dibaca, dikurangi, dan di-debug.
Kalau kamu masih baru di Android development, langsung mulai dengan Compose jangan buang waktu belajar XML layout. Dan kalau kamu sudah experienced dengan XML, mulai migrate pelan-pelan. Compose bisa dicampur dengan View lama, jadi tidak perlu rewrite semuanya sekaligus.
Mulai dari project kecil dulu bikin to-do app atau weather app. Setelah terbiasa dengan composable, state, dan navigation, kamu bisa mulai explore fitur yang lebih advanced seperti custom layout, animation, dan gesture handling. Selamat coding!