Lesson 6
`derivedStateOf` tracks dependencies and values
Explain how derived state caches a result, records dependencies, and compares the result seen by an observer.
Status: Derived state as cached state plus dependency tracking is Durable .
ResultRecord, nested read levels, readable hashes, and observer maps are Version-specific at the pinned revision.
Outcome
Given a derivedStateOf calculation, you should be able to explain what is cached, which state objects are dependencies, and why an unchanged derived result can avoid a downstream observation change.
It is a state object with a calculation record
derivedStateOf { ... } returns a State<T>, but the pinned implementation creates an internal DerivedSnapshotState. It is a StateObjectImpl, so it participates in the same snapshot record machinery as other state objects. Each ResultRecord stores a cached result, a map of StateObject dependencies, and a hash used to decide whether the record is still readable for the current snapshot (DerivedState.kt).
The first read can run the calculation. During it, Snapshot.observe records each snapshot-aware state read and nested level in newDependencies. Later reads can return a valid cached result without rerunning the calculation, while still participating in observation (DerivedState.kt).
flowchart TD
ReadDerived[Read derived state] --> Valid{Record valid?}
Valid -->|Yes| Cached[Return cached result]
Valid -->|No| Calculate[Run calculation]
Calculate --> Dependencies[Record dependencies]
Dependencies --> Result[Store result and hash]
Result --> CachedDependency identity and result value are different
A dependency changing does not automatically mean the derived value changed. The implementation recalculates when the record is invalid, then compares the new result with the old result using the derived state’s policy. If the policy says the result is equivalent, the existing record can be updated with new dependencies and a new readable hash without replacing the observed result (DerivedState.kt).
With the no-policy overload, the policy is null. The public KDoc says dependency updates trigger updates regardless of the produced value. The policy overload supplies a SnapshotMutationPolicy when result equivalence matters (DerivedState.kt). This is not the same as caching an arbitrary function: the dependency set and result policy jointly describe when the derived state can matter to an observer.
The readable hash is an implementation detail that checks whether recorded dependencies still represent the same snapshot-visible inputs. It also accounts for nested derived states. Do not expose it as a stable API or treat it as the derived value’s equality comparison.
Composition stores the edge at the right level
When composition reads a derived state, CompositionImpl.recordReadOf maps its dependencies to the derived object and stores the value observed by the scope (Composition.kt). SnapshotStateObserver similarly records the current derived value and compares it with the previously recorded value before notifying a scope (SnapshotStateObserver.kt). Thus “a dependency changed” and “the observed derived value changed” are distinct.
Nested derived state makes the distinction important. If outer reads inner, and inner reads source, the dependency maps preserve that relationship. This lesson stops at dependency/value tracking; Chapter 6 will explain how composition turns those facts into restart work.
Reproduce cache and value tracking
This is a pinned upstream-test procedure, not an experiment run for this lesson. At AndroidX revision cc1caf65677fc10a8ce8116eba46e716f4cef222, run:
./gradlew :compose:runtime:runtime:desktopTest \
--tests 'androidx.compose.runtime.snapshots.DerivedSnapshotStateTests.theCalculationIsCached' \
--tests 'androidx.compose.runtime.snapshots.DerivedSnapshotStateTests.stateReadsCanBeObservedEvenIfCached' \
--tests 'androidx.compose.runtime.CompositionAndDerivedStateTests.onlyInvalidatesIfResultIsDifferent' \
--tests 'androidx.compose.runtime.snapshots.SnapshotStateObserverTestsCommon.nestedDerivedStateOfInvalidatesObserver'
The control is theCalculationIsCached: repeat a read without changing its dependency and expect one calculation. stateReadsCanBeObservedEvenIfCached checks that a cached read still reports dependencies. onlyInvalidatesIfResultIsDifferent changes two inputs so their sum stays constant, then changes one so the result differs. Interpretation limit: these tests bound caching and comparison at this revision; they do not show a universal performance gain or scheduler policy.
Misconceptions
- “
derivedStateOfis only memoization.” It caches a result and records snapshot dependencies for observation. - “Any dependency change means the derived value changed.” The result policy can treat the new result as equivalent.
- “The dependency map is the cached value.” Dependencies explain when to reconsider; the result is stored separately.
- “Derived state forces recomposition.” It participates in observation; scheduling is a later runtime concern.
Check yourself
a + b is derived. A mutable snapshot increments a and decrements b, leaving the sum unchanged. Which event should occur—dependency reconsideration, result replacement, both, or neither—and what policy decides the second step?
Source notes
| Claim | Direct evidence | Status |
|---|---|---|
| Derived state stores a cached result and dependency map in a record | DerivedState.kt | Durable
Version-specific |
| Recalculation records dependencies and compares result policy | DerivedState.kt | Version-specific |
Composition and SnapshotStateObserver track derived dependencies/value observations | Composition.kt and SnapshotStateObserver.kt | Version-specific |
| Caching, cached observation, nested state, and result changes are tested | DerivedSnapshotStateTests.kt and CompositionAndDerivedStateTests.kt | Version-specific |
Freshness
Refresh this lesson when DerivedSnapshotState, dependency read levels, readable hashes, result-policy behavior, or derived-state observation maps change. The current release notes include fixes for derived-state tracking and nested dependency handling, so re-run the pinned tests before making a stronger statement about memory retention or notification behavior.
Finished this lesson?
Your progress stays only in this browser.