Pernah nggak sih kamu buka Android Studio, lihat file XML layout yang isinya ratusan baris, terus mikir: "Ini harusnya lebih simpel dong." Kalau iya, selamat kamu sudah siap untuk Jetpack Compose.
Saya ingat pertama kali migrasi dari XML ke Compose. Awalnya skeptis masa sih deklaratif UI bisa lebih enak? Tapi setelah seminggu pakai, saya nggak mau balik lagi. Code yang tadinya 200 baris XML + 50 baris adapter, sekarang jadi 80 baris Kotlin aja.
Jetpack Compose adalah toolkit UI modern dari Google untuk Android. Berbeda dengan pendekatan tradisional yang pakai XML terpisah dari logic, Compose menggunakan pendekatan deklaratif kamu tulis apa yang mau ditampilkan, bukan bagaimana cara menampilkannya.
Konsepnya mirip dengan React atau SwiftUI. Kamu buat composable function yang menghasilkan UI berdasarkan state. Kalau state berubah, UI otomatis update. Nggak perlu findViewById, nggak perlu notifyDataSetChanged().
// Tradisional: XML + Activity
// setContentView(R.layout.activity_main)
// val textView = findViewById<TextView>(R.id.textView)
// textView.text = "Hello"
// Compose: semua dalam satu fungsi
@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name!")
}
Untuk memulai, kamu butuh Android Studio Hedgehog atau yang lebih baru. Buat project baru, pilih template "Empty Compose Activity". Android Studio akan otomatis setup dependency yang diperlukan.
Tapi kalau mau migrasi project lama, tambahkan dependency ini di build.gradle (Module level):
dependencies {
val composeBom = platform("androidx.compose:compose-bom:2024.12.01")
implementation(composeBom)
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")
implementation("androidx.activity:activity-compose:1.9.3")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
implementation("androidx.navigation:navigation-compose:2.8.5")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}
Di build.gradle bagian android, pastikan ada blok buildFeatures:
android {
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.8"
}
}
Setiap elemen UI di Compose adalah fungsi yang diberi anotasi @Composable. Fungsi ini bisa dipanggil dari fungsi lain, sehingga kamu bisa bikin komponen yang reusable.
@Composable
fun ProfileCard(name: String, role: String) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Avatar
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary),
contentAlignment = Alignment.Center
) {
Text(
text = name.first().toString(),
color = Color.White,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.width(16.dp))
// Info
Column {
Text(
text = name,
style = MaterialTheme.typography.titleMedium
)
Text(
text = role,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
Lihat bedanya? Semua ada di satu tempat. Mau ganti warna? Tinggal ubah MaterialTheme.colorScheme.primary. Mau tambah padding? Tambah .padding() di modifier. Nggak perlu bolak-balik ke file XML.
Ini bagian yang paling powerful di Compose. Kamu pakai remember dan mutableStateOf untuk bikin state. Kalau state berubah, composable yang menggunakan state itu otomatis di-recompose (re-render).
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Kamu sudah klik $count kali",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { count++ }) {
Text("Tambah")
}
}
}
Kunci di sini: remember menyimpan state supaya nggak hilang saat recomposition. mutableStateOf bikin state yang bisa di-observe oleh Compose. by adalah Kotlin delegate property jadi kamu bisa langsung count++ tanpa count.value++.
Untuk state yang lebih complex, pakai ViewModel:
class UserViewModel : ViewModel() {
private val _users = mutableStateListOf<User>()
val users: List<User> = _users
private val _isLoading = mutableStateOf(false)
val isLoading: Boolean by _isLoading
fun loadUsers() {
viewModelScope.launch {
_isLoading.value = true
_users.clear()
_users.addAll(userRepository.fetchUsers())
_isLoading.value = false
}
}
}
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
LaunchedEffect(Unit) {
viewModel.loadUsers()
}
if (viewModel.isLoading) {
CircularProgressIndicator()
} else {
LazyColumn {
items(viewModel.users) { user ->
UserItem(user)
}
}
}
}
Navigation di Compose jauh lebih clean dibanding Fragment-based navigation. Kamu pakai NavHost dan NavController:
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") {
HomeScreen(
onNavigateToDetail = { userId ->
navController.navigate("detail/$userId")
}
)
}
composable(
"detail/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.IntType })
) { backStackEntry ->
val userId = backStackEntry.arguments?.getInt("userId") ?: 0
DetailScreen(
userId = userId,
onBack = { navController.popBackStack() }
)
}
}
}
Yang bikin enak: argument langsung typed. Nggak perlu intent.putExtra() dan getIntent().getIntExtra(). Semua ter-compile dan type-safe.
Kalau dulu kamu pakai RecyclerView + Adapter + ViewHolder (ribet banget), sekarang cukup LazyColumn:
@Composable
fun ArticleList(articles: List<Article>) {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(
items = articles,
key = { it.id }
) { article ->
ArticleCard(article)
}
}
}
@Composable
fun ArticleCard(article: Article) {
Card(
modifier = Modifier.fillMaxWidth(),
onClick = { /* navigate to detail */ }
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = article.title,
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = article.summary,
style = MaterialTheme.typography.bodyMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
Perhatikan key = { it.id } ini penting untuk performa. Compose tahu item mana yang berubah, ditambah, atau dihapus. Tanpa key, dia bakal recompose semua item.
Compose sudah built-in support Material Design 3. Dark mode? Tinggal ganti colorScheme:
@Composable
fun MyApp(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = if (isSystemInDarkTheme()) darkColorScheme(
primary = Color(0xFF80DEEA),
secondary = Color(0xFFCE93D8),
background = Color(0xFF121212)
) else lightColorScheme(
primary = Color(0xFF00838F),
secondary = Color(0xFF7B1FA2),
background = Color(0xFFFAFAFA)
),
typography = Typography(
headlineLarge = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 28.sp
)
)
) {
content()
}
}
isSystemInDarkTheme() mendeteksi pengaturan sistem user. Nggak perlu bikin dua set layout XML seperti dulu.
remember untuk callback yang nggak berubah:
// Buruk: lambda baru setiap recompose
Button(onClick = { viewModel.onButtonClicked() }) { Text("Click") }
// Lebih baik: lambda stabil
val onClick = remember { { viewModel.onButtonClicked() } }
Button(onClick = onClick) { Text("Click") }
derivedStateOf untuk computed state. Supaya nggak hitung ulang setiap recompose:
val sortedUsers by remember {
derivedStateOf { users.sortedBy { it.name } }
}
LazyColumn di dalam Column. Ini bikin infinite height conflict. Pakai weight() modifier atau restructure layout.Jetpack Compose bukan cuma "cara baru bikin UI" ini paradigm shift. Dari imperative ke deklaratif, dari XML terpisah ke code terpadu, dari manual state management ke reactive UI.
Butuh waktu untuk berpikir dalam "Compose way". Tapi begitu kamu terbiasa, produktivitas naik drastis. Saya pribadi bisa bikin UI yang dulu butuh 2 hari, sekarang selesai dalam setengah hari.
Kalau kamu masih ragu, mulai dari yang kecil. Convert satu screen dari XML ke Compose. Lihat sendiri bedanya. Trust me kamu nggak akan mau balik.
Pertanyaan atau pengalaman migrasi ke Compose? Share di kolom komentar ya!