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 views | Describe what UI looks like |
| Mutate views on state change | Recompose on state change |
| Separate XML + Kotlin | Everything in Kotlin |
| ViewGroup hierarchy | Composition 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
@Composablefunction. - Should be side-effect free (no writing to shared state directly).
- Named in PascalCase.
- Returns
Unittypically.
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 (usesSavedStateHandleinternally). Only works with types that areParcelableorSerializable, or with a customSaver.
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?
- Use
derivedStateOffor computed values. - Pass lambdas as stable types (
() -> Unit) instead of re-creating them. - Use
key()to help Compose identity-track composables in loops. - Annotate classes with
@Stableor@Immutableto tell Compose they won't change. - Use
remember {}to avoid recreating objects on every recompose. - 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 ifkeychanges. 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
keyfor 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 ofModifier.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()
}