Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Opening the shutter on snapshots (dcSF 23)

Opening the shutter on snapshots (dcSF 23)

Jetpack Compose shows the power of a custom compiler plugin. But not all the magic happens during compilation. A lot of Compose features are based on a runtime library that doesn't require any compiler support: the snapshot system. It might seem like magic at first, but it's just built on top of things you might already know: ThreadLocals, linked lists, and, yes, even regular old callbacks. Once you understand how Compose thinks about state, you might find ways to use its tools in your own code – even outside of Compose.

Zach Klippenstein

June 07, 2023
Tweet

More Decks by Zach Klippenstein

Other Decks in Programming

Transcript

  1. not this talk this talk snapshots Not snapshots intro: Low-level

    talk You don’t need to know this to use compose! Target: Used compose a bit, snapshots seem magic 2 Goals: - convince you they’re not magic - give you enough context to read impl code Questions: might not have time, tweet me
  2. What for? What do they do? How do they work?

    What can we do with them? Before talking about snapshots, I want to talk about fi les.
  3. Git Capture state of codebase Edit fi les without worrying

    about others When ready, discard or merge branches Deal with con fl icts at merge
  4. Snapshots are Git for your variables Git for your variables

    Layout, event handlers are the programmers Capture state of program Edit values without worrying about others When ready, discard or apply snapshots
  5. Composition is the release manager Composition needs to see a

    consistent state Next: Other similarities…
  6. Git Snapshots branches snapshots main branch global snapshot commits state

    object writes merge apply snapshot resolve conflicts merge strategy hooks read, write, apply listeners Discuss comparisons There’s a word for this set of features, from DBs…
  7. AC ID solation On-disk “a”, Red “a”, Blue “b”, Red

    “b”, Blue onsistency urability Don’t see other snapshots changes while you’re working, even across threads.
  8. How do they work? That’s a lot of features How

    do they even work? Must be… {next: magic}
  9. Compiler plugin? from A Hitchhiker’s Guide to Jetpack Compose Compose

    uses a compiler plugin Are they part of that?
  10. (but shipped with it) Snapshots ≠ Compose Library within compose

    Fundamental to compose: Composition needs a consistent view of state while it’s running Observe changes Decoupled Only a few fi les
  11. (but shipped with it) Snapshots ≠ Compose snapshots the rest

    of Compose ThreadLocal integers linked lists Library within compose Fundamental to compose: Composition needs a consistent view of state while it’s running Observe changes Decoupled Only a few fi les Again, no magic
  12. Giant state container? (i.e. Redux) snapA “a” “foo” Range(0, 100)

    snapB “a” “bar” Range(0, 50) snapC “b” “foo” Range(25, 75) all state in app Giant state container? Redux, at least conceptually Immutable Lots of memory Lots of copying - bad for animation Awkward API - think every var
  13. mutableStateOf mutableStateOf Range “a” “b” “foo” “bar” snapA snapB snapC

    Instead of state in snapshot, snapshot in state “Snapshots” are metadata Compare with previous slide Snapshots DO NOT contain pointers to state…
  14. Snapshots State Objects records mutableStateOf mutableStateOf Range “a” “b” “foo”

    “bar” snapA snapB snapC The parts of a state are called “records” Snapshots know which record to look at Cool properties: Create & apply is cheap: just metadata Quickly capture/apply every piece of state in the world Change data is cheap: Only touch state object in question API is clean
  15. reads: 96 snapshots records 128 167 181 64 75 111

    209 52 53 87 many : state “foo” Many snapshots can select a single record New snapshots always select existing records No pointers - Inside a snapshot, which record you’ll “see”
  16. writes: : “bar” 210 96 128 167 181 64 75

    111 209 52 53 87 state “foo” A record may have no writers or exactly 1 First write: Copy record, reuse if possible. Subsequent writes: Write directly to record
  17. Snapshots concepts State Objects low-level API concrete ID previous IDs

    invalid set primary API abstract map of IDs to values written by snapshot with ID i.e. “records” API: low vs high Concrete vs abstract All snapshots have unique ID State records have ID
  18. Snapshots API State Objects takeNestedSnapshot takeNestedMutableSnapshot withMutableSnapshot registerApplyObserver …and more

    val state = mutableStateOf(“”) 
 mutableStateListOf mutableStateMapOf mutableIntStateOf …whatever! State objects: Library ships with some Can de fi ne custom New: mutableIntStateOf
  19. Snapshots operations State Objects Create: choose new ID Enter: set

    ThreadLocal restore ThreadLocal Apply: commit ID notify listeners Read: find record read from record Write: find or copy record write to record discuss Next: example of relation
  20. Selecting record for read message id value “he” “hello asdf”

    tombstoned “hello ” written by 72 “hello world” color id value Red Blue written by 80 snapshot ID invalid set Walk through every selection Next: this rule is…
  21. 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 The snapshot is a query against the record list of a state object. Instead of looking in snapshot for state object, snapshot looks in state object Next: snapshot IDs
  22. Snapshot IDs Git: commit SHAs Back to the git metaphor,

    Git has SHAs, snapshots have IDs {tap} Every snapshot has a unique ID Each merge point also has an ID IDs not arbitrary Monotonically increasing Clock, order of creation new snapshot next highest ID Next: snapshot example
  23. Walkthrough: create child, apply val message = mutableStateOf("") val snapshot

    = Snapshot.takeMutableSnapshot() snapshot.enter { message.value = "hello" } snapshot.apply() message Won’t show exactly as code works Trying to give high-level idea, context for reading code
  24. Walkthrough: create child, apply message val message = mutableStateOf("") “”

    global snapshot Start: global snapshot {tap} Write initial record Next: create child
  25. Walkthrough: create child, apply message val snapshot = Snapshot.takeMutableSnapshot() “”

    global snapshot snapshot Child sees same record Next: advance parent
  26. Walkthrough: create child, apply message val snapshot = Snapshot.takeMutableSnapshot() “”

    global snapshot snapshot Advanced parent: child won’t see parent writes Add IDs between old and new (exclusive) to invalid set This is a superset of all open snapshots Next: update invalid
  27. Walkthrough: create child, apply message val snapshot = Snapshot.takeMutableSnapshot() “”

    global snapshot snapshot Done creating child Didn’t touch state object Next: write
  28. Walkthrough: create child, apply message message.value = "hello" “hello” “”

    global snapshot snapshot Create new record to write Parent won’t see it because invalid set Done: Didn’t touch snapshots Next: apply
  29. Walkthrough: create child, apply message snapshot.apply() “” “hello” global snapshot

    snapshot First thing: Move ID to parent’s history Parent “becomes” child
  30. Walkthrough: create child, apply message snapshot.apply() “” “hello” global snapshot

    snapshot Remove from invalid Next: parent now sees child record
  31. Snapshot operations: Create child: 1. Create new snapshot 2. Copy

    parent’s invalid list 3. Advance parent – child’s ID will be added to invalid set (see Advance rule 3 below) Apply: 1. Advance parent 2. Remove child ID from invalid set – parent sees child’s writes 3. Add child’s current and previous IDs to parent’s list of previous IDs – parent “absorbs” child Advance: 1. New ID is next highest ID 2. Add old ID to list of previous IDs 3. Add IDs between old and new (exclusive) to invalid set This is a superset of all open snapshots i.e. Unrelated snapshots created since old ID should be ignored Reference – will move on to next example Suggest taking a picture
  32. Walkthrough: create 2 children, apply val message = mutableStateOf("") val

    color = mutableStateOf(Red) val snapshotA = Snapshot.takeMutableSnapshot() val snapshotB = Snapshot.takeMutableSnapshot() snapshotA.enter { message.value = "hello" } snapshotB.enter { color.value = Blue } snapshotA.apply() snapshotB.apply()
  33. Walkthrough: create 2 children, apply val message = mutableStateOf("") val

    color = mutableStateOf(Red) message “” color Red global snapshot Global snapshot, initial writes
  34. Walkthrough: create 2 children, apply val snapshotA = Snapshot.takeMutableSnapshot() message

    “” color Red snapshotA global snapshot New child, same as before
  35. Walkthrough: create 2 children, apply val snapshotA = Snapshot.takeMutableSnapshot() message

    “” color Red snapshotA global snapshot Next: Create 2nd child
  36. Walkthrough: create 2 children, apply message “” color Red snapshotA

    global snapshot val snapshotB = Snapshot.takeMutableSnapshot() snapshotB Follow same rules Also sees initial records Next: copy parent invalid set
  37. Walkthrough: create 2 children, apply message “” color Red snapshotA

    global snapshot val snapshotB = Snapshot.takeMutableSnapshot() snapshotB Children are isolated from each other Next: advance parent
  38. Walkthrough: create 2 children, apply message “” color Red snapshotA

    global snapshot val snapshotB = Snapshot.takeMutableSnapshot() snapshotB Next: add IDs between old & new to invalid set
  39. Walkthrough: create 2 children, apply message “” color Red snapshotA

    global snapshot val snapshotB = Snapshot.takeMutableSnapshot() snapshotB Next: snapshotA writes
  40. Walkthrough: create 2 children, apply message “” color Red snapshotA

    global snapshot snapshotB snapshotA.enter { message.value = "hello" }
  41. Walkthrough: create 2 children, apply message “” color Red snapshotA

    global snapshot snapshotB “hello” snapshotA.enter { message.value = "hello" } Next: snapshot B writes
  42. Walkthrough: create 2 children, apply message “” color Red snapshotA

    global snapshot snapshotB “hello” snapshotB.enter { color.value = Blue }
  43. Walkthrough: create 2 children, apply message “” color Red snapshotA

    global snapshot snapshotB “hello” Blue snapshotB.enter { color.value = Blue } Again, writes don’t touch snapshots Next: apply A
  44. Walkthrough: create 2 children, apply message “” color Red snapshotA

    global snapshot snapshotB “hello” Blue snapshotA.apply() Next: move ID to parent history
  45. Walkthrough: create 2 children, apply message “” color Red snapshotA

    global snapshot snapshotB “hello” Blue snapshotA.apply() Next: remove from invalid
  46. Walkthrough: create 2 children, apply message “” color Red global

    snapshot snapshotB “hello” Blue snapshotA.apply() See A’s write Next: advance parent
  47. Walkthrough: create 2 children, apply message “” color Red global

    snapshot snapshotA.apply() snapshotB “hello” Blue Next: apply B
  48. Walkthrough: create 2 children, apply message “” color Red global

    snapshot snapshotB.apply() snapshotB “hello” Blue Next: move ID to parent’s history
  49. Walkthrough: create 2 children, apply message “” color Red global

    snapshot snapshotB.apply() snapshotB “hello” Blue
  50. Walkthrough: create 2 children, apply message “” color Red global

    snapshot snapshotB.apply() “hello” Blue See B’s write. Next: advance parent, and done
  51. Walkthrough: create 2 children, apply message “” color Red global

    snapshot snapshotB.apply() “hello” Blue
  52. Walkthrough: create 2 children, apply message “” color Red global

    snapshot “hello” Blue snapshotB.apply() merged! Next: induction/nesting
  53. Walkthrough: created nested child, apply val message = mutableStateOf("") val

    color = mutableStateOf(Red) val snapshotA = Snapshot.takeMutableSnapshot() snapshotA.enter { message.value = "hello" val snapshotB = Snapshot.takeMutableSnapshot() snapshotB.enter { color.value = Blue } snapshotB.apply() } snapshotA.apply() Discuss: Induction! There is nothing special here
  54. val message = mutableStateOf("") val color = mutableStateOf(Red) message “”

    color Red global snapshot Walkthrough: created nested child, apply
  55. val snapshotA = Snapshot.takeMutableSnapshot() message “” color Red snapshotA global

    snapshot Walkthrough: created nested child, apply Next: A writes
  56. snapshotA.enter { message.value = "hello" message “” color Red snapshotA

    global snapshot Walkthrough: created nested child, apply
  57. snapshotA.enter { message.value = "hello" message “” color Red snapshotA

    global snapshot “hello” Walkthrough: created nested child, apply Next: create nested snapshot
  58. snapshotA.enter { val snapshotB = Snapshot.takeMutableSnapshot() message “” color Red

    snapshotA global snapshot snapshotB “hello” Walkthrough: created nested child, apply B can see A’s write because it’s lower ID Next: advance parent
  59. snapshotA.enter { val snapshotB = Snapshot.takeMutableSnapshot() message “” color Red

    snapshotA global snapshot snapshotB “hello” Walkthrough: created nested child, apply Next: update invalid
  60. snapshotA.enter { val snapshotB = Snapshot.takeMutableSnapshot() message “” color Red

    snapshotA global snapshot snapshotB “hello” Walkthrough: created nested child, apply This time we get parent AND child Discuss isolation Next: B writes
  61. snapshotA.enter { snapshotB.enter { color.value = Blue } message “”

    color Red snapshotA global snapshot snapshotB “hello” Walkthrough: created nested child, apply
  62. snapshotA.enter { snapshotB.enter { color.value = Blue } message “”

    color Red snapshotA global snapshot snapshotB “hello” Blue Walkthrough: created nested child, apply Next: apply B
  63. snapshotA.enter { snapshotB.apply() } message “” color Red snapshotA global

    snapshot snapshotB “hello” Blue Walkthrough: created nested child, apply
  64. snapshotA.enter { snapshotB.apply() } message “” color Red snapshotA global

    snapshot snapshotB “hello” Blue Walkthrough: created nested child, apply Next: remove invalid
  65. snapshotA.enter { snapshotB.apply() } message “” color Red snapshotA global

    snapshot “hello” Blue Walkthrough: created nested child, apply Can see write Next: advance parent
  66. snapshotA.enter { snapshotB.apply() } message “” color Red snapshotA global

    snapshot “hello” Blue Walkthrough: created nested child, apply Next: apply A
  67. message “” color Red snapshotA global snapshot “hello” Blue snapshotA.apply()

    Walkthrough: created nested child, apply Next: move current AND previous IDs to parent
  68. snapshotA.apply() message “” color Red snapshotA global snapshot “hello” Blue

    Walkthrough: created nested child, apply Next: remove invalid and see A’s write
  69. snapshotA.apply() message “” color Red global snapshot “hello” Blue Walkthrough:

    created nested child, apply No new invalids: all IDs in history Next: see B’s write
  70. snapshotA.apply() message “” color Red global snapshot “hello” Blue merged!

    Walkthrough: created nested child, apply Done: Snapshots + state objects How are state objects implemented?
  71. MutableState message “” “hello” interface MutableState<T> : State<T> { override

    var value: T operator fun component1(): T operator fun component2(): (T) -> Unit } MutableState: an interface
  72. MutableState message “” “hello” interface MutableState<T> : State<T> { override

    var value: T operator fun component1(): T operator fun component2(): (T) -> Unit } Component funs are sugar, ignore
  73. SnapshotMutableState MutableState message “” “hello” interface SnapshotMutableState<T> : MutableState<T> {

    val policy: SnapshotMutationPolicy<T> } interface MutableState<T> : State<T> { override var value: T operator fun component1(): T operator fun component2(): (T) -> Unit } : Still not concrete Interface adds mutation policy
  74. SnapshotMutableStateImpl SnapshotMutableState message “” “hello” internal class SnapshotMutableStateImpl<T>( value: T,

    override val policy: SnapshotMutationPolicy<T> ) : StateObject, SnapshotMutableState<T> { override var value: T get() = TODO("???") set(value) { TODO("???") } } interface SnapshotMutableState<T> : MutableState<T> { val policy: SnapshotMutationPolicy<T> } interface MutableState<T> : State<T> { override var value: T operator fun component1(): T operator fun component2(): (T) -> Unit } : Now concrete Implements 1 other interface Next: StateObject
  75. SnapshotMutableStateImpl StateObject message “” “hello” internal class SnapshotMutableStateImpl<T>( value: T,

    override val policy: SnapshotMutationPolicy<T> ) : StateObject, SnapshotMutableState<T> { override var value: T get() = TODO("???") set(value) { TODO("???") } } interface StateObject { val firstStateRecord: StateRecord fun prependStateRecord(value: StateRecord) fun mergeRecords( previous: StateRecord, current: StateRecord, applied: StateRecord ): StateRecord? = null } interface SnapshotMutableState<T> : MutableState<T> { val policy: SnapshotMutationPolicy<T> } interface MutableState<T> : State<T> { override var value: T operator fun component1(): T operator fun component2(): (T) -> Unit } : discuss members
  76. message “” “hello” interface StateObject { val firstStateRecord: StateRecord fun

    prependStateRecord(value: StateRecord) fun mergeRecords( previous: StateRecord, current: StateRecord, applied: StateRecord ): StateRecord? = null } interface SnapshotMutableState<T> : MutableState<T> { val policy: SnapshotMutationPolicy<T> } interface MutableState<T> : State<T> { override var value: T operator fun component1(): T operator fun component2(): (T) -> Unit } internal class SnapshotMutableStateImpl<T>( value: T, override val policy: SnapshotMutationPolicy<T> ) : StateObject, SnapshotMutableState<T> { override var value: T get() = TODO("???") set(value) { TODO("???") } } SnapshotMutableStateImpl StateObject : Implement required
  77. message “” “hello” interface StateObject { val firstStateRecord: StateRecord fun

    prependStateRecord(value: StateRecord) fun mergeRecords( previous: StateRecord, current: StateRecord, applied: StateRecord ): StateRecord? = null } interface SnapshotMutableState<T> : MutableState<T> { val policy: SnapshotMutationPolicy<T> } interface MutableState<T> : State<T> { override var value: T operator fun component1(): T operator fun component2(): (T) -> Unit } internal class SnapshotMutableStateImpl<T>( value: T, override val policy: SnapshotMutationPolicy<T> ) : StateObject, SnapshotMutableState<T> { override var value: T get() = TODO("???") set(value) { TODO("???") } override val firstStateRecord: StateRecord get() = TODO("???") override fun prependStateRecord(value: StateRecord) { TODO("???") } } SnapshotMutableStateImpl StateObject : Got basic shape, Next: Need a record!
  78. internal class SnapshotMutableStateImpl<T>( value: T, override val policy: SnapshotMutationPolicy<T> )

    : StateObject, SnapshotMutableState<T> { override var value: T get() = TODO("???") set(value) { TODO("???") } override val firstStateRecord: StateRecord get() = TODO("???") override fun prependStateRecord(value: StateRecord) { TODO("???") } } Create private record class Subclass StateRecord Internal linked list
  79. internal class SnapshotMutableStateImpl<T>( value: T, override val policy: SnapshotMutationPolicy<T> )

    : StateObject, SnapshotMutableState<T> { override var value: T get() = TODO("???") set(value) { TODO("???") } override val firstStateRecord: StateRecord get() = TODO("???") override fun prependStateRecord(value: StateRecord) { TODO("???") } private class StateStateRecord<T>(myValue: T) : StateRecord() { var value: T = myValue override fun create(): StateRecord = TODO("???") override fun assign(value: StateRecord) { TODO("???") } } } Create private record class Subclass StateRecord Internal linked list
  80. internal class SnapshotMutableStateImpl<T>( value: T, override val policy: SnapshotMutationPolicy<T> )

    : StateObject, SnapshotMutableState<T> { override var value: T get() = TODO("???") set(value) { TODO("???") } override val firstStateRecord: StateRecord get() = TODO("???") override fun prependStateRecord(value: StateRecord) { TODO("???") } private class StateStateRecord<T>(myValue: T) : StateRecord() { var value: T = myValue override fun create(): StateRecord = StateStateRecord(value) override fun assign(value: StateRecord) { TODO("???") } } } Discuss create
  81. internal class SnapshotMutableStateImpl<T>( value: T, override val policy: SnapshotMutationPolicy<T> )

    : StateObject, SnapshotMutableState<T> { override var value: T get() = TODO("???") set(value) { TODO("???") } override val firstStateRecord: StateRecord get() = TODO("???") override fun prependStateRecord(value: StateRecord) { TODO("???") } private class StateStateRecord<T>(myValue: T) : StateRecord() { var value: T = myValue override fun create(): StateRecord = StateStateRecord(value) override fun assign(value: StateRecord) { this.value = (value as StateStateRecord<T>).value } } } Discuss assign Next: Need to store record somewhere
  82. internal class SnapshotMutableStateImpl<T>( value: T, override val policy: SnapshotMutationPolicy<T> )

    : StateObject, SnapshotMutableState<T> { private var next: StateStateRecord<T> = StateStateRecord(value) override var value: T get() = TODO("???") set(value) { TODO("???") } override val firstStateRecord: StateRecord get() = TODO(“???") override fun prependStateRecord(value: StateRecord) { TODO("???") } private class StateStateRecord<T>(myValue: T) : StateRecord() { var value: T = myValue override fun create(): StateRecord = StateStateRecord(value) override fun assign(value: StateRecord) { this.value = (value as StateStateRecord<T>).value } } } Create private var Next: wire up StateObject linked list…
  83. internal class SnapshotMutableStateImpl<T>( value: T, override val policy: SnapshotMutationPolicy<T> )

    : StateObject, SnapshotMutableState<T> { private var next: StateStateRecord<T> = StateStateRecord(value) override var value: T get() = TODO("???") set(value) { TODO("???") } override val firstStateRecord: StateRecord get() = next override fun prependStateRecord(value: StateRecord) { TODO("???") } private class StateStateRecord<T>(myValue: T) : StateRecord() { var value: T = myValue override fun create(): StateRecord = StateStateRecord(value) override fun assign(value: StateRecord) { this.value = (value as StateStateRecord<T>).value } } } Getter for head of list
  84. internal class SnapshotMutableStateImpl<T>( value: T, override val policy: SnapshotMutationPolicy<T> )

    : StateObject, SnapshotMutableState<T> { private var next: StateStateRecord<T> = StateStateRecord(value) override var value: T get() = TODO("???") set(value) { TODO("???") } override val firstStateRecord: StateRecord get() = next override fun prependStateRecord(value: StateRecord) { next = value as StateStateRecord<T> } private class StateStateRecord<T>(myValue: T) : StateRecord() { var value: T = myValue override fun create(): StateRecord = StateStateRecord(value) override fun assign(value: StateRecord) { this.value = (value as StateStateRecord<T>).value } } } Add to list That’s it - system links nodes together Next: impl value property
  85. internal class SnapshotMutableStateImpl<T>( value: T, override val policy: SnapshotMutationPolicy<T> )

    : StateObject, SnapshotMutableState<T> { private var next: StateStateRecord<T> = StateStateRecord(value) override var value: T get() = next.value set(value) { TODO("???") } override val firstStateRecord: StateRecord get() = next override fun prependStateRecord(value: StateRecord) { next = value as StateStateRecord<T> } private class StateStateRecord<T>(myValue: T) : StateRecord() { var value: T = myValue override fun create(): StateRecord = StateStateRecord(value) override fun assign(value: StateRecord) { this.value = (value as StateStateRecord<T>).value } } } Get the latest record Read from it Not quite: Need to select the correct record for snapshot
  86. internal class SnapshotMutableStateImpl<T>( value: T, override val policy: SnapshotMutationPolicy<T> )

    : StateObject, SnapshotMutableState<T> { private var next: StateStateRecord<T> = StateStateRecord(value) override var value: T get() = next.readable(this).value set(value) { TODO("???") } override val firstStateRecord: StateRecord get() = next override fun prependStateRecord(value: StateRecord) { next = value as StateStateRecord<T> } private class StateStateRecord<T>(myValue: T) : StateRecord() { var value: T = myValue override fun create(): StateRecord = StateStateRecord(value) override fun assign(value: StateRecord) { this.value = (value as StateStateRecord<T>).value } } } Extension function on StateRecord Reads ThreadLocal, returns record Noti fi es read observers
  87. internal class SnapshotMutableStateImpl<T>( value: T, override val policy: SnapshotMutationPolicy<T> )

    : StateObject, SnapshotMutableState<T> { private var next: StateStateRecord<T> = StateStateRecord(value) override var value: T get() = next.readable(this).value set(value) { next.writable(this) { this.value = value } } override val firstStateRecord: StateRecord get() = next override fun prependStateRecord(value: StateRecord) { next = value as StateStateRecord<T> } private class StateStateRecord<T>(myValue: T) : StateRecord() { var value: T = myValue override fun create(): StateRecord = StateStateRecord(value) override fun assign(value: StateRecord) { this.value = (value as StateStateRecord<T>).value } } } Write: similar function Passes to lambda instead (synchronization) Noti fi es write observers. For a basic impl, done. Next: mergeRecords
  88. internal class SnapshotMutableStateImpl<T>( value: T, override val policy: SnapshotMutationPolicy<T> )

    : StateObject, SnapshotMutableState<T> { private var next: StateStateRecord<T> = StateStateRecord(value) override var value: T get() = next.readable(this).value set(value) { next.writable(this) { this.value = value } } override val firstStateRecord: StateRecord get() = next override fun prependStateRecord(value: StateRecord) { next = value as StateStateRecord<T> } private class StateStateRecord<T>(myValue: T) : StateRecord() { var value: T = myValue override fun create(): StateRecord = StateStateRecord(value) override fun assign(value: StateRecord) { this.value = (value as StateStateRecord<T>).value } } } interface StateObject { val firstStateRecord: StateRecord fun prependStateRecord(value: StateRecord) fun mergeRecords( previous: StateRecord, current: StateRecord, applied: StateRecord ): StateRecord? = null } StateObject has one optional method Called when con fl icting writes This is how MutableState impls mutation policy
  89. internal class SnapshotMutableStateImpl<T>( value: T, override val policy: SnapshotMutationPolicy<T> )

    : StateObject, SnapshotMutableState<T> { override fun mergeRecords( previous: StateRecord, current: StateRecord, applied: StateRecord ): StateRecord? { } } Override
  90. internal class SnapshotMutableStateImpl<T>( value: T, override val policy: SnapshotMutationPolicy<T> )

    : StateObject, SnapshotMutableState<T> { override fun mergeRecords( previous: StateRecord, current: StateRecord, applied: StateRecord ): StateRecord? { val previousRecord = previous as StateStateRecord<T> val currentRecord = current as StateStateRecord<T> val appliedRecord = applied as StateStateRecord<T> return if (policy.equivalent(currentRecord.value, appliedRecord.value)) current else { val merged = policy.merge( previousRecord.value, currentRecord.value, appliedRecord.value ) if (merged != null) { appliedRecord.create().also { (it as StateStateRecord<T>).value = merged } } else { null } } } } Implement Equal vs equivalent Delegate merge Return null if can’t resolve
  91. Custom state objects class Range { var start: Int =

    0 var end: Int = 0 } Custom mutable int range Implement like MutableState Next: De fi ne Record class - GO FAST next 3 slides!!
  92. Custom state objects class Range { private var next =

    Record() var start: Int = 0 var end: Int = 0 private class Record : StateRecord() { var start: Int = 0 var end: Int = 0 override fun create(): StateRecord = Record() override fun assign(value: StateRecord) { value as Record start = value.start end = value.end } } } Next: wire up start/end properties
  93. Custom state objects class Range { private var next =

    Record() var start: Int get() = next.readable(this).start set(value) { next.writable(this) { start = value } } var end: Int get() = next.readable(this).end set(value) { next.writable(this) { end = value } } private class Record : StateRecord() { var start: Int = 0 var end: Int = 0 override fun create(): StateRecord = Record() override fun assign(value: StateRecord) { value as Record start = value.start end = value.end } } } Next: Implement StateObject
  94. Custom state objects class Range : StateObject { private var

    next = Record() var start: Int get() = next.readable(this).start set(value) { next.writable(this) { start = value } } var end: Int get() = next.readable(this).end set(value) { next.writable(this) { end = value } } override val firstStateRecord: StateRecord get() = next override fun prependStateRecord(value: StateRecord) { next = value as Record } private class Record : StateRecord() { var start: Int = 0 var end: Int = 0 override fun create(): StateRecord = Record() override fun assign(value: StateRecord) { value as Record start = value.start end = value.end } } } Done Now let’s validate start ≤ end {tap} In the setters Next: withCurrent
  95. Custom state objects class Range : StateObject { private var

    next = Record() var start: Int get() = next.readable(this).start set(value) { require(value <= next.readable(this).end) next.writable(this) { start = value } } var end: Int get() = next.readable(this).end set(value) { require(value >= next.readable(this).start) next.writable(this) { end = value } } override val firstStateRecord: StateRecord get() = next override fun prependStateRecord(value: StateRecord) { next = value as Record } private class Record : StateRecord() { var start: Int = 0 var end: Int = 0 override fun create(): StateRecord = Record() override fun assign(value: StateRecord) { value as Record start = value.start end = value.end } } } We need to get the current value Use readable? Discuss observer implications Instead, use withCurrent…
  96. Custom state objects class Range : StateObject { private var

    next = Record() var start: Int get() = next.readable(this).start set(value) { next.withCurrent { require(value <= it.end) } next.writable(this) { start = value } } var end: Int get() = next.readable(this).end set(value) { next.withCurrent { require(value >= it.start) } next.writable(this) { end = value } } override val firstStateRecord: StateRecord get() = next override fun prependStateRecord(value: StateRecord) { next = value as Record } private class Record : StateRecord() { var start: Int = 0 var end: Int = 0 override fun create(): StateRecord = Record() override fun assign(value: StateRecord) { value as Record start = value.start end = value.end } } } withCurrent: like readable, but doesn’t track read
  97. Custom state objects class Range : StateObject { override fun

    mergeRecords( previous: StateRecord, current: StateRecord, applied: StateRecord ): StateRecord? { } }
  98. Custom state objects class Range : StateObject { override fun

    mergeRecords( previous: StateRecord, current: StateRecord, applied: StateRecord ): StateRecord? { val previousRecord = previous as Record val currentRecord = current as Record val appliedRecord = applied as Record if (currentRecord.start == appliedRecord.start && currentRecord.end == appliedRecord.end ) { return current } } } No con fl icts: trivial
  99. Custom state objects class Range : StateObject { override fun

    mergeRecords( previous: StateRecord, current: StateRecord, applied: StateRecord ): StateRecord? { val previousRecord = previous as Record val currentRecord = current as Record val appliedRecord = applied as Record if (currentRecord.start == appliedRecord.start && currentRecord.end == appliedRecord.end ) { return current } val currentChangedStart = previousRecord.start != currentRecord.start val currentChangedEnd = previousRecord.end != currentRecord.end val appliedChangedStart = previousRecord.start != appliedRecord.start val appliedChangedEnd = previousRecord.end != appliedRecord.end if ((currentChangedStart && appliedChangedStart) || (currentChangedEnd && appliedChangedEnd) ) { return null } } } Di ff erent snapshots wrote same properties: Can’t resolve
  100. Custom state objects class Range : StateObject { override fun

    mergeRecords( previous: StateRecord, current: StateRecord, applied: StateRecord ): StateRecord? { val previousRecord = previous as Record val currentRecord = current as Record val appliedRecord = applied as Record if (currentRecord.start == appliedRecord.start && currentRecord.end == appliedRecord.end ) { return current } val currentChangedStart = previousRecord.start != currentRecord.start val currentChangedEnd = previousRecord.end != currentRecord.end val appliedChangedStart = previousRecord.start != appliedRecord.start val appliedChangedEnd = previousRecord.end != appliedRecord.end if ((currentChangedStart && appliedChangedStart) || (currentChangedEnd && appliedChangedEnd) ) { return null } return Record().apply { start = if (currentChangedStart) currentRecord.start else appliedRecord.start end = if (currentChangedEnd) currentRecord.end else appliedRecord.end } } } Actual con fl ict resolve: Di ff erent snapshots wrote di ff erent properties Next: State di ff i ng
  101. State diffing suspend fun withAnimation(changes: () -> Unit) { val

    statesToAnimate = mutableSetOf<Any>() val targetValues = mutableMapOf<Any, Any>() val snapshot = Snapshot.takeMutableSnapshot( writeObserver = { stateObject -> statesToAnimate += stateObject } ) snapshot.enter { changes() statesToAnimate.forEach { stateObject -> targetValues[stateObject] = stateObject.value } } snapshot.dispose() coroutineScope { statesToAnimate.forEach { stateObject -> launch { animate(stateObject.value, targetValues[stateObject]) } } } } suspend fun, because animations Next: takeMutableSnapshot
  102. State diffing suspend fun withAnimation(changes: () -> Unit) { val

    statesToAnimate = mutableSetOf<Any>() val targetValues = mutableMapOf<Any, Any>() val snapshot = Snapshot.takeMutableSnapshot( writeObserver = { stateObject -> statesToAnimate += stateObject } ) snapshot.enter { changes() statesToAnimate.forEach { stateObject -> targetValues[stateObject] = stateObject.value } } snapshot.dispose() coroutineScope { statesToAnimate.forEach { stateObject -> launch { animate(stateObject.value, targetValues[stateObject]) } } } } Take child snapshot Record every state object written to
  103. State diffing suspend fun withAnimation(changes: () -> Unit) { val

    statesToAnimate = mutableSetOf<Any>() val targetValues = mutableMapOf<Any, Any>() val snapshot = Snapshot.takeMutableSnapshot( writeObserver = { stateObject -> statesToAnimate += stateObject } ) snapshot.enter { changes() statesToAnimate.forEach { stateObject -> targetValues[stateObject] = stateObject.value } } snapshot.dispose() coroutineScope { statesToAnimate.forEach { stateObject -> launch { animate(stateObject.value, targetValues[stateObject]) } } } } Run change function in snapshot Isolation No other snapshots see state changes Record fi nal target values
  104. State diffing suspend fun withAnimation(changes: () -> Unit) { val

    statesToAnimate = mutableSetOf<Any>() val targetValues = mutableMapOf<Any, Any>() val snapshot = Snapshot.takeMutableSnapshot( writeObserver = { stateObject -> statesToAnimate += stateObject } ) snapshot.enter { changes() statesToAnimate.forEach { stateObject -> targetValues[stateObject] = stateObject.value } } snapshot.dispose() coroutineScope { statesToAnimate.forEach { stateObject -> launch { animate(stateObject.value, targetValues[stateObject]) } } } } Don’t apply change immediately, animate later
  105. State diffing suspend fun withAnimation(changes: () -> Unit) { val

    statesToAnimate = mutableSetOf<Any>() val targetValues = mutableMapOf<Any, Any>() val snapshot = Snapshot.takeMutableSnapshot( writeObserver = { stateObject -> statesToAnimate += stateObject } ) snapshot.enter { changes() statesToAnimate.forEach { stateObject -> targetValues[stateObject] = stateObject.value } } snapshot.dispose() coroutineScope { statesToAnimate.forEach { stateObject -> launch { animate(stateObject.value, targetValues[stateObject]) } } } } Launch animations for every object {tap} Key: Use snapshot to capture target values without actually changing anything
  106. Persistence “In-memory” tomicity onsistency solation urability ACID ACID without the

    D {tap} Maybe we can implement durability Next: Wrap a Room database
  107. Persistence fun observeUserAsState(name: String): MutableState<User?> { val cached = cachedStates.getOrPut(name)

    { val flow = users.findByName(name) val state = mutableStateOf<User?>(null) val job = scope.launch { flow.collect { state.value = it } } return@getOrPut UserState(state, job) } return cached.state } Read Skip details, just sketch Get a state for a key Next: getOrPut
  108. Persistence Read fun observeUserAsState(name: String): MutableState<User?> { val cached =

    cachedStates.getOrPut(name) { val flow = users.findByName(name) val state = mutableStateOf<User?>(null) val job = scope.launch { flow.collect { state.value = it } } return@getOrPut UserState(state, job) } return cached.state }
  109. Persistence Read fun observeUserAsState(name: String): MutableState<User?> { val cached =

    cachedStates.getOrPut(name) { val flow = users.findByName(name) val state = mutableStateOf<User?>(null) val job = scope.launch { flow.collect { state.value = it } } return@getOrPut UserState(state, job) } return cached.state } Get observable results from Room
  110. Persistence Read fun observeUserAsState(name: String): MutableState<User?> { val cached =

    cachedStates.getOrPut(name) { val flow = users.findByName(name) val state = mutableStateOf<User?>(null) val job = scope.launch { flow.collect { state.value = it } } return@getOrPut UserState(state, job) } return cached.state }
  111. Persistence Read fun observeUserAsState(name: String): MutableState<User?> { val cached =

    cachedStates.getOrPut(name) { val flow = users.findByName(name) val state = mutableStateOf<User?>(null) val job = scope.launch { flow.collect { state.value = it } } return@getOrPut UserState(state, job) } return cached.state } Add to cache
  112. Persistence Update Snapshot.registerApplyObserver { changedStateObjects, snapshot -> val changedDbStates =

    changedStateObjects.mapNotNull { if (it in cachedStates) it as MutableState<User?> else null } if (changedDbStates.isNotEmpty()) { scope.launch(Dispatchers.IO) { val readSnapshot = snapshot.takeNestedSnapshot() try { readSnapshot.enter { changedDbStates.forEach { (user, _) -> user?.let { users.updateUser(user) } } } } finally { readSnapshot.dispose() } } } } Discuss registerApplyObserver Discuss arguments Global snapshot where those objects were applied
  113. Persistence Update Snapshot.registerApplyObserver { changedStateObjects, snapshot -> val changedDbStates =

    changedStateObjects.mapNotNull { if (it in cachedStates) it as MutableState<User?> else null } if (changedDbStates.isNotEmpty()) { scope.launch(Dispatchers.IO) { val readSnapshot = snapshot.takeNestedSnapshot() try { readSnapshot.enter { changedDbStates.forEach { (user, _) -> user?.let { users.updateUser(user) } } } } finally { readSnapshot.dispose() } } } } Find objects vended from database
  114. Persistence Update Snapshot.registerApplyObserver { changedStateObjects, snapshot -> val changedDbStates =

    changedStateObjects.mapNotNull { if (it in cachedStates) it as MutableState<User?> else null } if (changedDbStates.isNotEmpty()) { scope.launch(Dispatchers.IO) { val readSnapshot = snapshot.takeNestedSnapshot() try { readSnapshot.enter { changedDbStates.forEach { (user, _) -> user?.let { users.updateUser(user) } } } } finally { readSnapshot.dispose() } } } } Going to do blocking IO
  115. Persistence Update Snapshot.registerApplyObserver { changedStateObjects, snapshot -> val changedDbStates =

    changedStateObjects.mapNotNull { if (it in cachedStates) it as MutableState<User?> else null } if (changedDbStates.isNotEmpty()) { scope.launch(Dispatchers.IO) { val readSnapshot = snapshot.takeNestedSnapshot() try { readSnapshot.enter { changedDbStates.forEach { (user, _) -> user?.let { users.updateUser(user) } } } } finally { readSnapshot.dispose() } } } } Can’t enter an applied snapshot, create a read-only child
  116. Persistence Update Snapshot.registerApplyObserver { changedStateObjects, snapshot -> val changedDbStates =

    changedStateObjects.mapNotNull { if (it in cachedStates) it as MutableState<User?> else null } if (changedDbStates.isNotEmpty()) { scope.launch(Dispatchers.IO) { val readSnapshot = snapshot.takeNestedSnapshot() try { readSnapshot.enter { changedDbStates.forEach { (user, _) -> user?.let { users.updateUser(user) } } } } finally { readSnapshot.dispose() } } } } Dispose to avoid leaks
  117. Persistence Update Snapshot.registerApplyObserver { changedStateObjects, snapshot -> val changedDbStates =

    changedStateObjects.mapNotNull { if (it in cachedStates) it as MutableState<User?> else null } if (changedDbStates.isNotEmpty()) { scope.launch(Dispatchers.IO) { val readSnapshot = snapshot.takeNestedSnapshot() try { readSnapshot.enter { changedDbStates.forEach { (user, _) -> user?.let { users.updateUser(user) } } } } finally { readSnapshot.dispose() } } } } Blocking calls
  118. Persistence Usage @Composable fun UserNameEditor(database: SnapshotDatabase) { val userState =

    remember(database) { database.observeUserAsState("Rhaenyra") } userState.value?.let { user -> BasicTextField( value = user.name, onValueChange = { userState.value = user.copy(name = it) } ) } }
  119. Persistence Usage @Composable fun UserNameEditor(database: SnapshotDatabase) { val userState =

    remember(database) { database.observeUserAsState("Rhaenyra") } userState.value?.let { user -> BasicTextField( value = user.name, onValueChange = { userState.value = user.copy(name = it) } ) } }
  120. Snapshots are Git for your variables If you remember, nothing

    else, snapshots are git for your variables
  121. Further reading: More info about implementing custom StateObjects: bit.ly/45NPxxH Whitepaper

    on multiversion concurrency control: bit.ly/43tHZyw Slides: bit.ly/45PDvnC
  122. Questions? @zachklipp.com More info about implementing custom StateObjects: bit.ly/45NPxxH Whitepaper

    on multiversion concurrency control: bit.ly/43tHZyw Slides: bit.ly/45PDvnC androiddev.social/@zachklipp