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 |
|---|---|
| Deklaratif | Cukup deskripsikan UI berdasarkan state, Compose menangani rendering otomatis |
| Kotlin-first | Dirancang khusus untuk Kotlin dengan leveraging coroutines, extension functions, dan type-safety |
| Interoperability | Bisa digunakan bersama XML View dan sistem Android yang ada |
| Hot Reload | Perubahan UI langsung terlihat tanpa rebuild seluruh aplikasi |
| Material Design 3 | Didukung penuh dengan desain modern dan kustomisasi yang fleksibel |
| Animasi Built-in | Sistem animasi yang kuat dan mudah digunakan langsung dari toolkit |
XML View Tradisional vs Jetpack Compose
| Aspek | XML View | Jetpack Compose |
|---|---|---|
| Deklarasi UI | XML file terpisah | Kotlin code langsung |
| State Management | Manual (findViewById, ViewBinding) | Automatic recomposition |
| Learning Curve | π’ Mudah untuk pemula | π‘ Butuh pemahaman Kotlin |
| Boilerplate Code | π΄ Banyak | π’ Sedikit |
| Performance | Sudah optimal | Sangat optimal (smart recomposition) |
| Cocok untuk | Legacy, kompatibilitas | Proyek baru, modern UI |
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 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:
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")
}
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!")
}
}
}
}
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
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// 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
}
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 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:
// 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
// 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:
// 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
)
}
- Nama composable harus berawalan huruf kapital (seperti nama kelas/konstruktor)
- Gunakan
PascalCase:UserProfile, bukanuserProfile - 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
// βββ 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.
// βββ 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
// 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) }
)
}
}
}
}
| Fungsi | Penjelasan |
|---|---|
remember | Menyimpan nilai selama recomposition. Nilai hilang saat composable keluar tree. |
rememberSaveable | Seperti remember, tetapi bertahan saat configuration change (rotasi layar). |
derivedStateOf | Menghitung value dari state lain secara efisien. Hanya recompose saat hasil akhir berubah. |
mutableStateListOf | Mutable 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
// βββ 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):
// 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
// 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))
)
}
}
}
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
// βββ 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
// βββ 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")
}
}
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.
7. Navigation dalam Compose
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
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
// βββ 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
// βββ 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() }
}
}
}
- Gunakan sealed class atau enum untuk mendefinisikan routes secara type-safe
- Gunakan
launchSingleTop = trueuntuk menghindari push duplikat ke back stack - Gunakan
saveStatedanrestoreStateuntuk 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: