Slide 1

Slide 1 text

Zach Klippenstein (he/him) Google deriv(ing)StateOf

Slide 2

Slide 2 text

Zach Klippenstein (he/him) Google deriv(ing)StateOf

Slide 3

Slide 3 text

val state by remember { derivedStateOf { ... } }

Slide 4

Slide 4 text

val state by remember { derivedStateOf { ... } } fun derivedStateOf( calculation: () -> T ): State

Slide 5

Slide 5 text

Use derivedStateOf: to reduce invalidations to cache expensive calculations that can be performed lazily

Slide 6

Slide 6 text

Don't use derivedStateOf: if calculation is cheap if inputs change as frequently as outputs

Slide 7

Slide 7 text

Don’t use derivedStateOf unless you know you should.

Slide 8

Slide 8 text

Questions? Opening the Shutter on Snapshots: bit.ly/3x5zH53 Slides: bit.ly/45cqHYl androiddev@zachklipp.com Snapshots blog series: bit.ly/3KAPprT androiddev.social/@zachklipp

Slide 9

Slide 9 text

Agenda What problems does derivedStateOf solve? How does derivedStateOf work?

Slide 10

Slide 10 text

@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!

Slide 11

Slide 11 text

Frame 1 Frame 2 Frame 3 Frame 4 Frame 5 Frame 6 Frame 7 Calculation derivedStateOf Render Cheap calculation, changes frequently

Slide 12

Slide 12 text

Frame 1 Frame 2 Frame 3 Frame 4 Frame 5 Frame 6 Frame 7 Calculation derivedStateOf Render Cheap calculation, changes frequently

Slide 13

Slide 13 text

@Composable fun Foo() { val state1 = remember { mutableStateOf(0) } val state2 = remember { mutableStateOf(1) } val result = state1.value + state2.value Text(result.toString()) }

Slide 14

Slide 14 text

@Composable fun Foo() { val state1 = remember { mutableStateOf(0) } val state2 = remember { mutableStateOf(1) } val result = 0 < state1.value + state2.value Text(result.toString()) }

Slide 15

Slide 15 text

@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()) }

Slide 16

Slide 16 text

@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

Slide 17

Slide 17 text

Frame 1 Frame 2 Frame 3 Frame 4 Frame 5 Frame 6 Frame 7 Calculation derivedStateOf Render Cheap calculation, used infrequently

Slide 18

Slide 18 text

Frame 1 Frame 2 Frame 3 Frame 4 Frame 5 Frame 6 Frame 7 Calculation derivedStateOf Render Cheap calculation, used infrequently

Slide 19

Slide 19 text

@Composable fun Foo(items: List) { val sortedItems = items.sorted() LazyColumn( Modifier.draw { sortedItems.forEach { drawDecoration(it) } } ) { items(sortedItems) { ... } } } Not used until layout or draw Expensive call in composition

Slide 20

Slide 20 text

@Composable fun Foo(items: List) { val sortedItems = remember { { items.sorted() } } LazyColumn( Modifier.draw { sortedItems.forEach { drawDecoration(it) } } ) { items(sortedItems) { ... } } }

Slide 21

Slide 21 text

@Composable fun Foo(items: List) { val sortedItems = remember { { items.sorted() } } LazyColumn( Modifier.draw { sortedItems().forEach { drawDecoration(it) } } ) { items(sortedItems()) { ... } } } sorted called lazily, but still called twice per frame

Slide 22

Slide 22 text

@Composable fun Foo(items: List) { val sortedItems = remember { derivedStateOf { items.sorted() } } LazyColumn( Modifier.draw { sortedItems().forEach { drawDecoration(it) } } ) { items(sortedItems()) { ... } } }

Slide 23

Slide 23 text

@Composable fun Foo(items: List) { val sortedItems = remember { derivedStateOf { items.sorted() } } LazyColumn( Modifier.draw { sortedItems.value.forEach { drawDecoration(it) } } ) { items(sortedItems.value) { ... } } }

Slide 24

Slide 24 text

@Composable fun Foo(items: List) { 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

Slide 25

Slide 25 text

Frame 1 Frame 2 Frame 3 Frame 4 Frame 5 Frame 6 Frame 7 Calculation derivedStateOf Render Expensive calculation, changes infrequently, used frequently

Slide 26

Slide 26 text

Frame 1 Frame 2 Frame 3 Frame 4 Frame 5 Frame 6 Frame 7 Calculation derivedStateOf Render Expensive calculation, changes infrequently, used frequently

Slide 27

Slide 27 text

@Composable fun rememberFoo(input: Input): State { return remember(input) { derivedStateOf { someCalculation(input) } } } BAD Returning a new value each time Capturing value, not state of value

Slide 28

Slide 28 text

@Composable fun rememberFoo(input: Input): Output { return remember(input) { someCalculation(input) } } When someCalculation: is used immediately in composition changes frequently with input

Slide 29

Slide 29 text

@Composable fun rememberFoo(input: Input): State { return remember(input) { derivedStateOf { someCalculation(input) } } } BAD

Slide 30

Slide 30 text

@Composable fun rememberFoo(input: Input): State { return remember { derivedStateOf { someCalculation(input) } } }

Slide 31

Slide 31 text

@Composable fun rememberFoo(input: Input): State { val updatedInput = rememberUpdatedState(input) return remember { derivedStateOf { someCalculation(updatedInput) } } } When someCalculation: is expensive and not read in composition changes infrequently with input

Slide 32

Slide 32 text

Don’t use derivedStateOf unless you know you should.

Slide 33

Slide 33 text

How does derivedStateOf work?

Slide 34

Slide 34 text

derivedStateOf Dedup Invalidations Cache Result

Slide 35

Slide 35 text

Invalidations= ?

Slide 36

Slide 36 text

Function Invalidations= ?

Slide 37

Slide 37 text

Function Invalidations = 1. Marked as needing to rerun 2. Scheduled to rerun in the future 3. Eventually ran again

Slide 38

Slide 38 text

Invalidations “Invalidation scopes”:

Slide 39

Slide 39 text

“Invalidation scopes”: Composable functions val state = mutableStateOf("") @Composable fun Foo() { state.value } Invalidations

Slide 40

Slide 40 text

“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

Slide 41

Slide 41 text

“Invalidation scopes”: (Some) Composable functions val state = mutableStateOf("") @Composable fun Foo() { val bar = remember { ... } val value = state.value Text(value) } Invalidations

Slide 42

Slide 42 text

“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

Slide 43

Slide 43 text

“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

Slide 44

Slide 44 text

“Invalidation scopes”: (Some) Composable functions snapshotFlow val state = mutableStateOf("") snapshotFlow { state.value } Invalidations

Slide 45

Slide 45 text

“Invalidation scopes”: (Some) Composable functions snapshotFlow layout, draw, graphicsLayer, etc. Invalidations Layout(content) { measurable, constraints -> // … } Modifier.graphicsLayer { // … } Modifier.draw { // … } // etc.

Slide 46

Slide 46 text

“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

Slide 47

Slide 47 text

“Invalidation scopes”: (Some) Composable functions snapshotFlow layout, draw, graphicsLayer, etc. SnapshotStateObserver derivedStateOf val state = mutableStateOf("") val derivedState = derivedStateOf { state.value } Invalidations

Slide 48

Slide 48 text

Invalidations Based on snapshot state Two phases: 1. Know what state objects were read 2. Listen for when those objects are written later

Slide 49

Slide 49 text

Invalidations Read observation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0)

Slide 50

Slide 50 text

Read observation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() readSet Invalidations

Slide 51

Slide 51 text

Read observation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() fun printResult() { val result = state1.value + state2.value println(result) } readSet Invalidations

Slide 52

Slide 52 text

Read observation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() fun printResult() { Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) } } readSet Invalidations

Slide 53

Slide 53 text

Read observation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() fun printResult() { Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) } } readSet Invalidations

Slide 54

Slide 54 text

Read observation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() fun printResult() { Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) } } readSet state1 Invalidations

Slide 55

Slide 55 text

Read observation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() fun printResult() { Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) } } readSet state1 Invalidations

Slide 56

Slide 56 text

Read observation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() fun printResult() { Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) } } readSet state1 state2 Invalidations

Slide 57

Slide 57 text

Read observation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() fun printResult() { Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) } } readSet state1 state2 Invalidations

Slide 58

Slide 58 text

Read observation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() fun printResult() { Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) // "0" } } readSet state1 state2 Invalidations

Slide 59

Slide 59 text

Read invalidation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() readSet state1 state2 Invalidations

Slide 60

Slide 60 text

Read invalidation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() Snapshot.registerApplyObserver { changedObjects, _ -> if (changedObjects.any { it in readSet }) { schedulePrintResult() } } readSet state1 state2 Invalidations

Slide 61

Slide 61 text

Read invalidation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() Snapshot.registerApplyObserver { changedObjects, _ -> if (changedObjects.any { it in readSet }) { schedulePrintResult() } } readSet state1 state2 changedObjects Invalidations

Slide 62

Slide 62 text

Read invalidation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() Snapshot.registerApplyObserver { changedObjects, _ -> if (changedObjects.any { it in readSet }) { schedulePrintResult() } } Snapshot.withMutableSnapshot { state1.value = -1 state2.value = 1 } changedObjects readSet state1 state2 Invalidations

Slide 63

Slide 63 text

Read invalidation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() Snapshot.registerApplyObserver { changedObjects, _ -> if (changedObjects.any { it in readSet }) { schedulePrintResult() } } Snapshot.withMutableSnapshot { state1.value = -1 state2.value = 1 } changedObjects state1 readSet state1 state2 Invalidations

Slide 64

Slide 64 text

Read invalidation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() Snapshot.registerApplyObserver { changedObjects, _ -> if (changedObjects.any { it in readSet }) { schedulePrintResult() } } Snapshot.withMutableSnapshot { state1.value = -1 state2.value = 1 } changedObjects state1 readSet state1 state2 Invalidations

Slide 65

Slide 65 text

Read invalidation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() 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

Slide 66

Slide 66 text

Read invalidation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() 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

Slide 67

Slide 67 text

Read invalidation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() 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

Slide 68

Slide 68 text

Read invalidation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() readSet state1 state2 Invalidations

Slide 69

Slide 69 text

Read invalidation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() fun printResult() { Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) } } readSet state1 state2 Invalidations

Slide 70

Slide 70 text

Read invalidation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) } } readSet state1 state2 Invalidations

Slide 71

Slide 71 text

Read invalidation: Normal state val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) } } readSet Invalidations

Slide 72

Slide 72 text

val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() 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

Slide 73

Slide 73 text

val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { val result = state1.value + state2.value println(result) } } Read observation: Derived state (no optimization) Invalidations

Slide 74

Slide 74 text

Read observation: Derived state (no optimization) val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val readSet = mutableSetOf() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { val result = derivedStateOf { state1.value + state2.value } println(result.value) } } Invalidations

Slide 75

Slide 75 text

Read observation: Derived state (no optimization) val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { println(result.value) } } Invalidations

Slide 76

Slide 76 text

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() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { println(result.value) } } Invalidations

Slide 77

Slide 77 text

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() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { println(result.value) } } Invalidations

Slide 78

Slide 78 text

Read observation: Derived state (no optimization) val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { println(result.value) } } readSet result Invalidations

Slide 79

Slide 79 text

Read observation: Derived state (no optimization) val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { println(result.value) } } readSet result state1 Invalidations

Slide 80

Slide 80 text

Read observation: Derived state (no optimization) val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { println(result.value) } } readSet result state1 Invalidations

Slide 81

Slide 81 text

Read observation: Derived state (no optimization) val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { println(result.value) } } readSet result state1 state2 Invalidations

Slide 82

Slide 82 text

Read observation: Derived state (no optimization) val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { println(result.value) // "0" } } readSet result state1 state2 Invalidations

Slide 83

Slide 83 text

Read observation: Derived state (no optimization) val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf() 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

Slide 84

Slide 84 text

Read observation: Derived state (no optimization) val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf() 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

Slide 85

Slide 85 text

Read observation: Derived state (no optimization) val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { println(result.value) } } readSet result state1 state2 Invalidations

Slide 86

Slide 86 text

Read observation: Derived state (no optimization) val state1 = mutableStateOf(0) val state2 = mutableStateOf(0) val result = derivedStateOf { state1.value + state2.value } val readSet = mutableSetOf() fun printResult() { readSet.clear() Snapshot.observe( readObserver = { readSet += it } ) { println(result.value) // "0" } } ☹ readSet result state1 state2 Invalidations

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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)

Slide 89

Slide 89 text

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)

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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 🥳

Slide 111

Slide 111 text

derivedStateOf Dedup Invalidations Cache Result

Slide 112

Slide 112 text

derivedStateOf Dedup Invalidations Cache Result

Slide 113

Slide 113 text

@Composable fun Foo(items: List) { 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

Slide 114

Slide 114 text

How do they work?

Slide 115

Slide 115 text

How do they work? www.droidcon.com/2023/07/20/opening-the-shutter-on-snapshots-2/

Slide 116

Slide 116 text

Snapshot IDs

Slide 117

Slide 117 text

Selecting records message id value “he” “hello asdf” “hello ” written by 72 “hello world” color id value Red Blue written by 80 snapshot ID

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

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.

Slide 120

Slide 120 text

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?

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

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)

Slide 123

Slide 123 text

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)

Slide 124

Slide 124 text

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)

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

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)

Slide 127

Slide 127 text

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)

Slide 128

Slide 128 text

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)

Slide 129

Slide 129 text

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)

Slide 130

Slide 130 text

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)

Slide 131

Slide 131 text

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)

Slide 132

Slide 132 text

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)

Slide 133

Slide 133 text

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)

Slide 134

Slide 134 text

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)

Slide 135

Slide 135 text

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)

Slide 136

Slide 136 text

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)

Slide 137

Slide 137 text

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)

Slide 138

Slide 138 text

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

Slide 139

Slide 139 text

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"

Slide 140

Slide 140 text

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

Slide 141

Slide 141 text

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

Slide 142

Slide 142 text

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

Slide 143

Slide 143 text

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

Slide 144

Slide 144 text

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

Slide 145

Slide 145 text

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

Slide 146

Slide 146 text

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

Slide 147

Slide 147 text

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

Slide 148

Slide 148 text

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

Slide 149

Slide 149 text

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

Slide 150

Slide 150 text

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

Slide 151

Slide 151 text

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

Slide 152

Slide 152 text

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

Slide 153

Slide 153 text

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

Slide 154

Slide 154 text

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

Slide 155

Slide 155 text

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

Slide 156

Slide 156 text

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

Slide 157

Slide 157 text

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

Slide 158

Slide 158 text

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

Slide 159

Slide 159 text

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

Slide 160

Slide 160 text

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

Slide 161

Slide 161 text

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

Slide 162

Slide 162 text

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

Slide 163

Slide 163 text

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

Slide 164

Slide 164 text

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

Slide 165

Slide 165 text

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

Slide 166

Slide 166 text

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

Slide 167

Slide 167 text

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

Slide 168

Slide 168 text

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

Slide 169

Slide 169 text

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

Slide 170

Slide 170 text

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

Slide 171

Slide 171 text

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

Slide 172

Slide 172 text

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

Slide 173

Slide 173 text

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

Slide 174

Slide 174 text

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

Slide 175

Slide 175 text

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

Slide 176

Slide 176 text

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

Slide 177

Slide 177 text

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

Slide 178

Slide 178 text

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

Slide 179

Slide 179 text

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

Slide 180

Slide 180 text

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

Slide 181

Slide 181 text

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

Slide 182

Slide 182 text

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

Slide 183

Slide 183 text

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

Slide 184

Slide 184 text

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

Slide 185

Slide 185 text

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

Slide 186

Slide 186 text

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

Slide 187

Slide 187 text

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

Slide 188

Slide 188 text

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

Slide 189

Slide 189 text

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

Slide 190

Slide 190 text

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

Slide 191

Slide 191 text

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

Slide 192

Slide 192 text

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

Slide 193

Slide 193 text

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

Slide 194

Slide 194 text

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" }

Slide 195

Slide 195 text

🥵

Slide 196

Slide 196 text

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.

Slide 197

Slide 197 text

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

Slide 198

Slide 198 text

Recap

Slide 199

Slide 199 text

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.

Slide 200

Slide 200 text

Use derivedStateOf: to reduce invalidations to cache expensive calculations that can be performed lazily

Slide 201

Slide 201 text

Don't use derivedStateOf: if calculation is cheap if inputs change as frequently as outputs

Slide 202

Slide 202 text

Action Items: Open your codebase. Find at least one derivedStateOf that doesn't need to be there …or is used wrong. Remove it.

Slide 203

Slide 203 text

Questions? Opening the Shutter on Snapshots: bit.ly/3x5zH53 Slides: bit.ly/45cqHYl androiddev@zachklipp.com Snapshots blog series: bit.ly/3KAPprT androiddev.social/@zachklipp