Lesson 12 / 17
Scaffold Structure
Measuring how the padding a screen scaffold gives its content travels down the tree, while content written into the scaffold's slot but reading the screen's state is fed along a deep path.
Published Updated
In this lesson
- Measures that the padding the scaffold gives its content goes down
- Shows that the place where a slot lambda is written differs from its place in the tree
- Distinguishes that the screen's state skips the scaffold and goes deep to the slot
- Explains why the padding comes from the scaffold and the state from where it is written
Contents
A screen has a title bar at the top, a navigation bar at the bottom and content in the middle. Placing them by hand is possible, but the heights of the bars have to be reported to the content; otherwise the content’s first row ends up under the title bar. The scaffold takes over this job: it accepts the top bar, the bottom bar and the content in separate slots, measures the space the bars occupy and gives it to the content as padding.
This lesson compares two pieces of data. One is the padding the scaffold gives its content, and its source is the scaffold itself. The other is the screen’s state, and it reaches the content without passing through the scaffold. Both arrive at the same slot, but their directions differ.
// Direction.kt — classifies which direction a value travels in the UI tree
class Tree(private val parentOf: Map<String, String>) {
fun contains(node: String): Boolean = node in parentOf.keys || node in parentOf.values
fun parent(node: String): String? = parentOf[node]
fun ancestors(node: String): List<String> {
val chain = mutableListOf<String>()
var current = parentOf[node]
while (current != null) {
chain.add(current)
current = parentOf[current]
}
return chain
}
}
class Move(val label: String, val from: String, val to: String, tree: Tree) {
val direction: String = when {
!tree.contains(from) || !tree.contains(to) -> "outside"
to in tree.ancestors(from) -> "up"
tree.parent(to) == from -> "down"
from in tree.ancestors(to) -> "deep"
else -> error("unrelated: $from and $to")
}
fun describe() {
println("$label: from=$from | to=$to -> $direction")
}
}
fun countDirections(moves: List<Move>) {
for (name in listOf("down", "up", "deep", "outside")) {
println("$name: ${moves.count { it.direction == name }}")
}
}
Slots and Padding
The scaffold accepts named slots: topBar, bottomBar and content. The content slot is a lambda, and this lambda takes the padding the scaffold computed as a parameter. The parameter is of type PaddingValues and carries the padding of the four edges.
// Platform code: a scaffold and the padding it gives its content
@Composable
fun CartScreen(cartCount: Int) {
Scaffold(
topBar = { CartTopBar(title = "Cart ($cartCount)") },
) { innerPadding ->
CartContent(Modifier.padding(innerPadding), cartCount)
}
}
The innerPadding in the slot lambda is the value the scaffold gave; the content uses it as its own edge padding. If it is not used, the content overflows under the bars. The same lambda also reads cartCount. This value is a parameter of the function the lambda is written in, that is, of CartScreen.
The model captures this platform rule; the model is not the platform itself. The helper file places the scaffold’s slots in the tree and computes the padding for the content from the bar heights.
// Scaffold.kt — slots and padding (the model's calculation)
class Insets(val top: Int, val bottom: Int)
fun scaffoldSlots(hasBottomBar: Boolean): List<String> =
if (hasBottomBar) listOf("TopBar", "Content", "BottomBar") else listOf("TopBar", "Content")
fun scaffoldTree(slots: List<String>): Tree {
val parentOf = mutableMapOf("Screen" to "App", "Scaffold" to "Screen")
for (slot in slots) {
parentOf[slot] = "Scaffold"
}
return Tree(parentOf)
}
fun contentInsets(topBar: Int, bottomBar: Int): Insets = Insets(topBar, bottomBar)
The Padding Comes from the Scaffold
In the first measurement the top bar is 56 and the bottom bar 80 units tall. From these the scaffold computes the padding for the content and gives it to the content slot.
// Main.kt — the padding the scaffold gives its content
fun main() {
val slots = scaffoldSlots(true)
val tree = scaffoldTree(slots)
val insets = contentInsets(56, 80)
println("inner padding: top=${insets.top} bottom=${insets.bottom}")
val move = Move("inner-padding", "Scaffold", slots[1], tree)
move.describe()
}
inner padding: top=56 bottom=80 inner-padding: from=Scaffold | to=Content -> down
The padding reached the content as 56 and 80; the move went from the scaffold directly to its child: down. Because the scaffold builds the content as its child, this move always descends one level. Whatever bar layout the content ends up with, the padding comes from the scaffold’s calculation.
LC1. The model takes the bar heights as two integers. The bars growing with their content, the edge insets occupied by the system bars and the keyboard inset are not in the model.
The Two Places of the Slot Lambda
The slot lambda is written inside CartScreen, so it can capture cartCount. In the tree, however, the content the lambda produces is a child of the scaffold; there is a scaffold node between it and CartScreen. Because an item is written inside CartScreen in the source code, it is not a direct child of it in the tree.
This difference determines the direction. cartCount goes from CartScreen to the content. The move jumps over the scaffold node: the scaffold neither reads nor transforms this value, it only places the slot. The value arrives through capture.
// Main.kt — the screen's state passes over the scaffold
fun main() {
val slots = scaffoldSlots(true)
val tree = scaffoldTree(slots)
val moves = listOf(
Move("cart-count", "Screen", slots[1], tree),
Move("title", "Screen", slots.first(), tree),
)
for (move in moves) {
move.describe()
}
countDirections(moves)
}
cart-count: from=Screen | to=Content -> deep title: from=Screen | to=TopBar -> deep down: 0 up: 0 deep: 2 outside: 0
Both are deep. The cart count went to the content, and the title to the top bar, skipping the scaffold. No parameter was given to the scaffold; the values were read inside the slot lambdas. This makes the scaffold robust: the scaffold knows nothing about the screen’s state, and adding a new piece of state does not change the scaffold call.
LC2. The model shows capture and the tree relation on the same line. Under which condition the platform reruns the lambda and how the content is updated when the captured value changes are not in the model.
The Bounding Measurement
The lesson’s direction does not depend on one of the two moves: both arrive at the same slots and both are real. The direction comes from separating the data that belongs to the scaffold from the data that belongs to the screen. The measurement below shows four together: the slots the screen gives the scaffold, the padding the scaffold gives the content, and the screen’s state going to the content and the top bar.
// Main.kt — four moves around the scaffold
fun main() {
val slots = scaffoldSlots(true)
val tree = scaffoldTree(slots)
val moves = listOf(
Move("slots", "Screen", "Scaffold", tree),
Move("inner-padding", "Scaffold", slots[1], tree),
Move("cart-count", "Screen", slots[1], tree),
Move("title", "Screen", slots.first(), tree),
)
for (move in moves) {
move.describe()
}
countDirections(moves)
}
slots: from=Screen | to=Scaffold -> down inner-padding: from=Scaffold | to=Content -> down cart-count: from=Screen | to=Content -> deep title: from=Screen | to=TopBar -> deep down: 2 up: 0 deep: 2 outside: 0
Both ends of the slots line are written by hand: this line is written to show the rule; it is not a measurement. The inner-padding, cart-count and title lines, which count as measurements, take one end from the model’s slots: down 1, deep 2.
Two down, two deep. Everything that enters the scaffold and everything that leaves the scaffold for the content descends one level: this is the scaffold’s own contract. The screen’s state, on the other hand, lies outside this contract; it descends through capture and skips the scaffold. The padding derives from the bar heights, which are known only in the scaffold; the state is known only where it is written. Each piece of data comes from where it is known. The direction of the move in the scaffold’s contract: down.
LC3. The model builds the four moves on a single bar layout. In a scaffold without bars the padding would be zero, but this lesson’s measurements do not cover it.
Summary
- This lesson’s direction: down. The move in the scaffold’s own contract is the inner padding going down from the scaffold to the content.
- The scaffold places the top bar, bottom bar and content slots and gives the space the bars occupy to the content as padding.
- The padding goes from the scaffold to the content, a direct child: down.
- The slot lambda is written inside the screen, but in the tree it is a child of the scaffold.
- The screen state the lambda captures skips the scaffold: deep.
Next Step
The scaffold places its content inside the screen; but when the user moves from one screen to another, the content to be placed changes. The next lesson measures navigation between screens, choosing the destination by routes and carrying an argument to a destination that is not yet in the tree.
To keep your progress and take notes, Log in
My notes
Log in to take notes.