Lesson 6

`derivedStateOf` tracks dependencies and values

Explain how derived state caches a result, records dependencies, and compares the result seen by an observer.

4 min readUpdated Jul 12, 2026

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).

Diagram
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 --> Cached

Dependency 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

  • derivedStateOf is 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

ClaimDirect evidenceStatus
Derived state stores a cached result and dependency map in a recordDerivedState.ktDurable Version-specific
Recalculation records dependencies and compares result policyDerivedState.ktVersion-specific
Composition and SnapshotStateObserver track derived dependencies/value observationsComposition.kt and SnapshotStateObserver.ktVersion-specific
Caching, cached observation, nested state, and result changes are testedDerivedSnapshotStateTests.kt and CompositionAndDerivedStateTests.ktVersion-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.