---
title: 'UI Development with Jetpack Compose'
source: 'https://academia.sh/en/courses/compose'
language: en
updated: '2026-10-21T05:02:37+00:00'
license: 'CC BY-SA 4.0'
lessons: 17
outcomes:
    - 'Builds declarative interfaces with composable functions'
    - 'Explains when recomposition is triggered'
    - 'Manages side effects with the right API'
    - 'Distinguishes whether a value travels down, up, deep across several levels, or out of the UI tree'
---

# UI Development with Jetpack Compose

In a declarative interface data is always on the move, and not every move goes the same way: screen state descends to the list one level below, a tap on a row rises to the owner of the state, a theme color skips the levels in between to reach the lowest composable function, and a payment request leaves the tree for a service. The course measures every mechanism with a single question — which direction does this value travel in the UI tree? Composable functions, state and remembering, state hoisting, recomposition, side effects, stability, basic layouts, modifiers, constraint-based layout, lazy lists, prebuilt UI components, the scaffold, navigation, theming, animation, accessibility and the classic view system all pass through the same frame.

## The Component Model

The interface as a function, state and remembering, state hoisting, the triggers and skip conditions of recomposition, lifecycle-bound side effects, immutability and stability.

### Composable Functions

Source: https://academia.sh/en/courses/compose/composable-functions

A composable function takes data as parameters and adds nodes to the UI tree. Data travels down, up, deep, or outside the tree; the lesson classifies eight moves with a single rule.

In K02 we looked at what the app wants from the platform and measured every request with a single question: how does the platform fulfill this request? This course moves to the UI of the same app. A declarative UI is the return of a function: data goes in, a UI tree comes out. Data keeps moving in that tree, and the moves do not all go the same way. Screen state goes down to a list one level below, a tap in a row goes up to the owner of the state, a theme color skips the levels in between and reaches the composable at the bottom, and a payment request leaves the tree and goes to a service.

For this reason the measure of this course differs from that of K02. We do not look at whether a move received a value; we look at **from where to where** it goes. The question of the course is this: **which direction does this value travel in the UI tree?** There are four answers, and they do not change throughout the course: **down**, **up**, **deep**, and **outside**.

The classifier that makes the measurement is in the helper file below. The same file appears as the first block in every lesson.

```kotlin
// 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 }}")
    }
}
```

`Tree` carries the UI tree as a map from child to parent. `Move` carries the label, source, and target of a move; the direction comes from the `direction` property, not from the label, and the property asks its questions in order. If one of the ends is not in the tree, the move is outside. If the target is an ancestor of the source, it is up. If the parent of the target is the source, it is down. If the source is an ancestor of the target but not its direct parent, it is deep. If the two ends are unrelated, it raises an `error`. That last case is an error, not a measurement: a direct move cannot be built between siblings, and neither can a move whose source and target are the same.

#### Data Goes In, a Tree Comes Out

A composable function (`@Composable`) takes its parameters and adds nodes to the UI tree at the place where it is called. The UI does not appear as a value the function returns; it appears as the tree formed by the other composable functions called inside the function. A composable function can be called only from another composable function. Every call sets up a link between a parent and a child, and the tree is made of these links.

```kotlin
// Platform code: CartScreen
import androidx.compose.runtime.Composable

@Composable
fun CartScreen(items: List<CartItem>, total: String) {
    Header(title = "Cart")
    CartList(items = items)
    CheckoutButton(total = total)
}
```

`CartScreen` calls three composable functions; `Header`, `CartList`, and `CheckoutButton` are its children. The `items` and `total` parameters go down to the functions called from `CartScreen`. This fragment is platform code, and here we make no claim about output, layout, or duration. What the lesson measures is the parent and child relationship that comes out of this call structure; a model built with plain Kotlin values does the measuring. The model builds this rule of the platform; the model is not the platform itself.

#### Eight Moves, Four Directions

The model is a shopping cart screen. The root `App` carries `Screen`; under `Screen` stand `Header`, `CartList`, and `CheckoutButton`, and the child of `CartList` is `CartRow`. The ends of the moves are not chosen by hand; they come from two model functions. `hoistTo` finds the lowest common ancestor of the composable functions that read the state; that is the state owner. `nearestProvider` looks among the ancestors of a node for the first node that provides the given key.

```kotlin
// Main.kt
fun hoistTo(readers: List<String>, tree: Tree): String {
    val paths = readers.map { listOf(it) + tree.ancestors(it) }
    return paths.first().first { node -> paths.all { node in it } }
}

fun nearestProvider(node: String, key: String, provides: Map<String, Set<String>>, tree: Tree): String? =
    tree.ancestors(node).firstOrNull { key in provides.getOrDefault(it, emptySet()) }

fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "Header" to "Screen",
        "CartList" to "Screen",
        "CartRow" to "CartList",
        "CheckoutButton" to "Screen",
    ))
    val owner = hoistTo(listOf("CartList", "CheckoutButton", "Header"), tree)
    val themeSource = nearestProvider("CartRow", "theme", mapOf("App" to setOf("theme")), tree)!!
    println("state owner: $owner")
    println("theme provider: $themeSource")

    val moves = listOf(
        Move("cart-list", owner, "CartList", tree),
        Move("total-amount", owner, "CheckoutButton", tree),
        Move("title", owner, "Header", tree),
        Move("delete-row", "CartRow", owner, tree),
        Move("checkout-tap", "CheckoutButton", owner, tree),
        Move("theme-color", themeSource, "CartRow", tree),
        Move("payment-service", owner, "PaymentApi", tree),
        Move("event-log", owner, "Analytics", tree),
    )
    moves.forEach { it.describe() }
    countDirections(moves)
}
```

```text
state owner: Screen
theme provider: App
cart-list: from=Screen | to=CartList -> down
total-amount: from=Screen | to=CheckoutButton -> down
title: from=Screen | to=Header -> down
delete-row: from=CartRow | to=Screen -> up
checkout-tap: from=CheckoutButton | to=Screen -> up
theme-color: from=App | to=CartRow -> deep
payment-service: from=Screen | to=PaymentApi -> outside
event-log: from=Screen | to=Analytics -> outside
down: 3
up: 2
deep: 1
outside: 2
```

Of the eight moves, three are down, two are up, one is deep, and two are outside. The state owner came out as `Screen`. The cart list, the total amount, and the title go from the owner straight to its children: down. The delete in a row and the tap on the checkout button go to the owner, which is their ancestor: up. The theme color goes from `App` to `CartRow`; `Screen` and `CartList` lie in between and the color does not stop at them: deep. `PaymentApi` and `Analytics` are not in the tree; the payment request and the log line leave the tree: outside. This distribution is the binding count of the course; later lessons measure only their own slice and do not change this count.

**CM1.** The model carries the tree only as a parent relationship. How the real UI tree is built, and in which order the composable functions are called, was not measured.

#### The Parameter Chain

If a composable function gives its parameter directly to its child, the move is a single one. `Banner` calling `Greeting(name)` is one move from `Banner` to `Greeting`. The ends of the move come from the model function of the lesson: `parameterChain` orders the nodes from the owner of the value to its reader, using the ancestors in the tree.

```kotlin
// Chain.kt — parameter chain from the owner of a value to its reader
fun parameterChain(owner: String, reader: String, tree: Tree): List<String> =
    ((listOf(reader) + tree.ancestors(reader)).takeWhile { it != owner } + owner).reversed()
```

```kotlin
// Main.kt — Greeting(name) call: a single move from parent to child
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "Header" to "Screen",
        "Banner" to "Header",
        "Greeting" to "Banner",
    ))
    val path = parameterChain("Banner", "Greeting", tree)
    val moves = path.zipWithNext().map { (from, to) -> Move("greeting-name", from, to, tree) }
    println("chain: ${path.joinToString(" > ")}")
    moves.forEach { it.describe() }
    countDirections(moves)
}
```

```text
chain: Banner > Greeting
greeting-name: from=Banner | to=Greeting -> down
down: 1
up: 0
deep: 0
outside: 0
```

The name went straight from parent to child: a single down move. Now let the same name be known at the top of the screen, in `Screen`, and let it need to reach `Greeting`. `Header` and `Banner` do not use the name themselves; they only pass it one level down.

```kotlin
// Main.kt — the same name descends three levels: three separate down moves
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "Header" to "Screen",
        "Banner" to "Header",
        "Greeting" to "Banner",
    ))
    val path = parameterChain("Screen", "Greeting", tree)
    val moves = path.zipWithNext().map { (from, to) -> Move("name-${to.lowercase()}", from, to, tree) }
    println("chain: ${path.joinToString(" > ")}")
    moves.forEach { it.describe() }
    countDirections(moves)
}
```

```text
chain: Screen > Header > Banner > Greeting
name-header: from=Screen | to=Header -> down
name-banner: from=Header | to=Banner -> down
name-greeting: from=Banner | to=Greeting -> down
down: 3
up: 0
deep: 0
outside: 0
```

When a value is passed through parameters at every level, there are three separate down moves; none of them is deep. The parameter chain leaves no skipped level. `Header` and `Banner` in the middle have to carry the name in their signatures even though they do not use it.

#### No Moves Between Siblings

On the cart screen, `Header` and `CartList` are siblings. Suppose a value in the header needs to be read by the list. These two ends are unrelated: neither is the ancestor or the child of the other, so the line `Move("x", "Header", "CartList", tree)` falls into the last branch inside `direction` and raises an `error`. That means a move that cannot be built, not a classification. There is no real path between composable functions either: two siblings talk through their common parent. The value first goes up to the common ancestor, that is, it moves up; from there it goes down to the other sibling, that is, it moves down. A single move between siblings splits into two moves, and this split will explain in the next lessons why state stays at the common ancestor.

A line with both ends written by hand, such as `Move("x", "Screen", "Header", tree)`, can be used to show the rule but does not count as a measurement. In this lesson and in the rest of the course, at least one end of every move counted as a measurement comes from a model function of the lesson; lines written to show the rule are named as such in the prose. The direction is never written by hand.

#### The Bounding Measurement

The classifier sees the geometry, not the channel. It does not tell apart whether the value is carried by a parameter, by a callback, or by an implicit provider; it looks only at the positions of the two ends in the tree. To see this, take one provider, one channel, and two readers. `App` provides the theme. One of the readers is its direct child `Screen`; the other is `CartRow`, two levels down.

```kotlin
// Main.kt — the same provider, the same channel, two directions
fun nearestProvider(node: String, key: String, provides: Map<String, Set<String>>, tree: Tree): String? =
    tree.ancestors(node).firstOrNull { key in provides.getOrDefault(it, emptySet()) }

fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "Header" to "Screen",
        "CartList" to "Screen",
        "CartRow" to "CartList",
    ))
    val provides = mapOf("App" to setOf("theme"))
    val moves = listOf("Screen", "CartRow").map { reader ->
        Move("theme-${reader.lowercase()}", nearestProvider(reader, "theme", provides, tree)!!, reader, tree)
    }
    moves.forEach { it.describe() }
    countDirections(moves)
}
```

```text
theme-screen: from=App | to=Screen -> down
theme-cartrow: from=App | to=CartRow -> deep
down: 1
up: 0
deep: 1
outside: 0
```

The provider is the same, the channel is the same, the value is the same; the result is two directions. `Screen` is a direct child of the provider, and the value goes down to it. `CartRow` is two levels down, and the value goes deep to it without touching the nodes in between. Saying that a provider value is always deep is wrong: the distance of the provider to the target decides the direction. Had we passed the same value from `Screen` to `CartList` and from there to `CartRow` through parameters, there would have been two down moves. Telling the channels apart is not the job of the rule; the lesson says which channel it builds each time.

**CM2.** The model carries the provider as a set of keys and looks only for the nearest provider. A node in between providing the same key again does not occur in any of the eight moves measured in this lesson.

#### Summary

- This lesson's direction: down. A composable function takes data through parameters, and every move from a parent straight to a child is down.
- In the core measurement of the course, the distribution of the eight moves is down 3, up 2, deep 1, outside 2; later lessons do not change this count.
- When the same name is passed through parameters across three levels, there are three separate down moves: down 3, deep 0.
- The same provider value goes down to a direct child and deep to a descendant two levels down: down 1, deep 1.
- The rule sees the geometry, not the channel; the lesson says which channel it builds.

#### Next Step

In this lesson the data went down from its owner to its reader through parameters. The next lesson, `State and Remembering`, measures how state is remembered across calls of a composable function and which direction a value travels from the owner of the state to its reader.

### State and Remembering

Source: https://academia.sh/en/courses/compose/state-and-remember

A composable function keeps nothing from call to call; it ties the value to be kept to a node with remember. The lesson measures that state is tied to a node and the direction of the value that goes from its owner to its reader.

In the previous lesson the data went down from its owner to its reader through parameters. A parameter is the path a value passes along; where does the value itself live? A composable function runs from the beginning every time it is called, and its local variables disappear when the call ends. The value of a counter, the content of a text field, or whether a list is open has to be carried from call to call. The mechanism that does this is remembering: `remember` ties a value to a node in the UI tree.

The lesson measures two things. The first is that state is tied to a node. The second is the direction of the value that goes from the owner of the state to its reader: **which direction does this value travel in the UI tree?** The classifier stands in the first block, as in the previous lessons.

```kotlin
// 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 }}")
    }
}
```

#### State Is Tied to a Node

On the platform, state is built with `mutableStateOf`, which makes a value observable. `remember` writes the result of creating this state object into the UI tree and gives back the same object when the composable function is recomposed. If the calling composable function leaves the tree, the remembered object is forgotten too. The platform documentation says it this way: `remember` stores the value during composition and returns the stored value on recomposition; it forgets the object when the composable function that calls `remember` is removed from the tree.

```kotlin
// Platform code: Counter
import androidx.compose.runtime.*

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}
```

`Counter` ties the value `count` to its own node. Every tap on the button writes the value, and the write asks for the composable functions that read the value to be recomposed. This fragment is platform code, and here we make no claim about output, layout, or duration. A model built with plain Kotlin values does the measuring. The model builds this rule of the platform; the model is not the platform itself.

The model carries remembering as a map keyed by the node name. The platform ties state to the position in the tree; in the model, the name of the node takes the place of the position.

```kotlin
// Slots.kt — remembering model that ties state to a node
class Slots {
    private val values = mutableMapOf<String, Int>()
    fun remember(node: String, initial: Int): Int = values.getOrPut(node) { initial }
    fun write(node: String, value: Int) { values[node] = value }
    fun leave(node: String) { values.remove(node) }
    fun holds(node: String): Boolean = node in values
}
```

`remember` writes the initial value on the first call and gives back the written value on later calls. `write` changes the state. `leave` builds the exit of the node from the tree: the row of the node is deleted.

```kotlin
// Main.kt — state is tied to the node and is lost when the node leaves
fun main() {
    val slots = Slots()
    println("first composition: ${slots.remember("Screen", 0)}")
    slots.write("Screen", 3)
    println("recomposition: ${slots.remember("Screen", 0)}")
    slots.leave("Screen")
    println("left the tree: stored=${slots.holds("Screen")}")
    println("re-entry: ${slots.remember("Screen", 0)}")
}
```

```text
first composition: 0
recomposition: 3
left the tree: stored=false
re-entry: 0
```

On recomposition the initial value 0 was given again, but what came back was 3: the remembered value beat the initial value. When the node left the tree and entered again, the state started from scratch. So when a composable function disappears from the screen and comes back, the written value is gone.

**CM3.** The model builds remembering only with integer values and by node name. That on the platform the key is the position of the call in the tree, that keys given to `remember` empty the cache, and that observable state marks its readers on a write are not in the model.

#### Every Call Has Its Own State

If the same composable function is called in two places in the tree, two nodes are created and each node keeps its own state. The cart rows are an example: the same `CartRow` function is called twice, but increasing the quantity of one does not change the other.

```kotlin
// Main.kt — the same function, two calls, two separate states
fun main() {
    val slots = Slots()
    val rows = listOf("CartRow1", "CartRow2")
    rows.forEach { slots.remember(it, 1) }
    slots.write("CartRow1", 5)
    rows.forEach { println("$it: ${slots.remember(it, 1)}") }
}
```

```text
CartRow1: 5
CartRow2: 1
```

State belongs not to the function but to the call of the function in the tree. This distinction gains weight in the next lesson: if two siblings are to share a value, that value cannot stay in the node of one of the siblings.

#### The Remembered Value Goes Down to Its Reader

The node that remembers the state is its owner. The children take the value from the owner through parameters. The model finds the owner of the state a node reads by walking from the node itself toward its ancestors: the first node that remembers the value is the owner.

```kotlin
// Holder.kt — finds which node remembers the state a node reads
fun holderOf(node: String, slots: Slots, tree: Tree): String? =
    (listOf(node) + tree.ancestors(node)).firstOrNull { slots.holds(it) }
```

On the cart screen, let `Screen` remember the quantity, and let `Header` and `CartList` read it. One end of the moves comes from the return of `holderOf`; the direction is not written by hand.

```kotlin
// Main.kt — Screen remembers, children read: two down moves
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "Header" to "Screen",
        "CartList" to "Screen",
        "CartRow" to "CartList",
    ))
    val slots = Slots()
    slots.remember("Screen", 2)
    val readers = listOf("Header", "CartList")
    val moves = readers.map { reader ->
        Move("count-${reader.lowercase()}", holderOf(reader, slots, tree)!!, reader, tree)
    }
    moves.forEach { it.describe() }
    countDirections(moves)
}
```

```text
count-header: from=Screen | to=Header -> down
count-cartlist: from=Screen | to=CartList -> down
down: 2
up: 0
deep: 0
outside: 0
```

The owner came out as `Screen`, and the value goes down to its two direct children: two down moves. This is the direction of this lesson. Had `CartRow` also read the quantity, it would have taken the value through `CartList` by parameter; as the previous lesson measured, that would be two separate down moves.

This distinction differs from M30/K03. The question of that course, "what triggered this recomputation", classifies why a view is recomputed from the point of view of the reader: local, parent, observation, environment. Here too a state write asks for the readers to be recomposed, but the lesson does not make that its measure. What we measure is not the trigger but the path of the value from its owner to its reader.

#### Saveable State

According to the platform documentation, state held with `remember` is kept across recompositions but is not preserved across configuration changes; `rememberSaveable` is used for that. `rememberSaveable` preserves the state with the saved instance state mechanism when the activity or the process is recreated, for example when the screen is rotated. It does not preserve it if the user closes the activity completely.

```kotlin
// Platform code: SavedCounter
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable

@Composable
fun SavedCounter() {
    var count by rememberSaveable { mutableStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}
```

The way of writing it is the same as with `remember`; the difference is where the value is stored. Values that can be written to a `Bundle` are saved automatically, and a saver is given for other types. Because our model does not build the environment that the system recreates, this behavior is not measured here; the fragment stays unmeasured.

#### The Bounding Measurement

The rule classifies a move; it does not classify the absence of one. If the state is read only in its own node, the source and the target are the same, and such a move is not built; `Move` does not try to classify it, because there is no move. We build the same `Counter` node in two ways: when it remembers the state itself, and when the state stands in a node above.

```kotlin
// Main.kt — local state is not a move
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "Counter" to "Screen",
        "Header" to "Screen",
    ))
    for ((holder, label) in listOf("Counter" to "local", "Screen" to "shared")) {
        val slots = Slots()
        slots.remember(holder, 0)
        val moves = listOf("Counter")
            .filter { holderOf(it, slots, tree) != it }
            .map { Move("count-read", holderOf(it, slots, tree)!!, it, tree) }
        println("state $label:")
        moves.forEach { it.describe() }
        countDirections(moves)
    }
}
```

```text
state local:
down: 0
up: 0
deep: 0
outside: 0
state shared:
count-read: from=Screen | to=Counter -> down
down: 1
up: 0
deep: 0
outside: 0
```

With local state the list of moves stays empty and all four directions print zero. When the state moves up to a node above, the same read turns into one down move. The rule does not say that the state "does not exist" while it is local; it only does not count what does not enter the list. This difference gains weight in the next lesson: hoisting the state up is making a local value produce a move.

**CM4.** The model builds the read move only from the owner of the state to its reader. The reader writing the state, that is, the event in the opposite direction, was not measured in this lesson; it is the subject of the next lesson.

#### Summary

- This lesson's direction: down. The value that goes from the owner of the remembered state to its reader goes down to a direct child.
- `remember` ties the value to a node: on recomposition the stored 3 came back, and when the node left the tree and entered again, the value returned to 0.
- Two calls of the same function keep separate state: 5 for `CartRow1`, 1 for `CartRow2`.
- When two direct children read the quantity that `Screen` remembers, the measurement gave down 2.
- Local state read only in its own node produces no move: all four directions are 0; when the state stands in a node above, down 1.

#### Next Step

In this lesson the value went down from its owner to its reader. What happens when the reader wants to change the value? The next lesson, `State Hoisting`, moves the state to the common ancestor of the readers and measures which direction the request to change comes back to the owner.

### State Hoisting

Source: https://academia.sh/en/courses/compose/state-hoisting

State hoisting moves state to the common ancestor of the composable functions that read and write it; the value goes down and the event goes up. The lesson measures the direction of the event that goes to the owner and how the height of the owner changes the number of moves.

In the previous lesson `Screen` remembered a quantity and its two children read the value. The value went down, but when a reader wanted to change that value there was nothing it could do: the state belonged not to the reader but to its ancestor. In this lesson the request of the reader returns to the owner. Moving state to the common ancestor of the composable functions that read and write it is called state hoisting. The question of the lesson is the same as in the earlier lessons: **which direction does this value travel in the UI tree?**

```kotlin
// 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 }}")
    }
}
```

#### Value Down, Event Up

The platform documentation recommends hoisting state to the lowest common ancestor of the composable functions that read and write it. The owner of the state offers the consumers the immutable state and the events that will change it. A composable function receives this through two parameters: the `value` to show and the `onValueChange` to call when a change is requested. A function that does not hold the state itself is called stateless; a function that holds the state and gives it to a stateless function is called stateful.

```kotlin
// Platform code: QuantityScreen
import androidx.compose.runtime.*

@Composable
fun QuantityField(value: Int, onValueChange: (Int) -> Unit) {
    Button(onClick = { onValueChange(value + 1) }) {
        Text("Quantity: $value")
    }
}

@Composable
fun QuantityScreen() {
    var quantity by remember { mutableStateOf(1) }
    QuantityField(value = quantity, onValueChange = { quantity = it })
}
```

`QuantityField` does not hold the state. It takes the value through a parameter and reports the tap through a callback; writing the new value is the job of `QuantityScreen`. The value goes from the parent to the child, and the event goes from the child to its parent. This is called unidirectional data flow: every move goes one way, but two moves go in opposite directions. This fragment is platform code, and here we make no claim about output, layout, or duration. The model builds this rule of the platform; the model is not the platform itself.

This distinction differs from M12/K03. The question of that course, "who can change this result", classifies the owner of the result. In this lesson a model function finds the owner, but the measure is not the owner: the measure is the direction of the move that goes to the owner and the move that leaves the owner.

#### The Common Ancestor Becomes the Owner

The model starts with a function that finds the lowest common ancestor of the readers. The name is new in this lesson; it does the same job as `hoistTo` in the opening lesson.

```kotlin
// Owner.kt — the lowest common ancestor of the readers is the owner of the state
fun commonOwner(readers: List<String>, tree: Tree): String {
    val paths = readers.map { listOf(it) + tree.ancestors(it) }
    return paths.first().first { node -> paths.all { node in it } }
}
```

On the cart screen, `CartList` changes the quantity and `CheckoutButton` reads it to show the total. They are siblings. We know from the previous lesson that a direct move cannot be built between siblings; the value they share passes through the common ancestor. A helper builds this split: one up move from the source to the common owner, and one down move from the owner to the target.

```kotlin
// Siblings.kt — splits a request between siblings into two moves through the common ancestor
fun viaOwner(label: String, from: String, to: String, tree: Tree): List<Move> {
    val owner = commonOwner(listOf(from, to), tree)
    return listOf(
        Move("$label-up", from, owner, tree),
        Move("$label-down", owner, to, tree),
    )
}
```

```kotlin
// Main.kt — a move from sibling to sibling is two moves
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "Header" to "Screen",
        "CartList" to "Screen",
        "CartRow" to "CartList",
        "CheckoutButton" to "Screen",
    ))
    println("state owner: ${commonOwner(listOf("CartList", "CheckoutButton"), tree)}")
    val moves = viaOwner("quantity", "CartList", "CheckoutButton", tree)
    moves.forEach { it.describe() }
    countDirections(moves)
}
```

```text
state owner: Screen
quantity-up: from=CartList | to=Screen -> up
quantity-down: from=Screen | to=CheckoutButton -> down
down: 1
up: 1
deep: 0
outside: 0
```

The owner came out as `Screen`. The request that goes from `CartList` to `CheckoutButton` first went up to the owner: up. From the owner it went down to the button: down. Up, the direction of this lesson, is the first half of every share between siblings.

**CM5.** The model finds the owner only from the node names of the readers and the writer. That on the platform the logic of writing and reading the owner can also stand outside the tree, for example in a `ViewModel`, depending on whether it is UI logic or business logic, is not in the model.

#### The Full Flow

Now we build the whole flow. The owner is chosen with `commonOwner`. A value goes down from the owner to each reader, and an event goes up from the writing node to the owner. The value and the event pass through every level in between with a parameter and a callback; no level is skipped. The helper that does this produces a down path.

```kotlin
// Flow.kt — values descending from the owner to the readers, the event ascending from the writer to the owner
fun downPath(owner: String, node: String, tree: Tree): List<String> =
    ((listOf(node) + tree.ancestors(node)).takeWhile { it != owner } + owner).reversed()

fun flowMoves(owner: String, readers: List<String>, writer: String, tree: Tree): List<Move> {
    val values = readers
        .flatMap { reader -> downPath(owner, reader, tree).zipWithNext() }
        .distinct()
        .map { (from, to) -> Move("value-${to.lowercase()}", from, to, tree) }
    val events = downPath(owner, writer, tree).reversed().zipWithNext()
        .map { (from, to) -> Move("event-${from.lowercase()}", from, to, tree) }
    return values + events
}

fun liftTo(node: String, levels: Int, tree: Tree): String = tree.ancestors(node)[levels - 1]
```

`flowMoves` does not count the same move twice for two readers; `distinct` makes the shared segment of the path a single move. `liftTo` gives the node a few levels above a node and will be used in the next section.

```kotlin
// Main.kt — value down, event up
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "Header" to "Screen",
        "CartList" to "Screen",
        "CartRow" to "CartList",
        "CheckoutButton" to "Screen",
    ))
    val readers = listOf("CartList", "CheckoutButton")
    val owner = commonOwner(readers, tree)
    println("state owner: $owner")
    val moves = flowMoves(owner, readers, "CartList", tree)
    moves.forEach { it.describe() }
    countDirections(moves)
}
```

```text
state owner: Screen
value-cartlist: from=Screen | to=CartList -> down
value-checkoutbutton: from=Screen | to=CheckoutButton -> down
event-cartlist: from=CartList | to=Screen -> up
down: 2
up: 1
deep: 0
outside: 0
```

Two values go down from the owner to its children: two down. The tap that changes the quantity goes from `CartList` to the owner: one up. By count, down leads, but the move that hoisting newly brings is the event: while the state was local there was no event, and once it moved to the owner a request that goes up was born. This is why the direction of the lesson is up.

If the state belongs to only one composable function, there is nothing to hoist. If both the reader and the writer are `CartList`, the common ancestor is `CartList` itself and no move is built.

```kotlin
// Main.kt — a single reader: the owner is the reader itself
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "CartList" to "Screen",
        "CheckoutButton" to "Screen",
    ))
    val owner = commonOwner(listOf("CartList"), tree)
    println("state owner: $owner")
    countDirections(flowMoves(owner, listOf("CartList"), "CartList", tree))
}
```

```text
state owner: CartList
down: 0
up: 0
deep: 0
outside: 0
```

Hoisting is needed only when there is sharing. Local state produces no move, as in the previous lesson.

#### The Bounding Measurement

What changes if we hoist the state higher than needed, to `App`? The event still goes to the owner; its direction is up. But because the owner has moved away, the value and the event have to pass through every level in between.

```kotlin
// Main.kt — when the same state is hoisted one level higher, the number of moves grows
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "Header" to "Screen",
        "CartList" to "Screen",
        "CartRow" to "CartList",
        "CheckoutButton" to "Screen",
    ))
    val readers = listOf("CartList", "CheckoutButton")
    val needed = commonOwner(readers, tree)
    for (owner in listOf(needed, liftTo(needed, 1, tree))) {
        println("state owner: $owner")
        val moves = flowMoves(owner, readers, "CartList", tree)
        countDirections(moves)
        println("total: ${moves.size}")
    }
}
```

```text
state owner: Screen
down: 2
up: 1
deep: 0
outside: 0
total: 3
state owner: App
down: 3
up: 2
deep: 0
outside: 0
total: 5
```

When the owner is `App`, the value goes down to `Screen` and from there to the two readers: three down moves. The event goes from `CartList` to `Screen` and from there to `App`: two up moves. The total rose from three to five. The names of the four categories did not change; `deep` and `outside` are zero again. `Screen` became only a relay: it does not use the value, but it carries it in its signature. This is also the advice of the platform documentation: hoist state to its lowest common ancestor and do not pass it to composable functions that do not need it.

That a move between siblings splits into two moves through the common ancestor, and that the height of the owner changes the number of moves, depend on the same rule: the rule looks only at the positions of the two ends in the tree. The lesson, not the rule, says which path we chose.

**CM6.** The model assumes that the relay nodes pass the value and the event on as they are. That on the platform a composable function in between transforms the value or handles the event itself is not in the model.

#### Summary

- This lesson's direction: up. When state is hoisted, a new move is born from the reader to the owner: the change request.
- The owner is the lowest common ancestor of the readers: `Screen` for `CartList` and `CheckoutButton`.
- A request between siblings splits into two moves: up 1, down 1.
- In the full flow two values go down and one event goes up: down 2, up 1.
- When the state is hoisted to `App`, it becomes down 3, up 2; the total number of moves rises from 3 to 5 and the categories stay the same.

#### Next Step

In this lesson the event returned to the owner and the owner sent the value down again. Which composable functions will run again when the value changes is a separate question. The next lesson, `Recomposition`, measures which nodes a change reaches from its owner and how deferring the reading of a value changes the direction.

### Recomposition

Source: https://academia.sh/en/courses/compose/recomposition

When state changes, the platform reruns the composable functions that read the change and skips those whose inputs did not change. The lesson measures the path a change takes from its owner to its reader and how deferring the read changes the direction.

In the previous lesson the event returned to its owner and the owner wrote the new value. After the write the screen has to be updated, but the whole tree does not have to run again. When state changes, the platform reruns only the composable functions that the change can affect. This is called recomposition. The question of the lesson is again the same: **which direction does this value travel in the UI tree?** Here the data is the new value of the state, or the news that it changed; and its direction depends on where the read happens.

```kotlin
// 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 }}")
    }
}
```

#### Recomposition and Skipping

According to the platform documentation, when state changes the platform schedules a recomposition: it reruns the composable functions that may have changed and updates the composed tree. Recomposition usually starts when a `State<T>` object changes; the platform runs the composable functions that read this object and the composable functions they call that cannot be skipped. A suitable composable function whose inputs have not changed since the previous composition can be skipped entirely. According to the documentation, stable inputs are compared with `equals` and unstable inputs are compared by reference equality in strong skipping mode; if the inputs are equal to the previous values, the composable function is skipped.

The set of composable functions that a recomposition covers is called the recomposition scope. The composable function that reads the changing state is inside the scope. If a composable function inside the scope passes a changed value to its child, the child enters the scope too; a child called with unchanged inputs is skipped.

On the cart screen, let `CartScreen` remember the total and let the bottom-most `CartRow` show it. First we look at the form where the value is read at its owner and passed down through parameters.

```kotlin
// Platform code: ValueChannel
import androidx.compose.runtime.*

@Composable
fun CartScreen() {
    var total by remember { mutableStateOf(0) }
    Header()
    CartList(total = total)
}

@Composable
fun CartList(total: Int) {
    CartRow(total = total)
}

@Composable
fun CartRow(total: Int) {
    Text("Total: $total")
}
```

`CartScreen` reads the value itself and passes it to `CartList`; `CartList` passes it on to `CartRow` without using it. `Header` is called with unchanged inputs. This fragment is platform code, and here we make no claim about output, layout, the number of recompositions, or duration. The model builds this rule of the platform; the model is not the platform itself.

The model builds recomposition with three functions. `scopeChain` gives the path between two nodes. `recomposedScopes` counts the path from the node where the read happens to the node that uses the value as the scope. `skippedScopes` counts the nodes that are children of the scope but not in the scope as skipped. It also builds the moves of the two channels, `valueMoves` and `deferredMoves`; we will use the second later in this lesson.

```kotlin
// Channels.kt — the two paths a change takes from its owner to the reader
fun scopeChain(from: String, to: String, tree: Tree): List<String> =
    ((listOf(to) + tree.ancestors(to)).takeWhile { it != from } + from).reversed()

fun recomposedScopes(readAt: String, consumer: String, tree: Tree): List<String> =
    scopeChain(readAt, consumer, tree)

fun skippedScopes(recomposed: List<String>, nodes: List<String>, tree: Tree): List<String> =
    nodes.filter { it !in recomposed && tree.parent(it) in recomposed }

fun valueMoves(owner: String, consumer: String, tree: Tree): List<Move> =
    scopeChain(owner, consumer, tree).zipWithNext()
        .map { (from, to) -> Move("change-${to.lowercase()}", from, to, tree) }

fun deferredMoves(owner: String, consumer: String, tree: Tree): List<Move> =
    listOf(Move("change-${consumer.lowercase()}", owner, consumer, tree))
```

```kotlin
// Main.kt — the read is at the owner: the scope is the three nodes on the path, Header is skipped
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "Header" to "Screen",
        "CartList" to "Screen",
        "CartRow" to "CartList",
    ))
    val nodes = listOf("App", "Screen", "Header", "CartList", "CartRow")
    val recomposed = recomposedScopes("Screen", "CartRow", tree)
    println("recomposed: ${recomposed.sorted()}")
    println("skipped: ${skippedScopes(recomposed, nodes, tree).sorted()}")
}
```

```text
recomposed: [CartList, CartRow, Screen]
skipped: [Header]
```

Because the read happens at `Screen`, the scope starts at `Screen` and includes `CartList` and `CartRow` along the path the value descends. `Header` is a child of the scope but its inputs did not change: it is skipped. `App` is not affected at all. Note that the model assumes the inputs are stable and equal for skipping; stability is the subject of a later lesson.

**CM7.** The model builds the scope as the path from the node of the read to the node that uses the value. That inline functions are not counted as scopes on the platform, that the scope is determined by the boundaries of composable functions, and how many times a recomposition runs were not measured.

#### The Parameter Channel: Two Down

We measure the same change as moves. When the read stays at `Screen`, the new value goes to `CartList` through a parameter and `CartList` passes it to `CartRow`. Every pass is from a parent directly to its child.

```kotlin
// Main.kt — the parameter channel: one down move at every level
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "Header" to "Screen",
        "CartList" to "Screen",
        "CartRow" to "CartList",
    ))
    val moves = valueMoves("Screen", "CartRow", tree)
    moves.forEach { it.describe() }
    countDirections(moves)
}
```

```text
change-cartlist: from=Screen | to=CartList -> down
change-cartrow: from=CartList | to=CartRow -> down
down: 2
up: 0
deep: 0
outside: 0
```

There are two down moves. `CartList` in between does not use the value, but because the new value arrives at it, it is recomposed. The place of the read determines the number of moves and the scope.

#### Deferring the Read: One Deep

The platform documentation describes deferring a read as a performance technique. If the state is held above and read in a child, the read can be wrapped in a callback; this way the read happens only when it is really needed and where it is needed. According to the documentation, when the value is read only inside the child, the nearest scope becomes the child itself when it changes.

```kotlin
// Platform code: DeferredChannel
import androidx.compose.runtime.*

@Composable
fun CartScreen() {
    var total by remember { mutableStateOf(0) }
    Header()
    CartList(total = { total })
}

@Composable
fun CartList(total: () -> Int) {
    CartRow(total = total)
}

@Composable
fun CartRow(total: () -> Int) {
    Text("Total: ${total()}")
}
```

`CartScreen` no longer reads the value; it passes down a callback that reads it. The read happens inside `CartRow`, when `total()` is called. The callback itself passes through every level as a parameter in the first composition; but what changes is not the callback but the value of the state, and the news of this change reaches the scope of the reader directly. In the model the change move goes from the owner to the reader in a single step.

```kotlin
// Main.kt — the read is deferred: the scope is a single node, the change is a single deep move
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "Header" to "Screen",
        "CartList" to "Screen",
        "CartRow" to "CartList",
    ))
    val nodes = listOf("App", "Screen", "Header", "CartList", "CartRow")
    val recomposed = recomposedScopes("CartRow", "CartRow", tree)
    println("recomposed: ${recomposed.sorted()}")
    println("skipped: ${skippedScopes(recomposed, nodes, tree).sorted()}")
    val moves = deferredMoves("Screen", "CartRow", tree)
    moves.forEach { it.describe() }
    countDirections(moves)
}
```

```text
recomposed: [CartRow]
skipped: []
change-cartrow: from=Screen | to=CartRow -> deep
down: 0
up: 0
deep: 1
outside: 0
```

The scope is a single node. `Screen` and `CartList` are not recomposed, because neither of them reads the value. The change reaches `CartRow` from `Screen` in a single move, without stopping at the `CartList` in between: deep. This is the direction of the lesson.

This lesson differs from M30/K03. The question of that course, "what triggered this recalculation", classifies why a view was recalculated, from the reader's point of view. Here it is not the cause but the path of the change from its owner to its reader that is measured.

**CM8.** That in the deferred channel the change goes directly to the scope of the reader is the observation rule of the platform, and it is built this way in the model. How the observation works, that is, how the read is associated with a scope, is not in the model.

#### The Bounding Measurement

The same data, the same owner, the same reader, two channels. The rule does not see the channel; it looks only at the positions of the two ends. But the difference in direction comes from the channel, and so does the size of the scope.

```kotlin
// Main.kt — the same data, two channels, two directions
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "Header" to "Screen",
        "CartList" to "Screen",
        "CartRow" to "CartList",
    ))
    val byValue = valueMoves("Screen", "CartRow", tree)
    val deferred = deferredMoves("Screen", "CartRow", tree)
    println("parameter: scope=${recomposedScopes("Screen", "CartRow", tree).size} directions=${byValue.map { it.direction }}")
    println("deferred: scope=${recomposedScopes("CartRow", "CartRow", tree).size} directions=${deferred.map { it.direction }}")
}
```

```text
parameter: scope=3 directions=[down, down]
deferred: scope=1 directions=[deep]
```

The parameter channel builds two down moves and a scope of three nodes. The deferred channel builds a single deep move and a scope of a single node. Deep does not mean "better": in a deferred read the content of `CartRow` is read through a callback, and the cost of this is the readability of the code. The lesson does not measure this cost. What it measures is that the same change reaches `CartRow` through two channels with two different directions and two different scopes.

#### Summary

- This lesson's direction: deep. When the read is deferred to the bottom-most node, the change reaches the reader from its owner in a single move.
- When the read stays at the owner, the recomposed nodes are `CartList`, `CartRow` and `Screen`; `Header` is skipped.
- In the parameter channel the change descends through every level: down 2, deep 0.
- In the deferred channel the same change goes in a single move: down 0, deep 1.
- The two channels connect the same two ends; the scope drops from 3 nodes to 1 node. The rule does not see the channel; the difference in direction comes from the channel.

#### Next Step

In this lesson we assumed that composable functions whose inputs did not change are skipped. Which input counts as unchanged is a separate question and is the subject of lesson 06. Before that, the `Side Effects` lesson looks outside recomposition, at effects that leave a trace outside the tree: what is the direction when the end of the move is not in the tree?

### Side Effects

Source: https://academia.sh/en/courses/compose/side-effects

While building the tree, a composable function can also touch things outside the tree: it sends a request to a service, writes a log entry, subscribes to a listener. The lesson measures these moves and shows that all of them go in the same direction, outside.

So far data has moved around inside the tree: it went down, it went up, it skipped the nodes in between and went deep. But not everything a composable function does is adding a node to the tree. A composable function can send a request to a service, write a log line, subscribe to a listener of the platform. These are side effects: the trace a function leaves outside the tree. The question of the lesson is again the same: **which direction does this value travel in the UI tree?** The answer for a side effect is always the same.

```kotlin
// 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 }}")
    }
}
```

#### The Move That Leaves the Tree

The platform documentation describes a side effect as a change to the state of the app that happens outside the scope of a composable function, and it reminds us that composable functions can rerun at any moment because of recomposition. For this reason an effect must not be written directly in the body of the function; it has to be wrapped in one of the effect functions the documentation offers. About `LaunchedEffect` the documentation says: "When `LaunchedEffect` enters the Composition, it launches a coroutine with the block of code passed as a parameter." If the key changes, the current coroutine is cancelled and a new one is launched.

```kotlin
// Platform code: LaunchedEffect
import androidx.compose.runtime.*

@Composable
fun CartRow(productId: String) {
    LaunchedEffect(productId) {
        service.loadPrice(productId)
    }
    Text("Loading price")
}
```

As long as `productId` stays the same, recomposition does not restart the effect. If it changes, the previous job is cancelled and a new job starts with the new key. This fragment is platform code, and here we make no claim about output. The model builds this rule of the platform; the model is not the platform itself.

In the model, the effect is defined by its owner node, its target, and its key. Because the target is a name that is not in the tree, every move that leaves the effect is outside.

```kotlin
// Effects.kt — an effect that restarts when its key changes
class Effect(val owner: String, val target: String, private var key: String) {
    fun keyChanged(newKey: String): Boolean {
        val changed = newKey != key
        key = newKey
        return changed
    }
}

fun runIfKeyChanged(effect: Effect, newKey: String, tree: Tree): List<Move> =
    if (effect.keyChanged(newKey)) {
        listOf(Move("request-$newKey", effect.owner, effect.target, tree))
    } else {
        emptyList()
    }

fun contactMove(label: String, effect: Effect, tree: Tree): Move =
    Move(label, effect.owner, effect.target, tree)

fun replyMove(label: String, effect: Effect, tree: Tree): Move =
    Move(label, effect.target, effect.owner, tree)

fun writeToOwner(label: String, effect: Effect, tree: Tree): Move =
    Move(label, effect.owner, tree.ancestors(effect.owner).dropLast(1).last(), tree)

fun writeToParent(label: String, effect: Effect, tree: Tree): Move =
    Move(label, effect.owner, tree.ancestors(effect.owner).first(), tree)
```

These helpers derive the ends of the moves from the `Effect` fields and from the tree. `contactMove` goes from the owner to the target, and `replyMove` goes from the target to the owner. `writeToOwner` picks the screen that holds the state from the tree, and `writeToParent` picks the nearest parent. These functions read the ends of the move from the model's `Effect` record; the outside end is a name that is not in the tree and that the model's record defines. The owner and target names are written in the `Effect(...)` line, but the move is not built by hand from those names.

```kotlin
// Main.kt — four recompositions, two keys, two requests
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "CartList" to "Screen",
        "CartRow" to "CartList",
    ))
    val effect = Effect("CartRow", "Service", "")
    val keys = listOf("k1", "k1", "k2", "k2")
    val moves = keys.flatMap { runIfKeyChanged(effect, it, tree) }
    println("recomposition: ${keys.size} | running effect: ${moves.size}")
    moves.forEach { it.describe() }
    countDirections(moves)
}
```

```text
recomposition: 4 | running effect: 2
request-k1: from=CartRow | to=Service -> outside
request-k2: from=CartRow | to=Service -> outside
down: 0
up: 0
deep: 0
outside: 2
```

In four recompositions the key changed only twice: the first key and `k2`. The effect ran twice, two moves came out, and both are outside. Because `Service` is not in the tree, the rule looks at nothing else.

**CM9.** The model builds the difference of the key with text equality. That the platform compares the key with `equals`, the moment the coroutine is cancelled, and the timing of the first run were not measured.

#### Subscription and Cleanup

Some effects attach to something and need to detach from it. About `DisposableEffect` the documentation says: the last statement of a `DisposableEffect` block must be `onDispose`; otherwise an error is given at compile time. If the key changes or the composable function leaves the tree, the platform runs `onDispose` first.

```kotlin
// Platform code: DisposableEffect
import androidx.compose.runtime.*

@Composable
fun CartScreen(logger: Logger) {
    DisposableEffect(logger) {
        logger.subscribe()
        onDispose { logger.unsubscribe() }
    }
}
```

Both subscribing and unsubscribing go outside the tree. Two moves to the same target both go outside; one does not go out while the other comes back.

```kotlin
// Main.kt — subscribe and clean up: two moves, both outside
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "CartList" to "Screen",
    ))
    val effect = Effect("Screen", "Log", "")
    val moves = listOf(
        contactMove("subscribe", effect, tree),
        contactMove("unsubscribe", effect, tree),
    )
    moves.forEach { it.describe() }
    countDirections(moves)
}
```

```text
subscribe: from=Screen | to=Log -> outside
unsubscribe: from=Screen | to=Log -> outside
down: 0
up: 0
deep: 0
outside: 2
```

Although cleanup looks like a return, the direction is not going back. Because the log is not in the tree, "from `Screen`, to `Log`" goes outside; there is no other path the rule could resolve.

The third and fourth effect functions do something different. According to the documentation, `SideEffect` runs after every successful recomposition and is meant for publishing platform state to code outside the platform. `rememberUpdatedState` keeps a value current when you do not want the effect to restart.

```kotlin
// Platform code: SideEffect and rememberUpdatedState
import androidx.compose.runtime.*

@Composable
fun CartRow(onTimeout: () -> Unit, user: User) {
    val currentOnTimeout by rememberUpdatedState(onTimeout)
    LaunchedEffect(true) {
        delay(3000)
        currentOnTimeout()
    }
    SideEffect {
        analytics.setUser(user.id)
    }
}
```

Because the key of `LaunchedEffect(true)` never changes, the effect does not restart; thanks to `rememberUpdatedState`, the callback still carries the most current value. `SideEffect` writes to the `analytics` object outside the tree at every successful recomposition. Both are moves that go outside.

#### How the Response Returns to the Tree

When the work the side effect asked for is done, a response comes, and the response has to return to the tree, or nothing on the screen changes. This return is not written directly to a UI node. The effect gives the response to the owner of the state with a state write. There are three moves: request, response, state write.

```kotlin
// Main.kt — the request and the response are outside, the state write is inside the tree
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "CartList" to "Screen",
        "CartRow" to "CartList",
    ))
    val effect = Effect("CartRow", "Service", "")
    val moves = listOf(
        contactMove("request", effect, tree),
        replyMove("response", effect, tree),
        writeToOwner("state-write", effect, tree),
    )
    moves.forEach { it.describe() }
    countDirections(moves)
}
```

```text
request: from=CartRow | to=Service -> outside
response: from=Service | to=CartRow -> outside
state-write: from=CartRow | to=Screen -> up
down: 0
up: 1
deep: 0
outside: 2
```

The request and the response go in the same direction, outside. What tells which is which is not the direction but `from` and `to`: in the request the source is in the tree, in the response it is not. This is the direction of the lesson: outside. The third move is inside the tree. The effect writes the response from `CartRow` to the owner, and this is an up; the event path of Lesson 03 connects here.

This lesson differs from M12/K02. That course classifies which ends the network reaches with a request, and the spread. What is asked here is not the spread but whether the move ends inside the tree or outside it.

**CM10.** That the service response returns to the tree with a state write is a rule the model builds. Which thread the coroutine runs on, the timing of cancellation, and the response arriving more than once are not in the model.

#### The Bounding Measurement

Two owners, the same network, and the same number of moves. In one, the effect sends two moves from a node in the tree to a target outside. In the other, the effect makes writes that stay inside the tree.

```kotlin
// Main.kt — the effect goes outside, the write stays inside
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "CartList" to "Screen",
        "CartRow" to "CartList",
    ))
    val service = Effect("CartRow", "Service", "")
    val log = Effect("CartRow", "Log", "")
    val outside = listOf(
        contactMove("request", service, tree),
        contactMove("log", log, tree),
    )
    val inside = listOf(
        writeToOwner("write-1", service, tree),
        writeToParent("write-2", service, tree),
    )
    println("effect: ${outside.map { it.direction }}")
    println("write: ${inside.map { it.direction }}")
}
```

```text
effect: [outside, outside]
write: [up, up]
```

The same source, two families of targets, two directions. The difference is whether the target is in the tree; which effect function wraps the effect does not change the direction.

#### Summary

- This lesson's direction: outside. Every move that leaves an effect goes to a target outside the tree.
- In four recompositions the key changed twice, so the effect ran 2 times and both came out outside.
- Subscribing and cleaning up were measured as two separate moves: outside 2.
- The service response returns to the tree with a state write: outside 2, up 1.
- Two writes that stay in the tree measured up, two effects that leave the tree measured outside; the kind of effect function does not change the direction.

#### Next Step

What restarted the effect was the change of the key. How the platform understands whether a value has changed is the basis of skipping in recomposition. The next lesson, `Immutability and Stability`, measures this question and its direction toward the owner.

### Immutability and Stability

Source: https://academia.sh/en/courses/compose/immutability-and-stability

If the platform knows whether a value changed, it can skip during recomposition. The lesson measures that a list modified in place and an event that produces a new list go in the same direction, up, but that only one of them is observed.

The previous lesson assumed that a composable function whose inputs did not change is skipped. This lesson opens up that assumption: how does the platform know whether a value changed? The answer does not change the direction of the move, but it changes whether the move is seen. The question of the lesson is again the same: **which direction does this value travel in the UI tree?** It can go up in two different ways, and only in one of them does the platform notice.

```kotlin
// 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 }}")
    }
}
```

#### What the Platform Knows

According to the platform documentation, a type is stable if it is immutable, or if the platform can know whether its value changed between recompositions. A type whose value the platform cannot know is unstable. The documentation's example: a data class whose properties are `val` is immutable; in the version whose properties are `var`, the platform would not become aware if the properties changed, because the platform tracks only the changes in its own state objects.

For list interfaces the documentation says this: the standard collection interfaces (`List`, `Set`, `Map`) are considered unstable, because the implementation behind them may be mutable. According to the documentation, strong skipping mode is on by default in Kotlin 2.0.20 and above. With the mode on, unstable parameters are compared by reference equality (`===`), and if the same object is passed, the composable function is skipped.

```kotlin
// Platform code: MutableList and data class
import androidx.compose.runtime.*

class CartState(val items: MutableList<String>)

@Composable
fun CartRow(state: CartState) {
    Button(onClick = { state.items.add("c") }) {
        Text("Add: ${state.items.size}")
    }
}

data class CartItems(val items: List<String>)

@Composable
fun CartList(cart: CartItems, onAdd: (CartItems) -> Unit) {
    Button(onClick = { onAdd(cart.copy(items = cart.items + "c")) }) {
        Text("Add: ${cart.items.size}")
    }
}
```

The first composable function modifies the list where it stands: `state` and its list remain the same object. The second produces a new `CartItems` and gives the change to its owner as an event. This fragment is platform code, and here we make no claim about output. The model builds this rule of the platform; the model is not the platform itself.

The model builds "observed" with a simple rule: if the lists before and after the change are the same object, the platform cannot notice; if they are different objects, it does. An `Edit` carries the one who makes the change, the owner of the list, and the lists before and after.

```kotlin
// Edits.kt — the direction of a change and whether it was observed
class Edit(
    val label: String,
    val by: String,
    val owner: String,
    before: List<String>,
    val after: List<String>,
    tree: Tree,
) {
    val move = Move(label, by, owner, tree)
    val observed: Boolean = before !== after

    fun report() {
        move.describe()
        println("  observed=$observed | content=$after")
    }
}
```

**CM11.** The model builds being observed with a reference difference. How the platform infers stability for which types, the effect of the compiler setting, and the cost of the equality comparison were not measured.

#### Modifying in Place

The child modifies the owner's list where it stands. The data goes from the child to the owner; the owner is `Screen`, the one who modifies is `CartRow`.

```kotlin
// Main.kt — modifying in place: the direction is up, not observed
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "CartRow" to "Screen",
    ))
    val list = mutableListOf("a", "b")
    val before: List<String> = list
    list.add("c")
    val edit = Edit("modify", "CartRow", "Screen", before, list, tree)
    edit.report()
}
```

```text
modify: from=CartRow | to=Screen -> up
  observed=false | content=[a, b, c]
```

The direction of the move is up, because the target is an ancestor of the source. But because the list remained the same object, `observed=false`: the content changed, the platform did not learn of the change. The screen may stay with the old content.

#### A New List with an Event

The child does not modify the list; it gives an event to its owner, and the owner produces a new list.

```kotlin
// Main.kt — a new list with an event: the direction is up, observed
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "CartRow" to "Screen",
    ))
    val before = listOf("a", "b")
    val after = before + "c"
    val edit = Edit("event", "CartRow", "Screen", before, after, tree)
    edit.report()
}
```

```text
event: from=CartRow | to=Screen -> up
  observed=true | content=[a, b, c]
```

The direction is the same: up. Because the list is a new object, `observed=true`. This is the direction of the lesson: up.

The story of the event does not end here. When the owner knows the new list, it gives it down to the reader.

```kotlin
// Main.kt — the full flow: one up, one down
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "CartRow" to "Screen",
    ))
    val moves = listOf(
        Move("event", "CartRow", "Screen", tree),
        Move("new-list", "Screen", "CartRow", tree),
    )
    moves.forEach { it.describe() }
    countDirections(moves)
}
```

```text
event: from=CartRow | to=Screen -> up
new-list: from=Screen | to=CartRow -> down
down: 1
up: 1
deep: 0
outside: 0
```

In modifying in place there is only one move and no return. A new list with an event consists of two moves: the event up, the new list down. It is the same flow as in Lesson 03. The two lines above are written to show the rule; they are not a measurement: both ends are written by hand.

This lesson differs from M31/K01. That course built whether two values would be counted as equal. Here equality is not measured; what is measured is the direction of the move and whether the change was seen.

**CM12.** The owner giving the new list down is built in the model as a move. Which scopes the new list causes to recompose on the platform was not measured.

#### The Bounding Measurement

The same one who modifies, the same owner, the same content. The two methods of change side by side.

```kotlin
// Main.kt — the same direction, different observation
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "CartRow" to "Screen",
    ))
    val inPlace = mutableListOf("a", "b")
    val beforeInPlace: List<String> = inPlace
    inPlace.add("c")
    val copied = listOf("a", "b")
    val a = Edit("in-place", "CartRow", "Screen", beforeInPlace, inPlace, tree)
    val b = Edit("copy", "CartRow", "Screen", copied, copied + "c", tree)
    println("in-place: direction=${a.move.direction} observed=${a.observed}")
    println("copy: direction=${b.move.direction} observed=${b.observed}")
}
```

```text
in-place: direction=up observed=false
copy: direction=up observed=true
```

The two changes go in the same direction. The difference is not in the direction but in the observation. Because the rule looks at the direction, it gives the same answer to both; whether it is seen depends on the list changing as an object.

#### Summary

- This lesson's direction: up. A child modifying the owner's list and a child giving an event to its owner both go up.
- In modifying in place the content changed but it was not observed: up 1, observed false.
- In producing a new list with an event, the same direction was measured with observed true.
- Producing a new list with an event is shown as two moves: the event up, the new list down. This line is written to show the rule; it is not a measurement, since both ends are written by hand.
- The two methods of the same direction were measured side by side: both up, and only the copy was observed.

#### Next Step

This lesson closes the composable function model: composable function, state, state hoisting, recomposition, side effects, and stability. The next topic, `Basic Layouts`, builds the layout on the screen on top of this model.

## Layout and Components

Column, row and box layouts, order-dependent modifiers, constraint-based layout, lazy lists, prebuilt UI components, scaffold structure, navigation and routes, theme and typography, animations, accessibility and interoperating with the classic view system.

### Basic Layouts

Source: https://academia.sh/en/courses/compose/basic-layouts

Measuring which direction constraints, sizes and positions travel in the UI tree for column, row and box layouts.

The previous lesson measured how a change can quietly travel out to its owner. This lesson looks at a value that travels the opposite way in the same tree: the limit a parent gives its child. A column layout passes the width it was given to its children; the children stay within that width and report their own heights; the column then derives each child's position from those heights. The measuring axis is the same throughout the course: which direction does this value travel in the UI tree? There are four directions. A value that goes from a parent straight to its child travels **down**; one that goes from a child to any ancestor in its parent chain travels **up**; one that goes from an ancestor to a descendant, skipping the parent, travels **deep**; and if either end is not in the tree, it travels **outside**.

This lesson's question is the three values of the layout pass: constraint, size and position. All three move between the same two nodes, but not in the same direction. Which one decides the lesson's direction is settled in the last section. First, the core that does the measuring:

```kotlin
// 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 }}")
    }
}
```

#### Column, Row and Box

All three basic layout functions are composable functions (`@Composable`) and take their children in a lambda. `Column` arranges its children from top to bottom, `Row` from start to end (left to right in a left-to-right language); `Box` stacks its children on top of one another. The axis along which they are arranged is the main axis, and the axis perpendicular to it is the cross axis. The distribution of space along the main axis is given by `Arrangement`, and the position along the cross axis by `Alignment`.

```kotlin
// Platform code: Column, Row and Box
@Composable
fun Summary() {
    Column(
        modifier = Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(8.dp),
    ) {
        Text("Title")
        Row(horizontalArrangement = Arrangement.SpaceBetween) {
            Text("Name")
            Text("Total")
        }
        Box(contentAlignment = Alignment.BottomEnd) {
            Text("Note")
        }
    }
}
```

This snippet sets up three tree relationships. Each of `Column`, `Text`, `Row` and `Box` is a node in the UI tree; `Text("Title")`, `Row` and `Box` are children of `Column`; the two `Text` calls in `Row` are its children. The order is the order written in the source. The alignment and spacing parameters carry a rule about the node's children; they say nothing about the node itself.

#### The Layout's Single Pass

The platform processes a layout node in three steps. First the parent gives the child its constraints: the smallest and largest width and height the child may take. Then the child picks a size within that range and reports it; this is called measuring. Finally the parent looks at the reported sizes and decides each child's position; this is called placing. The three steps happen in a single pass: the parent gives a child its constraint, the child is measured once and placed once.

The model captures this platform rule. The model is not the platform itself; the helper file carries only the column rule. Its input is the parent's width, the `(name, width, height)` triples of the children and the cross-axis alignment. Its output is the `x` and `y` position of each child. `y` is the sum of the heights of the children before it; `x` depends on the alignment. The numbers here are the model's calculation, not pixels measured by the platform.

```kotlin
// Layout.kt — the single pass of a column layout (the model's calculation)
class Placed(val node: String, val x: Int, val y: Int)

fun placeColumn(parentWidth: Int, items: List<Triple<String, Int, Int>>, alignment: String): List<Placed> {
    var y = 0
    return items.map { (node, width, height) ->
        val x = when (alignment) {
            "center" -> (parentWidth - width) / 2
            "end" -> parentWidth - width
            else -> 0
        }
        val placed = Placed(node, x, y)
        y += height
        placed
    }
}
```

The first measurement is the three values of a single child. The tree puts `Screen` under `App`, `Column` under `Screen` and two children under `Column`. `Title` is the first child. The constraint is given from `Column` to `Title`. The size returns from `Title` to `Column`. The position again goes from `Column` to `Title`; the target of the move is the name of the node the model placed.

```kotlin
// Main.kt — the constraint, size and position of a single child
fun main() {
    val tree = Tree(mapOf("Screen" to "App", "Column" to "Screen", "Title" to "Column", "Body" to "Column"))
    val placed = placeColumn(360, listOf(Triple("Title", 200, 48), Triple("Body", 300, 120)), "start")
    val first = placed[0]
    val moves = listOf(
        Move("constraint-title", "Column", "Title", tree),
        Move("size-title", first.node, "Column", tree),
        Move("position-title", "Column", first.node, tree),
    )
    for (move in moves) {
        move.describe()
    }
    println("${first.node}: x=${first.x} y=${first.y}")
    countDirections(moves)
}
```

```text
constraint-title: from=Column | to=Title -> down
size-title: from=Title | to=Column -> up
position-title: from=Column | to=Title -> down
Title: x=0 y=0
down: 2
up: 1
deep: 0
outside: 0
```

Both ends of the `constraint-title` line are written by hand: this line is written to show the rule; it is not a measurement. The `size-title` and `position-title` lines, which count as measurements, take one end from the node the model placed.

The constraint and the position went down; the size went up. `Title`'s position is `x=0`, `y=0`: the topmost child of the column sits in the top-left corner because the alignment is at the start. The two values that went down carry different data. The constraint says what the child may choose; the position says where the chosen size will be put. Both are in the parent's hands, because the child does not know its own position.

**LC1.** The model reduces the constraint to a single number, the parent's width. On the platform, a constraint is four bounds (the smallest and largest width and height), and the case where the height constraint is unbounded was not measured.

#### Order Changes the Position, Not the Direction

`Column` arranges its children in the order they are written in the source. If the same two children are given in reverse order, their positions swap. The measurement below places two children in two orders and classifies each position move in the tree.

```kotlin
// Main.kt — position moves in two orders
fun main() {
    val tree = Tree(mapOf("Screen" to "App", "Column" to "Screen", "Title" to "Column", "Body" to "Column"))
    val items = listOf(Triple("Title", 200, 48), Triple("Body", 300, 120))
    val forward = placeColumn(360, items, "start")
    val backward = placeColumn(360, items.reversed(), "start")
    val moves = mutableListOf<Move>()
    for ((name, order) in listOf("forward" to forward, "backward" to backward)) {
        for (p in order) {
            moves.add(Move("position-$name-${p.node.lowercase()}", "Column", p.node, tree))
            println("$name ${p.node}: y=${p.y}")
        }
    }
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
forward Title: y=0
forward Body: y=48
backward Body: y=0
backward Title: y=120
position-forward-title: from=Column | to=Title -> down
position-forward-body: from=Column | to=Body -> down
position-backward-body: from=Column | to=Body -> down
position-backward-title: from=Column | to=Title -> down
down: 4
up: 0
deep: 0
outside: 0
```

`Title` is at `y=0` in the forward order and `y=120` in the backward order; `Body` swaps between `y=48` and `y=0`. All four position moves went in the same direction: down. The order changes the value at the end of the move; it does not change which two nodes the move is between. So reordering changes a layout result, not the flow of data.

**LC2.** The model leaves no space between children; main-axis spacing such as `Arrangement` values and `spacedBy` did not enter the `y` calculation.

#### Alignment Is Only Position

In `Column`, `horizontalAlignment` picks the position along the cross axis; in `Row`, `verticalAlignment` does the same job on the vertical axis. Because the model builds the column's cross axis as `x`, the same measurement holds for the cross axis of `Row`: the axes swap, the relationship stays the same. The alignment calculation uses the difference between the child's width and the parent's width. That difference is computed in place in the parent; what is sent to the child is the result of the calculation.

```kotlin
// Main.kt — the position under three alignments
fun main() {
    val tree = Tree(mapOf("Screen" to "App", "Column" to "Screen", "Title" to "Column"))
    val moves = mutableListOf<Move>()
    for (alignment in listOf("start", "center", "end")) {
        val title = placeColumn(360, listOf(Triple("Title", 200, 48)), alignment)[0]
        println("$alignment: x=${title.x}")
        moves.add(Move("position-$alignment", "Column", title.node, tree))
    }
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
start: x=0
center: x=80
end: x=160
position-start: from=Column | to=Title -> down
position-center: from=Column | to=Title -> down
position-end: from=Column | to=Title -> down
down: 3
up: 0
deep: 0
outside: 0
```

The same child got `x=0`, `x=80` and `x=160` under the three alignments. All three moves are the same: from the parent to the child, down. Alignment is not data but a calculation parameter; it sets up no new relationship in the tree. `Box` carries this relationship to the second axis: its children stack in the same box, and `contentAlignment` picks the horizontal and vertical position together.

**LC3.** The model reduces alignment to three values. The full set of `Alignment` values, the start and end meaning in a left-to-right language, and the case where the child is larger than the parent were not measured.

#### Row and Box Set Up the Same Relationship

`Row` is `Column` with its axes swapped. The main axis becomes horizontal: the children's widths are summed, and each child's `x` position is the sum of the widths before it. The cross axis is vertical, and `y` depends on the alignment. The shape of the calculation is the same; what changes is only which size is summed and which is compared with the parent. In the tree, nothing changes: `Row` also gives its children constraints, takes sizes from them and places them. The ends of the moves are the same, so their directions are the same.

`Box` behaves differently, but not in terms of tree relationships. None of the children affects the position of another; each child is measured independently within the box's constraint and placed according to `contentAlignment`. The box's own size fits the largest of its children. This is a case where the size goes up and the parent's size comes from its children. But one child's size does not change its sibling's constraint. There is no direct data move between siblings; this boundary will come up often in the coming lessons.

What these three layouts share is that a child does not know its own position. The position arises not as a parameter of the child but as the result of the parent's placing step. If a child wants to say "lean to the right of the screen", it does so with the parent's alignment parameter or with a modifier that widens its own constraint. The subject of modifiers belongs to the second lesson.

**LC4.** No separate model was built for `Row` and `Box`; the calculation described in this section is an account derived from the column model and was not measured.

#### The Bounding Measurement

The lesson's direction is chosen by reading the three values together, not a single move. The measurement below counts four moves together: the constraint `Screen` gives `Column`, the constraint `Column` gives `Body`, the size `Body` reports and the position `Column` gives `Body`.

```kotlin
// Main.kt — the direction tally of four moves
fun main() {
    val tree = Tree(mapOf("Screen" to "App", "Column" to "Screen", "Title" to "Column", "Body" to "Column"))
    val placed = placeColumn(360, listOf(Triple("Title", 200, 48), Triple("Body", 300, 120)), "start")
    val body = placed[1]
    val moves = listOf(
        Move("constraint-column", "Screen", "Column", tree),
        Move("constraint-body", "Column", "Body", tree),
        Move("size-body", body.node, "Column", tree),
        Move("position-body", "Column", body.node, tree),
    )
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
constraint-column: from=Screen | to=Column -> down
constraint-body: from=Column | to=Body -> down
size-body: from=Body | to=Column -> up
position-body: from=Column | to=Body -> down
down: 3
up: 1
deep: 0
outside: 0
```

Both ends of the `constraint-column` and `constraint-body` lines are written by hand: these lines are written to show the rule; they are not measurements. The moves that count as measurements are `size-body` (up) and `position-body` (down), which take one end from the `Body` node the model placed; two of the three downs in the count belong to the demonstration lines.

Three down, one up. The only value that goes up is the size; the constraint and the position go down. A size never exists without a constraint, because a child can pick a size only within the range it was given; so every size that goes up is preceded by a constraint that goes down. That is why the lesson's direction is the constraint's direction: **down**.

**LC5.** The model sets up four moves for one `Column` and a single `Body` under it. As the number of children grows, the number of size and position moves grows with it; this measurement is limited to one child.

#### Summary

- This lesson's direction: down. Constraint and position go from the parent to the child; the measured position move goes down as well.
- The layout's single pass carries three values: constraint, size and position.
- The constraint goes from the parent to the child and the position goes from the parent to the child; both are down.
- The size a child reports returns to the parent; that move is up.
- Order and alignment change the position's value, not the move's direction.

#### Next Step

The column is not the only node that passes the constraint to a child. Every modifier chained onto a composable function also passes its own constraint on to whatever follows it. When the order changes, the same constraint travels through a different node. The next lesson measures this chain, whose order changes the direction.

### Modifiers

Source: https://academia.sh/en/courses/compose/modifiers

Measuring how the order of a modifier chain changes the final width, the area a background covers and the direction the constraint travels.

In the previous lesson a column passed the width it was given to its children, and the constraint was seen to travel from parent to child, that is, down. This lesson looks at the same constraint passing not through one node but through a chain. The modifier chain (`Modifier`) given to a composable's `modifier` parameter adds size, padding, background and click behavior. The chain is written from left to right, and the order carries meaning: the same two modifiers give a different result in the opposite order.

The reason is that each modifier in the chain wraps the ones that come after it. The outer modifier takes the constraint, changes it and hands it to the inner one; the size that returns from the inner one, it enlarges or limits with its own share. What is to be measured is how this wrapping looks in the UI tree.

```kotlin
// 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 }}")
    }
}
```

#### The Chain and the Order

The snippet below sets up two chains. In the first, the size comes before the padding; in the second, after. The third example is a chain whose order changes whether the click area includes the padding.

```kotlin
// Platform code: chains whose order carries meaning
@Composable
fun Chains() {
    Box(Modifier.size(200.dp).padding(16.dp).background(Color.LightGray))
    Box(Modifier.padding(16.dp).size(200.dp).background(Color.LightGray))
    Text(
        text = "Open",
        modifier = Modifier.fillMaxWidth().clickable { open() }.padding(16.dp),
    )
}
```

The first two `Box` calls carry the same three modifiers, but in a different order. If `size(200.dp)` comes first, the node's total width is 200 and the padding is taken out of that 200. If `padding(16.dp)` comes first, the content is 200 wide and the padding is added outside it. In the third example, `clickable` comes before `padding`, so the clickable area includes the padding; if the order is reversed, it covers only the content.

The platform's rule is this: the first modifier in the chain takes the constraint, processes it and passes it to the second. At the end of the chain, the content is measured with the narrowed constraints. The model captures this rule. The model is not the platform itself. The helper file knows three modifiers: `SizeMod` limits the width, `PaddingMod` sets aside padding on both sides, and `BackgroundMod` records the area it covers. The content takes the largest width it was given; this is a modeling assumption. The `wrapChain` function also turns the chain into a UI tree: the first modifier is the parent's child, each following modifier is the child of the one before it, and the content is innermost.

```kotlin
// Chain.kt — the measurement of a modifier chain (the model's calculation)
sealed interface Mod
class SizeMod(val width: Int) : Mod
class PaddingMod(val amount: Int) : Mod
object BackgroundMod : Mod

class Measured(val total: Int, val content: Int, val painted: Int?)

fun measureChain(chain: List<Mod>, min: Int, max: Int): Measured {
    if (chain.isEmpty()) return Measured(max, max, null)
    val rest = chain.drop(1)
    return when (val mod = chain.first()) {
        is SizeMod -> {
            val fixed = mod.width.coerceIn(min, max)
            val inner = measureChain(rest, fixed, fixed)
            Measured(fixed, inner.content, inner.painted)
        }
        is PaddingMod -> {
            val inner = measureChain(rest, maxOf(0, min - 2 * mod.amount), maxOf(0, max - 2 * mod.amount))
            Measured((inner.total + 2 * mod.amount).coerceIn(min, max), inner.content, inner.painted)
        }
        BackgroundMod -> {
            val inner = measureChain(rest, min, max)
            Measured(inner.total, inner.content, inner.total)
        }
    }
}

class Wrapped(val tree: Tree, val nodes: List<String>)

fun wrapChain(chain: List<Mod>, parent: String): Wrapped {
    val nodes = chain.map {
        when (it) {
            is SizeMod -> "Size"
            is PaddingMod -> "Padding"
            BackgroundMod -> "Background"
        }
    } + "Content"
    val parentOf = nodes.indices.associate { nodes[it] to (if (it == 0) parent else nodes[it - 1]) }
    return Wrapped(Tree(parentOf), nodes)
}
```

#### Size Inside the Padding

The first measurement sets up the chain `size(200)` followed by `padding(16)`. The parent gives a constraint of at most 360 wide. The tree has `Size` under `Row`, `Padding` under `Size` and `Content` under `Padding`. Each constraint passes from a wrapper to the one immediately inside it.

```kotlin
// Main.kt — size first, padding second
fun main() {
    val chain = listOf(SizeMod(200), PaddingMod(16))
    val wrapped = wrapChain(chain, "Row")
    val measured = measureChain(chain, 0, 360)
    println("total=${measured.total} content=${measured.content}")
    val moves = listOf(
        Move("constraint-size", wrapped.nodes[0], wrapped.nodes[1], wrapped.tree),
        Move("constraint-padding", wrapped.nodes[1], wrapped.nodes[2], wrapped.tree),
    )
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
total=200 content=168
constraint-size: from=Size | to=Padding -> down
constraint-padding: from=Padding | to=Content -> down
down: 2
up: 0
deep: 0
outside: 0
```

The node's total width is 200 and the content is 168. The padding was taken out of the 200: 16 on each side, 32 in total. Both constraint moves are down, because each goes from a wrapper to the node immediately inside it. The constraint a modifier gives straight to what it wraps is classified as down.

**LC1.** The model limits the `size` modifier to a single width and keeps it within the constraint with `coerceIn`. Modifiers that can exceed the constraint, such as the platform's `requiredSize`, were not measured.

#### The Final Width When the Order Changes

Now the order of the two modifiers is reversed: padding first, then size. The parent's constraint is the same. The difference is that the constraint reaches the padding first; the padding narrows the constraint, the size picks 200 within the narrowed constraint, and the padding is added outside that 200.

```kotlin
// Main.kt — the final width in two orders
fun main() {
    val orders = listOf(
        "size-padding" to listOf(SizeMod(200), PaddingMod(16)),
        "padding-size" to listOf(PaddingMod(16), SizeMod(200)),
    )
    for ((name, chain) in orders) {
        val measured = measureChain(chain, 0, 360)
        println("$name: total=${measured.total} content=${measured.content}")
    }
}
```

```text
size-padding: total=200 content=168
padding-size: total=232 content=200
```

The same two modifiers, the same constraint; the total width is 200 against 232, and the content is 168 against 200. The order of the chain decides in which wrapper the value is added. These are the model's calculations. The tree counterpart of the order is measured in the last section.

**LC2.** The parent constraint was fixed at 360. The lower bound of the constraint was taken as zero; constraints with a lower bound above zero were not measured in this lesson.

#### The Constraint Goes In, the Size Comes Out

There are two directions in reading the chain, and both live in the same chain. The constraint descends from the outside in: each link processes the constraint and passes it to the one inside. The size returns from the inside out: the content reports its size, each link adds its own share to that size or subtracts it, and reports to the one outside. In the first measurement above, the content returned 168 wide, `Padding` added 32 to it and `Size` reported 200. In the second order, the same return started with 200 instead of 168 and ended with 232.

This distinction prevents two common misreadings. The first is the idea that the `size` modifier always settles the final size. `size` only pins the constraint; the value it pins still stays within the constraint of the links that come before it. The model captured this with `coerceIn`. The platform also has a separate modifier that can exceed the constraint, `requiredSize`; this lesson did not measure it. The second is the idea that order affects only appearance. The order decides which link sees which constraint; appearance is the result.

The chain's place in the tree should also be noted separately. On the platform, each link is not assumed to be a separate UI node. The model, though, counts each link as a node, because the relationship being measured is which link receives the constraint before which. This is a simplification, and its results hold to the extent that it reflects the order in which the constraint passes.

**LC3.** The model counts each modifier as a node. How the platform represents the modifier chain internally was not measured in this lesson.

#### The Background Follows the Order

A background paints the area its own node covers; that is, as much as the total of the chain it wraps. If `padding` comes before the background, the background stays inside the padding. If the background comes first, it paints the padding too.

```kotlin
// Main.kt — the area the background covers
fun main() {
    val orders = listOf(
        "padding-background" to listOf(PaddingMod(16), BackgroundMod),
        "background-padding" to listOf(BackgroundMod, PaddingMod(16)),
    )
    for ((name, chain) in orders) {
        val measured = measureChain(chain, 0, 360)
        println("$name: painted=${measured.painted} content=${measured.content}")
    }
}
```

```text
padding-background: painted=328 content=328
background-padding: painted=360 content=328
```

The content is 328 wide in both cases. The painted area is 328 against 360. This shows that the `background` modifier has no width of its own: its width is the total of what comes after it in the chain. The click area works by the same logic: `clickable` listens to the area covered by the chain it wraps.

**LC4.** The model measures the painted area only as a width; shape information such as height and corner rounding is not in the model. The click area was also not measured in this lesson; it was only said to follow the same logic as the background.

#### The Bounding Measurement

The lesson's direction is decided by the question of whether the order changes the direction. `Size`'s constraint reaches the content in both orders. But in one order `Padding` is immediately inside it, and in the other `Size` wraps the content directly. The measurement classifies the constraint move from `Size` to `Content` in both orders.

```kotlin
// Main.kt — the direction of the same constraint in two orders
fun main() {
    val orders = listOf(
        "size-padding" to listOf(SizeMod(200), PaddingMod(16)),
        "padding-size" to listOf(PaddingMod(16), SizeMod(200)),
    )
    val moves = mutableListOf<Move>()
    for ((name, chain) in orders) {
        val wrapped = wrapChain(chain, "Row")
        moves.add(Move("size-to-content-$name", "Size", wrapped.nodes.last(), wrapped.tree))
    }
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
size-to-content-size-padding: from=Size | to=Content -> deep
size-to-content-padding-size: from=Size | to=Content -> down
down: 1
up: 0
deep: 1
outside: 0
```

The same `Size` constraint reaches `Content` deep in the `size-padding` order, because `Padding` sits in between. In the `padding-size` order, `Size` wraps the content directly and the constraint goes down. The modifier is the same, the constraint is the same; when the order changes, the wrapper that steps in changes the direction. Each link in the chain gives the constraint to the one immediately inside it; those moves are down. Deep is the case where the intervening links are skipped, and only the order produces it. The constraint's real path through the chain descends link by link, so the lesson's direction is: **down**.

**LC5.** The model classifies the direction for only one constraint and two orders. In a chain with three or more links, the move is still deep as the number of skipped links grows, but that number was not measured.

#### Summary

- This lesson's direction: down. In the chain, the constraint goes straight from the wrapping link to the one it wraps.
- A modifier chain is nested nodes, each link wrapping the one after it.
- The outer modifier's constraint goes down to the one it directly wraps.
- When the order of the same two modifiers changes, the final width changes; the padding stays inside or outside the size.
- The area a background covers is the total of the chain that comes after it.

#### Next Step

Modifiers pass the constraint along a chain; but each link in the chain is still inside a parent-child relationship. There is no direct constraint between siblings. This boundary becomes visible in layouts that give an item's position relative to another sibling. The next lesson measures constraint-based layout.

### Constraint-Based Layout

Source: https://academia.sh/en/courses/compose/constraint-based-layout

Measuring that the position of items anchored to a sibling is computed through the parent and that a direct move from sibling to sibling cannot be set up.

In the previous lesson the children of a column were arranged in the order written in the source. Each child took its position only from its parent; it did not need to know where its siblings were. But in some interfaces an item's place depends on another item's place: the name goes to the right of the icon, and the badge goes under the name. This dependency can be built with nested columns and rows, but each dependency adds a level. Constraint-based layout (`ConstraintLayout`) solves it in a single level: items are anchored to one another, to the parent and to guidelines.

This lesson measures what anchoring to a sibling means in the UI tree. Siblings are children of the same parent, and there is no edge between them in the tree. If an item's position depends on another item, the question arises of which path this information travels.

```kotlin
// 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 }}")
    }
}
```

#### Anchoring Items to One Another

In a constraint-based layout, items are first created as references, and then each anchors its edges to the edge of another reference. On the horizontal axis, one item's start can be anchored to another's end; on the vertical axis, its top can be anchored to another's bottom. A margin can be put between them. A guideline, on the other hand, is a helper that is not visible on screen: it sits at a fraction of the parent's width, and items can be anchored to it.

```kotlin
// Platform code: items anchored to a sibling and a guideline
import androidx.constraintlayout.compose.ConstraintLayout

@Composable
fun ProfileRow() {
    ConstraintLayout(Modifier.fillMaxWidth()) {
        val (avatar, name, badge) = createRefs()
        val guide = createGuidelineFromStart(0.3f)
        Box(Modifier.size(48.dp).constrainAs(avatar) {
            start.linkTo(parent.start)
            top.linkTo(parent.top)
        })
        Text("Ada", Modifier.constrainAs(name) {
            start.linkTo(avatar.end, margin = 8.dp)
            top.linkTo(parent.top)
        })
        Text("New", Modifier.constrainAs(badge) {
            start.linkTo(guide)
            top.linkTo(name.bottom)
        })
    }
}
```

In this snippet all three items are children of `ConstraintLayout`; `name` is anchored to the end of `avatar`, and `badge` to the bottom of `name` and to the guideline. The syntax keeps the dependency in the item's own lambda: `name` reads `avatar` through its own edge. In the tree, though, `name` and `avatar` are siblings; the link visible in the source is not an edge in the tree.

The model captures this platform rule. The model is not the platform itself. The helper file takes the items in dependency order: an item's start is the sum of the start of the item it is anchored to, that item's width and the gap between them; the start of an item anchored to nothing is only its gap. The dependency order comes from the order the items are given; the platform's resolving of the dependency graph is not in the model.

```kotlin
// Anchors.kt — the position of items anchored to a sibling (the model's calculation)
class Item(val name: String, val width: Int, val anchor: String?, val gap: Int)
class Placement(val name: String, val x: Int, val anchor: String?)

fun solveAnchors(items: List<Item>): List<Placement> {
    val placed = mutableMapOf<String, Int>()
    val widths = items.associate { it.name to it.width }
    return items.map { item ->
        val anchor = item.anchor
        val x = if (anchor == null) item.gap else placed.getValue(anchor) + widths.getValue(anchor) + item.gap
        placed[item.name] = x
        Placement(item.name, x, anchor)
    }
}
```

#### The Sibling's Position Goes Up First

The position of an item anchored to a sibling comes about in two steps. First the sibling's position is reported to the layout parent: the layout has to know which item is where, because the placing step is its job. Then the parent gives the anchored item its new position. The first move goes from child to parent, that is, up; the second goes from parent to child, that is, down.

The tree puts `ConstraintLayout` under `Screen` and three items under it: `Avatar`, `Name` and `Badge`. The first measurement places the first two items. `Avatar` is anchored to the parent; `Name` is anchored to `Avatar`.

```kotlin
// Main.kt — the sibling's position goes up to the parent and comes down from it
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App", "ConstraintLayout" to "Screen",
        "Avatar" to "ConstraintLayout", "Name" to "ConstraintLayout", "Badge" to "ConstraintLayout",
    ))
    val placements = solveAnchors(listOf(Item("Avatar", 48, null, 0), Item("Name", 120, "Avatar", 8)))
    val first = placements[0]
    val second = placements[1]
    println("${first.name}: x=${first.x}")
    println("${second.name}: x=${second.x}")
    val moves = listOf(
        Move("report-position-avatar", first.name, "ConstraintLayout", tree),
        Move("give-position-name", "ConstraintLayout", second.name, tree),
    )
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
Avatar: x=0
Name: x=56
report-position-avatar: from=Avatar | to=ConstraintLayout -> up
give-position-name: from=ConstraintLayout | to=Name -> down
down: 1
up: 1
deep: 0
outside: 0
```

`Name` got the position `x=56`: `Avatar`'s width is 48 and the gap between them is 8. This information did not go from `Avatar` to `Name` directly. First `Avatar` reported its own position to the layout, up; the layout used it to compute `Name`'s position and gave it to `Name`, down. What looks like a one-line link in the source is a two-move path in the tree.

**LC1.** The model takes the items in dependency order and places them only on the horizontal axis. Vertical links, anchoring to several items at once, and the case where dependencies form a cycle were not measured.

#### Two-Move Steps Along a Chain

When the chain gets longer, each link repeats the same two moves. `Badge` is anchored to `Name`; `Name`'s position goes up to the layout first, then down to `Badge`. This means that in a three-item chain `Name` plays two different roles: the item anchored relative to `Avatar`, and the item read by `Badge`.

```kotlin
// Main.kt — the moves of a three-item chain
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App", "ConstraintLayout" to "Screen",
        "Avatar" to "ConstraintLayout", "Name" to "ConstraintLayout", "Badge" to "ConstraintLayout",
    ))
    val placements = solveAnchors(listOf(
        Item("Avatar", 48, null, 0), Item("Name", 120, "Avatar", 8), Item("Badge", 32, "Name", 8),
    ))
    val moves = mutableListOf<Move>()
    for (p in placements) {
        println("${p.name}: x=${p.x}")
        val anchor = p.anchor ?: continue
        moves.add(Move("report-position-${anchor.lowercase()}", anchor, "ConstraintLayout", tree))
        moves.add(Move("give-position-${p.name.lowercase()}", "ConstraintLayout", p.name, tree))
    }
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
Avatar: x=0
Name: x=56
Badge: x=184
report-position-avatar: from=Avatar | to=ConstraintLayout -> up
give-position-name: from=ConstraintLayout | to=Name -> down
report-position-name: from=Name | to=ConstraintLayout -> up
give-position-badge: from=ConstraintLayout | to=Badge -> down
down: 2
up: 2
deep: 0
outside: 0
```

Three items, two links and four moves: two up, two down. `Badge` got the position `x=184`: `Name`'s start is 56, its width is 120 and the gap is 8. The number of up moves equals the number of links; each link sends the position of the sibling it is anchored to up to the parent once.

**LC2.** The model uses the right edge of the item a link is anchored to. Anchoring to a guideline is a separate relationship and was not measured.

#### A Guideline Is a Sibling

A guideline is an invisible helper that sits at the same level as the items. When items are anchored to it, the position information again flows through the parent: the line's position is computed from a fraction of the parent's width, and the layout gives it to the item anchored to the line. In this respect a guideline is the same as a sibling: in the tree it sits beside the items, and there is no direct edge between it and them. The model measured in this lesson did not build the guideline as a separate item; this sentence is a description of platform behavior, not a measurement result.

#### Sibling to Sibling Cannot Be Set Up

In the source code it is easy to write the line where `name` is anchored directly to `avatar`. In the tree, though, two siblings sending data to each other is not possible without going up to the common ancestor and coming down from it. The core classifies this case not as a direction but as an error: it is neither down, nor up, nor deep, nor outside. Code that tries to set up the move stops with an `unrelated` error.

```kotlin
// Main.kt — a direct move between siblings is rejected
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App", "ConstraintLayout" to "Screen",
        "Avatar" to "ConstraintLayout", "Name" to "ConstraintLayout",
    ))
    val direct = runCatching { Move("direct", "Avatar", "Name", tree) }
    println("built=${direct.isSuccess}")
    println(direct.exceptionOrNull()?.message)
    val viaParent = listOf(
        Move("up-path", "Avatar", "ConstraintLayout", tree),
        Move("down-path", "ConstraintLayout", "Name", tree),
    )
    for (move in viaParent) {
        move.describe()
    }
}
```

```text
built=false
unrelated: Avatar and Name
up-path: from=Avatar | to=ConstraintLayout -> up
down-path: from=ConstraintLayout | to=Name -> down
```

Both ends of the `up-path` and `down-path` lines are written by hand: these lines are written to show the rule; they are not measurements. The finding that a sibling constraint is two moves rests on the bounding measurement below, whose ends come from nodes the model placed.

The direct move could not be set up, and the core's own message `unrelated: Avatar and Name` was printed. The same information was carried through the parent in two moves: up and down. In the tree, a sibling constraint is the sum of these two moves.

#### The Bounding Measurement

The lesson's direction is chosen from the move that makes a sibling constraint possible. The constraint and position moves in the downward direction were measured in the previous lesson; what is new in this lesson is the anchored sibling's position going up to the parent. The measurement below splits a link into its moves and counts the two moves put in place of the direct path.

```kotlin
// Main.kt — the moves of a sibling constraint
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App", "ConstraintLayout" to "Screen",
        "Avatar" to "ConstraintLayout", "Name" to "ConstraintLayout",
    ))
    val placements = solveAnchors(listOf(Item("Avatar", 48, null, 0), Item("Name", 120, "Avatar", 8)))
    val moves = listOf(
        Move("report-position", placements[0].name, "ConstraintLayout", tree),
        Move("give-position", "ConstraintLayout", placements[1].name, tree),
    )
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
report-position: from=Avatar | to=ConstraintLayout -> up
give-position: from=ConstraintLayout | to=Name -> down
down: 1
up: 1
deep: 0
outside: 0
```

A sibling constraint is two moves: one up, one down. The down move is the position-giving already present in every parent-child relationship. What is specific to constraint-based layout is the up move, because this layout's ability to anchor to a sibling works only with this move. The lesson's direction: **up**.

**LC3.** The model splits a link into two moves. Whether the platform actually merges these two steps into a single solving pass was not measured.

#### Summary

- This lesson's direction: up. What is new in this lesson is the position of the bound sibling going up to the parent.
- In a constraint-based layout an item's position can be anchored to another item; in the tree this means the two items are siblings.
- A direct move from sibling to sibling cannot be set up; the core rejects it with an `unrelated` error.
- A sibling constraint is two moves: the sibling's position goes up to the parent, and from the parent down to the anchored item.
- A guideline sits at the same level as the items, and for it too the position information flows from the parent.

#### Next Step

Constraint-based layout assumes that all the items are visible together. In a list holding thousands of items this assumption breaks: for items that are not on screen, there is no node in the tree either. The next lesson measures the lazy list, in which only the visible items are created, and the scroll position's travel out to its owner.

### Lazy Lists

Source: https://academia.sh/en/courses/compose/lazy-lists

Measuring how only visible rows enter the UI tree, how the scroll position travels up to its owner, and how data sent to a row that is not in the tree stays outside.

In the previous lesson all the items were in the tree, and one item could be anchored to another only through the parent. In a list of rows this assumption gets expensive: building a cart of a thousand rows with a thousand nodes means measuring and placing a thousand rows even while five rows are visible on screen. A lazy list does not do this. It creates nodes only for the rows in the visible range; as the user scrolls, the range shifts, the node of a row leaving the screen drops out of the tree, and a new one is built for each row entering.

This directly affects the tree measurement. Data cannot be sent to a node that is not in the tree. This lesson measures how a row's being or not being in the tree changes the direction of the data going to it. In the same lesson, a value that travels the opposite way is also measured: how does the list's scroll position reach the screen that uses the list?

```kotlin
// 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 }}")
    }
}
```

#### The Lazy List and the Item

`LazyColumn` is the vertical lazy list; its horizontal counterpart is `LazyRow`. Rows are defined with `items`: you give a collection and a composable function for each item. Where column and row layouts measure their children all at once, a lazy list limits measuring to the visible range. The scroll position is held in a state object (`LazyListState`); the composable that creates and remembers this object can read the position.

```kotlin
// Platform code: a lazy list, its items and the scroll state
@Composable
fun CartList(products: List<Product>) {
    val listState = rememberLazyListState()
    LazyColumn(state = listState) {
        items(products, key = { it.id }) { product ->
            ProductRow(product)
        }
    }
    val first = listState.firstVisibleItemIndex
}
```

Here `CartList` owns `listState`: it creates and remembers the state. `LazyColumn` takes this state as a parameter and updates it as the scroll changes. The rows are created in the `items` lambda, so in the tree they are children of `LazyColumn`. `CartList` is the parent of `LazyColumn`.

The model captures this platform rule. The model is not the platform itself. The helper file does two things: from the total number of rows, the index of the first visible row and the number of rows that fit on screen, it derives the visible range; then it builds a UI tree from the rows in that range. The tree carries `LazyList` under `Screen`, and under it only the visible rows. Two functions also supply the ends of the measurements: `rowName` produces the node name of a row, and `scrollOwner` reads the list's parent from the tree. In this lesson the ends of a move are not typed by hand; at least one end comes from these functions.

```kotlin
// Window.kt — the visible rows of a lazy list (the model's calculation)
fun visibleRows(total: Int, firstVisible: Int, viewportRows: Int): List<String> =
    (firstVisible until minOf(total, firstVisible + viewportRows)).map { rowName(it) }

fun rowName(index: Int): String = "Row$index"

fun scrollOwner(tree: Tree): String = tree.parent("LazyList") ?: error("list is the root")

fun listTree(rows: List<String>): Tree {
    val parentOf = mutableMapOf("Screen" to "App", "LazyList" to "Screen")
    for (row in rows) {
        parentOf[row] = "LazyList"
    }
    return Tree(parentOf)
}
```

#### The Visible Range

The first measurement takes a collection of a thousand rows. Five rows fit on screen and the list is at the very top. It prints how many rows are in the tree and the direction of the data going to the first visible row.

```kotlin
// Main.kt — the rows in the tree for a thousand-row list
fun main() {
    val rows = visibleRows(1000, 0, 5)
    val tree = listTree(rows)
    println("rows in tree: ${rows.size}")
    println("rows: ${rows.joinToString(",")}")
    val moves = listOf(Move("data-first-row", "LazyList", rows.first(), tree))
    for (move in moves) {
        move.describe()
    }
}
```

```text
rows in tree: 5
rows: Row0,Row1,Row2,Row3,Row4
data-first-row: from=LazyList | to=Row0 -> down
```

Five of the thousand rows are in the tree. The data the list gave the first row went from parent to child, that is, down. This is the lazy-list form of the same move as the column child in the earlier lessons.

**LC1.** The model takes the number of rows that fit on screen as fixed and does not account for row heights. The platform's ability to prepare rows just beyond the visible range ahead of time is also not in the model.

#### The Scroll Position Goes Out to the Owner

When the user scrolls, the first visible row changes. The list produces this information: the list, which handles the scroll gesture, writes the new position into the state object. If the state's owner is `Screen` or a composable under it, the position has traveled from the list to its owner. The list is under its owner, that is, the owner is the list's ancestor; the move goes from a child to an ancestor: up.

The measurement below scrolls the list by three rows. The range shifts to `Row3` through `Row7`; `Row0` leaves the tree, and `Row5` through `Row7` are new rows that were not there before. The scroll position goes to `Screen`.

```kotlin
// Main.kt — the range after scrolling and the position's travel out to the owner
fun main() {
    val before = visibleRows(1000, 0, 5)
    val after = visibleRows(1000, 3, 5)
    val tree = listTree(after)
    println("before: ${before.joinToString(",")}")
    println("after: ${after.joinToString(",")}")
    println("first visible index: 3")
    val moves = listOf(Move("scroll-position", "LazyList", scrollOwner(tree), tree))
    for (move in moves) {
        move.describe()
    }
}
```

```text
before: Row0,Row1,Row2,Row3,Row4
after: Row3,Row4,Row5,Row6,Row7
first visible index: 3
scroll-position: from=LazyList | to=Screen -> up
```

The range stayed five rows, but its content changed: `Row0`, `Row1` and `Row2` left the tree; `Row5`, `Row6` and `Row7` entered. The scroll position went up from `LazyList` to `Screen`. This move comes not from the user's finger but from the list's own state; this is how the screen can read information such as "the user is nearing the fifth row".

**LC2.** The model carries the scroll position as a single integer, the index of the first visible row. Whether the platform also keeps the scroll offset within a row separately was not measured in this lesson.

#### The Key Is the Identity

The state of a row that drops out of the tree and enters again has to be preserved by which row it is. The `key` given to the `items` call determines the row's identity. If no key is given, the platform derives the identity from the row's position; adding a new item to the start of the list shifts every row's position by one, and position-bound state passes to the wrong row. If a key is given, the state moves with the item. A key must be unique in the list.

This section contains no measurement. Separating identity from position is not a question of direction but of matching: did the same data pass from one row to another, or stay in the same row? Such matching concerns the node's identity, not the tree relationship. The direction, which is this lesson's axis, does not change even if the key changes: data still goes to the row from the list.

#### A Row That Is Not in the Tree

After scrolling, `Row0` is not in the tree. The list still holds the data belonging to `Row0`, because the collection is kept outside the list. But there is no node to receive that data. The core classifies every move in which one end is not in the tree as outside.

```kotlin
// Main.kt — the same data to a visible and a non-visible row
fun main() {
    val tree = listTree(visibleRows(1000, 3, 5))
    val moves = listOf(
        Move("data-row-3", "LazyList", rowName(3), tree),
        Move("data-row-7", "LazyList", rowName(7), tree),
        Move("data-row-0", "LazyList", rowName(0), tree),
    )
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
data-row-3: from=LazyList | to=Row3 -> down
data-row-7: from=LazyList | to=Row7 -> down
data-row-0: from=LazyList | to=Row0 -> outside
down: 2
up: 0
deep: 0
outside: 1
```

Data of the same shape from the same source went down to the two visible rows and went outside to the row that is not in the tree. What changes the direction is not the data's content or source but whether the target is in the tree. That is why it is not possible to read the state of an off-screen row or to write a change straight to it. Everything about that row is given again from the collection the list holds when the row re-enters the tree.

**LC3.** The model does not build what happens to the state of a row that drops out of the tree. Whether the values the row itself remembers are lost when it drops out, and how savable state changes this, were not measured in this lesson.

#### The Bounding Measurement

Which of the data at the two ends of the list is specific to the lesson's subject decides the lesson's direction. Item data going to a visible row is the same as the column child in the earlier lessons: down. Two things are specific to the lazy list: data going to a row that is not in the tree staying outside, and the scroll position going out to its owner. The measurement below brings them together.

```kotlin
// Main.kt — the direction tally of the list's four moves
fun main() {
    val rows = visibleRows(1000, 3, 5)
    val tree = listTree(rows)
    val moves = listOf(
        Move("data-first-row", "LazyList", rows.first(), tree),
        Move("data-last-row", "LazyList", rows.last(), tree),
        Move("data-invisible", "LazyList", rowName(0), tree),
        Move("scroll-position", "LazyList", scrollOwner(tree), tree),
    )
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
data-first-row: from=LazyList | to=Row3 -> down
data-last-row: from=LazyList | to=Row7 -> down
data-invisible: from=LazyList | to=Row0 -> outside
scroll-position: from=LazyList | to=Screen -> up
down: 2
up: 1
deep: 0
outside: 1
```

Two down, one up, one outside. The two moves that go down are the ordinary data a parent gives its child. The outside move is the price of the list being lazy. The up move is the information the lazy list carries for the screen: the position. The list works anyway to give data to the rows; for the screen to be able to follow the list, the position has to go up. The lesson's direction: **up**.

**LC4.** The model sets up four moves for a single scroll position. As the position changes, the move counts change too; this measurement holds only when `firstVisible` is 3.

#### Summary

- This lesson's direction: up. The move specific to a lazy list is the scroll position going from the list up to the owner of the state.
- A lazy list creates nodes only for the rows in the visible range; the number of rows in the tree is smaller than the total.
- The data the list gives a visible row goes down.
- The scroll position goes from the list to the state's owner, that is, an ancestor: up.
- Data going to a row that is not in the tree is outside; it waits until the row enters the tree.

#### Next Step

A lazy list creates its rows itself, but a composable determines the content of a row. Prebuilt UI components provide this content with ready-made parts, and these parts give their content their own colors. The next lesson measures the direction of the color a button gives its content.

### Prebuilt UI Components

Source: https://academia.sh/en/courses/compose/prebuilt-ui-components

Measuring which direction the color a prebuilt component such as a button gives its content travels in the tree, and how wrapping the content in a layout changes that direction.

A design system offers the parts that are needed often ready-made: button, card, text field, switch, checkbox. A developer who uses these parts does not rewrite the color, the corner rounding or the touch behavior. In return there is something to ask: where do the icon and text the developer puts inside a button get their color? If the button's background is dark, the text inside it must be light; this decision is made somewhere and reaches the content along some path.

The question on this lesson's measuring axis is the direction of that path. The button supplies a color to its content. If the content is directly under the button, the color goes down one level. If the content is wrapped in a layout, the color goes down skipping the intervening node. The direction carries different names in these two cases.

```kotlin
// 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 }}")
    }
}
```

#### The Button and the Content Slot

A prebuilt button takes a click function and a content slot. The slot lets the caller decide what will sit inside the button: just a text, just an icon, or both. The button places this slot into its own child tree and supplies the content color that the items in the slot will read.

```kotlin
// Platform code: a prebuilt button and its content slot
@Composable
fun SaveButton(onSave: () -> Unit) {
    Button(onClick = onSave) {
        Row {
            Icon(Icons.Default.Check, contentDescription = null)
            Text("Save")
        }
    }
}
```

The `Row` here is written inside the slot, so it is a child of the button; `Icon` and `Text` are children of the `Row`. The button supplies the content color through a composition local (`LocalContentColor`). `Icon` and `Text` read the color from this local instead of taking it as a parameter. The details of the local are in the theme lesson; this lesson measures only the direction of the color's move.

The model captures this platform rule; the model is not the platform itself. The helper file below builds the button's tree in two forms: the content directly under the button, or inside a `Row`. It also contains a small rule that picks the content color from the button's background color, and it says which items read the color.

```kotlin
// Button.kt — the button tree and the content color (the model's calculation)
fun buttonTree(content: List<String>, wrapped: Boolean): Tree {
    val parentOf = mutableMapOf("Screen" to "App", "Button" to "Screen")
    val holder = if (wrapped) "Row" else "Button"
    if (wrapped) {
        parentOf["Row"] = "Button"
    }
    for (child in content) {
        parentOf[child] = holder
    }
    return Tree(parentOf)
}

fun contentColorFor(container: String): String =
    if (container == "dark") "light" else "dark"

fun colorReaders(content: List<String>): List<String> =
    content.filter { it.startsWith("Icon") || it.startsWith("Text") }
```

#### Wrapped Content

In the first measurement the content is wrapped in a `Row`. The button carries a dark background; the content color is chosen as light, and it goes from the button to every item that reads the color.

```kotlin
// Main.kt — content wrapped in a Row
fun main() {
    val content = listOf("Icon", "Text")
    val tree = buttonTree(content, wrapped = true)
    println("content color: ${contentColorFor("dark")}")
    val moves = colorReaders(content).map { Move("color-${it.lowercase()}", "Button", it, tree) }
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
content color: light
color-icon: from=Button | to=Icon -> deep
color-text: from=Button | to=Text -> deep
down: 0
up: 0
deep: 2
outside: 0
```

Both moves are deep. The color left the button; because the target's parent is the `Row` and not the button, the move did not arrive at a direct child but descended skipping a node. The `Row` that steps in does nothing with the color: it neither reads it nor changes it. The color passes by it.

**LC1.** The model takes the button's background color as one of two values and picks the content color from that pair. The platform's real color calculation, the contrast ratio and the faded color of a disabled button are not in the model.

#### Unwrapped Content

In the second measurement the same two items are directly under the button. The button gives the same color, and the same items read the same color. The only thing that changes is the absence of the `Row` in between.

```kotlin
// Main.kt — content directly underneath
fun main() {
    val content = listOf("Icon", "Text")
    val tree = buttonTree(content, wrapped = false)
    val moves = colorReaders(content).map { Move("color-${it.lowercase()}", "Button", it, tree) }
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
color-icon: from=Button | to=Icon -> down
color-text: from=Button | to=Text -> down
down: 2
up: 0
deep: 0
outside: 0
```

This time both moves are down. The color is the same, the source is the same, the target is the same; the direction changed depending on the presence of the node in between. Because the direction of a move is not a property of the data itself, when reading code it is necessary to say "this color goes down in this tree" instead of "this color goes down".

**LC2.** The model compares only two tree forms. The button being wrapped several levels, the wrapper supplying its own local value, or the content being taken out of the slot were not measured in the model.

#### The Bounding Measurement

The lesson's direction can be seen by putting the moves in the two trees side by side. The measurement below contains four moves: the background color from the screen to the button, then the content color from the button to the icon for the two wrapping forms.

```kotlin
// Main.kt — the two trees side by side
fun main() {
    val content = listOf("Icon", "Text")
    val wrappedTree = buttonTree(content, wrapped = true)
    val plainTree = buttonTree(content, wrapped = false)
    val moves = listOf(
        Move("background-color", "Screen", "Button", plainTree),
        Move("wrapped-icon", "Button", "Icon", wrappedTree),
        Move("wrapped-text", "Button", "Text", wrappedTree),
        Move("unwrapped-icon", "Button", "Icon", plainTree),
    )
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
background-color: from=Screen | to=Button -> down
wrapped-icon: from=Button | to=Icon -> deep
wrapped-text: from=Button | to=Text -> deep
unwrapped-icon: from=Button | to=Icon -> down
down: 2
up: 0
deep: 2
outside: 0
```

Both ends of all four lines in this block are written by hand: these lines are written to show the two trees side by side; they are not measurements. The lesson's deep direction rests on the earlier measurement, whose ends come from the `colorReaders` function.

Two down, two deep. The background color going from the screen to the button goes to a direct child: down. The content color going to the same item is deep if there is a layout in between, down if there is not. This is the interesting thing about prebuilt UI components: the button gives its content the color, but it does not know at what depth the content stands. Whatever the caller writes into the slot, the color reaches it. The deep direction is the kind of move that makes this possible.

So arranging the content of a prebuilt UI component is safe for the color; wrapping the content in a `Row` or a `Box` does not cut the color. As long as the color is supplied by an ancestor along the tree, it reaches the deep item. The lesson's direction: **deep**.

**LC3.** The model assumes that only the items named `Icon` and `Text` read the color. What a custom item that does its own drawing must do to read the color was not measured in this lesson.

#### Summary

- This lesson's direction: deep. When a layout sits between the button and its content, the color skips the node in between.
- A prebuilt UI component offers shared behavior and appearance ready-made; the caller decides the content through a slot.
- The button gives its content the color; if the content is directly under the button, the color goes down.
- If the content is wrapped in a layout, the same color goes deep, skipping the node in between.
- The direction is a property of the tree, not of the data: the same color carries two different directions in two trees.

#### Next Step

The button gave its content a color; the scaffold gives its content padding. The next lesson measures the direction of the padding the scaffold, which holds the screen's top bar, bottom bar and content together, gives its content.

### Scaffold Structure

Source: https://academia.sh/en/courses/compose/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.

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.

```kotlin
// 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.

```kotlin
// 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.

```kotlin
// 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.

```kotlin
// 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()
}
```

```text
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.

```kotlin
// 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)
}
```

```text
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.

```kotlin
// 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)
}
```

```text
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.

### Navigation and Routes

Source: https://academia.sh/en/courses/compose/navigation-and-routes

Measuring that a destination chosen by a route is not in the UI tree until navigation finishes, so the argument sent to it goes outside first and then down, and the result returned goes outside.

Until now both ends of every measured move were in the tree. Navigation changes this. When the user moves from a list to a product detail, the detail screen is not in the tree before the transition. The product number to be carried to it, however, is known at the start of the transition. The data exists before its destination.

This lesson measures how the navigation host (`NavHost`) closes this gap. The route selects the destination; the destination enters the tree; the argument arrives then. The subject of the lesson is that the same argument carries two different directions at two moments.

```kotlin
// 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 }}")
    }
}
```

#### Route, Destination and Back Stack

The navigation setup consists of three parts. The navigation controller (`NavController`) keeps which destination you are at and the back stack. The navigation host (`NavHost`) stands in the tree and draws the content of the current destination as its child. A route is the text address of a destination; the argument is carried inside the route: `detail/42` carries the argument `42` to the `detail` destination.

```kotlin
// Platform code: a navigation host, routes and a destination with an argument
@Composable
fun AppNavigation(navController: NavHostController) {
    NavHost(navController, startDestination = "list") {
        composable("list") {
            ProductList(onOpen = { id -> navController.navigate("detail/$id") })
        }
        composable("detail/{id}") { entry ->
            ProductDetail(entry.arguments?.getString("id"))
        }
    }
}
```

When `navigate("detail/42")` is called, the platform parses the route, finds the matching destination, adds a new entry to the back stack and changes the host's content. The destination's content enters the tree only after these steps. Until that moment `ProductDetail` has not been drawn anywhere.

The model captures this platform rule; the model is not the platform itself. The helper file parses the route and returns a destination name, and it builds a tree in which a destination sits under the navigation host.

```kotlin
// Routes.kt — route parsing and destination selection (the model's calculation)
class Route(val destination: String, val argument: String)

fun parseRoute(route: String): Route {
    val parts = route.split("/")
    return Route(parts[0], parts.getOrElse(1) { "" })
}

fun routeTarget(route: String, graph: List<String>): String? {
    val destination = parseRoute(route).destination
    return graph.firstOrNull { it.lowercase() == destination }
}

fun navTree(current: String): Tree =
    Tree(mapOf("Screen" to "App", "NavHost" to "Screen", current to "NavHost"))
```

#### Before Navigation

In the first measurement the list destination is drawn in the tree. The user wants to go to the route `detail/42`. The model parses the route and selects the destination; but the tree is still the tree from before navigation.

```kotlin
// Main.kt — the argument sent to the destination before navigation starts
fun main() {
    val graph = listOf("List", "Detail")
    val route = "detail/42"
    val target = routeTarget(route, graph) ?: return
    println("target: $target")
    println("argument: ${parseRoute(route).argument}")
    val before = navTree("List")
    Move("argument", "NavHost", target, before).describe()
}
```

```text
target: Detail
argument: 42
argument: from=NavHost | to=Detail -> outside
```

The destination `Detail` was selected and the argument `42` was parsed; but because `Detail` is not in the tree, the move came out outside. The data is ready, the receiver is not. In this interval the platform keeps the argument in the entry on the back stack; when drawing starts, the destination reads it from there.

**LC1.** The model carries at most one argument in a route and leaves it as text. The platform's argument types, default values and parsing of query parameters in the route are not in the model.

#### After Navigation

In the second measurement navigation is complete: there is now a `Detail` under the host. The same argument goes from the same host to the same destination.

```kotlin
// Main.kt — the argument after navigation finishes
fun main() {
    val graph = listOf("List", "Detail")
    val target = routeTarget("detail/42", graph) ?: return
    val after = navTree(target)
    Move("argument", "NavHost", target, after).describe()
}
```

```text
argument: from=NavHost | to=Detail -> down
```

The same two ends, down this time. Because the destination entered the tree, it is a direct child of the host. Only the tree changed: the data, the source and the target are the same.

**LC2.** The model builds navigation as a single step, as the tree passing from its before to its after in one instant. The old and new destinations being in the tree at the same time during the platform's transition animation is not in the model.

#### The Result Coming Back

Navigation is not one-way. When the user returns from the detail, the detail screen leaves the tree and the list is drawn again. If the detail screen left a result, for example that the product was added to the cart, this result reaches the list through the entry on the back stack. At the moment the result is written the detail screen is still in the tree, but when the list reads it the detail is long gone.

```kotlin
// Main.kt — the result the detail left on the way back
fun main() {
    val back = navTree("List")
    Move("result", "Detail", "List", back).describe()
}
```

```text
result: from=Detail | to=List -> outside
```

Both ends of this line are written by hand: this line is written to show the rule; it is not a measurement. That the returned result is outside also rests on the `result` line in the bounding measurement, whose sender end is the output of `routeTarget`.

Outside. When the return is complete, the sending node is not in the tree. The result is carried not over the tree but over the entry the navigation controller keeps. The argument is outside on the way forward, the result on the way back; both depend on an end that is not in the tree on one side of the transition.

**LC3.** The model measures the result only from the moment of return. The moment the result is written while the detail screen is still in the tree was not built as a separate move in the model.

#### The Bounding Measurement

The lesson's direction comes from how the argument is split before and after navigation. The measurement below shows four moves together: the start route the screen gives the navigation host, the argument before navigation, the argument after navigation and the result coming back.

```kotlin
// Main.kt — the four moves of navigation
fun main() {
    val graph = listOf("List", "Detail")
    val target = routeTarget("detail/42", graph) ?: return
    val before = navTree("List")
    val after = navTree(target)
    val moves = listOf(
        Move("start-route", "Screen", "NavHost", before),
        Move("argument-before", "NavHost", target, before),
        Move("argument-after", "NavHost", target, after),
        Move("result", target, "List", before),
    )
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
start-route: from=Screen | to=NavHost -> down
argument-before: from=NavHost | to=Detail -> outside
argument-after: from=NavHost | to=Detail -> down
result: from=Detail | to=List -> outside
down: 2
up: 0
deep: 0
outside: 2
```

Both ends of the `start-route` line are written by hand: this line is written to show the rule; it is not a measurement. One end of each of the three lines that count as measurements is the output of `routeTarget`: down 1, outside 2.

Two down, two outside. The start route and the argument after navigation went down because they are between two connected nodes in the tree. The argument before navigation and the returning result stayed outside because one end is not in the tree. Navigation exists to manage the two moves outside the tree; the direction depends on at which moment the move is measured. The lesson's direction: **outside**.

**LC4.** The model takes the four moves from two moments of the tree. The time that passes between these moments on the platform, which entry the argument rests in, or what is preserved when the process restarts are not measured in this lesson.

#### Summary

- This lesson's direction: outside. The argument sent before navigation and the returned result are tied to an end that is not in the tree.
- A route is the text address of a destination; the argument is carried inside the route.
- Before navigation finishes, the destination is not in the tree, so the argument going to it is outside.
- Once the destination enters the tree, the same argument goes down to the host's child.
- The returning result is outside because it is read after the sender has left the tree.

#### Next Step

Navigation selects which content will be drawn, while the theme determines how every drawn piece of content will look. The next lesson measures how a theme chosen at the root descends all the way to the deep text, and how the direction changes when a subtree provides its own theme.

### Theme and Typography

Source: https://academia.sh/en/courses/compose/theme-and-typography

Measuring how a theme provided at the root descends to the deep text in the tree, how the closest provider changes when a subtree provides its own theme, and how the direction changes with it.

An application's colors, font sizes and corner roundings are chosen in one place and used on every screen. Giving these values to every component function as parameters is possible, but as the tree gets deeper the parameter chain gets longer, and every function in between carries a parameter only to pass it on. Design systems solve this differently: the value is provided at one point of the tree and everyone below reads it.

This lesson measures the direction of that solution. The theme is provided at the root and descends to the deep text; no function in between carries it. When a subtree provides its own theme, the closest provider changes for the reader, and so does the node the move leaves from.

```kotlin
// 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 }}")
    }
}
```

#### The Theme Descends from a Provider

The theme is provided by a wrapper function: this function sets up the values containing the color scheme, the typography and the shapes for the content below it. A text below reaches these values through the theme object. Under the access there is a mechanism called a composition local: a local takes a value at one node of the tree and every function below that node can read it.

```kotlin
// Platform code: a theme provider and a text reading the theme
@Composable
fun AppScreen() {
    MaterialTheme(colorScheme = lightColorScheme()) {
        Header {
            Text(
                text = "Orders",
                color = MaterialTheme.colorScheme.primary,
                style = MaterialTheme.typography.titleLarge,
            )
        }
    }
}
```

`Text` does not take the color and the text style from `AppScreen` as parameters; it reads them from the theme. The `Header` in between knows nothing about the theme and does not pass it on. The local carries the value to those below on its own.

The model captures this platform rule; the model is not the platform itself. The helper file does two things: it builds a screen's tree and it picks the closest theme provider for a reader among its ancestors.

```kotlin
// Theme.kt — the tree and the closest provider (the model's calculation)
fun themeTree(): Tree = Tree(
    mapOf(
        "Screen" to "App",
        "Header" to "Screen",
        "Title" to "Header",
        "Panel" to "Screen",
        "Label" to "Panel",
    )
)

fun closestTheme(tree: Tree, providers: List<String>, reader: String): String =
    tree.ancestors(reader).first { it in providers }
```

The `ancestors` list is ordered from the reader toward the root; the first matching provider is the closest one.

#### From the Root to the Deep

In the first measurement only the screen root provides the theme. There are two readers: the title text inside `Header` and the label inside `Panel`. Both take the theme from the closest provider, that is, from the root.

```kotlin
// Main.kt — theme moves with a single provider
fun main() {
    val tree = themeTree()
    val providers = listOf("Screen")
    val moves = listOf("Title", "Label").map { reader ->
        Move("theme-$reader", closestTheme(tree, providers, reader), reader, tree)
    }
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
theme-Title: from=Screen | to=Title -> deep
theme-Label: from=Screen | to=Label -> deep
down: 0
up: 0
deep: 2
outside: 0
```

Both moves are deep. The theme left `Screen` and reached the text two levels below, skipping the nodes in between. No node in between knew the theme.

**LC1.** The model takes the providers as a list of names and does not carry the theme's content. Which color or which text style arrives is not in the model; only which node the value goes from and to is measured.

#### A Subtree Provides Its Own Theme

In the second measurement `Panel` provides its own theme; say it is a dark panel. Nothing changes for `Title`, but for `Label` the closest provider is now `Panel`.

```kotlin
// Main.kt — while Panel provides its own theme
fun main() {
    val tree = themeTree()
    val providers = listOf("Screen", "Panel")
    val moves = listOf("Title", "Label").map { reader ->
        Move("theme-$reader", closestTheme(tree, providers, reader), reader, tree)
    }
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
theme-Title: from=Screen | to=Title -> deep
theme-Label: from=Panel | to=Label -> down
down: 1
up: 0
deep: 1
outside: 0
```

For `Title` the move stayed the same: deep. For `Label` the starting point moved from the root to `Panel`, and since `Label` is a direct child of the panel, the move became down. The same reader, the same theme object; but when the provider changed, the direction changed. `Label` no longer gets the root's theme by any path: the provider that stepped in shadows it.

**LC2.** The model assumes that a reader reads from a single provider. Changing only part of a value on the panel (for example only the color) and leaving the rest to the root is not in the model.

#### The Bounding Measurement

The lesson's direction comes from the root giving the theme to the deep item; the panel providing for itself narrows this. The measurement below contains three moves: the theme the root gives the title, the theme the panel gives the label, and where the panel itself gets its theme.

```kotlin
// Main.kt — three moves while the panel provides its own theme
fun main() {
    val tree = themeTree()
    val providers = listOf("Screen", "Panel")
    val moves = listOf(
        Move("title", closestTheme(tree, providers, "Title"), "Title", tree),
        Move("label", closestTheme(tree, providers, "Label"), "Label", tree),
        Move("panel-source", "Screen", "Panel", tree),
    )
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
title: from=Screen | to=Title -> deep
label: from=Panel | to=Label -> down
panel-source: from=Screen | to=Panel -> down
down: 2
up: 0
deep: 1
outside: 0
```

Both ends of the `panel-source` line are written by hand: this line is written to show the rule; it is not a measurement. The provider end of the `title` and `label` lines, which count as measurements, comes from the `closestTheme` function: deep 1, down 1.

One deep, two down. The panel deriving the theme from the root and the panel giving it to the label both go to a direct child. The only deep move is the title's theme, which skips the `Header` node in between; this is the real job of the local mechanism. Giving a theme to a direct child would not need a local: a parameter would be enough. The local gains meaning when the number of nodes in between is unknown or changes. The lesson's direction: **deep**.

**LC3.** The model does not measure what happens when the theme changes. The functions that read a provider's value rerunning when it changes, and the cost of that, were not covered in this lesson.

#### Summary

- This lesson's direction: deep. The theme provided at the root reaches the title by skipping the `Header` node in between.
- The theme is provided at one point of the tree; every function below it can read it.
- A theme provided at the root reaches the deep text, skipping the nodes in between: deep.
- The reader's provider is the closest provider among its ancestors.
- If a subtree provides its own theme, the root is shadowed for the readers in that tree; a value going to a direct child goes down.

#### Next Step

The theme distributes values from a fixed point in the tree. An animation, on the other hand, is a value changing over time, and the change is driven by a clock outside the tree. The next lesson measures where the intermediate value coming from the frame clock arrives from in the tree, and in which direction the target value travels.

### Animations

Source: https://academia.sh/en/courses/compose/animations

Measuring that an animation carries two moves: the target value goes down from the owner of the state, while the per-frame intermediate value comes from the frame clock outside the tree and reaches the node with an outside move.

Say a panel's width goes from 0 to 100 when a button is pressed. The state changes in one step: the width must be 100. On the screen, though, the change shows up in a series of frames, and in every frame the width differs a little from the previous one. What produces these intermediate values is not the owner of the state, because the owner knows only the target.

This lesson separates the two pieces of data an animation carries. One is the target, and it comes from an ancestor in the tree. The other is the per-frame value, and it comes from a clock outside the tree. The same animation reaches the same node from two different directions.

```kotlin
// 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 }}")
    }
}
```

#### Target and Intermediate Value

State-based animations take a target value and walk the current value toward the target. The target is a piece of state, and the owner of the state is a component function in the tree. The platform's frame clock, on the other hand, runs outside the tree: every time the screen refreshes the clock advances one frame, and the animation computes the value in that frame.

```kotlin
// Platform code: an animation toward a target
@Composable
fun ExpandingPanel(expanded: Boolean) {
    val width by animateDpAsState(
        targetValue = if (expanded) 100.dp else 0.dp,
        label = "panelWidth",
    )
    Box(Modifier.width(width))
}
```

`expanded` is the state given to `ExpandingPanel` from outside, and it determines the target. `animateDpAsState` produces an intermediate value heading to that target. `Box` is measured again with this value in every frame. When the target changes the animation starts again; the frame clock goes on.

The model captures this platform rule; the model is not the platform itself. The helper file does two things: it computes the intermediate values the frame clock produces linearly, and it places an animated panel under the screen. The linear interpolation is the model's calculation; the platform's easing curve and duration setting are not here. Two functions also supply the ends of the measurements: `targetOwner` reads the panel's parent from the tree, and `clockSource` returns the name of the frame clock. In this lesson one end of a move is found from the tree (`targetOwner`); the other end is a source the model names that is not in the tree (`clockSource` returns a fixed name).

```kotlin
// Frames.kt — the intermediate values the frame clock produces (the model's calculation)
fun frameValues(from: Int, to: Int, steps: Int): List<Int> =
    (0..steps).map { from + (to - from) * it / steps }

fun animatedTree(): Tree = Tree(mapOf("Screen" to "App", "Panel" to "Screen"))

// the target's owner: the panel's parent, read from the tree
fun targetOwner(tree: Tree): String = tree.parent("Panel") ?: error("panel is the root")

// the source that produces the intermediate values: the frame clock outside the tree
fun clockSource(): String = "FrameClock"
```

#### The Target Goes Down

The first measurement follows the target. `Screen` knows whether the panel is open or closed and gives it the target width.

```kotlin
// Main.kt — the animation's target value
fun main() {
    val tree = animatedTree()
    println("target: ${frameValues(0, 100, 2).last()}")
    Move("target", targetOwner(tree), "Panel", tree).describe()
}
```

```text
target: 100
target: from=Screen | to=Panel -> down
```

The target went down from `Screen` to `Panel`. This is the same move as the state flow in earlier lessons: the owner is the ancestor, the consumer is the child. The presence of an animation does not change the target's direction.

**LC1.** The model carries the target as a single integer. The target being a color, a distance or an opacity, and the animation being written with a separate function for each type, are not in the model.

#### The Intermediate Value Comes from Outside

The second measurement follows the per-frame values. In every frame the panel gets a new width between 0 and 100. The frame clock that produces these values is not a node in the tree.

```kotlin
// Main.kt — the intermediate values coming from the frame clock
fun main() {
    val tree = animatedTree()
    val frames = frameValues(0, 100, 2)
    println("frames: ${frames.joinToString(",")}")
    val moves = frames.map { Move("frame-$it", clockSource(), "Panel", tree) }
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
frames: 0,50,100
frame-0: from=FrameClock | to=Panel -> outside
frame-50: from=FrameClock | to=Panel -> outside
frame-100: from=FrameClock | to=Panel -> outside
down: 0
up: 0
deep: 0
outside: 3
```

All three of the three frames are outside. Because the clock is not in the tree, one end of the move is outside the tree; unlike the target value, the intermediate value does not come from any ancestor in the tree. The panel takes each frame not through an ancestor but directly from the clock.

**LC2.** The model produces three frames and divides the intervals equally. The platform's number and duration of frames, the curves that slow down and speed up, and the target changing while the animation runs were not measured in the model.

#### Who Holds the Intermediate Value

The intermediate value being outside is a design consequence. The intermediate value changes in every frame; if its owner were an ancestor, every frame would cause everything under that ancestor to be recomputed. Instead the node reading the intermediate value follows the value directly from the clock, and only that node is redrawn. The target, on the other hand, changes rarely, and when it changes it is natural for it to descend through the tree. The difference in speed is the reason the two pieces of data come by different paths; this lesson does not measure speed, only the direction of the path.

#### The Bounding Measurement

An animation carrying two moves is the lesson's main finding. There is also a third move: when the animation finishes, a notification goes to the screen. The measurement below shows the three together.

```kotlin
// Main.kt — the moves an animation carries
fun main() {
    val tree = animatedTree()
    val frames = frameValues(0, 100, 2)
    val moves = listOf(
        Move("target", targetOwner(tree), "Panel", tree),
        Move("frame-${frames[1]}", clockSource(), "Panel", tree),
        Move("finish", "Panel", targetOwner(tree), tree),
    )
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
target: from=Screen | to=Panel -> down
frame-50: from=FrameClock | to=Panel -> outside
finish: from=Panel | to=Screen -> up
down: 1
up: 1
deep: 0
outside: 1
```

One down, one up, one outside. The target is a move inside the tree, the intermediate value is a move outside the tree, and the finish notification is again a move inside the tree but in the opposite direction. The lesson's direction is the move that is specific to the animation: the intermediate value. The target and the finish notification would be found in the same way on a screen without an animation. The intermediate value exists only in an animation, and it always comes from outside the tree. The lesson's direction: **outside**.

The visibility animation and the transition object have the same structure. The visibility animation wraps an item's entry into and exit from the tree with an animation; the transition object (`updateTransition`) derives several values from a single target state. In both, the target comes from the tree and the per-frame values from the clock.

**LC3.** The model builds the finish notification as a single move. In which frame the notification is triggered and what happens when the animation is interrupted were not measured in the model. The visibility animation and the transition object are explained in this lesson only with a source snippet and are not measured.

#### Summary

- This lesson's direction: outside. The frame clock that produces the intermediate value is not in the tree, so the move the lesson measures comes from outside the tree.
- An animation carries two pieces of data: the target value and the per-frame intermediate value.
- The target comes from the owner of the state and goes down to the child.
- The frame clock produces the intermediate value; because the clock is not in the tree, the move is outside.
- The notification that goes to the screen when the animation finishes goes up.

#### Next Step

An animation shows the screen to everyone with the same change. For someone using a screen reader, however, the screen is a separate semantics tree derived from the tree. The next lesson measures how the labels of the children merge into the ancestor in this semantics tree, and the direction of that.

### Accessibility

Source: https://academia.sh/en/courses/compose/accessibility

Measuring that the labels of children in the semantics tree go up to the merging ancestor, that the same direction holds for direct and indirect children, and that the merged label reaches the screen reader with an outside move.

A user who cannot see the screen uses the interface with a screen reader. The reader looks not at pixels but at the semantics tree the platform derives from the UI tree. This tree carries what each item is, what it says and what it can do. If a row has an icon, a product name and a price, the screen reader is expected to read a single sentence instead of passing through three separate stops: product name, price.

This merging is a move that has a direction on the tree. The labels of the children join the label of the merging ancestor. The measure of this lesson is the direction of that move.

```kotlin
// 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 }}")
    }
}
```

#### The Semantics Tree and Merging

Every UI item is a node in the semantics tree. A node can carry a content description, a role and actions. By default every node is presented as a separate stop. When an ancestor node wants to merge its subtree, the descriptions of its children join it and the screen reader reads the ancestor node as a single stop.

```kotlin
// Platform code: merging a row semantically
@Composable
fun CartRow(name: String, price: String) {
    Row(Modifier.semantics(mergeDescendants = true) {}) {
        Icon(Icons.Default.ShoppingCart, contentDescription = null)
        Text(name)
        Text(price)
    }
}
```

`mergeDescendants = true` joins the descriptions of the nodes under this row to the row. The icon is given `contentDescription = null`: reading a decorative item adds nothing. The screen reader reads the row at once as "product name, price". To drop the merging and give its own description in place of the whole subtree, `clearAndSetSemantics` is used. Prebuilt UI components pad the touch target up to a minimum size when they can take an action; on a hand-written touchable item the developer provides this size. Like the size and spacing relationship in the layout lesson, this size is a platform rule and is not measured in this lesson.

The model captures this platform rule; the model is not the platform itself. The helper file builds the tree of the merging row and computes which nodes carry labels and the text of the merged label. Two functions also supply the ends of the measurements: `screenReader` returns the name of the screen reader, and `mergeOwner` reads the row's parent from the tree. In this lesson one end of a move is found from the tree (`mergeOwner`); the other end is a source the model names that is not in the tree (`screenReader` returns a fixed name).

```kotlin
// Semantics.kt — the semantics tree and the merged label (the model's calculation)
fun semanticsTree(): Tree = Tree(
    mapOf(
        "Screen" to "App",
        "Row" to "Screen",
        "Icon" to "Row",
        "Title" to "Row",
        "Wrap" to "Row",
        "Price" to "Wrap",
    )
)

fun screenReader(): String = "ScreenReader"

fun mergeOwner(tree: Tree): String = tree.parent("Row") ?: error("row is the root")

fun labelHolders(labels: Map<String, String>): List<String> = labels.keys.sorted()

fun mergedDescription(labels: Map<String, String>): String =
    labelHolders(labels).joinToString(", ") { labels.getValue(it) }
```

#### Labels Go Up to the Ancestor

In the first measurement two nodes carry labels: the product name is a direct child of the row, while the price is under a wrapper that steps in between, that is, a grandchild of the row. The icon has no label because it is decorative.

```kotlin
// Main.kt — the merging ancestor of the nodes that carry labels
fun main() {
    val tree = semanticsTree()
    val labels = mapOf("Title" to "Pen", "Price" to "Cost")
    println("merged label: ${mergedDescription(labels)}")
    val moves = labelHolders(labels).map { Move("label-$it", it, "Row", tree) }
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
merged label: Cost, Pen
label-Price: from=Price | to=Row -> up
label-Title: from=Title | to=Row -> up
down: 0
up: 2
deep: 0
outside: 0
```

Both moves are up. The direct child's label went up one level, the grandchild's label two. The deep direction is only toward below; in going up to an ancestor it makes no difference whether it is a child or a grandchild, the move is up. The wrapper that steps in neither carries nor changes the label.

**LC1.** The model takes the labels as single words and joins them in name order. The order in which the platform reads labels, its adding role and action information, and the conditions of merging (for example a clickable node keeping its own boundary) are not in the model.

#### The Label Goes to the Screen Reader

The merged label does not stay in the row; it is passed to the screen reader. The screen reader is not a node of the UI tree. It is the platform's accessibility service and it reads the tree from outside.

```kotlin
// Main.kt — the merged label's trip to the screen reader
fun main() {
    val tree = semanticsTree()
    val labels = mapOf("Title" to "Pen", "Price" to "Cost")
    println("read: ${mergedDescription(labels)}")
    Move("reading", "Row", screenReader(), tree).describe()
}
```

```text
read: Cost, Pen
reading: from=Row | to=ScreenReader -> outside
```

Outside. Because the screen reader is not in the tree, one end of the move is outside the tree. The row does not give the label to the reader; the reader takes the label from the row. There is a chain of two separate directions: the labels go up in the tree, and the merged label waiting at the top of the tree is read from outside.

**LC2.** The model builds the reader as a name that is not in the tree. The reader's focus order, which node the user touches and when the label is read are not in the model.

#### The Bounding Measurement

The lesson's direction comes from the labels going up to the ancestor. The label reaching the reader is outside, while the opening of the merging by the ancestor is down: the screen gives the row the merge flag. The measurement below shows all four together.

```kotlin
// Main.kt — the accessibility moves
fun main() {
    val tree = semanticsTree()
    val labels = mapOf("Title" to "Pen", "Price" to "Cost")
    val moves = labelHolders(labels).map { Move("label-$it", it, "Row", tree) } + listOf(
        Move("merge-flag", mergeOwner(tree), "Row", tree),
        Move("reading", "Row", screenReader(), tree),
    )
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
label-Price: from=Price | to=Row -> up
label-Title: from=Title | to=Row -> up
merge-flag: from=Screen | to=Row -> down
reading: from=Row | to=ScreenReader -> outside
down: 1
up: 2
deep: 0
outside: 1
```

Two up, one down, one outside. The merge flag descends from the screen to the row; the labels go up to the row; the merged label reaches the reader from outside. The separation of directions overlaps the separation of functions: the decision is made from top to bottom, the information is gathered from bottom to top, and the reading takes place outside. The lesson's direction: **up**.

**LC3.** The model builds merging in a single row. A merging ancestor sitting inside another merging ancestor, and how nested merging would join the labels, were not measured in this lesson.

#### Summary

- This lesson's direction: up. Labels go up from children and grandchildren to the merging ancestor.
- The screen reader looks not at pixels but at the semantics tree derived from the UI tree.
- The merging ancestor gathers the labels of its children and grandchildren; the labels go up to the ancestor.
- The label of a direct child and of a grandchild go in the same direction, up.
- The merged label going to the screen reader is a move outside the tree, an outside move.

#### Next Step

The tree of this course is the tree of the declarative interface. Many applications, though, still carry screens written with the classic view system. The next lesson measures using the two worlds together and how the direction is named when data crosses the boundary of the tree.

### The Classic View System

Source: https://academia.sh/en/courses/compose/the-classic-view-system

Measuring the comparison of the classic view hierarchy with the declarative tree, the outside direction of data going to a wrapped view, and the move crossing the boundary of the two trees; the closing table of the course's seventeen lessons.

The previous lesson closed the course's tree with a reader outside the tree by carrying the merged label to the screen reader. The tree of this course is the tree of the declarative interface. Many applications, though, carry screens written with the classic view system: the interface is declared in an `xml` file, the views are built from the file at runtime, and the screen's code reaches a view by its id and writes values to it directly. The two worlds can stand together on the same screen. The question of this lesson is how the direction is named when a piece of data crosses the boundary of the two. The core file is the same as in the earlier lessons.

```kotlin
// 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 }}")
    }
}
```

#### The Classic Hierarchy

In the classic view system the screen is an `xml` file. The nesting in the file builds the view hierarchy: the outer item is the parent, the ones inside are the children. The screen's code does not feed the hierarchy downward with parameters; it writes directly to the view it finds by id.

```xml
<!-- Platform code: a classic view layout, res/layout/screen.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <Button android:id="@+id/pay"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </FrameLayout>
</LinearLayout>
```

The model takes the file's indentation as a node list and builds a tree from it. The tree is the same `Tree` class; only the way it is built differs. The ends of the moves are not written by hand in this lesson's measurements: they are picked from the built tree and the node list. `deepest` gives the deepest node in the list, `firstAt` gives the first node at a given indentation level; the screen node is read from the end of the tree's `ancestors` chain.

```kotlin
// Inflate.kt — builds a tree from an indented xml node list and picks nodes (the model's calculation)
fun inflate(root: String, nodes: List<Pair<Int, String>>): Tree {
    val parents = linkedMapOf<String, String>()
    val path = mutableListOf(root)
    for ((depth, name) in nodes) {
        while (path.size > depth) path.removeAt(path.size - 1)
        parents[name] = path.last()
        path.add(name)
    }
    return Tree(parents)
}

fun deepest(nodes: List<Pair<Int, String>>): String = nodes.maxByOrNull { it.first }!!.second
fun firstAt(depth: Int, nodes: List<Pair<Int, String>>): String = nodes.first { it.first == depth }.second
```

The first measurement builds the same screen in two forms and carries the same two pieces of data: the title's text and the button's click event. On the classic side the screen's code writes to the title, skipping the intermediate views. On the declarative side the text descends through the column.

```kotlin
// Main.kt — the same screen: the xml hierarchy and the declarative tree
fun main() {
    val classicNodes = listOf(1 to "LinearLayout", 2 to "Title", 2 to "Frame", 3 to "PayButton")
    val classic = inflate("Screen", classicNodes)
    val button = deepest(classicNodes)
    val screen = classic.ancestors(button).last()
    val declarative = Tree(mapOf("Screen" to "App", "Column" to "Screen", "Title" to "Column", "PayButton" to "Column"))
    val column = declarative.parent("Title")!!
    val declarativeScreen = declarative.parent(column)!!
    val classicMoves = listOf(
        Move("write-title", screen, firstAt(2, classicNodes), classic),
        Move("button-listener", button, screen, classic),
    )
    val declarativeMoves = listOf(
        Move("title-path-1", declarativeScreen, column, declarative),
        Move("title-path-2", column, "Title", declarative),
        Move("button-event", "PayButton", declarativeScreen, declarative),
    )
    println("classic:")
    classicMoves.forEach { it.describe() }
    countDirections(classicMoves)
    println("declarative:")
    declarativeMoves.forEach { it.describe() }
    countDirections(declarativeMoves)
}
```

```text
classic:
write-title: from=Screen | to=Title -> deep
button-listener: from=PayButton | to=Screen -> up
down: 0
up: 1
deep: 1
outside: 0
declarative:
title-path-1: from=Screen | to=Column -> down
title-path-2: from=Column | to=Title -> down
button-event: from=PayButton | to=Screen -> up
down: 2
up: 1
deep: 0
outside: 0
```

The classic screen's code goes to the title with a single move, deep. On the declarative screen the same text descends with two down moves. The button's event is up on both sides. The difference is in the channel: access by id skips the nodes in between, while the parameter chain passes through every node. The rule does not see the channel; it classifies the geometry of the two moves.

**LC6.** The model takes the xml file as an indented node list. Attributes, styles, the cost of looking up by id, and the pass in which views are measured and placed have no counterpart in the model.

#### A View Inside a Component Function

The way to put a classic view into a declarative tree is `AndroidView`. The component function asks for the view to be built, and then to be updated when it is built and when the state the update block reads changes.

```kotlin
// Platform code: putting a classic view inside a component function
@Composable
fun LabelHost(caption: String) {
    AndroidView(
        factory = { context -> TextView(context) },
        update = { view -> view.text = caption },
    )
}
```

`LabelHost` is a node in the tree. The `TextView` inside it is not: the declarative tree does not know it as a child; the platform's view hierarchy owns it. In the model the wrapped view is built as a name that is not in the tree.

```kotlin
// Bridge.kt — the wrapped view stays outside the tree (the model's calculation)
class ClassicHost(val node: String, val view: String, tree: Tree) {
    val viewInTree: Boolean = tree.contains(view)
}
```

The measurement follows the data to the wrapper, to the wrapped view and back from the view.

```kotlin
// Main.kt — data going to the wrapped view and returning from it
fun main() {
    val tree = Tree(mapOf("Screen" to "App", "Header" to "Screen", "LabelHost" to "Screen"))
    val host = ClassicHost("LabelHost", "LabelView", tree)
    println("wrapped view in tree: ${host.viewInTree}")
    val moves = listOf(
        Move("wrapper", "Screen", host.node, tree),
        Move("update", host.node, host.view, tree),
        Move("direct", "Screen", host.view, tree),
        Move("touch", host.view, host.node, tree),
    )
    moves.forEach { it.describe() }
    countDirections(moves)
}
```

```text
wrapped view in tree: false
wrapper: from=Screen | to=LabelHost -> down
update: from=LabelHost | to=LabelView -> outside
direct: from=Screen | to=LabelView -> outside
touch: from=LabelView | to=LabelHost -> outside
down: 1
up: 0
deep: 0
outside: 3
```

The data going to the wrapper is down: the parent gives to its child. Every move that reaches the wrapped view is outside, and the touch returning from the view is outside too. Going through the wrapper and going directly from the screen give the same result, because the path of a move toward an end that is not in the tree does not change the rule.

**LC7.** The model builds the wrapped view as a name that is not in the tree. The separation of `factory` and `update`, the recomposition in which the update runs, and the view's lifecycle are not in the model.

#### Declarative Content Inside a View

The reverse direction is possible too: a `ComposeView` is placed inside a classic hierarchy and declarative content is created inside it.

```kotlin
// Platform code: putting declarative content into a classic view hierarchy
val composeView = ComposeView(context).apply {
    setContent {
        Panel { Label(text = "Cart") }
    }
}
frame.addView(composeView)
```

In this case there are two trees. In the classic hierarchy `ComposeView` is a leaf. The `Panel` and `Label` inside it belong to a second, declarative tree. The measurement builds the two trees separately and classifies the move crossing the boundary from both sides.

```kotlin
// Main.kt — the boundary of the two trees
fun main() {
    val classicNodes = listOf(1 to "LinearLayout", 2 to "Frame", 3 to "ComposeView")
    val classic = inflate("Screen", classicNodes)
    val host = deepest(classicNodes)
    val screen = classic.ancestors(host).last()
    val inner = Tree(mapOf("Label" to "Panel"))
    val panel = inner.parent("Label")!!
    val moves = listOf(
        Move("layout-data", screen, host, classic),
        Move("boundary-classic", host, panel, classic),
        Move("boundary-declarative", host, panel, inner),
        Move("inside", panel, "Label", inner),
    )
    moves.forEach { it.describe() }
    countDirections(moves)
}
```

```text
layout-data: from=Screen | to=ComposeView -> deep
boundary-classic: from=ComposeView | to=Panel -> outside
boundary-declarative: from=ComposeView | to=Panel -> outside
inside: from=Panel | to=Label -> down
down: 1
up: 0
deep: 1
outside: 2
```

In the classic tree, data going from the screen to `ComposeView` is deep. The move crossing beyond the boundary, however, is outside whichever tree it is looked at from: the source or the target is not in the tree being looked at. An in-tree move exists only from `Panel` to `Label`, that is, down inside the second tree.

**LC8.** The model builds the two trees as two separate `Tree`s. `ComposeView`'s lifecycle being bound to its owner, the boundary of the recomposition scope, and how the two trees are integrated in a single window were not measured.

#### The Bounding Measurement

The lesson's direction comes from the name outside being given when an end is not in the tree. The rule does not see whether an item is classic or declarative; it sees which tree it is in. Let the same label be built in two forms: with a local component function and with a view wrapped by `AndroidView`. The wrapped view's own internal hierarchy can be measured as a separate tree.

```kotlin
// Main.kt — the same label in two forms
fun main() {
    val tree = Tree(mapOf("Screen" to "App", "Header" to "Screen", "Label" to "Screen", "LabelHost" to "Screen"))
    val wrappedNodes = listOf(1 to "Glyphs")
    val wrapped = inflate("LabelView", wrappedNodes)
    val glyphs = deepest(wrappedNodes)
    val labelView = wrapped.parent(glyphs)!!
    val screen = tree.parent("Label")!!
    val moves = listOf(
        Move("local-label", screen, "Label", tree),
        Move("wrapped-label", screen, labelView, tree),
        Move("wrapped-inside", labelView, glyphs, wrapped),
    )
    moves.forEach { it.describe() }
    countDirections(moves)
}
```

```text
local-label: from=Screen | to=Label -> down
wrapped-label: from=Screen | to=LabelView -> outside
wrapped-inside: from=LabelView | to=Glyphs -> down
down: 2
up: 0
deep: 0
outside: 1
```

The same label goes down when local and outside when in a wrapped view. There is a hierarchy inside the wrapped view too, and it is down in its own tree; but the declarative tree cannot see this hierarchy. The lesson's direction: **outside**.

This distinction is not to be confused with two other questions. The question of M31/K02 is how the platform meets a request; what is asked and what is given are not compared here. The question of M30/K04 is whether a screen agrees with the model; the screen's agreement is not asked anywhere here. Neither is made a category or a measure name: the measure is, from start to finish, the direction of the data in the tree.

**LC9.** The model builds the wrapped view's internal structure with a single child. The number of children of a real view, and the redraw and measurement order inside it, were not measured.

#### Summary

- This lesson's direction: outside. The wrapped view is an end that is not in the tree, and every move with an end outside the tree comes out outside even when the measurement is built from nodes picked out of the `inflate` return.
- The classic view hierarchy is built from an `xml` file; the screen's code reaches the deep view directly by its id, and this move is deep.
- A classic view placed in a component function is not in the declarative tree; every move to it and back from it is outside.
- Declarative content placed inside a classic hierarchy is a second tree; the move crossing the boundary is outside from both sides.
- The same label goes down when local and outside when in a wrapped view; the rule sees tree membership, not the item type.

#### Course Wrap-Up

The course measured seventeen lessons with a single question: in which direction does this data travel in the UI tree? Each lesson's measured move, direction and bounding measurement are below. The direction cells are taken from the source lesson's summary.

| Lesson | Measured move | Direction | Bounding measurement |
|---|---|---|---|
| Composable Functions | parameter and provider value | down | same provider: down to a direct child, deep to a grandchild |
| State and Remembering | value from the owner to the reader | down | local state enters no move |
| State Hoisting | event to the owner, value to the reader | up | lifted to App: down 3, up 2 |
| Recomposition | the change reaching the reader | deep | parameter two down, deferred read a single deep |
| Side Effects | request from the effect to the service | outside | inner write up, the response outside too |
| Immutability and Stability | change going to the owner | up | two channels same direction, observation different |
| Basic Layouts | constraint, size, position | down | size up, constraint and position down |
| Modifiers | constraint, order of the chain | down | when the order changes the constraint is deep |
| Constraint-Based Layout | sibling constraint | up | the sibling move has no relative, splits into two moves |
| Lazy Lists | scroll position | up | data going to an invisible row is outside |
| Prebuilt UI Components | content color | deep | down to a direct child, deep when wrapped |
| Scaffold Structure | padding | down | captured state skips the scaffold, deep |
| Navigation and Routes | argument before navigation | outside | down once in the tree, result back outside |
| Theme and Typography | theme from the root to the deep | deep | subtree theme down to a direct child |
| Animations | per-frame intermediate value | outside | target down, finish up |
| Accessibility | labels merging into the ancestor | up | merged label outside to the reader |
| The Classic View System | data going to a wrapped view | outside | same label down when local, outside when wrapped |

The distribution of the seventeen lessons: 5 down, 5 up, 3 deep, 4 outside. The core's 3/2/1/2 distribution is a separate count: the core counts the eight moves in the opening lesson, while the table counts the directions of seventeen lessons; the two are not the same thing.

The next course (M31/K04) takes layered architecture as its subject. This course's measuring axis does not continue there; K04 asks its own question itself.

