Slot APIs
You've already used a slot API without necessarily naming it that:
Scaffold(topBar = { ... }, content = { ... }) doesn't take a giant list of
configuration flags describing what the top bar should look like — it takes an actual composable
lambda and simply places it where the top bar goes. That's the slot pattern: instead of a
composable exposing dozens of parameters trying to anticipate every possible customization
(title: String, titleColor: Color, showIcon: Boolean,
iconTint: Color...), it exposes a "slot" — a parameter typed
@Composable () -> Unit — and lets the caller put whatever composable content
they want into that position.
The Simplest Slot: a content Lambda
The most basic slot is a single content: @Composable () -> Unit parameter, which is
exactly what Column, Row, Box, and Card all
accept as their trailing lambda:
Card(modifier = Modifier.padding(8.dp)) {
// everything in here is the "content" slot — Card has no idea what's inside
Column(modifier = Modifier.padding(16.dp)) {
Text("Blueberry Pancakes")
Text("25 min")
}
}
Card only knows about styling — elevation, shape, background color. It has zero
knowledge of Text, Column, or anything else you put inside it, and that's exactly why the same
Card composable can wrap a simple label in one screen and a whole complex form in
another.
Named Slots: Multiple Composable Parameters
A composable isn't limited to one slot. Scaffold exposes several named
slots — topBar, bottomBar, floatingActionButton,
content — each a separate @Composable lambda parameter, so the caller
can fill in exactly the regions relevant to their screen and leave the rest at their defaults:
RecipeListScreen.kt — using Scaffold's named slots
@Composable
fun RecipeListScreen(recipes: List , onAddClick: () -> Unit) {
Scaffold(
topBar = { TopAppBar(title = { Text("RecipeBox") }) },
floatingActionButton = {
FloatingActionButton(onClick = onAddClick) {
Icon(Icons.Default.Add, contentDescription = "Add recipe")
}
}
) { padding ->
LazyColumn(modifier = Modifier.padding(padding)) {
items(recipes) { recipe -> RecipeRow(recipe) }
}
}
}
Note that the main content slot here receives a parameter of its own —
PaddingValues — so the caller knows how much space the top bar and any system bars
already occupy. A slot lambda can carry data outward like this just as easily as it carries
composables inward.
Worked Example: RecipeCardContainer
Suppose several RecipeBox screens want a card with a consistent header area (icon, title, maybe a favorite button) above completely different body content — one screen shows ingredients, another shows a photo, another shows a short summary. A single composable with a giant parameter list trying to cover every case would be unmanageable. A two-slot composable expresses this cleanly:
RecipeCardContainer.kt — a reusable two-slot card
@Composable
fun RecipeCardContainer(
modifier: Modifier = Modifier,
header: @Composable () -> Unit,
content: @Composable () -> Unit
) {
Card(modifier = modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
header()
Spacer(modifier = Modifier.height(8.dp))
content()
}
}
}
RecipeCardContainer itself knows nothing about recipes, ingredients, or photos — it
only knows "put the header slot, then a gap, then the content slot." Every screen fills those
slots differently:
Two very different uses of the same container
// Recipe list row: compact header + ingredient count
RecipeCardContainer(
header = {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.Restaurant, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text(recipe.title, style = MaterialTheme.typography.titleMedium)
}
},
content = { Text("${recipe.ingredients.size} ingredients") }
)
// Recipe detail screen: header with a favorite toggle + full steps list
RecipeCardContainer(
header = {
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
Text(recipe.title, style = MaterialTheme.typography.headlineSmall)
IconButton(onClick = onToggleFavorite) { Icon(Icons.Default.FavoriteBorder, contentDescription = null) }
}
},
content = {
recipe.steps.forEachIndexed { index, step -> Text("${index + 1}. $step") }
}
)
This is composition over configuration in practice: instead of RecipeCardContainer
trying to grow parameters to cover every future screen's needs, it stays small and stable, and
all the variation lives in the composables passed into its slots.
Slots Can Be Optional
A slot parameter can default to an empty lambda, making it fully optional for callers who don't
need that region at all — the same way Scaffold's bottomBar defaults to
doing nothing if you don't supply one:
@Composable
fun RecipeCardContainer(
modifier: Modifier = Modifier,
header: @Composable () -> Unit = {},
content: @Composable () -> Unit
) { /* ... */ }
Modifier extension.