Scaffold & TopAppBar
Every non-trivial screen needs somewhere consistent to put a title bar, maybe a bottom
navigation bar, maybe a floating "add" button, and the actual scrollable content in between —
all without those pieces overlapping or fighting each other for space. Scaffold
is Compose's answer to that layout problem: a single composable that reserves named slots for
the standard pieces of a Material screen and takes care of the padding math between them. From
here on, every RecipeBox screen we build — starting with the recipe list — will be wrapped in a
Scaffold.
Scaffold's Slots
Scaffold doesn't take a single content lambda like a plain layout —
it exposes several optional, named slots as parameters, and you fill in only the ones your
screen actually needs:
| Parameter | Purpose |
|---|---|
topBar | Usually a TopAppBar — title, navigation icon, actions |
bottomBar | A bottom navigation bar or bottom app bar |
floatingActionButton | A single FAB, positioned per floatingActionButtonPosition |
snackbarHost | Where snackbars are anchored (covered in the next lesson but one) |
content | The main screen body — Scaffold hands it PaddingValues to avoid overlapping the other slots |
TopAppBar: Title, Navigation Icon & Actions
TopAppBar (from androidx.compose.material3) takes a title
composable, an optional navigationIcon slot (typically a back arrow), and an
actions row rendered on the trailing edge:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RecipeListTopBar(
onSearchClick: () -> Unit,
onBackClick: (() -> Unit)? = null
) {
TopAppBar(
title = { Text("My Recipes") },
navigationIcon = {
if (onBackClick != null) {
IconButton(onClick = onBackClick) {
Icon(Icons.Filled.ArrowBack, contentDescription = "Back")
}
}
},
actions = {
IconButton(onClick = onSearchClick) {
Icon(Icons.Filled.Search, contentDescription = "Search recipes")
}
}
)
}
TopAppBar is marked @ExperimentalMaterial3Api in current Material 3
releases, which is why the composable that hosts it needs an @OptIn annotation —
this is normal and expected, not a sign something is broken.
FloatingActionButton: Adding a Recipe
A FloatingActionButton is a circular, elevated button reserved for the single most
important action on a screen. On the recipe list, that's adding a new recipe:
@Composable
fun AddRecipeFab(onAddClick: () -> Unit) {
FloatingActionButton(onClick = onAddClick) {
Icon(Icons.Filled.Add, contentDescription = "Add recipe")
}
}
Worked Example: The RecipeBox Recipe List Screen
Putting the pieces together, here is a full recipe list screen built on Scaffold.
Notice how content's innerPadding parameter is applied to the
LazyColumn so its first and last items never sit underneath the top bar or FAB:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RecipeListScreen(
recipes: List<Recipe>,
onRecipeClick: (Recipe) -> Unit,
onAddClick: () -> Unit,
onSearchClick: () -> Unit
) {
Scaffold(
topBar = { RecipeListTopBar(onSearchClick = onSearchClick) },
floatingActionButton = { AddRecipeFab(onAddClick = onAddClick) }
) { innerPadding ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentPadding = PaddingValues(12.dp)
) {
items(recipes, key = { it.id }) { recipe ->
Text(
text = recipe.title,
modifier = Modifier
.fillMaxWidth()
.clickable { onRecipeClick(recipe) }
.padding(16.dp)
)
}
}
}
}
Scaffold's content lambda always receives the reserved padding as its
single argument — forgetting to apply it (.padding(innerPadding)) is the single
most common Scaffold mistake, and it results in list content sliding underneath the top bar as
soon as the user scrolls.