Share
Embed
Deriving derived state: derivedStateOf explained (dcSF 24)
Jetpack Compose’s state library has a powerful but tricky tool in derivedStateOf. Much of the time you don’t need it at all. It’s deceptively expensive to use and complex to implement. To find out why we’ll derive it from first principles. Starting with the simplest solution for a typical use case, we’ll walk through how different parts of the snapshot state system can be leveraged to improve the solution, eventually ending up at derivedStateOf.
Zach Klippenstein
June 07, 2024
Transcript
203 slides
-
val state by remember { derivedStateOf { ... } }
fun <T> derivedStateOf( calculation: () -> T ): State<T> -
Use derivedStateOf: to reduce invalidations to cache expensive calculations that
can be performed lazily -
Don't use derivedStateOf: if calculation is cheap if inputs change
as frequently as outputs -
Questions? Opening the Shutter on Snapshots: bit.ly/3x5zH53 Slides: bit.ly/45cqHYl [email protected]
Snapshots blog series: bit.ly/3KAPprT androiddev.social/@zachklipp -
@Composable fun Foo() { val state1 = remember { mutableStateOf(0)
} val state2 = remember { mutableStateOf(1) } val result = state1.value + state2.value Text(result.toString()) } Don't use derivedStateOf! -
Frame 1 Frame 2 Frame 3 Frame 4 Frame 5
Frame 6 Frame 7 Calculation derivedStateOf Render Cheap calculation, changes frequently -
Frame 1 Frame 2 Frame 3 Frame 4 Frame 5
Frame 6 Frame 7 Calculation derivedStateOf Render Cheap calculation, changes frequently -
@Composable fun Foo() { val state1 = remember { mutableStateOf(0)
} val state2 = remember { mutableStateOf(1) } val result = state1.value + state2.value Text(result.toString()) } -
@Composable fun Foo() { val state1 = remember { mutableStateOf(0)
} val state2 = remember { mutableStateOf(1) } val result = 0 < state1.value + state2.value Text(result.toString()) } -
@Composable fun Foo() { val state1 = remember { mutableStateOf(0)
} val state2 = remember { mutableStateOf(1) } val result = derivedStateOf { 0 < state1.value + state2.value } Text(result.value.toString()) } -
@Composable fun Foo() { val state1 = remember { mutableStateOf(0)
} val state2 = remember { mutableStateOf(1) } val result = remember { derivedStateOf { 0 < state1.value + state2.value } } Text(result.value.toString()) } Only re-composes when result actually changes -
Frame 1 Frame 2 Frame 3 Frame 4 Frame 5
Frame 6 Frame 7 Calculation derivedStateOf Render Cheap calculation, used infrequently -
Frame 1 Frame 2 Frame 3 Frame 4 Frame 5
Frame 6 Frame 7 Calculation derivedStateOf Render Cheap calculation, used infrequently -
@Composable fun Foo(items: List<Bar>) { val sortedItems = items.sorted() LazyColumn(
Modifier.draw { sortedItems.forEach { drawDecoration(it) } } ) { items(sortedItems) { ... } } } Not used until layout or draw Expensive call in composition -
@Composable fun Foo(items: List<Bar>) { val sortedItems = remember {
{ items.sorted() } } LazyColumn( Modifier.draw { sortedItems.forEach { drawDecoration(it) } } ) { items(sortedItems) { ... } } } -
@Composable fun Foo(items: List<Bar>) { val sortedItems = remember {
{ items.sorted() } } LazyColumn( Modifier.draw { sortedItems().forEach { drawDecoration(it) } } ) { items(sortedItems()) { ... } } } sorted called lazily, but still called twice per frame -
@Composable fun Foo(items: List<Bar>) { val sortedItems = remember {
derivedStateOf { items.sorted() } } LazyColumn( Modifier.draw { sortedItems().forEach { drawDecoration(it) } } ) { items(sortedItems()) { ... } } } -
@Composable fun Foo(items: List<Bar>) { val sortedItems = remember {
derivedStateOf { items.sorted() } } LazyColumn( Modifier.draw { sortedItems.value.forEach { drawDecoration(it) } } ) { items(sortedItems.value) { ... } } } -
@Composable fun Foo(items: List<Bar>) { val updatedItems = rememberUpdatedState(items) val
sortedItems = remember { derivedStateOf { updatedItems.value.sorted() } } LazyColumn( Modifier.draw { sortedItems.value.forEach { drawDecoration(it) } } ) { items(sortedItems.value) { ... } } } Not executed until first used, in layout Then re-used in draw -
Frame 1 Frame 2 Frame 3 Frame 4 Frame 5
Frame 6 Frame 7 Calculation derivedStateOf Render Expensive calculation, changes infrequently, used frequently -
Frame 1 Frame 2 Frame 3 Frame 4 Frame 5
Frame 6 Frame 7 Calculation derivedStateOf Render Expensive calculation, changes infrequently, used frequently -
@Composable fun rememberFoo(input: Input): State<Output> { return remember(input) { derivedStateOf
{ someCalculation(input) } } } BAD Returning a new value each time Capturing value, not state of value -
@Composable fun rememberFoo(input: Input): Output { return remember(input) { someCalculation(input)
} } When someCalculation: is used immediately in composition changes frequently with input -
@Composable fun rememberFoo(input: Input): State<Output> { return remember(input) { derivedStateOf
{ someCalculation(input) } } } BAD -
@Composable fun rememberFoo(input: Input): State<Output> { return remember { derivedStateOf
{ someCalculation(input) } } } -
@Composable fun rememberFoo(input: Input): State<Output> { val updatedInput = rememberUpdatedState(input)
return remember { derivedStateOf { someCalculation(updatedInput) } } } When someCalculation: is expensive and not read in composition changes infrequently with input -
Function Invalidations = 1. Marked as needing to rerun 2.
Scheduled to rerun in the future 3. Eventually ran again -
“Invalidation scopes”: Composable functions val state = mutableStateOf("") @Composable fun
Foo() { state.value } Invalidations -
“Invalidation scopes”: Composable functions val state = mutableStateOf("") @Composable fun
Foo() { val bar = remember { ... } val value = state.value Text(value) } Invalidations Entire body of reader recomposes -
“Invalidation scopes”: (Some) Composable functions val state = mutableStateOf("") @Composable
fun Foo() { val bar = remember { ... } val value = state.value Text(value) } Invalidations -
“Invalidation scopes”: (Some) Composable functions val state = mutableStateOf("") @Composable
fun Foo() { val bar = remember { ... } val value = state.value Text(value) } @Composable fun rememberBar(): Bar { // ... } Invalidations -
“Invalidation scopes”: (Some) Composable functions val state = mutableStateOf("") @Composable
fun Foo() { val bar = remember { ... } val value = state.value Text(value) } @Composable fun rememberBar(): Bar { // ... } @Composable inline fun Baz() { // ... } Invalidations -
“Invalidation scopes”: (Some) Composable functions snapshotFlow val state = mutableStateOf("")
snapshotFlow { state.value } Invalidations -
“Invalidation scopes”: (Some) Composable functions snapshotFlow layout, draw, graphicsLayer, etc.
Invalidations Layout(content) { measurable, constraints -> // … } Modifier.graphicsLayer { // … } Modifier.draw { // … } // etc. -
“Invalidation scopes”: (Some) Composable functions snapshotFlow layout, draw, graphicsLayer, etc.
SnapshotStateObserver val state = mutableStateOf("") val observer = SnapshotStateObserver { it() } observer.observeReads(scope, onValueChanged) { state.value } Invalidations -
“Invalidation scopes”: (Some) Composable functions snapshotFlow layout, draw, graphicsLayer, etc.
SnapshotStateObserver derivedStateOf val state = mutableStateOf("") val derivedState = derivedStateOf { state.value } Invalidations -
Invalidations Based on snapshot state Two phases: 1. Know what
state objects were read 2. Listen for when those objects are written later -
Invalidations Read observation: Normal state val state1 = mutableStateOf(0) val
state2 = mutableStateOf(0) -
Read observation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() readSet Invalidations -
Read observation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() fun printResult() { val result = state1.value + state2.value println(result) } readSet Invalidations -
Read observation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() fun printResult() { Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) } } readSet Invalidations -
Read observation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() fun printResult() { Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) } } readSet Invalidations -
Read observation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() fun printResult() { Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) } } readSet state1 Invalidations -
Read observation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() fun printResult() { Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) } } readSet state1 Invalidations -
Read observation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() fun printResult() { Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) } } readSet state1 state2 Invalidations -
Read observation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() fun printResult() { Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) } } readSet state1 state2 Invalidations -
Read observation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() fun printResult() { Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) // "0" } } readSet state1 state2 Invalidations -
Read invalidation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() readSet state1 state2 Invalidations -
Read invalidation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() Snapshot.registerApplyObserver { changedObjects, _ -> if (changedObjects.any { it in readSet }) { schedulePrintResult() } } readSet state1 state2 Invalidations -
Read invalidation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() Snapshot.registerApplyObserver { changedObjects, _ -> if (changedObjects.any { it in readSet }) { schedulePrintResult() } } readSet state1 state2 changedObjects Invalidations -
Read invalidation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() Snapshot.registerApplyObserver { changedObjects, _ -> if (changedObjects.any { it in readSet }) { schedulePrintResult() } } Snapshot.withMutableSnapshot { state1.value = -1 state2.value = 1 } changedObjects readSet state1 state2 Invalidations -
Read invalidation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() Snapshot.registerApplyObserver { changedObjects, _ -> if (changedObjects.any { it in readSet }) { schedulePrintResult() } } Snapshot.withMutableSnapshot { state1.value = -1 state2.value = 1 } changedObjects state1 readSet state1 state2 Invalidations -
Read invalidation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() Snapshot.registerApplyObserver { changedObjects, _ -> if (changedObjects.any { it in readSet }) { schedulePrintResult() } } Snapshot.withMutableSnapshot { state1.value = -1 state2.value = 1 } changedObjects state1 readSet state1 state2 Invalidations -
Read invalidation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() Snapshot.registerApplyObserver { changedObjects, _ -> if (changedObjects.any { it in readSet }) { schedulePrintResult() } } Snapshot.withMutableSnapshot { state1.value = -1 state2.value = 1 } changedObjects state1 state2 readSet state1 state2 Invalidations -
Read invalidation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() Snapshot.registerApplyObserver { changedObjects, _ -> if (changedObjects.any { it in readSet }) { schedulePrintResult() } } Snapshot.withMutableSnapshot { state1.value = -1 state2.value = 1 } changedObjects state1 state2 readSet state1 state2 Invalidations -
Read invalidation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() Snapshot.registerApplyObserver { changedObjects, _ -> if (changedObjects.any { it in readSet }) { schedulePrintResult() } } Snapshot.withMutableSnapshot { state1.value = -1 state2.value = 1 } readSet state1 state2 changedObjects state1 state2 Invalidations -
Read invalidation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() readSet state1 state2 Invalidations -
Read invalidation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() fun printResult() { Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) } } readSet state1 state2 Invalidations -
Read invalidation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) } } readSet state1 state2 Invalidations -
Read invalidation: Normal state val state1 = mutableStateOf(0) val state2
= mutableStateOf(0) val readSet = mutableSetOf<Any>() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) } } readSet Invalidations -
val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet
= mutableSetOf<Any>() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) // "0" } } Read invalidation: Normal state (no optimization) readSet state1 state2 Invalidations -
val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet
= mutableSetOf<Any>() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) } } Read observation: Derived state (no optimization) Invalidations -
Read observation: Derived state (no optimization) val state1 = mutableStateOf(0)
val state2 = mutableStateOf(0) val readSet = mutableSetOf<Any>() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { val result = derivedStateOf { state1.value + state2.value } println(result.value) } } Invalidations -
Read observation: Derived state (no optimization) val state1 = mutableStateOf(0)
val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf<Any>() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { println(result.value) } } Invalidations -
Read observation: Derived state (no optimization) readSet val state1 =
mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf<Any>() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { println(result.value) } } Invalidations -
Read observation: Derived state (no optimization) readSet result val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf<Any>() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { println(result.value) } } Invalidations -
Read observation: Derived state (no optimization) val state1 = mutableStateOf(0)
val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf<Any>() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { println(result.value) } } readSet result Invalidations -
Read observation: Derived state (no optimization) val state1 = mutableStateOf(0)
val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf<Any>() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { println(result.value) } } readSet result state1 Invalidations -
Read observation: Derived state (no optimization) val state1 = mutableStateOf(0)
val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf<Any>() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { println(result.value) } } readSet result state1 Invalidations -
Read observation: Derived state (no optimization) val state1 = mutableStateOf(0)
val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf<Any>() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { println(result.value) } } readSet result state1 state2 Invalidations -
Read observation: Derived state (no optimization) val state1 = mutableStateOf(0)
val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf<Any>() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { println(result.value) // "0" } } readSet result state1 state2 Invalidations -
Read observation: Derived state (no optimization) val state1 = mutableStateOf(0)
val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf<Any>() Snapshot.registerApplyObserver { changedObjects, _ -> if (changedObjects.any { it in readSet }) { schedulePrintResult() } } Snapshot.withMutableSnapshot { state1.value = -1 state2.value = 1 } changedObjects state1 state2 readSet result state1 state2 Invalidations -
Read observation: Derived state (no optimization) val state1 = mutableStateOf(0)
val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf<Any>() Snapshot.registerApplyObserver { changedObjects, _ -> if (changedObjects.any { it in readSet }) { schedulePrintResult() } } Snapshot.withMutableSnapshot { state1.value = -1 state2.value = 1 } readSet result state1 state2 changedObjects state1 state2 Invalidations -
Read observation: Derived state (no optimization) val state1 = mutableStateOf(0)
val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf<Any>() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { println(result.value) } } readSet result state1 state2 Invalidations -
Read observation: Derived state (no optimization) val state1 = mutableStateOf(0)
val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf<Any>() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { println(result.value) // "0" } } ☹ readSet result state1 state2 Invalidations -
Read observation: Derived state (with optimization) Two phases: 1. Know
what state objects were read 2. Listen for when those objects are written later Invalidations -
Dedup invalidations Multiple phases: Know what state objects were read
Know what derivedState dependencies were used Know each derivedState’s calculation value Listen for when those objects are written later Check new calculation result, only invalidate if different Read observation: Derived state (with optimization) -
Dedup invalidations derivedStateOf tracks its own dependencies Readers query for
its dependencies, track those instead Only: Composition SnapshotStateObserver NOT snapshotFlow Read observation: Derived state (with optimization) -
Dedup invalidations Read observation: Derived state (with optimization) val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } @Composable fun Foo() { Text(result.value) } -
Dedup invalidations Read observation: Derived state (with optimization) val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } @Composable fun Foo() { Text(result.value) } Foo readSet derivedValues -
Dedup invalidations Read observation: Derived state (with optimization) val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } @Composable fun Foo() { Text(result.value) } Foo readSet derivedValues derived readSet -
Dedup invalidations Read observation: Derived state (with optimization) val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } @Composable fun Foo() { Text(result.value) } Foo readSet derivedValues derived readSet state1 state2 -
Dedup invalidations Read observation: Derived state (with optimization) val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value // 0 } @Composable fun Foo() { Text(result.value) } Foo readSet derivedValues derived readSet state1 state2 -
Dedup invalidations Read observation: Derived state (with optimization) val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value // 0 } @Composable fun Foo() { Text(result.value) } Foo readSet state1 state2 derivedValues derived readSet state1 state2 -
Dedup invalidations Read observation: Derived state (with optimization) val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value // 0 } @Composable fun Foo() { Text(result.value) } Foo readSet state1 result state2 derivedValues derived readSet state1 state2 -
Dedup invalidations Read observation: Derived state (with optimization) val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value // 0 } @Composable fun Foo() { Text(result.value) } Foo readSet state1 result state2 result derivedValues derived readSet state1 state2 -
Dedup invalidations Read observation: Derived state (with optimization) val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value // 0 } @Composable fun Foo() { Text(result.value) } derivedValues result Foo readSet state1 result state2 result derived readSet state1 state2 -
Dedup invalidations Read observation: Derived state (with optimization) val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value // 0 } @Composable fun Foo() { Text(result.value) } derivedValues result 0 Foo readSet state1 result state2 result derived readSet state1 state2 -
Dedup invalidations Read observation: Derived state (with optimization) val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } @Composable fun Foo() { Text(result.value) } derivedValues result 0 Foo readSet state1 result state2 result -
Dedup invalidations Read observation: Derived state (with optimization) val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } @Composable fun Foo() { Text(result.value) } Snapshot.withMutableSnapshot { state1.value = -1 state2.value = 1 } changed derivedValues result 0 Foo readSet state1 result state2 result -
Dedup invalidations Read observation: Derived state (with optimization) val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } @Composable fun Foo() { Text(result.value) } Snapshot.withMutableSnapshot { state1.value = -1 state2.value = 1 } changed state1 derivedValues result 0 Foo readSet state1 result state2 result -
Dedup invalidations Read observation: Derived state (with optimization) val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } @Composable fun Foo() { Text(result.value) } Snapshot.withMutableSnapshot { state1.value = -1 state2.value = 1 } changed state1 derivedValues result 0 Foo readSet state1 result state2 result -
Dedup invalidations Read observation: Derived state (with optimization) val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } @Composable fun Foo() { Text(result.value) } Snapshot.withMutableSnapshot { state1.value = -1 state2.value = 1 } changed state1 state2 derivedValues result 0 Foo readSet state1 result state2 result -
Dedup invalidations Read observation: Derived state (with optimization) val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } @Composable fun Foo() { Text(result.value) } Snapshot.withMutableSnapshot { state1.value = -1 state2.value = 1 } changed state1 state2 derivedValues result 0 Foo readSet state1 result state2 result -
Dedup invalidations Read observation: Derived state (with optimization) val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } @Composable fun Foo() { Text(result.value) } Snapshot.withMutableSnapshot { state1.value = -1 state2.value = 1 } derivedValues result 0 Foo readSet state1 result state2 result changed state1 state2 -
Dedup invalidations Read observation: Derived state (with optimization) val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } @Composable fun Foo() { Text(result.value) } Snapshot.withMutableSnapshot { state1.value = -1 state2.value = 1 } derivedValues result 0 Foo readSet state1 result state2 result changed state1 state2 -
Dedup invalidations Read observation: Derived state (with optimization) val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } @Composable fun Foo() { Text(result.value) } Snapshot.withMutableSnapshot { state1.value = -1 state2.value = 1 } derivedValues result 0 Foo readSet state1 result state2 result changed state1 state2 -
Dedup invalidations Read observation: Derived state (with optimization) val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value // 0 } @Composable fun Foo() { Text(result.value) } Snapshot.withMutableSnapshot { state1.value = -1 state2.value = 1 } derivedValues result 0 Foo readSet state1 result state2 result changed state1 state2 -
Dedup invalidations Read observation: Derived state (with optimization) val state1
= mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value // 0 } @Composable fun Foo() { Text(result.value) } Snapshot.withMutableSnapshot { state1.value = -1 state2.value = 1 } derivedValues result 0 Foo readSet state1 result state2 result changed state1 state2 🥳 -
@Composable fun Foo(items: List<Bar>) { val updatedItems = rememberUpdatedState(items) val
sortedItems = remember { derivedStateOf { updatedItems.value.sorted() } } LazyColumn( Modifier.draw { sortedItems.value.forEach { drawDecoration(it) } } ) { items(sortedItems.value) { ... } } } Not executed until first used, in layout Then re-used in draw -
Selecting records message id value “he” “hello asdf” “hello ”
written by 72 “hello world” color id value Red Blue written by 80 snapshot ID -
newest valid record “mutableState.value” get() = findRecord(SnapshotThreadLocal.get()) highest ID not
higher than snapshot ID AND not in snapshot invalid set AND not tombstoned -
Cache derivedStateOf: Custom StateObject subclass. Calculation result and metadata are
cached per-snapshot, in StateRecords. Uses snapshot IDs and write count to avoid recalculating result. -
Cache Algorithm: Have we ever calculated the result before? Is
the result from this snapshot? Was anything changed in the snapshot? Have any dependencies been written to? Re-calculate result—is it different? -
Cache val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val
derived = derivedStateOf { state1.value + state2.value } println(derived.value) Initializing snapshot ID 1 writeCount 0 -
Cache snapshot ID 1 writeCount 0 state1 RECORD 1 value
0 Initializing val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val derived = derivedStateOf { state1.value + state2.value } println(derived.value) -
Cache Initializing snapshot ID 1 writeCount 1 state1 RECORD 1
value 0 state2 RECORD 1 value 0 val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val derived = derivedStateOf { state1.value + state2.value } println(derived.value) -
Cache derived RECORD 1 value ∅ lastSnapshotId ∅ lastWriteCount ∅
Initializing snapshot ID 1 writeCount 2 state1 RECORD 1 value 0 state2 RECORD 1 value 0 val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val derived = derivedStateOf { state1.value + state2.value } println(derived.value) -
Cache derived RECORD 1 value ∅ lastSnapshotId ∅ lastWriteCount ∅
Checking cached value snapshot ID 1 writeCount 2 state1 RECORD 1 value 0 state2 RECORD 1 value 0 val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val derived = derivedStateOf { state1.value + state2.value } println(derived.value) unset -
Cache derived RECORD 1 value ∅ lastSnapshotId ∅ lastWriteCount ∅
Calculating result (tracking dependencies) snapshot ID 1 writeCount 2 state1 RECORD 1 value 0 state2 RECORD 1 value 0 val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val derived = derivedStateOf { state1.value + state2.value } println(derived.value) -
Cache derived RECORD 1 value ∅ lastSnapshotId ∅ lastWriteCount ∅
state1 snapshot ID 1 writeCount 2 state1 RECORD 1 value 0 state2 RECORD 1 value 0 val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val derived = derivedStateOf { state1.value + state2.value } println(derived.value) Calculating result (tracking dependencies) -
Cache derived RECORD 1 value ∅ lastSnapshotId ∅ lastWriteCount ∅
state1 1 snapshot ID 1 writeCount 2 state1 RECORD 1 value 0 state2 RECORD 1 value 0 val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val derived = derivedStateOf { state1.value + state2.value } println(derived.value) Calculating result (tracking dependencies) -
Cache derived RECORD 1 value ∅ lastSnapshotId ∅ lastWriteCount ∅
state1 1 state2 snapshot ID 1 writeCount 2 state1 RECORD 1 value 0 state2 RECORD 1 value 0 val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val derived = derivedStateOf { state1.value + state2.value } println(derived.value) Calculating result (tracking dependencies) -
Cache derived RECORD 1 value ∅ lastSnapshotId ∅ lastWriteCount ∅
state1 1 state2 1 snapshot ID 1 writeCount 2 state1 RECORD 1 value 0 state2 RECORD 1 value 0 val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val derived = derivedStateOf { state1.value + state2.value } println(derived.value) Calculating result (tracking dependencies) -
Cache derived RECORD 1 value ∅ lastSnapshotId ∅ lastWriteCount ∅
state1 1 state2 1 Calculating result snapshot ID 1 writeCount 2 state1 RECORD 1 value 0 state2 RECORD 1 value 0 val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val derived = derivedStateOf { state1.value + state2.value // 0 } println(derived.value) -
Cache derived RECORD 1 value 0 lastSnapshotId ∅ lastWriteCount ∅
state1 1 state2 1 Calculating result snapshot ID 1 writeCount 2 state1 RECORD 1 value 0 state2 RECORD 1 value 0 val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val derived = derivedStateOf { state1.value + state2.value // 0 } println(derived.value) -
Cache derived RECORD 1 value 0 lastSnapshotId ∅ lastWriteCount ∅
state1 1 state2 1 Calculating result (advancing snapshot) snapshot ID 1 writeCount 2 state1 RECORD 1 value 0 state2 RECORD 1 value 0 val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val derived = derivedStateOf { state1.value + state2.value } println(derived.value) -
Cache derived RECORD 1 value 0 lastSnapshotId ∅ lastWriteCount ∅
state1 1 state2 1 Calculating result (advancing snapshot) snapshot ID 2 writeCount 0 state1 RECORD 1 value 0 state2 RECORD 1 value 0 val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val derived = derivedStateOf { state1.value + state2.value } println(derived.value) -
Cache derived RECORD 1 value 0 lastSnapshotId ∅ lastWriteCount ∅
state1 1 state2 1 Calculating result (updating metadata) snapshot ID 2 writeCount 0 state1 RECORD 1 value 0 state2 RECORD 1 value 0 val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val derived = derivedStateOf { state1.value + state2.value } println(derived.value) -
Cache derived RECORD 1 value 0 lastSnapshotId 2 lastWriteCount 0
state1 1 state2 1 Calculating result (updating metadata) snapshot ID 2 writeCount 0 state1 RECORD 1 value 0 state2 RECORD 1 value 0 val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val derived = derivedStateOf { state1.value + state2.value } println(derived.value) -
Cache derived RECORD 1 value 0 lastSnapshotId 2 lastWriteCount 0
state1 1 state2 1 Calculating result snapshot ID 2 writeCount 0 state1 RECORD 1 value 0 state2 RECORD 1 value 0 val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val derived = derivedStateOf { state1.value + state2.value } println(derived.value) -
Cache derived RECORD 1 value 0 lastSnapshotId 2 lastWriteCount 0
state1 1 state2 1 Calculating result snapshot ID 2 writeCount 0 state1 RECORD 1 value 0 state2 RECORD 1 value 0 val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val derived = derivedStateOf { state1.value + state2.value } println(derived.value) // "0" Snapshot.withMutableSnapshot { println(derived.value) state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 2 lastWriteCount 0
state1 1 state2 1 Entering new snapshot snapshot ID 2 writeCount 0 state1 RECORD 1 value 0 state2 RECORD 1 value 0 Snapshot.withMutableSnapshot { println(derived.value) println(derived.value) state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val derived = derivedStateOf { state1.value + state2.value } println(derived.value) // "0" -
Cache derived RECORD 1 value 0 lastSnapshotId 2 lastWriteCount 0
state1 1 state2 1 Entering new snapshot snapshot ID 3 writeCount 0 state1 RECORD 1 value 0 state2 RECORD 1 value 0 Snapshot.withMutableSnapshot { println(derived.value) println(derived.value) state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 2 lastWriteCount 0
state1 1 state2 1 Checking cached value snapshot ID 3 writeCount 0 state1 RECORD 1 value 0 state2 RECORD 1 value 0 Snapshot.withMutableSnapshot { println(derived.value) println(derived.value) state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } set -
Cache derived RECORD 1 value 0 lastSnapshotId 2 lastWriteCount 0
state1 1 state2 1 Checking current snapshot snapshot ID 3 writeCount 0 state1 RECORD 1 value 0 state2 RECORD 1 value 0 Snapshot.withMutableSnapshot { println(derived.value) println(derived.value) state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } 3 ≠ 2 -
Cache derived RECORD 1 value 0 lastSnapshotId 2 lastWriteCount 0
state1 1 state2 1 Checking dependencies snapshot ID 3 writeCount 0 state1 RECORD 1 value 0 state2 RECORD 1 value 0 Snapshot.withMutableSnapshot { println(derived.value) println(derived.value) state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } 1 = 1 -
Cache derived RECORD 1 value 0 lastSnapshotId 2 lastWriteCount 0
state1 1 state2 1 Checking dependencies snapshot ID 3 writeCount 0 state1 RECORD 1 value 0 state2 RECORD 1 value 0 Snapshot.withMutableSnapshot { println(derived.value) println(derived.value) state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } 1 = 1 -
Cache derived RECORD 1 value 0 lastSnapshotId 2 lastWriteCount 0
state1 1 state2 1 Updating cache metadata snapshot ID 3 writeCount 0 state1 RECORD 1 value 0 state2 RECORD 1 value 0 Snapshot.withMutableSnapshot { println(derived.value) println(derived.value) state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 snapshot ID 3 writeCount 0 state1 RECORD 1 value 0 state2 RECORD 1 value 0 Snapshot.withMutableSnapshot { println(derived.value) println(derived.value) state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } Updating cache metadata -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 Checking dependencies snapshot ID 3 writeCount 0 state1 RECORD 1 value 0 state2 RECORD 1 value 0 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 Checking cached value snapshot ID 3 writeCount 0 state1 RECORD 1 value 0 state2 RECORD 1 value 0 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } set -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 Checking current snapshot snapshot ID 3 writeCount 0 state1 RECORD 1 value 0 state2 RECORD 1 value 0 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } 3 = 3 -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 Checking current snapshot snapshot ID 3 writeCount 0 state1 RECORD 1 value 0 state2 RECORD 1 value 0 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } 0 = 0 -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 Checking current snapshot snapshot ID 3 writeCount 0 state1 RECORD 1 value 0 state2 RECORD 1 value 0 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 Checking current snapshot snapshot ID 3 writeCount 0 state1 RECORD 1 value 0 state2 RECORD 1 value 0 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 Writing to dependency snapshot ID 3 writeCount 0 state1 RECORD 1 value 0 RECORD ∅ value ∅ state2 RECORD 1 value 0 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 Writing to dependency snapshot ID 3 writeCount 0 state1 RECORD 1 value 0 RECORD 3 value ∅ state2 RECORD 1 value 0 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 Writing to dependency snapshot ID 3 writeCount 0 state1 RECORD 1 value 0 RECORD 3 value -1 state2 RECORD 1 value 0 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 Writing to dependency snapshot ID 3 writeCount 1 state1 RECORD 1 value 0 RECORD 3 value -1 state2 RECORD 1 value 0 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 Writing to dependency snapshot ID 3 writeCount 1 state1 RECORD 1 value 0 RECORD 3 value -1 state2 RECORD 1 value 0 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 Writing to dependency snapshot ID 3 writeCount 1 state1 RECORD 1 value 0 RECORD 3 value -1 state2 RECORD 1 value 0 RECORD ∅ value ∅ Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 Writing to dependency snapshot ID 3 writeCount 1 state1 RECORD 1 value 0 RECORD 3 value -1 state2 RECORD 1 value 0 RECORD 3 value ∅ Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 Writing to dependency snapshot ID 3 writeCount 1 state1 RECORD 1 value 0 RECORD 3 value -1 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 Writing to dependency snapshot ID 3 writeCount 2 state1 RECORD 1 value 0 RECORD 3 value -1 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 Checking cached value snapshot ID 3 writeCount 2 state1 RECORD 1 value 0 RECORD 3 value -1 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } set -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 Checking current snapshot snapshot ID 3 writeCount 2 state1 RECORD 1 value 0 RECORD 3 value -1 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } 3 = 3 -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 Checking current snapshot snapshot ID 3 writeCount 2 state1 RECORD 1 value 0 RECORD 3 value -1 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } 2 ≠ 0 -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 Checking dependencies snapshot ID 3 writeCount 2 state1 RECORD 1 value 0 RECORD 3 value -1 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } 3 ≠ 1 -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 Recalculating result snapshot ID 3 writeCount 2 state1 RECORD 1 value 0 RECORD 3 value -1 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } 0 = 0 -1 + 1 = 0 -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 1 state2 1 Updating cache metadata snapshot ID 3 writeCount 2 state1 RECORD 1 value 0 RECORD 3 value -1 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 3 state2 1 Updating cache metadata snapshot ID 3 writeCount 2 state1 RECORD 1 value 0 RECORD 3 value -1 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 3 state2 3 Updating cache metadata snapshot ID 3 writeCount 2 state1 RECORD 1 value 0 RECORD 3 value -1 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 3 state2 3 Advancing snapshot snapshot ID 3 writeCount 2 state1 RECORD 1 value 0 RECORD 3 value -1 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 3 lastWriteCount 0
state1 3 state2 3 Advancing snapshot snapshot ID 4 writeCount 0 state1 RECORD 1 value 0 RECORD 3 value -1 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 Updating cache metadata snapshot ID 4 writeCount 0 state1 RECORD 1 value 0 RECORD 3 value -1 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 Updating cache metadata snapshot ID 4 writeCount 0 state1 RECORD 1 value 0 RECORD 3 value -1 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 Writing to dependency snapshot ID 4 writeCount 0 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) } state1 RECORD 1 value 0 RECORD 3 value -1 -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 Writing to dependency snapshot ID 4 writeCount 0 state1 RECORD 1 value 0 RECORD 3 value -1 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 Writing to dependency snapshot ID 4 writeCount 0 state1 RECORD 1 value 0 RECORD 3 value -1 RECORD ∅ value ∅ state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 Writing to dependency snapshot ID 4 writeCount 0 state1 RECORD 1 value 0 RECORD 3 value -1 RECORD 4 value ∅ state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 Writing to dependency snapshot ID 4 writeCount 0 state1 RECORD 1 value 0 RECORD 3 value -1 RECORD 4 value 42 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 Writing to dependency snapshot ID 4 writeCount 1 state1 RECORD 1 value 0 RECORD 3 value -1 RECORD 4 value 42 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 Checking cached value snapshot ID 4 writeCount 1 state1 RECORD 1 value 0 RECORD 3 value -1 RECORD 4 value 42 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) } set -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 Checking current snapshot snapshot ID 4 writeCount 1 state1 RECORD 1 value 0 RECORD 3 value -1 RECORD 4 value 42 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) } 4 = 4 -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 Checking current snapshot snapshot ID 4 writeCount 1 state1 RECORD 1 value 0 RECORD 3 value -1 RECORD 4 value 42 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) } 1 ≠ 0 -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 Checking dependencies snapshot ID 4 writeCount 1 state1 RECORD 1 value 0 RECORD 3 value -1 RECORD 4 value 42 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) } 4 ≠ 3 -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 Recalculating result snapshot ID 4 writeCount 1 state1 RECORD 1 value 0 RECORD 3 value -1 RECORD 4 value 42 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) } 43 ≠ 0 42 + 1 = 43 -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 RECORD ∅ value ∅ lastSnapshotId ∅ lastWriteCount ∅ state1 ∅ state2 ∅ Recalculating result (new cache record) snapshot ID 4 writeCount 1 state1 RECORD 1 value 0 RECORD 3 value -1 RECORD 4 value 42 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) } 42 + 1 = 43 -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 RECORD 4 value ∅ lastSnapshotId ∅ lastWriteCount ∅ state1 ∅ state2 ∅ Recalculating result (new cache record) snapshot ID 4 writeCount 1 state1 RECORD 1 value 0 RECORD 3 value -1 RECORD 4 value 42 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) } 42 + 1 = 43 -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 RECORD 4 value ∅ lastSnapshotId ∅ lastWriteCount ∅ state1 4 state2 ∅ Recalculating result (new cache record) snapshot ID 4 writeCount 1 state1 RECORD 1 value 0 RECORD 3 value -1 RECORD 4 value 42 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) } 42 + 1 = 43 -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 RECORD 4 value ∅ lastSnapshotId ∅ lastWriteCount ∅ state1 4 state2 3 Recalculating result (new cache record) snapshot ID 4 writeCount 1 state1 RECORD 1 value 0 RECORD 3 value -1 RECORD 4 value 42 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) } 42 + 1 = 43 -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 RECORD 4 value 43 lastSnapshotId ∅ lastWriteCount ∅ state1 4 state2 3 Recalculating result (new cache record) snapshot ID 4 writeCount 1 state1 RECORD 1 value 0 RECORD 3 value -1 RECORD 4 value 42 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) } 42 + 1 = 43 -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 RECORD 4 value 43 lastSnapshotId ∅ lastWriteCount ∅ state1 4 state2 3 Recalculating result (advancing snapshot) snapshot ID 4 writeCount 1 state1 RECORD 1 value 0 RECORD 3 value -1 RECORD 4 value 42 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 RECORD 4 value 43 lastSnapshotId ∅ lastWriteCount ∅ state1 4 state2 3 Recalculating result (advancing snapshot) snapshot ID 5 writeCount 0 state1 RECORD 1 value 0 RECORD 3 value -1 RECORD 4 value 42 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 RECORD 4 value 43 lastSnapshotId ∅ lastWriteCount ∅ state1 4 state2 3 Recalculating result (updating metadata) snapshot ID 5 writeCount 0 state1 RECORD 1 value 0 RECORD 3 value -1 RECORD 4 value 42 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 RECORD 4 value 43 lastSnapshotId 5 lastWriteCount 0 state1 4 state2 3 Recalculating result (updating metadata) snapshot ID 5 writeCount 0 state1 RECORD 1 value 0 RECORD 3 value -1 RECORD 4 value 42 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) } -
Cache derived RECORD 1 value 0 lastSnapshotId 4 lastWriteCount 0
state1 3 state2 3 RECORD 4 value 43 lastSnapshotId 5 lastWriteCount 0 state1 4 state2 3 Recalculating result snapshot ID 5 writeCount 0 state1 RECORD 1 value 0 RECORD 3 value -1 RECORD 4 value 42 state2 RECORD 1 value 0 RECORD 3 value 1 Snapshot.withMutableSnapshot { println(derived.value) // "0" println(derived.value) // "0" state1.value = -1 state2.value = 1 println(derived.value) // "0" state1.value = 42 println(derived.value) // "43" } -
Cache 5 cases: Reading for the first time in a
snapshot, but no dependencies has been written to. at least one dependency has been written to. Reading multiple times in same snapshot, with no writes in-between. with at least one write in between. Nested derivedStateOf reads. -
Cache Calculation function must be pure. Only read snapshot states.
Doesn't actually keep a list of dependencies, just stores a hash of (record, id) Actual value not tracked value's hashcode() is never called -
derivedStateOf Dedup invalidations Cache result • Coordinates with invalidation scopes.
• Each scope determines for itself whether it needs to invalidate. • Uses caching when checking for changes. • Uses snapshot metadata to avoid recalculating result in most cases. • Uses StateRecords to keep cache isolated in different snapshots. -
Use derivedStateOf: to reduce invalidations to cache expensive calculations that
can be performed lazily -
Don't use derivedStateOf: if calculation is cheap if inputs change
as frequently as outputs -
Action Items: Open your codebase. Find at least one derivedStateOf
that doesn't need to be there …or is used wrong. Remove it. -
Questions? Opening the Shutter on Snapshots: bit.ly/3x5zH53 Slides: bit.ly/45cqHYl [email protected]
Snapshots blog series: bit.ly/3KAPprT androiddev.social/@zachklipp