Mobile Development

Jetpack Compose: UI Modern Android

Tutorial lengkap belajar Jetpack Compose untuk membangun antarmuka Android modern β€” deklaratif UI, composables, state, layout, theming, dan navigation dengan contoh kode praktis

1. Pengenalan Jetpack Compose

Jetpack Compose adalah toolkit UI modern dari Google untuk membangun antarmuka native Android menggunakan pendekatan deklaratif. Compose menggantikan pendekatan XML layout tradisional yang telah digunakan selama bertahun-tahun dalam pengembangan Android.

Dengan Compose, Anda tidak lagi perlu membuat layout XML secara terpisah dan menghubungkannya ke Activity atau Fragment melalui findViewById(). Sebaliknya, Anda mendeskripsikan UI secara langsung dalam kode Kotlin menggunakan fungsi-fungsi komponen yang disebut composable.

Mengapa Jetpack Compose?

Keunggulan Penjelasan
DeklaratifCukup deskripsikan UI berdasarkan state, Compose menangani rendering otomatis
Kotlin-firstDirancang khusus untuk Kotlin dengan leveraging coroutines, extension functions, dan type-safety
InteroperabilityBisa digunakan bersama XML View dan sistem Android yang ada
Hot ReloadPerubahan UI langsung terlihat tanpa rebuild seluruh aplikasi
Material Design 3Didukung penuh dengan desain modern dan kustomisasi yang fleksibel
Animasi Built-inSistem animasi yang kuat dan mudah digunakan langsung dari toolkit

XML View Tradisional vs Jetpack Compose

Aspek XML View Jetpack Compose
Deklarasi UIXML file terpisahKotlin code langsung
State ManagementManual (findViewById, ViewBinding)Automatic recomposition
Learning Curve🟒 Mudah untuk pemula🟑 Butuh pemahaman Kotlin
Boilerplate CodeπŸ”΄ Banyak🟒 Sedikit
PerformanceSudah optimalSangat optimal (smart recomposition)
Cocok untukLegacy, kompatibilitasProyek baru, modern UI
Diagram: Arsitektur Jetpack Compose
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  JETPACK COMPOSE STACK                   β”‚
β”‚                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚              Your Composable UI Code               β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚  β”‚
β”‚  β”‚  β”‚   @Composableβ”‚  β”‚  @Composable β”‚               β”‚  β”‚
β”‚  β”‚  β”‚   Screen()   │──│  Card()      β”‚               β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚              Compose Runtime                        β”‚  β”‚
β”‚  β”‚  β€’ State tracking & recomposition                  β”‚  β”‚
β”‚  β”‚  β€’ Slot API untuk rendering                        β”‚  β”‚
β”‚  β”‚  β€’ Smart diffing algorithm                         β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚              Android View System                   β”‚  β”‚
β”‚  β”‚  β€’ Canvas rendering                               β”‚  β”‚
β”‚  β”‚  β€’ Touch event handling                            β”‚  β”‚
β”‚  β”‚  β€’ Layout measurement                             β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Setup Proyek Jetpack Compose

Untuk memulai menggunakan Jetpack Compose, Anda perlu mengonfigurasi build.gradle dengan dependency Compose dan mengaktifkan Compose compiler:

Kotlin β€” build.gradle.kts (Module)
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("org.jetbrains.kotlin.plugin.compose")
}

android {
    namespace = "com.example.composeapp"
    compileSdk = 35

    defaultConfig {
        applicationId = "com.example.composeapp"
        minSdk = 24
        targetSdk = 35
        versionCode = 1
        versionName = "1.0"
    }

    buildFeatures {
        compose = true  // Aktifkan Compose
    }

    kotlinOptions {
        jvmTarget = "17"
    }
}

dependencies {
    // Compose BOM β€” mengelola versi Compose secara konsisten
    implementation(platform("androidx.compose:compose-bom:2025.01.00"))

    // 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")

    // Activity Compose
    implementation("androidx.activity:activity-compose:1.9.3")

    // Navigation Compose
    implementation("androidx.navigation:navigation-compose:2.8.5")

    // Lifecycle
    implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")

    // Debug
    debugImplementation("androidx.compose.ui:ui-tooling")
}
Kotlin β€” MainActivity.kt
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.Text
import androidx.compose.material3.MaterialTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Text(text = "Hello, Jetpack Compose!")
            }
        }
    }
}
πŸ’‘ Tips

Gunakan Compose BOM (Bill of Materials) untuk mengelola versi semua library Compose secara konsisten. Ini memastikan semua komponen Compose kompatibel satu sama lain tanpa perlu mendefinisikan versi masing-masing secara manual.

2. Paradigma Deklaratif UI

Paradigma deklaratif berarti Anda mendeskripsikan apa yang ingin ditampilkan UI berdasarkan state saat ini, bukan secara imperatif menginstruksikan bagaimana UI harus diperbarui. Ketika state berubah, Compose secara otomatis memperbarui bagian UI yang terpengaruh melalui proses yang disebut recomposition.

Imperatif vs Deklaratif

Kotlin β€” Perbandingan Pendekatan
// ═══════════════════════════════════════════════════════
// PENDEKATAN IMPERATIF (XML View Tradisional)
// ═══════════════════════════════════════════════════════

// activity_main.xml
// <TextView android:id="@+id/counterText" android:text="0" />
// <Button android:id="@+id/incrementBtn" android:text="Tambah" />

class MainActivity : AppCompatActivity() {
    private var count = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val counterText = findViewById<TextView>(R.id.counterText)
        val incrementBtn = findViewById<Button>(R.id.incrementBtn)

        // Secara imperatif: update UI secara manual
        incrementBtn.setOnClickListener {
            count++
            counterText.text = count.toString()  // manual update
        }
    }
}

// ═══════════════════════════════════════════════════════
// PENDEKATAN DEKLARATIF (Jetpack Compose)
// ═══════════════════════════════════════════════════════

@Composable
fun CounterScreen() {
    var count by remember { mutableIntStateOf(0) }

    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = "Counter: $count",
            style = MaterialTheme.typography.headlineMedium
        )
        Button(onClick = { count++ }) {
            Text("Tambah")
        }
    }
    // UI otomatis diperbarui saat count berubah!
    // Tidak perlu manual memanggil setText atau invalidate
}
Diagram: Alur Recomposition
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              RECOMPOSITION FLOW                     β”‚
β”‚                                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚  State   │───▢│  Recompose   │───▢│ Updated   β”‚ β”‚
β”‚  β”‚  Changes β”‚    β”‚  Composable  β”‚    β”‚ UI       β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚       β”‚                                        β”‚    β”‚
β”‚       β”‚         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚    β”‚
β”‚       β”‚         β”‚  Smart       β”‚               β”‚    β”‚
β”‚       └────────▢│  Diffing     β”‚β—€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                 β”‚  Algorithm   β”‚                    β”‚
β”‚                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                    β”‚
β”‚                       β”‚                             β”‚
β”‚                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                    β”‚
β”‚                 β”‚  Hanya bagianβ”‚                    β”‚
β”‚                 β”‚  yang berubahβ”‚                    β”‚
β”‚                 β”‚  di-update   β”‚                    β”‚
β”‚                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Side Effects dalam Paradigma Deklaratif

Karena Compose menggunakan recomposition, Anda perlu memahami side effects β€” operasi yang terjadi di luar scope UI seperti logging, animasi, atau fetch data:

Kotlin β€” Side Effects
// LaunchedEffect β€” menjalankan coroutine saat composable masuk tree
@Composable
fun UserProfile(userId: String) {
    var user by remember { mutableStateOf<User?>(null) }

    // Fetch data saat composable pertama kali muncul
    LaunchedEffect(userId) {
        user = api.fetchUser(userId)
    }

    user?.let {
        Text(text = it.name)
    }
}

// DisposableEffect β€” untuk cleanup resource
@Composable
fun LocationListener(locationManager: LocationManager) {
    DisposableEffect(locationManager) {
        val listener = LocationListener { /* handle */ }
        locationManager.requestUpdates(listener)
        onDispose {
            locationManager.removeUpdates(listener)
        }
    }
}

// SideEffect β€” menjalankan kode setiap recomposition成功
@Composable
fun AnalyticsScreen(screenName: String) {
    SideEffect {
        Analytics.logScreenView(screenName)
    }
}

3. Composable Functions

Composable function adalah blok pembangun utama dalam Jetpack Compose. Setiap fungsi yang mendeskripsikan sebagian UI diberi anotasi @Composable. Composable bersifat stateless secara default β€” mereka menerima input melalui parameter dan menampilkan output UI.

Anatomy of a Composable

Kotlin β€” Composable Dasar
// Composable sederhana
@Composable
fun Greeting(name: String) {
    Text(
        text = "Hello, $name!",
        style = MaterialTheme.typography.headlineSmall
    )
}

// Composable dengan parameter default
@Composable
fun UserProfile(
    name: String,
    bio: String = "Pengguna baru",
    avatarUrl: String? = null
) {
    Row(
        modifier = Modifier.padding(16.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        // Conditional avatar
        if (avatarUrl != null) {
            AsyncImage(
                model = avatarUrl,
                contentDescription = "Avatar $name"
            )
        }
        Column(modifier = Modifier.padding(start = 12.dp)) {
            Text(text = name, style = MaterialTheme.typography.titleMedium)
            Text(
                text = bio,
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.onSurfaceVariant
            )
        }
    }
}

// Preview function untuk Android Studio
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    MaterialTheme {
        Greeting(name = "Android")
    }
}

@Preview(showBackground = true)
@Composable
fun UserProfilePreview() {
    MaterialTheme {
        UserProfile(
            name = "Budi Santoso",
            bio = "Android Developer",
            avatarUrl = null
        )
    }
}

Slot API & Composable Higher-Order

Compose menggunakan Slot API β€” fungsi komponen menerima composable lain sebagai parameter untuk membuat komponen yang fleksibel:

Kotlin β€” Slot API
// Komponen Card yang menerima content sebagai slot
@Composable
fun CustomCard(
    title: String,
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit  // Slot untuk konten
) {
    Card(
        modifier = modifier
            .fillMaxWidth()
            .padding(8.dp),
        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = title,
                style = MaterialTheme.typography.titleLarge
            )
            Spacer(modifier = Modifier.height(8.dp))
            content()  // Render konten dari slot
        }
    }
}

// Menggunakan komponen dengan slot
@Composable
fun ProductList(products: List<Product>) {
    LazyColumn {
        items(products) { product ->
            CustomCard(title = product.name) {
                // Slot content β€” bisa berupa apa saja
                Row {
                    Text(text = "Harga: ${product.price}")
                    Spacer(modifier = Modifier.weight(1f))
                    Button(onClick = { /* add to cart */ }) {
                        Text("Beli")
                    }
                }
            }
        }
    }
}

// Komponen Scaffold dengan multiple slots
@Composable
fun AppScaffold(
    topBar: @Composable () -> Unit = {},
    bottomBar: @Composable () -> Unit = {},
    content: @Composable (PaddingValues) -> Unit
) {
    Scaffold(
        topBar = topBar,
        bottomBar = bottomBar,
        content = content
    )
}
⚠️ Aturan Naming Composable
  • Nama composable harus berawalan huruf kapital (seperti nama kelas/konstruktor)
  • Gunakan PascalCase: UserProfile, bukan userProfile
  • Parameter harus immutable β€” jangan mengubah parameter di dalam composable
  • Composable bisa dipanggil dari composable lain, dari preview, atau dari setContent

4. State Management dalam Compose

State adalah nilai yang dapat berubah selama lifetime komponen. Dalam Compose, state management dilakukan melalui remember dan berbagai fungsi state holder. Saat state berubah, Compose secara otomatis melakukan recomposition untuk memperbarui UI.

remember dan mutableStateOf

Kotlin β€” State Dasar
// ═══ remember + mutableStateOf ═══
// Menyimpan state yang bertahan saat recomposition
@Composable
fun Counter() {
    var count by remember { mutableIntStateOf(0) }

    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = "Count: $count",
            style = MaterialTheme.typography.displayMedium
        )
        Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            Button(onClick = { count-- }) {
                Text("βˆ’")
            }
            Button(onClick = { count++ }) {
                Text("+")
            }
        }
    }
}

// ═══ rememberSaveable β€” bertahan saat configuration change ═══
@Composable
fun SearchScreen() {
    var query by rememberSaveable { mutableStateOf("") }

    TextField(
        value = query,
        onValueChange = { query = it },
        label = { Text("Cari...") },
        modifier = Modifier.fillMaxWidth()
    )
    // query tetap tersimpan saat layar rotasi!
}

State Hoisting

State hoisting adalah pola di mana state dipindahkan ke parent composable agar komponen menjadi stateless dan lebih reusable. Ini adalah best practice dalam Compose.

Kotlin β€” State Hoisting
// ═══ Composable STATEFUL (tidak reusable) ═══
@Composable
fun NameInputStateful() {
    var name by remember { mutableStateOf("") }

    TextField(
        value = name,
        onValueChange = { name = it },
        label = { Text("Nama") }
    )
    Text(text = "Halo, $name!")
}

// ═══ Composable STATELESS (reusable) ═══
// Stateless β€” tidak memiliki state sendiri
@Composable
fun NameInput(
    name: String,              // Dikirim dari parent
    onNameChange: (String) -> Unit,  // Callback ke parent
    modifier: Modifier = Modifier
) {
    Column(modifier = modifier) {
        TextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Nama") }
        )
        Text(text = "Halo, $name!")
    }
}

// ═══ Parent menangani state ═══
@Composable
fun RegistrationScreen() {
    var firstName by remember { mutableStateOf("") }
    var lastName by remember { mutableStateOf("") }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        NameInput(
            name = firstName,
            onNameChange = { firstName = it }
        )
        Spacer(modifier = Modifier.height(8.dp))
        NameInput(
            name = lastName,
            onNameChange = { lastName = it }
        )
        Spacer(modifier = Modifier.height(16.dp))
        Button(
            onClick = { /* submit */ },
            enabled = firstName.isNotEmpty() && lastName.isNotEmpty()
        ) {
            Text("Daftar")
        }
    }
}

ViewModel Integration

Kotlin β€” ViewModel + State
// ViewModel untuk mengelola state bisnis
class TodoViewModel : ViewModel() {
    private val _todos = mutableStateListOf<Todo>()
    val todos: List<Todo> = _todos

    private val _filterState = mutableStateOf(TodoFilter.ALL)
    val filterState: TodoFilter by _filterState

    val filteredTodos: List<Todo>
        @Composable get() = when (_filterState.value) {
            TodoFilter.ALL -> _todos
            TodoFilter.ACTIVE -> _todos.filter { !it.done }
            TodoFilter.DONE -> _todos.filter { it.done }
        }

    fun addTodo(title: String) {
        _todos.add(Todo(id = UUID.randomUUID().toString(), title = title))
    }

    fun toggleTodo(id: String) {
        val index = _todos.indexOfFirst { it.id == id }
        if (index >= 0) {
            _todos[index] = _todos[index].copy(done = !_todos[index].done)
        }
    }

    fun setFilter(filter: TodoFilter) {
        _filterState.value = filter
    }
}

// Composable yang menggunakan ViewModel
@Composable
fun TodoScreen(viewModel: TodoViewModel = hiltViewModel()) {
    var text by remember { mutableStateOf("") }

    Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
        // Input area
        Row(verticalAlignment = Alignment.CenterVertically) {
            TextField(
                value = text,
                onValueChange = { text = it },
                label = { Text("Tambah todo...") },
                modifier = Modifier.weight(1f)
            )
            Button(onClick = {
                if (text.isNotEmpty()) {
                    viewModel.addTodo(text)
                    text = ""
                }
            }) {
                Text("Tambah")
            }
        }

        // List
        LazyColumn(modifier = Modifier.weight(1f)) {
            items(viewModel.filteredTodos) { todo ->
                TodoItem(
                    todo = todo,
                    onToggle = { viewModel.toggleTodo(todo.id) }
                )
            }
        }
    }
}
πŸ“Œ Perbedaan State Tipe
FungsiPenjelasan
rememberMenyimpan nilai selama recomposition. Nilai hilang saat composable keluar tree.
rememberSaveableSeperti remember, tetapi bertahan saat configuration change (rotasi layar).
derivedStateOfMenghitung value dari state lain secara efisien. Hanya recompose saat hasil akhir berubah.
mutableStateListOfMutable list yang memicu recomposition saat item ditambah/dihapus.

5. Layout System

Compose menyediakan sistem layout yang kuat dan fleksibel dengan tiga komponen layout utama: Row (horizontal), Column (vertikal), dan Box (stacking). Semua layout mengukur dan menempatkan children menggunakan mekanisme measure/layout satu pass.

Row, Column, dan Box

Kotlin β€” Layout Dasar
// ═══ Column β€”εΈƒε±€ vertikal ═══
@Composable
fun UserProfileCard() {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        Image(
            painter = painterResource(R.drawable.avatar),
            contentDescription = "Avatar",
            modifier = Modifier.size(80.dp)
        )
        Text("Budi Santoso", style = MaterialTheme.typography.titleLarge)
        Text("Android Developer", style = MaterialTheme.typography.bodyMedium)
        Button(onClick = { /* follow */ }) {
            Text("Ikuti")
        }
    }
}

// ═══ Row β€” Layout horizontal ═══
@Composable
fun ContactItem(name: String, phone: String) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clickable { /* call */ }
            .padding(12.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.SpaceBetween
    ) {
        Row(verticalAlignment = Alignment.CenterVertically) {
            Icon(Icons.Default.Person, contentDescription = null)
            Spacer(modifier = Modifier.width(12.dp))
            Column {
                Text(name, style = MaterialTheme.typography.bodyLarge)
                Text(phone, style = MaterialTheme.typography.bodySmall)
            }
        }
        Icon(Icons.Default.Call, contentDescription = "Panggil")
    }
}

// ═══ Box β€” Stack children di atas satu sama lain ═══
@Composable
fun BadgeBox() {
    Box(modifier = Modifier.size(100.dp)) {
        Image(
            painter = painterResource(R.drawable.product),
            contentDescription = null,
            modifier = Modifier.fillMaxSize()
        )
        // Badge di pojok kanan atas
        Box(
            modifier = Modifier
                .align(Alignment.TopEnd)
                .background(Color.Red, shape = CircleShape)
                .padding(4.dp)
        ) {
            Text("3", color = Color.White, fontSize = 12.sp)
        }
    }
}

Modifier β€” Membentuk Tampilan

Modifier adalah objek yang memodifikasi tampilan, perilaku, atau layout elemen. Modifier dibaca dari kiri ke kanan (atau dari luar ke dalam):

Kotlin β€” Modifier Chain
// Modifier chain β€” urutan penting!
Text(
    text = "Hello World",
    modifier = Modifier
        .fillMaxWidth()           // 1. Ambil lebar penuh
        .padding(16.dp)          // 2. Tambah padding
        .background(Color.Blue)  // 3. Tambah background
        .clip(RoundedCornerShape(8.dp))  // 4. Bulatkan sudut
        .clickable { /* click */ }  // 5. Tambah click handler
        .padding(8.dp)           // 6. Padding inner
)

// Kombinasi Modifier
Card(
    modifier = Modifier
        .fillMaxWidth()
        .height(120.dp)
        .padding(8.dp)
        .shadow(4.dp, shape = RoundedCornerShape(16.dp))
) {
    Row(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        // Content here
    }
}

// Modifier dengan custom size
Box(
    modifier = Modifier
        .width(200.dp)
        .heightIn(min = 100.dp, max = 300.dp)
        .aspectRatio(16f / 9f)
) {
    // Content adapts to constraints
}

LazyLayout untuk Daftar

Kotlin β€” LazyColumn & LazyRow
// LazyColumn β€” daftar vertikal yang efisien
@Composable
fun MessageList(messages: List<Message>) {
    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        // Header
        item {
            Text(
                text = "Pesan Terkini",
                style = MaterialTheme.typography.headlineSmall,
                modifier = Modifier.padding(bottom = 8.dp)
            )
        }

        // Daftar pesan
        items(
            items = messages,
            key = { it.id }  // Key untuk performance
        ) { message ->
            MessageBubble(message = message)
        }

        // Footer
        item {
            Text(
                text = "Tidak ada lagi pesan",
                modifier = Modifier.padding(top = 16.dp)
            )
        }
    }
}

// LazyRow β€” daftar horizontal (misal: story highlights)
@Composable
fun StoryHighlights(stories: List<Story>) {
    LazyRow(
        contentPadding = PaddingValues(horizontal = 16.dp),
        horizontalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        items(stories) { story ->
            StoryAvatar(story = story)
        }
    }
}

// LazyVerticalGrid β€” grid layout
@Composable
fun PhotoGrid(photos: List<Photo>) {
    LazyVerticalGrid(
        columns = GridCells.Adaptive(minSize = 120.dp),
        contentPadding = PaddingValues(8.dp),
        verticalArrangement = Arrangement.spacedBy(4.dp),
        horizontalArrangement = Arrangement.spacedBy(4.dp)
    ) {
        items(photos) { photo ->
            AsyncImage(
                model = photo.url,
                contentDescription = null,
                modifier = Modifier
                    .aspectRatio(1f)
                    .clip(RoundedCornerShape(8.dp))
            )
        }
    }
}
πŸ’‘ Tips Layout

Gunakan Modifier.weight() dalam Row atau Column untuk mendistribusikan ruang secara proporsional β€” mirip dengan layout_weight di XML atau flex di CSS. Misalnya: Modifier.weight(1f) mengambil sisa ruang yang tersedia.

6. Theming & Styling

Compose menggunakan sistem Material 3 yang menyediakan komponen theming lengkap dengan dynamic color, typography, dan shape. Anda bisa kustomisasi seluruh tema aplikasi dalam satu tempat.

Theme Setup

Kotlin β€” Theme Configuration
// ═══ Custom Color Scheme ═══
private val LightColorScheme = lightColorScheme(
    primary = Color(0xFF1976D2),
    onPrimary = Color.White,
    primaryContainer = Color(0xFFBBDEFB),
    secondary = Color(0xFF26A69A),
    background = Color(0xFFFFFBFE),
    surface = Color(0xFFFFFBFE),
    error = Color(0xFFD32F2F),
)

private val DarkColorScheme = darkColorScheme(
    primary = Color(0xFF90CAF9),
    onPrimary = Color(0xFF003258),
    primaryContainer = Color(0xFF00497D),
    secondary = Color(0xFF80CBC4),
    background = Color(0xFF1C1B1F),
    surface = Color(0xFF1C1B1F),
    error = Color(0xFFEF9A9A),
)

// ═══ Custom Typography ═══
private val AppTypography = Typography(
    headlineLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Bold,
        fontSize = 32.sp,
        lineHeight = 40.sp,
    ),
    bodyLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.5.sp,
    ),
    labelSmall = TextStyle(
        fontFamily = FontFamily.Monospace,
        fontWeight = FontWeight.Medium,
        fontSize = 11.sp,
        lineHeight = 16.sp,
    ),
)

// ═══ Theme Composable ═══
@Composable
fun BeebaneAppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    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
        else -> LightColorScheme
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = AppTypography,
        content = content
    )
}

Custom Component Styling

Kotlin β€” Custom Styling
// ═══ Custom Button Styles ═══
@Composable
fun PrimaryButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true
) {
    Button(
        onClick = onClick,
        modifier = modifier
            .fillMaxWidth()
            .height(56.dp),
        enabled = enabled,
        shape = RoundedCornerShape(12.dp),
        colors = ButtonDefaults.buttonColors(
            containerColor = MaterialTheme.colorScheme.primary,
            contentColor = MaterialTheme.colorScheme.onPrimary,
            disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant,
        )
    ) {
        Text(
            text = text,
            style = MaterialTheme.typography.titleMedium,
            fontWeight = FontWeight.SemiBold
        )
    }
}

// ═══ Chip / Tag Component ═══
@Composable
fun CustomChip(
    label: String,
    selected: Boolean,
    onClick: () -> Unit
) {
    val bgColor = if (selected)
        MaterialTheme.colorScheme.primaryContainer
    else
        MaterialTheme.colorScheme.surfaceVariant

    val textColor = if (selected)
        MaterialTheme.colorScheme.onPrimaryContainer
    else
        MaterialTheme.colorScheme.onSurfaceVariant

    Surface(
        onClick = onClick,
        shape = RoundedCornerShape(20.dp),
        color = bgColor,
        modifier = Modifier.padding(4.dp)
    ) {
        Text(
            text = label,
            color = textColor,
            style = MaterialTheme.typography.labelLarge,
            modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
        )
    }
}

// ═══ Dark/Light Theme Toggle ═══
@Composable
fun ThemeToggle() {
    val darkTheme = isSystemInDarkTheme()
    val icon = if (darkTheme) Icons.Default.LightMode else Icons.Default.DarkMode

    IconButton(onClick = { /* toggle theme */ }) {
        Icon(imageVector = icon, contentDescription = "Toggle tema")
    }
}
πŸ“Œ Dynamic Color (Material You)

Android 12+ mendukung Dynamic Color yang mengambil warna dari wallpaper pengguna secara otomatis. Aktifkan dengan dynamicLightColorScheme() dan dynamicDarkColorScheme(). Ini memberikan pengalaman personalisasi yang unik untuk setiap pengguna.

Navigation Compose adalah library resmi untuk navigasi antar layar dalam aplikasi Compose. Library ini menyediakan type-safe navigation, deep linking, dan dukungan untuk back stack management.

Basic Navigation Setup

Kotlin β€” Navigation Dasar
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument

// ═══ Routes ═══
object Routes {
    const val HOME = "home"
    const val PROFILE = "profile/{userId}"
    const val SETTINGS = "settings"
    const val DETAIL = "detail/{itemId}"
}

// ═══ Navigation Host ═══
@Composable
fun AppNavigation() {
    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = Routes.HOME
    ) {
        // Home Screen
        composable(Routes.HOME) {
            HomeScreen(
                onNavigateToProfile = { userId ->
                    navController.navigate("profile/$userId")
                },
                onNavigateToSettings = {
                    navController.navigate(Routes.SETTINGS)
                }
            )
        }

        // Profile Screen with argument
        composable(
            route = Routes.PROFILE,
            arguments = listOf(
                navArgument("userId") {
                    type = NavType.StringType
                }
            )
        ) { backStackEntry ->
            val userId = backStackEntry.arguments?.getString("userId")
            ProfileScreen(userId = userId ?: "")
        }

        // Settings Screen
        composable(Routes.SETTINGS) {
            SettingsScreen(
                onBack = { navController.popBackStack() }
            )
        }

        // Detail with query parameters
        composable(
            route = Routes.DETAIL,
            arguments = listOf(
                navArgument("itemId") { type = NavType.StringType }
            )
        ) { backStackEntry ->
            val itemId = backStackEntry.arguments?.getString("itemId")
            DetailScreen(itemId = itemId ?: "")
        }
    }
}

Screen Implementation

Kotlin β€” Screen Composables
// ═══ Home Screen ═══
@Composable
fun HomeScreen(
    onNavigateToProfile: (String) -> Unit,
    onNavigateToSettings: () -> Unit
) {
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("Beranda") },
                actions = {
                    IconButton(onClick = onNavigateToSettings) {
                        Icon(Icons.Default.Settings, contentDescription = "Pengaturan")
                    }
                }
            )
        }
    ) { padding ->
        LazyColumn(
            modifier = Modifier
                .fillMaxSize()
                .padding(padding)
        ) {
            items(20) { index ->
                Card(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(8.dp)
                        .clickable { onNavigateToProfile("user_$index") }
                ) {
                    Text(
                        text = "User $index",
                        modifier = Modifier.padding(16.dp)
                    )
                }
            }
        }
    }
}

// ═══ Profile Screen ═══
@Composable
fun ProfileScreen(userId: String) {
    Scaffold(
        topBar = {
            TopAppBar(title = { Text("Profil: $userId") })
        }
    ) { padding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(padding)
                .padding(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Icon(
                imageVector = Icons.Default.AccountCircle,
                contentDescription = null,
                modifier = Modifier.size(120.dp)
            )
            Spacer(modifier = Modifier.height(16.dp))
            Text(userId, style = MaterialTheme.typography.headlineMedium)
        }
    }
}

// ═══ Settings Screen ═══
@Composable
fun SettingsScreen(onBack: () -> Unit) {
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("Pengaturan") },
                navigationIcon = {
                    IconButton(onClick = onBack) {
                        Icon(Icons.Default.ArrowBack, contentDescription = "Kembali")
                    }
                }
            )
        }
    ) { padding ->
        Column(modifier = Modifier.padding(padding)) {
            // Settings items...
        }
    }
}

Bottom Navigation

Kotlin β€” Bottom Navigation Bar
// ═══ Bottom Navigation with NavHost ═══
sealed class BottomNavItem(
    val route: String,
    val title: String,
    val icon: ImageVector
) {
    object Home : BottomNavItem("home_nav", "Beranda", Icons.Default.Home)
    object Explore : BottomNavItem("explore_nav", "Jelajahi", Icons.Default.Search)
    object Profile : BottomNavItem("profile_nav", "Profil", Icons.Default.Person)
}

@Composable
fun MainScreen() {
    val navController = rememberNavController()
    val items = listOf(
        BottomNavItem.Home,
        BottomNavItem.Explore,
        BottomNavItem.Profile
    )

    Scaffold(
        bottomBar = {
            NavigationBar {
                val navBackStackEntry by navController.currentBackStackEntryAsState()
                val currentRoute = navBackStackEntry?.destination?.route

                items.forEach { item ->
                    NavigationBarItem(
                        icon = { Icon(item.icon, contentDescription = item.title) },
                        label = { Text(item.title) },
                        selected = currentRoute == item.route,
                        onClick = {
                            navController.navigate(item.route) {
                                // Pop up to start destination untuk menghindari duplikat
                                popUpTo(navController.graph.findStartDestination().id) {
                                    saveState = true
                                }
                                launchSingleTop = true
                                restoreState = true
                            }
                        }
                    )
                }
            }
        }
    ) { padding ->
        NavHost(
            navController = navController,
            startDestination = BottomNavItem.Home.route,
            modifier = Modifier.padding(padding)
        ) {
            composable(BottomNavItem.Home.route) { HomeScreen() }
            composable(BottomNavItem.Explore.route) { ExploreScreen() }
            composable(BottomNavItem.Profile.route) { ProfileScreen() }
        }
    }
}
πŸ’‘ Navigation Best Practices
  • Gunakan sealed class atau enum untuk mendefinisikan routes secara type-safe
  • Gunakan launchSingleTop = true untuk menghindari push duplikat ke back stack
  • Gunakan saveState dan restoreState untuk bottom navigation agar state setiap tab tersimpan
  • Pisahkan navigation graph dari screen composables untuk maintainability
  • Gunakan deep links untuk integrasi dengan notification atau link eksternal

8. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Jetpack Compose:

Pertanyaan 1: Apa perbedaan utama antara remember dan rememberSaveable dalam Jetpack Compose?

a) remember untuk state global, rememberSaveable untuk state lokal
b) rememberSaveable mempertahankan state saat konfigurasi berubah (rotasi layar), sedangkan remember tidak
c) Tidak ada perbedaan, keduanya sama
d) remember hanya bisa digunakan dengan ViewModel

Pertanyaan 2: Dalam urutan modifier chain, modifier mana yang diterapkan terlebih dahulu?

a) Modifier terakhir (paling kanan)
b) Modifier pertama (paling kiri, paling deklaratif)
c) Modifier terbesar secara ukuran
d) Modifier yang paling sering digunakan

Pertanyaan 3: Apa yang dimaksud dengan "recomposition" dalam Jetpack Compose?

a) Proses kompilasi kode Kotlin menjadi bytecode
b) Proses memanggil ulang composable functions saat state berubah untuk memperbarui UI
c) Proses rebuild seluruh aplikasi dari awal
d) Proses menginisialisasi komponen ViewModel

Pertanyaan 4: Fungsi layout apa yang digunakan untuk menumpuk children di atas satu sama lain dalam Compose?

a) Row
b) Column
c) Box
d) Stack

Pertanyaan 5: Apa kepanjangan dari "BOM" dalam dependency Jetpack Compose?

a) Build Order Manager
b) Bill of Materials
c) Build Optimization Module
d) Basic Object Manager
πŸ” Zoom
100%
🎨 Tema