Jetpack Compose Fundamentals
Composable functions, modifiers, layout, recomposition, Material 3
- 01
What is Jetpack Compose? How is it different from the View system?
EasyJetpack Compose replaces the View system with
@Composablefunctions that describe the UI for a given state.@Composable fun Greeting(name: String) { Column( modifier = Modifier.fillMaxWidth().padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { … - 02
What are Modifiers in Compose and how do they work?
EasyA
Modifieris an ordered chain of decorations applied to a composable: size, padding, background, border, click handling, semantics.// Order matters Text( "A", modifier = Modifier .padding(16.dp) // outer 16dp, stays transparent .background(Color.Red) // red covers the content only … - 03
What are the main layout containers in Compose?
EasyCompose has three basic layouts — Column stacks children vertically, Row horizontally, Box on the Z axis — plus lazy variants for long content.
@Composable fun ProductRow(p: Product) { Row( modifier = Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp) … - 04
What is recomposition, and what decides whether Compose skips a composable?
MediumRecomposition is Compose re-running the composable functions whose inputs changed, to bring the UI back in line with state.
@Composable fun Counter() { var count by remember { mutableIntStateOf(0) } Column { // Only Text reads `count`, so only Text recomposes when it changes. Text("Count: \$count") … - 05
How does theming and Material 3 work in Compose?
MediumMaterial 3 theming in Compose is one composable —
MaterialTheme— holding a color scheme, a typography set and shapes that every component reads through composition locals.private val LightColors = lightColorScheme( primary = Color(0xFF1565C0), onPrimary = Color.White, background = Color(0xFFFAFAFA), onBackground = Color(0xFF1A1A1A) ) private val DarkColors = darkColorScheme( primary = Color(0xFF90CAF9), onPrimary = Color(0xFF003258), … - 06
How do you set up navigation in an Android app built with Jetpack Compose?
MediumNavigation Compose gives an Android app one
NavHostthat owns a back stack of composable destinations, driven by aNavController.@Serializable data object Home @Serializable data object Settings @Serializable data class Profile(val id: Long) @Composable fun AppNav() { … - 07
After the list is reordered, your LazyColumn rows keep the wrong expanded state and animate to the wrong place. What is missing?
MediumWithout a
key, a lazy list identifies each item by its position, so an insert or a reorder silently shifts the identity of every item after it.@Composable fun TaskList(tasks: List<Task>, state: LazyListState = rememberLazyListState()) { LazyColumn(state = state) { items( items = tasks, key = { it.id }, // stable identity … - 08
How do you write a reusable composable that lets the caller decide what goes inside it?
EasyA slot is a parameter of type
@Composable () -> Unitthat the component invokes wherever the caller's content belongs.@Composable fun SettingRow( title: String, modifier: Modifier = Modifier, subtitle: String? = null, leading: (@Composable () -> Unit)? = null, // optional slot … - 09
Your content draws under the status bar and the last list row hides behind the navigation bar. How do you handle window insets in Compose?
MediumModern Android draws every app edge to edge, so the system bars sit on top of your content until you handle the window insets yourself.
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() // draw behind the system bars setContent { AppTheme { HomeScreen() } } } … - 10
You need a MapView on a Compose screen, and one Compose screen inside an existing XML Fragment. How do the two directions of interop work?
MediumAndroidViewhosts a View inside a composable andComposeViewhosts a composable inside an XML layout, which is what makes a screen-by-screen migration possible.@Composable fun MapPanel(zoom: Float, modifier: Modifier = Modifier) { val lifecycleOwner = LocalLifecycleOwner.current val mapView = remember { mutableStateOf<MapView?>(null) } DisposableEffect(lifecycleOwner) { … - 11
Row, Column and Box cannot express the layout you need. How do you write your own, and what rule must you respect?
HardThe
Layoutcomposable hands you the children as measurables plus the incoming constraints, and the rule is that each child may be measured exactly once.@Composable fun TwoColumnGrid(modifier: Modifier = Modifier, content: @Composable () -> Unit) { Layout(content = content, modifier = modifier) { measurables, constraints -> val columnWidth = constraints.maxWidth / 2 val childConstraints = constraints.copy( minWidth = 0, … - 12
What do you reach for to animate a single value, and to animate content appearing and disappearing?
Easyanimate*AsStateturns a state change into an animation, andAnimatedVisibilityanimates a composable entering and leaving the composition.@Composable fun ExpandableCard(item: Item) { var expanded by rememberSaveable { mutableStateOf(false) } val height by animateDpAsState( targetValue = if (expanded) 240.dp else 96.dp, animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), … - 13
Your screen composable takes a ViewModel and its @Preview renders an error instead of UI. How do you make screens previewable?
EasyA preview builds your composable with no Activity, no
ViewModelStoreOwnerand no DI graph, so anything the function reaches for outside its own parameters is what breaks it.// Stateful route: knows about DI, coroutines, navigation. Never previewed. @Composable fun HomeRoute( onOpen: (String) -> Unit, viewModel: HomeViewModel = hiltViewModel(), ) { … - 14
Where do the app bar, the FAB and the snackbar belong on a Material 3 screen, and why does showing a snackbar need a coroutine?
EasyScaffoldhas a named slot for each of them —topBar,bottomBar,floatingActionButtonandsnackbarHost— andSnackbarHostState.showSnackbaris a suspend function because it returns only once that snackbar has been dismissed.@Composable fun InboxScreen(events: Flow<UiEvent>, onCompose: () -> Unit) { val hostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() … - 15
A card handles taps via Modifier.pointerInput: TalkBack cannot activate it, and after the first state change the handler acts on a stale value. What went wrong?
MediumpointerInputis a raw event stream with no semantics attached, and its block is a coroutine that restarts only when its key changes — so a value captured inside it stays frozen at whatever it was when the block launched.// WRONG: keyed on Unit, so the block never restarts and `enabled` is frozen // at its first value; and the semantics tree has no clickable node at all. @Composable fun CardBad(item: Item, enabled: Boolean, onOpen: (Item) -> Unit) { Card( Modifier.pointerInput(Unit) { … - 16
TalkBack reads your product row as four separate items and the favourite button as an unlabelled button. What does the semantics tree need?
MediumCompose builds a parallel semantics tree out of your modifiers and accessibility services read only that tree, so grouping and labelling are things you declare — layout never implies them.
// BEFORE: four separate nodes, one of them announced as "button, unlabelled". @Composable fun ProductRowBad(p: Product, onToggle: () -> Unit) { Row { Image(painterResource(p.image), contentDescription = "product image") Text(p.name) … - 17
Your Compose UI test hangs and times out on a screen that shows a loading spinner. What is the test rule waiting for, and how do you write tests that do not do this?
MediumThe Compose test rule keeps the test in step with the UI by waiting for composition and the animation clock to go idle, and an indefinite animation — a
CircularProgressIndicator, arememberInfiniteTransitionshimmer — never becomes idle.class CounterTest { @get:Rule val rule = createComposeRule() @Test fun incrementsOnClick() { rule.setContent { AppTheme { Counter() } } … - 18
You call sheetState.hide() when the user confirms, and the ModalBottomSheet stays on screen. Why, and how are sheets and dialogs meant to be driven?
MediumThe sheet is on screen because your own state put it in the composition, and
hide()only runs the exit animation — until the flag that composed it flips to false, the sheet is still there.@Composable fun OrderScreen(vm: OrderViewModel) { // A nullable target doubles as the flag and as the sheet's content. var editing by rememberSaveable(stateSaver = ItemSaver) { mutableStateOf<Item?>(null) } var confirmDelete by rememberSaveable { mutableStateOf(false) } val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) … - 19
Why can a @Composable function only be called from another composable, and why does a remember inside a loop hand back the wrong value after the list reorders?
HardThe Compose compiler plugin rewrites every
@Composablefunction to take a hidden$composerparameter and to wrap its body in groups, andrememberidentifies its value by that group's position in the slot table — so when children change order, positions line up with the wrong items.// What you write. @Composable fun Greeting(name: String) { val id = remember { UUID.randomUUID() } // slot 0 of this group Text("$name $id") } … - 20
A custom modifier written with Modifier.composed allocates and re-runs on every recomposition. How do you rewrite it with the Node API instead?
HardSplit it in two: an immutable
ModifierNodeElementthat is nothing but a comparable value, and a long-livedModifier.Nodethat holds the state and does the work.// BEFORE: a composition per call site, nothing comparable, state read in // composition — every recomposition rebuilds this piece of the chain. fun Modifier.dotBad(color: Color) = composed { val alpha by animateFloatAsState(if (color == Color.Red) 1f else 0.5f, label = "a") drawBehind { drawCircle(color = color, radius = 6.dp.toPx(), alpha = alpha) } } …