Lesson 3
`remember` is a slot cache
Trace a remember call from key comparison to cached value, calculation, and slot update.
Status:
rememberas composition-local caching is Durable .Composer.cache, slot traversal, and the selected gap/link storage path are Version-specific . The link-buffer route is Experimental at this snapshot.
Outcome
You should be able to explain why a remember calculation runs once, when a keyed calculation runs again, and why the cache belongs to composition memory rather than to the Kotlin function.
The cache protocol is small
The public overloads in Composables.kt all end at currentComposer.cache.
remember { calculation }passesinvalid = false.remember(key) { calculation }callschanged(key)and passes that result.- Multiple keys combine their
changedresults; the vararg form also checks the key count.
Composer.changed compares the supplied value with the value from the previous composition using equality at the protocol boundary (Composer.kt). A changed key invalidates this cache entry. It does not by itself move the surrounding group; that is the job of key(...) and movable-group identity.
The generic cache implementation is direct:
val previous = rememberedValue()
return if (invalid || previous === Composer.Empty) {
val value = block()
updateRememberedValue(value)
value
} else {
previous
}
Composer.Empty means that no value is available at this slot, commonly during insertion. On a cache miss, the calculation runs and its result is scheduled into the current slot. On a hit, the calculation is skipped and the previous value is returned. The pinned Composer.cache implementation is the protocol-level evidence for this sequence (Composer.kt).
The gap-buffer composer makes the storage relationship visible: it reads the next cache slot, invokes the block when the slot is empty or invalid, and updates the slot through its change writer (GapComposer.kt). The root composition chooses gap-buffer or link-buffer storage from a runtime flag; that choice is not an application contract (Composition.kt).
remember(key) is not key { remember }
These forms answer different questions:
val connection = remember(server) { connect(server) }
key(server) {
val localState = remember { LocalState(server) }
}
The first keeps the same cache location and recalculates its value when server is unequal to the previous key. The second changes the identity of the surrounding group when server changes, so the old group can leave and a new group can enter. A real program may use both, but do not describe them as interchangeable “keys.”
flowchart TD
Call[remember call] --> Compare[Compare remember keys]
Compare --> Hit{Empty or invalid?}
Hit -->|No| Return[Return slot value]
Hit -->|Yes| Calculate[Run calculation]
Calculate --> Store[Update current slot]
Store --> ReturnReproduce cache behavior
Add a counter inside remember { Counter(++runs) } and a separate remember(input) { Counter(++runs) }. Invalidate the same restart scope without changing input; only the first composition should increment either counter. Change input; the keyed counter should increment while the unkeyed one should not. Then wrap the calls in key(input) and observe the lifecycle of an observer-valued result. This isolates cache invalidation from group replacement.
Check yourself
If a remember calculation returns the same object every time, can the calculation still be skipped? Which branch of the cache protocol decides?
Source notes
| Claim | Direct evidence | Status |
|---|---|---|
Public remember overloads convert keys into cache invalidation | Pinned Composables.kt | Durable
Version-specific |
cache reads, calculates on empty/invalid, and updates the value | Pinned Composer.kt | Durable
Version-specific |
| Gap-buffer cache reads and writes the next slot | Pinned GapComposer.kt | Version-specific |
| Composition selects storage through the current runtime flag | Pinned Composition.kt | Version-specific
Experimental |
| Remember reuse and keyed recalculation are tested | Pinned CompositionTests.kt | Version-specific |
Freshness
Refresh this lesson when remember overloads, Composer.cache, cache-slot traversal, or the storage-selection flag changes. The Android Review history for Composition.kt is useful for distinguishing a stable model from a transitional storage detail.
Finished this lesson?
Your progress stays only in this browser.