Pernah nggak sih kamu buka Android Studio, lihat file XML layout yang isinya ratusan baris, dan langsung pusing? Saya dulu banget ngalamin itu. Setiap kali mau ganti posisi tombol aja harus bolak-balik antara XML dan Activity, belum lagi kalau ada error di preview tapi nggak muncul di runtime. Bikin frustasi.
Nah, di 2026 ini, Google udah bikin solusi yang namanya Jetpack Compose framework declarative UI yang bikin kamu bisa bikin seluruh tampilan Android cuma pake Kotlin. Nggak ada lagi XML. Nggak ada lagi findViewById. Semua ditulis dalam bahasa yang sama, di satu tempat yang sama.
Kalau kamu masih ragu buat migrasi atau baru mau mulai belajar Android development, artikel ini bakal jadi panduan lengkap kamu. Kita bakal mulai dari nol, bikin beberapa contoh app sederhana, dan bahas konsep-konsep penting yang wajib kamu pahami.
Android View system yang lama (XML-based) udah ada sejak Android pertama kali rilis. Dia bekerja secara imperatif kamu harus bilang sistem "lakukan ini, lalu itu, lalu update ini." Kalau ada perubahan data, kamu harus manual update setiap UI element yang terpengaruh.
Jetpack Compose bekerja secara declarative. Kamu cukup bilang "kalau data-nya seperti ini, tampilannya harusnya seperti ini." Compose yang bakal handle semua proses update-nya otomatis.
Perbedaan utamanya:
Pastikan kamu pakai Android Studio terbaru (Ladybug atau versi lebih baru). Saat bikin project baru, pilih template "Empty Activity" yang sudah menggunakan Compose. Android Studio otomatis setup dependencies yang diperlukan.
Kalau kamu mau menambahkan Compose ke project yang sudah ada, tambahkan ini di build.gradle.kts (Module level):
plugins {
id("org.jetbrains.kotlin.plugin.compose")
}
android {
namespace = "com.example.myapp"
compileSdk = 35
defaultConfig {
minSdk = 24
targetSdk = 35
}
buildFeatures {
compose = true
}
}
dependencies {
// Compose BOM - mengatur versi semua library Compose
val composeBom = platform("androidx.compose:compose-bom:2025.01.01")
implementation(composeBom)
// Core Compose
implementation("androidx.compose.ui:ui")
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.9.3")
// ViewModel + Compose integration
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
// Navigation Compose
implementation("androidx.navigation:navigation-compose:2.8.5")
// Debug tools
debugImplementation("androidx.compose.ui:ui-tooling")
}
Tips: Pakai compose-bom supaya semua library Compose kamu pakai versi yang kompatibel. Nggak perlu lagi mikirin versi satu per satu.
Di Compose, semua UI dibuat dari Composable functions. Ini adalah function biasa yang didekorasi dengan @Composable. Mari kita mulai dengan contoh sederhana:
@Composable
fun Greeting(name: String) {
Text(
text = "Halo, $name! Selamat datang di app ini.",
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface
)
}
// Preview untuk Android Studio
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
MyApplicationTheme {
Greeting("Developer")
}
}
Beberapa hal yang perlu kamu perhatikan:
@Preview bikin kamu bisa lihat hasilnya di panel Preview tanpa run appMaterialTheme memberikan akses ke tema aplikasi (warna, font, bentuk)Compose punya tiga layout dasar yang wajib kamu kuasai:
Column menyusun elemen secara vertikal (atas ke bawah):
@Composable
fun UserProfile() {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Avatar
Image(
painter = painterResource(R.drawable.avatar),
contentDescription = "Foto profil",
modifier = Modifier
.size(100.dp)
.clip(CircleShape)
)
Spacer(modifier = Modifier.height(12.dp))
// Nama
Text(
text = "Budi Santoso",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
// Bio
Text(
text = "Android Developer | Jakarta",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Row menyusun elemen secara horizontal (kiri ke kanan):
@Composable
fun ActionButtons() {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Button(onClick = { /* handle click */ }) {
Icon(Icons.Default.Favorite, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("Like")
}
OutlinedButton(onClick = { /* handle click */ }) {
Icon(Icons.Default.Share, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("Share")
}
IconButton(onClick = { /* handle click */ }) {
Icon(Icons.Default.Bookmark, contentDescription = "Bookmark")
}
}
}
Box menumpuk elemen di atas satu sama lain (seperti FrameLayout):
@Composable
fun ImageWithBadge() {
Box(modifier = Modifier.size(120.dp)) {
Image(
painter = painterResource(R.drawable.product),
contentDescription = "Produk",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
// Badge "Baru" di pojok kanan atas
Surface(
color = MaterialTheme.colorScheme.error,
shape = RoundedCornerShape(4.dp),
modifier = Modifier
.align(Alignment.TopEnd)
.padding(4.dp)
) {
Text(
text = "Baru",
color = MaterialTheme.colorScheme.onError,
fontSize = 10.sp,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
)
}
}
}
Ini adalah konsep paling penting di Compose. State adalah data yang bisa berubah seiring waktu. Ketika state berubah, Compose otomatis me-render ulang UI yang terpengaruh.
Cara paling dasar pakai remember dan mutableStateOf:
@Composable
fun Counter() {
// remember = state disimpan saat recomposition
var count by remember { mutableIntStateOf(0) }
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Kamu sudah klik $count kali",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = { count++ }) {
Text("Tambah")
}
OutlinedButton(onClick = { count = 0 }) {
Text("Reset")
}
}
}
}
Penting: Jangan pernah modify state di luar onClick atau callback. Compose bisa me-recompose function berkali-kali, dan kalau kamu langsung ubah state di body function, hasilnya nggak terduga.
Untuk app yang lebih kompleks, gunakan ViewModel:
// ViewModel
class TodoViewModel : ViewModel() {
private val _tasks = mutableStateListOf<Task>()
val tasks: List<Task> = _tasks
fun addTask(title: String) {
_tasks.add(Task(title = title, done = false))
}
fun toggleTask(index: Int) {
_tasks[index] = _tasks[index].copy(done = !_tasks[index].done)
}
}
data class Task(val title: String, val done: Boolean)
// Composable yang pakai ViewModel
@Composable
fun TodoScreen(viewModel: TodoViewModel = viewModel()) {
var newTaskText by remember { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) {
// Input field
Row(verticalAlignment = Alignment.CenterVertically) {
OutlinedTextField(
value = newTaskText,
onValueChange = { newTaskText = it },
label = { Text("Tugas baru") },
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Button(
onClick = {
if (newTaskText.isNotBlank()) {
viewModel.addTask(newTaskText)
newTaskText = ""
}
}
) {
Text("Tambah")
}
}
Spacer(modifier = Modifier.height(16.dp))
// Daftar tugas
LazyColumn {
itemsIndexed(viewModel.tasks) { index, task ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { viewModel.toggleTask(index) }
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = task.done,
onCheckedChange = { viewModel.toggleTask(index) }
)
Text(
text = task.title,
textDecoration = if (task.done)
TextDecoration.LineThrough else TextDecoration.None
)
}
}
}
}
}
Material 3 (M3) adalah sistem desain terbaru dari Google. Compose sudah terintegrasi penuh dengan M3, termasuk Dynamic Color yang bisa mengambil warna dari wallpaper pengguna di Android 12+.
Setup tema di Theme.kt:
@Composable
fun MyAppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true, // Android 12+ dynamic color
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
}
darkTheme -> darkColorScheme(
primary = Color(0xFF80DEEA), // Cyan terang
secondary = Color(0xFFA5D6A7), // Hijau lembut
tertiary = Color(0xFFFFCC80) // Oranye pastel
)
else -> lightColorScheme(
primary = Color(0xFF00838F), // Cyan gelap
secondary = Color(0xFF388E3C), // Hijau
tertiary = Color(0xFFFF9800) // Oranye
)
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography(
headlineLarge = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 28.sp,
letterSpacing = (-0.5).sp
),
bodyLarge = TextStyle(
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
),
shapes = Shapes(
small = RoundedCornerShape(8.dp),
medium = RoundedCornerShape(12.dp),
large = RoundedCornerShape(16.dp)
),
content = content
)
}
Dengan setup ini, semua komponen M3 (Button, Card, TextField, dll.) otomatis pakai warna dan bentuk yang sudah kamu definisikan. Kalau kamu ganti tema, seluruh app berubah tanpa perlu edit satu per satu.
Kalau kamu pernah pakai RecyclerView di View system lama, kamu tau ribetnya bikin Adapter, ViewHolder, layout manager, DiffUtil. Di Compose, semua itu jadi jauh lebih sederhana:
@Composable
fun ArticleList(articles: List<Article>) {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(
items = articles,
key = { it.id } // Penting untuk animasi yang smooth
) { article ->
ArticleCard(article = article)
}
}
}
@Composable
fun ArticleCard(article: Article) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
shape = RoundedCornerShape(12.dp)
) {
Column {
// Gambar artikel
AsyncImage(
model = article.imageUrl,
contentDescription = article.title,
modifier = Modifier
.fillMaxWidth()
.height(180.dp),
contentScale = ContentScale.Crop
)
Column(modifier = Modifier.padding(16.dp)) {
// Kategori chip
Surface(
color = MaterialTheme.colorScheme.primaryContainer,
shape = RoundedCornerShape(20.dp)
) {
Text(
text = article.category,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = article.title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = article.summary,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
// Footer: tanggal + author
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = article.date,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.outline
)
Text(
text = article.author,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary
)
}
}
}
}
}
Keuntungan LazyColumn:
key membantu Compose mengenali item yang pindah posisiCompose punya library navigasi sendiri yang terintegrasi. Setup-nya seperti ini:
// Definisikan route
sealed class Screen(val route: String) {
object Home : Screen("home")
object Detail : Screen("detail/{articleId}") {
fun createRoute(articleId: Int) = "detail/$articleId"
}
object Profile : Screen("profile")
}
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.Home.route
) {
composable(Screen.Home.route) {
HomeScreen(
onArticleClick = { articleId ->
navController.navigate(Screen.Detail.createRoute(articleId))
},
onProfileClick = {
navController.navigate(Screen.Profile.route)
}
)
}
composable(
route = Screen.Detail.route,
arguments = listOf(navArgument("articleId") { type = NavType.IntType })
) { backStackEntry ->
val articleId = backStackEntry.arguments?.getInt("articleId") ?: 0
DetailScreen(
articleId = articleId,
onBack = { navController.popBackStack() }
)
}
composable(Screen.Profile.route) {
ProfileScreen()
}
}
}
1. Hindari Recomposition yang Nggak Perlu
Recomposition adalah proses Compose me-render ulang Composable. Kalau kamu nggak hati-hati, function yang nggak perlu di-render ulang bisa bikin app jadi lambat.
// BURUK: lambda baru dibuat setiap recomposition
@Composable
fun BadExample(viewModel: MyViewModel) {
Button(onClick = { viewModel.doSomething() }) {
Text("Click")
}
}
// BAIK: lambda stabil
@Composable
fun GoodExample(viewModel: MyViewModel) {
val onClick = remember { { viewModel.doSomething() } }
Button(onClick = onClick) {
Text("Click")
}
}
2. Pakai derivedStateOf untuk Computed State
@Composable
fun SearchResults(query: String, items: List<String>) {
// Hanya recompute kalau items atau query berubah
val filteredItems by remember {
derivedStateOf {
if (query.isBlank()) items
else items.filter { it.contains(query, ignoreCase = true) }
}
}
LazyColumn {
items(filteredItems) { item ->
Text(item, modifier = Modifier.padding(16.dp))
}
}
}
3. Side Effects: LaunchedEffect dan DisposableEffect
@Composable
fun Timer() {
var seconds by remember { mutableIntStateOf(0) }
// LaunchedEffect: berjalan saat Composable masuk, cancel saat keluar
LaunchedEffect(Unit) {
while (true) {
delay(1000)
seconds++
}
}
Text("Waktu: ${seconds}s")
}
@Composable
fun LifecycleAwareComponent() {
val lifecycleOwner = LocalLifecycleOwner.current
// DisposableEffect: setup + cleanup
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_PAUSE) {
// Pause sensor, stop animation, etc.
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
Jetpack Compose benar-benar mengubah cara kita bikin UI Android. Dari yang dulu harus bolak-balik XML dan Kotlin, sekarang semua di satu tempat. Learning curve-nya memang ada, terutama kalau kamu udah terbiasa dengan View system lama. Tapi begitu kamu paham konsep Composable, State, dan Recomposition, development bakal jadi jauh lebih cepat dan menyenangkan.
Saran saya: mulai dari project kecil. Coba rebuild salah satu screen dari app kamu yang existing pakai Compose. Dari situ kamu bakal ngerasain sendiri bedanya lebih sedikit kode, lebih sedikit bug, dan preview real-time yang bikin iterasi desain jadi super cepat.
Ada pertanyaan atau pengalaman kamu pakai Compose? Share di kolom komentar ya. Apakah kamu team Compose atau masih setia sama XML? Saya penasaran!