Lesson 2
Sibling reorder, `joinKey`, and moving state
Explain why keyed siblings move remembered state and how compound data keys are represented.
Status: State following a keyed sibling is Durable behavior.
joinKey,JoinedKey, and the exact reconciliation paths are Version-specific at the pinned revisions. The link-buffer path remains Experimental where noted by the implementation.
Outcome
After inserting, deleting, reversing, or shuffling siblings, you should be able to predict which remembered object follows each data item and explain the role of joinKey when a key has several parts.
The reorder problem is a matching problem
Suppose each row creates a remembered token:
for (item in items) {
Row {
val token = remember { Token() }
Text("${item.id}: ${token.id}")
}
}
With no explicit key, the runtime sees repeated work at the same call site. The first row’s remembered slot is paired with the first row on the next pass, the second with the second, and so on. If a new item is inserted at the front, every old row after it shifts. The composition can still be internally consistent while the token that used to belong to A is now printed beside X.
Wrap each item in key(item.id). The data key lets a movable group be found by its identity among the current siblings. The group, including its remembered slots and emitted nodes, can move to the new sibling position. AndroidX tests exercise this property by reversing and shuffling keyed content, and separately assert that duplicate keys produce duplicate composite hashes (CompoundHashKeyTests.kt).
What joinKey joins
A key call accepts a vararg. The compiler must turn key(region, itemId) into one data-key object that compares as a pair, not into a collision-prone string. At the pinned compiler revision, visitKeyCall collects the arguments and calls irJoinKeyChain; that helper reduces the arguments through Composer.joinKey (ComposableFunctionBodyTransformer.kt).
Conceptually, two arguments become:
joinKey(region, itemId) → JoinedKey(region, itemId)
For more arguments, the compiler builds a left-associated chain. JoinedKey stores left and right, combines their hash codes, and uses enum ordinals in this private helper (JoinedKey.kt). The runtime reuses a stored key before allocating a JoinedKey when needed (GapComposer.kt). This makes joinKey a compiler/runtime protocol, not an application utility.
Do not treat this as a guarantee of hash uniqueness; a hash is an implementation aid and can collide.
flowchart TD
Region[Region key] --> Join[joinKey]
Item[Item key] --> Join
Join --> Data[Compound data key]
CallSite[Keyed call site] --> Group[Movable group]
Data --> Group
Group --> State[Remembered state moves]A reproducible reorder trace
Use a list of records with stable ids and a RememberObserver token whose callbacks append to a log. Compose [A, B, C], then apply these mutations one at a time: insert X at index zero, reverse the list, and replace B with a new id. First omit key; then use key(item.id). Record item.id → token identity after each apply. The useful result is a table showing positional reuse first and identity-preserving movement second.
Misconceptions to remove
- “Keys are only for lazy lists.”
keyis a general composition grouping primitive. - “A key must be globally unique.” It must be unique among the relevant sibling invocations.
- “
currentCompositeKeyHashCodeis the key.” It is a hash view, not the full group match.
Check yourself
If two different parent groups each contain key(7), why can both be correct? What would make two key(7) calls unsafe in one parent?
Source notes
| Claim | Direct evidence | Status |
|---|---|---|
The compiler lowers vararg key arguments through a joinKey chain | Pinned ComposableFunctionBodyTransformer.kt | Version-specific |
JoinedKey represents two key parts and combines their hashes | Pinned JoinedKey.kt | Version-specific |
| Keyed content moves and duplicate keys are observable in tests | Pinned CompoundHashKeyTests.kt | Version-specific |
| The runtime can reuse a stored joined key before allocating one | Pinned GapComposer.kt | Version-specific |
Freshness
Refresh this lesson when compiler key lowering, joinKey, JoinedKey, or keyed reorder tests change. Compare the Android Review history for Composer.kt and check whether the link-buffer implementation is still experimental before generalizing storage behavior.
Finished this lesson?
Your progress stays only in this browser.