Jetpack Compose Fundamentals
Irbisa · cheatsheetSeptember 13, 2026

Jetpack Compose Fundamentals

Composable functions, modifiers, layout, recomposition, Material 3

Junior Developer20 itemscompressed for a skim
  1. 01

    What is Jetpack Compose? How is it different from the View system?

    Easy

    Jetpack Compose replaces the View system with @Composable functions 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)
      ) {
    …
  2. 02

    What are Modifiers in Compose and how do they work?

    Easy

    A Modifier is 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
    …
  3. 03

    What are the main layout containers in Compose?

    Easy

    Compose 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)
    …
  4. 04

    What is recomposition, and what decides whether Compose skips a composable?

    Medium

    Recomposition 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")
    …
  5. 05

    How does theming and Material 3 work in Compose?

    Medium

    Material 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),
    …
  6. 06

    How do you set up navigation in an Android app built with Jetpack Compose?

    Medium

    Navigation Compose gives an Android app one NavHost that owns a back stack of composable destinations, driven by a NavController.

    @Serializable data object Home
    @Serializable data object Settings
    @Serializable data class Profile(val id: Long)
    
    @Composable
    fun AppNav() {
    …
  7. 07

    After the list is reordered, your LazyColumn rows keep the wrong expanded state and animate to the wrong place. What is missing?

    Medium

    Without 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
    …
  8. 08

    How do you write a reusable composable that lets the caller decide what goes inside it?

    Easy

    A slot is a parameter of type @Composable () -> Unit that 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
    …
  9. 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?

    Medium

    Modern 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. 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?

    Medium

    AndroidView hosts a View inside a composable and ComposeView hosts 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. 11

    Row, Column and Box cannot express the layout you need. How do you write your own, and what rule must you respect?

    Hard

    The Layout composable 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. 12

    What do you reach for to animate a single value, and to animate content appearing and disappearing?

    Easy

    animate*AsState turns a state change into an animation, and AnimatedVisibility animates 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. 13

    Your screen composable takes a ViewModel and its @Preview renders an error instead of UI. How do you make screens previewable?

    Easy

    A preview builds your composable with no Activity, no ViewModelStoreOwner and 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. 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?

    Easy

    Scaffold has a named slot for each of them — topBar, bottomBar, floatingActionButton and snackbarHost — and SnackbarHostState.showSnackbar is 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. 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?

    Medium

    pointerInput is 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. 16

    TalkBack reads your product row as four separate items and the favourite button as an unlabelled button. What does the semantics tree need?

    Medium

    Compose 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. 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?

    Medium

    The 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, a rememberInfiniteTransition shimmer — never becomes idle.

    class CounterTest {
      @get:Rule val rule = createComposeRule()
    
      @Test fun incrementsOnClick() {
        rule.setContent { AppTheme { Counter() } }
    …
  18. 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?

    Medium

    The 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. 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?

    Hard

    The Compose compiler plugin rewrites every @Composable function to take a hidden $composer parameter and to wrap its body in groups, and remember identifies 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. 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?

    Hard

    Split it in two: an immutable ModifierNodeElement that is nothing but a comparable value, and a long-lived Modifier.Node that 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) }
    }
    …