Jetpack Compose in Depth: Layouts, Theming and Effects

Android Development with Kotlin: 21 Real Projects · Architecture, Data and Networking · 4 min read · Updated

Effects live outside recomposition

  1. A composable can run again and again as state changes. Do not do outside work directly in it.
  2. Put it in an effect block with a key.
  3. The effect runs on entering and again only when the key changes.
  4. When the composable leaves the screen, cleanup code runs.

You built screens in the projects. This lesson fills the gaps you need for polished, real apps: app structure, material components, theming, side effects, animation and accessibility.

App structure with Scaffold

Scaffold gives a standard Material 3 page with slots for a top bar, bottom bar, floating action button and snackbar host. Your content receives innerPadding, which you must apply so it is not hidden behind bars.

Scaffold(
    topBar = { TopAppBar(title = { Text("Home") }) },
    bottomBar = {
        NavigationBar {
            NavigationBarItem(selected = true, onClick = { }, icon = { Icon(Icons.Default.Home, null) }, label = { Text("Home") })
            NavigationBarItem(selected = false, onClick = { }, icon = { Icon(Icons.Default.Search, null) }, label = { Text("Search") })
        }
    },
    floatingActionButton = { FloatingActionButton(onClick = { }) { Icon(Icons.Default.Add, "Add") } },
    snackbarHost = { SnackbarHost(snackbarHostState) }
) { innerPadding ->
    Content(Modifier.padding(innerPadding))
}

For a navigation drawer, wrap the screen in ModalNavigationDrawer and open it with drawerState.open() inside a coroutine. Show a snackbar with snackbarHostState.showSnackbar("Saved") from a coroutine. An AlertDialog is just a composable you include when a flag is true.

Lists and grids

  • LazyColumn and LazyRow for vertical and horizontal lists.
  • LazyVerticalGrid(columns = GridCells.Fixed(2)) or GridCells.Adaptive(150.dp) for grids.
  • items(list, key = { it.id }) and stickyHeader for section headers.
  • rememberLazyListState() to read or control the scroll position.

Theming

MaterialTheme carries a colour scheme, typography and shapes. You define light and dark colour schemes (or use dynamic colour on Android 12 and later) and read values with MaterialTheme.colorScheme.primary. Never hard-code colours in screens: change the theme once and the whole app follows.

Side effects

A composable can run many times, so work with outside effects must go in special blocks.

  • LaunchedEffect(key): run a suspend block when the composable enters and when the key changes. Used to load data or collect events.
  • DisposableEffect(key): register something and clean it up in onDispose, such as a lifecycle observer or a sensor listener.
  • rememberCoroutineScope(): a scope for launching work from a click handler.
  • SideEffect: publish Compose state to non-Compose code after each successful composition.
  • derivedStateOf { ... }: a value calculated from other state that only changes when the result changes, for example showButton = listState.firstVisibleItemIndex > 0.
val listState = rememberLazyListState()
val showUp by remember { derivedStateOf { listState.firstVisibleItemIndex > 3 } }

LaunchedEffect(userId) {            // runs again only when userId changes
    viewModel.load(userId)
}

Animation

AnimatedVisibility fades or slides a composable in and out. animateColorAsState, animateDpAsState and animateFloatAsState move a value smoothly to a new target. AnimatedContent animates between two versions of content. Crossfade blends screens. For custom drawing use Canvas. Keep animations short, and respect the user's reduced-motion setting.

val size by animateDpAsState(if (expanded) 200.dp else 80.dp, label = "size")
AnimatedVisibility(visible = showDetails) { DetailsCard() }

Performance and recomposition

  • Give list items stable keys.
  • Pass small stable values or immutable data classes into composables.
  • Read state as late as possible (inside the lambda that needs it).
  • Use the Layout Inspector's recomposition counts to find screens that redraw too often.

Responsive layouts and accessibility

Use WindowSizeClass to choose a layout for phones, foldables and tablets, and Modifier.weight and fillMaxWidth instead of fixed sizes. For accessibility, give every icon a contentDescription (or null if it is decorative), keep touch targets at least 48 dp, make sure text contrast is high, support large font sizes, and group related items with Modifier.semantics. Test with TalkBack.

Your task

  1. Wrap one of your projects in a Scaffold with a top bar and a snackbar that confirms a save.
  2. Add a scroll-to-top button using derivedStateOf.
  3. Switch your app to dark theme and fix any hard-coded colours.
  4. Turn on TalkBack and complete one task in your app.

How to check it worked

  • Content is not hidden behind bars.
  • Dark mode looks right everywhere.
  • You can explain when to use LaunchedEffect and DisposableEffect.

Frequently asked questions

Why must I use innerPadding in Scaffold?

The bars cover the top and bottom of the screen. innerPadding tells your content how much space they take so nothing hides under them.

When do I use derivedStateOf?

When a value is calculated from fast-changing state but only matters when its result changes, like a boolean from a scroll position. It avoids needless recomposition.

How do I make Compose apps accessible?

Describe icons, use big enough touch targets, support large fonts and good contrast, and test with TalkBack.