PPrepLearn
Progress Β· 0/10 sections

🟣 🟣 Jetpack Compose

4 min read Β· Notion


🌱 Compose Fundamentals

What is Jetpack Compose? How does it differ from XML?

Jetpack Compose is Android's modern declarative UI toolkit. Instead of inflating XML layouts and mutating views imperatively, you describe UI as a function of state:

XML (Imperative)Compose (Declarative)
Inflate layout, find viewsDescribe what UI looks like
Mutate views on state changeRecompose on state change
Separate XML + KotlinEverything in Kotlin
ViewGroup hierarchyComposition tree
@Composable
fun Greeting(name: String) {
    Text(text = "Hello, $name!")
}
What is a @Composable function?

A function annotated with @Composable can call other composables and has access to the Compose runtime. Rules:

  • Must be called from another @Composable function.
  • Should be side-effect free (no writing to shared state directly).
  • Named in PascalCase.
  • Returns Unit typically.
What is recomposition?

When state read inside a composable changes, Compose schedules a recomposition β€” re-running only the affected composables, not the entire UI tree. This is done via the Compose Snapshot system.

Compose tracks which State<T> objects are read during composition. Only composables that read changed state are recomposed.


πŸ’‘ State Management

remember vs rememberSaveable
  • remember {}: Survives recomposition, lost on configuration change.
  • rememberSaveable {}: Survives recomposition AND configuration changes (uses SavedStateHandle internally). Only works with types that are Parcelable or Serializable, or with a custom Saver.
var count by remember { mutableStateOf(0) }
var name by rememberSaveable { mutableStateOf("") }
mutableStateOf, derivedStateOf, snapshotFlow
  • mutableStateOf(): Holds observable state that triggers recomposition.
  • derivedStateOf { }: Derives computed state from other states. Only recomposes when the derived value changes.
  • snapshotFlow { }: Converts Compose state into a Flow.
val scrollState = rememberLazyListState()
val showFab by remember {
    derivedStateOf { scrollState.firstVisibleItemIndex > 0 }
}
How to prevent unnecessary recompositions?
  1. Use derivedStateOf for computed values.
  2. Pass lambdas as stable types (() -> Unit) instead of re-creating them.
  3. Use key() to help Compose identity-track composables in loops.
  4. Annotate classes with @Stable or @Immutable to tell Compose they won't change.
  5. Use remember {} to avoid recreating objects on every recompose.
  6. Hoist state minimally β€” don't lift state higher than needed.

🎌 Side Effects

LaunchedEffect, DisposableEffect, SideEffect
  • LaunchedEffect(key): Launches a coroutine when the composable enters composition. Re-runs if key changes. Use for async work, navigation, one-time events.
LaunchedEffect(userId) {
    viewModel.loadUser(userId) // re-runs if userId changes
}
  • DisposableEffect(key): For effects that need cleanup (registering listeners, callbacks).
DisposableEffect(lifecycle) {
    lifecycle.addObserver(observer)
    onDispose { lifecycle.removeObserver(observer) }
}
  • SideEffect {}: Runs on every successful recomposition. For synchronizing Compose state to non-Compose code.
How to integrate ViewModel with Compose?
@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    // Use uiState to render UI
}

Use collectAsStateWithLifecycle() (Lifecycle-aware) instead of collectAsState() for lifecycle-safe collection.


πŸ“± Layout & Navigation

LazyColumn performance optimization
  • Always provide a stable key for list items to help Compose track identity:
LazyColumn {
    items(items, key = { it.id }) { item ->
        ItemCard(item)
    }
}
  • Avoid creating lambdas inside items {} block β€” hoist them.
  • Use Modifier.fillParentMaxWidth() instead of Modifier.fillMaxWidth() inside lazy lists.
  • Avoid nesting scrollable layouts (LazyColumn inside ScrollColumn).
Scaffold, TopAppBar, BottomNavigation
Scaffold(
    topBar = { TopAppBar(title = { Text("My App") }) },
    bottomBar = {
        NavigationBar {
            items.forEach { item ->
                NavigationBarItem(
                    selected = currentDest == item.route,
                    onClick = { navController.navigate(item.route) },
                    icon = { Icon(item.icon, null) },
                    label = { Text(item.label) }
                )
            }
        }
    }
) { paddingValues ->
    NavHost(modifier = Modifier.padding(paddingValues), ...) { ... }
}
Navigation in Compose
val navController = rememberNavController()
 
NavHost(navController, startDestination = "home") {
    composable("home") { HomeScreen(navController) }
    composable("detail/{id}",
        arguments = listOf(navArgument("id") { type = NavType.IntType })
    ) { backStackEntry ->
        DetailScreen(id = backStackEntry.arguments?.getInt("id")!!)
    }
}
 
// Navigate:
navController.navigate("detail/42")
Animations in Compose
  • AnimatedVisibility: Animate enter/exit of a composable.
  • AnimatedContent: Animate content changes (e.g., tab transitions).
  • animate*AsState(): Animate a single value (Float, Color, Dp).
  • updateTransition(): Coordinate multiple animations.
AnimatedVisibility(
    visible = isVisible,
    enter = fadeIn() + expandVertically(),
    exit = fadeOut() + shrinkVertically()
) {
    MyContent()
}

Related