Mobile Development

Jetpack Compose Advanced

TOKEN

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

📋 Daftar Isi
  1. Custom Layout
  2. Animation
  3. Side Effects
  4. State Hoisting
  5. Testing
  6. View Interop
  7. Best Practices
  8. Quiz Pemahaman

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)
            )
        }
    }
}

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