Lesson 8
Understand state and identity
Trace a state mutation into an in-place node update and predict when identity is retained, moved, or replaced.
Learning goal
Trace a state mutation into an in-place node update and predict when identity is retained, moved, or replaced.
Counter example
@Composable
@AtlasComposable
fun Counter(count: Int, onIncrement: () -> Unit) {
Group {
Text("Count: $count")
Button(label = "Increment", onActivate = onIncrement)
}
}
@Composable
@AtlasComposable
fun RememberingCounter() {
var count by remember { mutableStateOf(0) }
Counter(count = count, onIncrement = { count++ })
}
Button is developed fully after events are introduced; for now, focus on its state-writing callback.
Mutation pipeline
sequenceDiagram
participant Event
participant State
participant Recomposer
participant Composer
participant Changes as recorded changes
participant TextNode
participant Host
participant Renderer
participant Surface
Event->>State: count = count + 1
State->>Recomposer: applied changes reach its observer
Recomposer->>Composer: invalidate and recompose tracked scope
Composer->>Changes: record changed TextNode property
Recomposer->>Changes: apply committed change list
Changes->>TextNode: text = "Count: 1"
TextNode->>TextNode: invalidateLayout()
Host->>Renderer: render committed tree
Renderer->>Surface: submit later frameMore precisely, a write becomes relevant to the Recomposer when its snapshot is applied and apply observers are notified. Runtime then schedules compositions whose tracked reads were affected.
Positional identity
Compose identifies instances by compiler-generated call sites and surrounding groups. When one call site executes repeatedly, execution order distinguishes those instances unless key supplies identity.
flowchart TD
Parent[Group call site] --> P1[Position 1<br/>Text Count]
Parent --> P2[Position 2<br/>Button Increment]
P1 --> N1[retained TextNode]
P2 --> N2[retained ButtonNode]When count changes, position 1 still emits Text. Runtime can retain N1 and apply a property update.
This does not mean “the tree is never rebuilt.” Conditional content and changing collections legitimately insert, remove, or move nodes.
Identity in collections
Without explicit keys, repeated executions of the same call site are distinguished by execution order:
@Composable
@AtlasComposable
fun MessageList(messages: List<Message>) {
Group {
messages.forEach { message ->
Text(message.title)
}
}
}
Inserting at the front changes what every position means. Give each item domain identity:
import androidx.compose.runtime.key
@Composable
@AtlasComposable
fun MessageList(messages: List<Message>) {
Group {
messages.forEach { message ->
key(message.id) {
Text(message.title)
}
}
}
}
flowchart LR
subgraph Before
A1[key A] --> NA[node A]
B1[key B] --> NB[node B]
end
subgraph AfterInsert
X[key X] --> NX[new node X]
A2[key A] --> NA2[same node A]
B2[key B] --> NB2[same node B]
end
NA -. retained .-> NA2
NB -. retained .-> NB2Keys must be stable and unique among executions of the same key call site; combine values when one field is insufficient. Other key call sites may safely reuse the same values.
remember is composition memory
flowchart LR
CallPosition --> Slot[slot-table location]
Slot --> Remembered[remembered value]
CallPosition --> NodeIdentity[emitted node identity]remember stores values in the composition, not in the Atlas node. A node can retain visual properties while remember retains component-local state. They are coordinated by composition identity but remain different representations.
Property versus structural change
| App change | Typical Runtime result | Atlas consequence |
|---|---|---|
count changes | updater changes a property | existing text node becomes dirty |
if branch appears | node insertion | parent layout invalidated |
| branch disappears | node removal | detached node loses parent |
| keyed item reorders | node move | ordering changes; identity retained |
| key changes | remove + insert | old identity is discarded |
Observe identity in a test
val count = mutableStateOf(0)
host.setContent {
Counter(count = count.value, onIncrement = { count.value++ })
}
host.advanceUntilIdle()
val firstNode = root.findText("Count: 0")
count.value = 1
host.advanceUntilIdle()
val secondNode = root.findText("Count: 1")
assertSame(firstNode, secondNode)
advanceUntilIdle is an Atlas test-harness operation, not kotlinx.coroutines.test.advanceUntilIdle() or a Compose Runtime API. Its contract is to deliver pending snapshot apply notifications, provide required Compose frame-clock opportunities, await recomposition and apply, and—when frame output is asserted—await Atlas rendering.
Visual checkpoint
Draw keyed items A, B, and C. Insert X at the front, remove B, then swap A and C. Use arrows to show which node identities survive each operation.
Review questions
- What connects a state read to later invalidation?
- How does
rememberdiffer from a retained Atlas node? - When should a list item use
key?
Recap
State invalidates composition work.
Call sites, execution order, and keys determine identity.
Stable identity enables in-place node updates.
Structural changes remain normal and necessary.
Sources
Finished this lesson?
Your progress stays only in this browser.