☀️Siang
Mobile Development

Jetpack Compose Advanced

TOKEN

Jetpack Compose Advanced — custom layout, animation, side effects, state hoisting, testing, dan View interop

Artikel: Android Compose Advanced Artikel: Android Compose Advanced


1. Custom Layout

Jetpack Compose memungkinkan pembuatan custom layout yang tidak tersedia di Compose standar. Dengan Layout composable, Anda mengontrol penuh measurement dan placement child elements.

Kotlin — Custom Circular Layout

@Composable
fun CircularLayout(
    radius: Float,
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(content = content, modifier = modifier) { measurables, constraints ->
        val placeables = measurables.map { measurable ->
            measurable.measure(constraints.copy(minWidth = 0, minHeight = 0))
        }

        val radiusPx = radius.toPx()
        val centerX = constraints.maxWidth / 2
        val centerY = constraints.maxHeight / 2
        val angleStep = 360f / placeables.size

        layout(constraints.maxWidth, constraints.maxHeight) {
            placeables.forEachIndexed { index, placeable ->
                val angle = Math.toRadians((angleStep * index - 90).toDouble())
                val x = centerX + radiusPx * cos(angle) - placeable.width / 2
                val y = centerY + radiusPx * sin(angle) - placeable.height / 2
                placeable.place(x.toInt(), y.toInt())
            }
        }
    }
}

// Penggunaan
@Composable
fun CircularMenu() {
    CircularLayout(radius = 120.dp) {
        repeat(6) { index ->
            FloatingActionButton(onClick = { /* action */ }) {
                Icon(Icons.Default.Star, "Item $index")
            }
        }
    }
}

1.1 Custom Flow Layout

Kotlin — Flow Layout

@Composable
fun FlowRow(
    modifier: Modifier = Modifier,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    content: @Composable () -> Unit
) {
    Layout(content = content, modifier = modifier) { measurables, constraints ->
        val placeables = measurables.map { it.measure(constraints.copy(minWidth = 0)) }
        val maxWidth = constraints.maxWidth
        var xPos = 0
        var yPos = 0
        var maxHeight = 0

        layout(maxWidth, constraints.maxHeight) {
            placeables.forEach { placeable ->
                if (xPos + placeable.width > maxWidth) {
                    xPos = 0
                    yPos += maxHeight
                    maxHeight = 0
                }
                placeable.place(xPos, yPos)
                xPos += placeable.width
                maxHeight = maxOf(maxHeight, placeable.height)
            }
        }
    }
}

// Tag chips
@Composable
fun TagCloud(tags: List) {
    FlowRow(modifier = Modifier.padding(8.dp)) {
        tags.forEach { tag ->
            SuggestionChip(
                onClick = { },
                label = { Text(tag) },
                modifier = Modifier.padding(4.dp)
            )
        }
    }
}
 ad-slot-wide"> 

2. Animation

Compose menyediakan API animasi yang powerful: animateXxxAsState, AnimatedVisibility, AnimatedContent, dan updateTransition.

Kotlin — Animations

@Composable
fun AnimationDemo() {
    var expanded by remember { mutableStateOf(false) }
    var visible by remember { mutableStateOf(true) }

    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        // AnimatedVisibility
        AnimatedVisibility(
            visible = visible,
            enter = fadeIn() + slideInVertically(),
            exit = fadeOut() + slideOutVertically()
        ) {
            Card(modifier = Modifier.padding(16.dp)) {
                Text("Hello!", modifier = Modifier.padding(16.dp))
            }
        }

        // animateXxxAsState
        val color by animateColorAsState(
            targetValue = if (expanded) MaterialTheme.colorScheme.primary
                          else MaterialTheme.colorScheme.secondary,
            animationSpec = tween(durationMillis = 500),
            label = "color"
        )

        val size by animateDpAsState(
            targetValue = if (expanded) 200.dp else 100.dp,
            animationSpec = spring(
                dampingRatio = Spring.DampingRatioMediumBouncy,
                stiffness = Spring.StiffnessLow
            ),
            label = "size"
        )

        Box(
            modifier = Modifier
                .size(size)
                .background(color, RoundedCornerShape(16.dp))
                .clickable { expanded = !expanded }
        )

        // AnimatedContent
        AnimatedContent(
            targetState = expanded,
            transitionSpec = {
                fadeIn() + slideInHorizontally { it } togetherWith
                fadeOut() + slideOutHorizontally { -it }
            },
            label = "content"
        ) { isExpanded ->
            if (isExpanded) {
                Text("Konten diperluas!", style = MaterialTheme.typography.headlineMedium)
            } else {
                Text("Konten ringkas", style = MaterialTheme.typography.bodyLarge)
            }
        }
    }
}

2.1 Animation Specs

SpecKarakteristikContoh Penggunaan
spring()Fisika realistis, bounceGesture feedback
tween()Kurva halus dengan durasiColor, opacity transition
keyframes()Kontrol per-frameAnimasi kompleks multi-step
repeatable()Animasi berulangLoading spinner
snaps()Langsung tanpa interpolasiTab indicator switch


3. Side Effects

Side effects di Compose dijalankan dengan lifecycle-aware functions: LaunchedEffect, DisposableEffect, SideEffect, dan rememberCoroutineScope.

Kotlin — Side Effects

@Composable
fun SideEffectsDemo(userId: String) {
    val viewModel: UserViewModel = viewModel()

    // LaunchedEffect — berjalan saat key berubah, auto-cancel
    LaunchedEffect(userId) {
        viewModel.loadUser(userId)
    }

    // DisposableEffect — cleanup saat dispose
    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_PAUSE) {
                // Save state
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }

    // SideEffect — berjalan setiap recomposition sukses
    SideEffect {
        analytics.trackScreen("UserProfile")
    }

    // rememberCoroutineScope — scope yang tied ke composable
    val scope = rememberCoroutineScope()
    Button(onClick = {
        scope.launch {
            viewModel.saveUser()
        }
    }) {
        Text("Simpan")
    }

    // derivedStateOf — compute dari state lain
    val sortedUsers by remember {
        derivedStateOf { viewModel.users.sortedBy { it.name } }
    }

    // snapshotFlow — observe Compose state sebagai Flow
    LaunchedEffect(Unit) {
        snapshotFlow { viewModel.searchQuery }
            .debounce(300)
            .collect { query ->
                viewModel.search(query)
            }
    }
}


4. State Hoisting

State hoisting adalah pola untuk memindahkan state ke parent composable, sehingga child menjadi stateless dan reusable.

Kotlin — State Hoisting

// Stateless composable (reusable)
@Composable
fun SearchBar(
    query: String,
    onQueryChange: (String) -> Unit,
    onSearch: () -> Unit,
    modifier: Modifier = Modifier
) {
    OutlinedTextField(
        value = query,
        onValueChange = onQueryChange,
        modifier = modifier.fillMaxWidth(),
        placeholder = { Text("Cari...") },
        trailingIcon = {
            IconButton(onClick = onSearch) {
                Icon(Icons.Default.Search, "Search")
            }
        },
        singleLine = true
    )
}

// Stateful composable (parent owns state)
@Composable
fun SearchScreen(viewModel: SearchViewModel = viewModel()) {
    var query by remember { mutableStateOf("") }

    Column {
        SearchBar(
            query = query,
            onQueryChange = { query = it },
            onSearch = { viewModel.search(query) }
        )

        LazyColumn {
            items(viewModel.results) { result ->
                SearchResultItem(result)
            }
        }
    }
}
📋 Prinsip State Hoisting

State hoisting membuat composable: (1) lebih reusable, (2) lebih mudah di-test, (3) single source of truth. Parameter: value: T dan onValueChange: (T) -> Unit.



5. Testing

Kotlin — Compose Testing

@RunWith(AndroidJUnit4::class)
class SearchBarTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun searchBar_displaysQuery() {
        composeTestRule.setContent {
            SearchBar(
                query = "Hello",
                onQueryChange = {},
                onSearch = {}
            )
        }
        composeTestRule
            .onNodeWithText("Hello")
            .assertIsDisplayed()
    }

    @Test
    fun searchBar_callsOnQueryChange() {
        var changedQuery = ""
        composeTestRule.setContent {
            SearchBar(
                query = "",
                onQueryChange = { changedQuery = it },
                onSearch = {}
            )
        }
        composeTestRule
            .onNodeWithText("Cari...")
            .performTextInput("Test")
        assertEquals("Test", changedQuery)
    }

    @Test
    fun listScroll_showsItems() {
        val items = (1..50).map { "Item $it" }
        composeTestRule.setContent {
            LazyColumn {
                items(items) { Text(it) }
            }
        }
        composeTestRule.onNodeWithText("Item 1").assertIsDisplayed()
        composeTestRule.onNodeWithText("Item 1")
            .performScrollToIndex(49)
        composeTestRule.onNodeWithText("Item 50").assertIsDisplayed()
    }
}


6. Interop dengan View System

Kotlin — View Interop

// AndroidView — gunakan View di Compose
@Composable
fun WebViewComposable(url: String) {
    AndroidView(
        factory = { context ->
            WebView(context).apply {
                webViewClient = WebViewClient()
                loadUrl(url)
            }
        },
        update = { webView ->
            webView.loadUrl(url)
        }
    )
}

// ComposeView — gunakan Compose di View
class ComposeFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
    ): View {
        return ComposeView(requireContext()).apply {
            setContent {
                MaterialTheme {
                    MyComposableScreen()
                }
            }
        }
    }
}

7. Best Practices

PraktikAlasan
Hindari mutableListOf di composableAkan di-reset saat recomposition
Gunakan key() di LazyColumnOptimasi recomposition
Gunakan remember {} untuk expensive computeHindari re-compute setiap recomposition
State hoistingReusability dan testability
derivedStateOf untuk computed stateHindari recomposition berlebihan
Stable types untuk parameterCompose skip recomposition jika data sama


Quiz Pemahaman

Pertanyaan 1: Composable apa untuk membuat custom layout di Compose?

a) @Composable
b) Layout {}
c) Box {}
d) Column {}

Pertanyaan 2: Side effect apa yang auto-cancel saat key berubah?

a) SideEffect
b) LaunchedEffect
c) DisposableEffect
d) rememberCoroutineScope

Pertanyaan 3: Pola apa yang memindahkan state ke parent?

a) Dependency Injection
b) Observer Pattern
c) State Hoisting
d) Builder Pattern

Pertanyaan 4: Animation spec apa yang berbasis fisika?

a) tween()
b) snap()
c) spring()
d) keyframes()

Pertanyaan 5: Fungsi apa untuk menghitung state turunan secara efisien?

a) remember {}
b) mutableStateOf()
c) derivedStateOf {}
d) snapshotFlow {}

← SebelumnyaKembali ke Beranda Selanjutnya →Lihat Kategori
🔍 Zoom
100%
🎨 Tema