Share
Embed
Opening the shutter on snapshots (dcNYC 22)
Video: https://www.droidcon.com/2022/09/29/opening-the-shutter-on-snapshots/
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
September 02, 2022
Transcript
199 slides
-
@zachklipp Pre-watch: A Hitchhiker’s Guide to Compose Post-watch: Using Compose
Runtime to create a client library not this talk this talk snapshots -
@zachklipp Pre-watch: A Hitchhiker’s Guide to Compose Post-watch: Using Compose
Runtime to create a client library these slides my twitter not this talk this talk snapshots -
@zachklipp What for? What do they do? How do they
work? What can we do with them? -
@zachklipp Git Snapshots branches snapshots main branch global snapshot commits
state object writes merge apply snapshot resolve conflicts merge records hooks read, write, apply listeners -
@zachklipp AC ID solation On-disk “a”, Red “a”, Blue “b”,
Red “b”, Blue onsistency urability -
@zachklipp (but shipped with it) Snapshots ≠ Compose snapshots the
rest of Compose ThreadLocal integers linked lists -
@zachklipp 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 -
@zachklipp Snapshots State Objects mutableStateOf mutableStateOf Range “a” “b” “foo”
“bar” snapA snapB snapC -
@zachklipp Snapshots State Objects records mutableStateOf mutableStateOf Range “a” “b”
“foo” “bar” snapA snapB snapC -
@zachklipp reads: 96 snapshots records 128 167 181 64 75
111 209 52 53 87 many : state “foo” -
@zachklipp writes: : “bar” 210 96 128 167 181 64
75 111 209 52 53 87 state “foo” -
@zachklipp 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” -
@zachklipp Snapshots API State Objects takeNestedSnapshot takeNestedMutableSnapshot withMutableSnapshot registerApplyObserver val
state = mutableStateOf(“”) mutableStateListOf mutableStateMapOf …whatever! -
@zachklipp 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 -
@zachklipp 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 -
@zachklipp 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 -
@zachklipp 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 -
@zachklipp 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 -
@zachklipp 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 -
@zachklipp 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 -
@zachklipp 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 -
newest valid record @zachklipp “mutableState.value” get() = findRecord(SnapshotThreadLocal.get()) highest ID
not higher than snapshot ID AND not in snapshot invalid set AND not tombstoned -
@zachklipp Walkthrough: create child, apply val message = mutableStateOf("") val
snapshot = Snapshot.takeMutableSnapshot() snapshot.enter { message.value = "hello" } snapshot.apply() message -
@zachklipp Walkthrough: create child, apply message val snapshot = Snapshot.takeMutableSnapshot()
“” global snapshot snapshot -
@zachklipp Walkthrough: create child, apply message val snapshot = Snapshot.takeMutableSnapshot()
“” global snapshot snapshot -
@zachklipp Walkthrough: create child, apply message val snapshot = Snapshot.takeMutableSnapshot()
“” global snapshot snapshot -
@zachklipp Walkthrough: create child, apply message val snapshot = Snapshot.takeMutableSnapshot()
“” global snapshot snapshot -
@zachklipp Walkthrough: create child, apply message val snapshot = Snapshot.takeMutableSnapshot()
“” global snapshot snapshot -
@zachklipp Walkthrough: create child, apply message message.value = "hello" “”
global snapshot snapshot -
@zachklipp Walkthrough: create child, apply message message.value = "hello" “hello”
“” global snapshot snapshot -
@zachklipp Snapshot rules: 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 -
@zachklipp 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() -
@zachklipp Walkthrough: create 2 children, apply val message = mutableStateOf("")
val color = mutableStateOf(Red) message color global snapshot -
@zachklipp Walkthrough: create 2 children, apply val message = mutableStateOf("")
val color = mutableStateOf(Red) message “” color Red global snapshot -
@zachklipp Walkthrough: create 2 children, apply val snapshotA = Snapshot.takeMutableSnapshot()
message “” color Red snapshotA global snapshot -
@zachklipp Walkthrough: create 2 children, apply val snapshotA = Snapshot.takeMutableSnapshot()
message “” color Red snapshotA global snapshot -
@zachklipp Walkthrough: create 2 children, apply val snapshotA = Snapshot.takeMutableSnapshot()
message “” color Red snapshotA global snapshot -
@zachklipp Walkthrough: create 2 children, apply val snapshotA = Snapshot.takeMutableSnapshot()
message “” color Red snapshotA global snapshot -
@zachklipp Walkthrough: create 2 children, apply val snapshotA = Snapshot.takeMutableSnapshot()
message “” color Red snapshotA global snapshot -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
snapshotA global snapshot val snapshotB = Snapshot.takeMutableSnapshot() snapshotB -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
snapshotA global snapshot val snapshotB = Snapshot.takeMutableSnapshot() snapshotB -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
snapshotA global snapshot val snapshotB = Snapshot.takeMutableSnapshot() snapshotB -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
snapshotA global snapshot val snapshotB = Snapshot.takeMutableSnapshot() snapshotB -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
snapshotA global snapshot val snapshotB = Snapshot.takeMutableSnapshot() snapshotB -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
snapshotA global snapshot val snapshotB = Snapshot.takeMutableSnapshot() snapshotB -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
snapshotA global snapshot snapshotB snapshotA.enter { message.value = "hello" } -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
snapshotA global snapshot snapshotB snapshotA.enter { message.value = "hello" } -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
snapshotA global snapshot snapshotB “hello” snapshotA.enter { message.value = "hello" } -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
snapshotA global snapshot snapshotB “hello” snapshotB.enter { color.value = Blue } -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
snapshotA global snapshot snapshotB “hello” snapshotB.enter { color.value = Blue } -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
snapshotA global snapshot snapshotB “hello” Blue snapshotB.enter { color.value = Blue } -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
snapshotA global snapshot snapshotB “hello” Blue snapshotA.apply() -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
snapshotA global snapshot snapshotB “hello” Blue snapshotA.apply() -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
global snapshot snapshotB “hello” Blue snapshotA.apply() -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
global snapshot snapshotB “hello” Blue snapshotA.apply() -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
global snapshot snapshotB “hello” Blue snapshotA.apply() -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
global snapshot snapshotA.apply() snapshotB “hello” Blue -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
global snapshot snapshotA.apply() snapshotB “hello” Blue -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
global snapshot snapshotB.apply() snapshotB “hello” Blue -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
global snapshot snapshotB.apply() snapshotB “hello” Blue -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
global snapshot snapshotB.apply() “hello” Blue -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
global snapshot snapshotB.apply() “hello” Blue -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
global snapshot snapshotB.apply() “hello” Blue -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
global snapshot snapshotB.apply() “hello” Blue -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
global snapshot snapshotB.apply() “hello” Blue -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
global snapshot “hello” Blue snapshotB.apply() -
@zachklipp Walkthrough: create 2 children, apply message “” color Red
global snapshot “hello” Blue merged! -
@zachklipp 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() -
@zachklipp val message = mutableStateOf("") val color = mutableStateOf(Red) message
color global snapshot Walkthrough: created nested child, apply -
@zachklipp val message = mutableStateOf("") val color = mutableStateOf(Red) message
“” color Red global snapshot Walkthrough: created nested child, apply -
@zachklipp val snapshotA = Snapshot.takeMutableSnapshot() message “” color Red snapshotA
global snapshot Walkthrough: created nested child, apply -
@zachklipp val snapshotA = Snapshot.takeMutableSnapshot() message “” color Red snapshotA
global snapshot Walkthrough: created nested child, apply -
@zachklipp val snapshotA = Snapshot.takeMutableSnapshot() message “” color Red snapshotA
global snapshot Walkthrough: created nested child, apply -
@zachklipp val snapshotA = Snapshot.takeMutableSnapshot() message “” color Red snapshotA
global snapshot Walkthrough: created nested child, apply -
@zachklipp val snapshotA = Snapshot.takeMutableSnapshot() message “” color Red snapshotA
global snapshot Walkthrough: created nested child, apply -
@zachklipp snapshotA.enter { message.value = "hello" message “” color Red
snapshotA global snapshot Walkthrough: created nested child, apply -
@zachklipp snapshotA.enter { message.value = "hello" message “” color Red
snapshotA global snapshot Walkthrough: created nested child, apply -
@zachklipp snapshotA.enter { message.value = "hello" message “” color Red
snapshotA global snapshot “hello” Walkthrough: created nested child, apply -
@zachklipp snapshotA.enter { val snapshotB = Snapshot.takeMutableSnapshot() message “” color
Red snapshotA global snapshot snapshotB “hello” Walkthrough: created nested child, apply -
@zachklipp snapshotA.enter { val snapshotB = Snapshot.takeMutableSnapshot() message “” color
Red snapshotA global snapshot snapshotB “hello” Walkthrough: created nested child, apply -
@zachklipp snapshotA.enter { val snapshotB = Snapshot.takeMutableSnapshot() message “” color
Red snapshotA global snapshot snapshotB “hello” Walkthrough: created nested child, apply -
@zachklipp snapshotA.enter { val snapshotB = Snapshot.takeMutableSnapshot() message “” color
Red snapshotA global snapshot snapshotB “hello” Walkthrough: created nested child, apply -
@zachklipp snapshotA.enter { snapshotB.enter { color.value = Blue } message
“” color Red snapshotA global snapshot snapshotB “hello” Walkthrough: created nested child, apply -
@zachklipp snapshotA.enter { snapshotB.enter { color.value = Blue } message
“” color Red snapshotA global snapshot snapshotB “hello” Walkthrough: created nested child, apply -
@zachklipp snapshotA.enter { snapshotB.enter { color.value = Blue } message
“” color Red snapshotA global snapshot snapshotB “hello” Blue Walkthrough: created nested child, apply -
@zachklipp snapshotA.enter { snapshotB.apply() } message “” color Red snapshotA
global snapshot snapshotB “hello” Blue Walkthrough: created nested child, apply -
@zachklipp snapshotA.enter { snapshotB.apply() } message “” color Red snapshotA
global snapshot snapshotB “hello” Blue Walkthrough: created nested child, apply -
@zachklipp snapshotA.enter { snapshotB.apply() } message “” color Red snapshotA
global snapshot “hello” Blue Walkthrough: created nested child, apply -
@zachklipp snapshotA.enter { snapshotB.apply() } message “” color Red snapshotA
global snapshot “hello” Blue Walkthrough: created nested child, apply -
@zachklipp snapshotA.enter { snapshotB.apply() } message “” color Red snapshotA
global snapshot “hello” Blue Walkthrough: created nested child, apply -
@zachklipp snapshotA.enter { snapshotB.apply() } message “” color Red snapshotA
global snapshot “hello” Blue Walkthrough: created nested child, apply -
@zachklipp snapshotA.enter { snapshotB.apply() } message “” color Red snapshotA
global snapshot “hello” Blue Walkthrough: created nested child, apply -
@zachklipp message “” color Red snapshotA global snapshot “hello” Blue
snapshotA.apply() Walkthrough: created nested child, apply -
@zachklipp snapshotA.apply() message “” color Red snapshotA global snapshot “hello”
Blue Walkthrough: created nested child, apply -
@zachklipp snapshotA.apply() message “” color Red global snapshot “hello” Blue
Walkthrough: created nested child, apply -
@zachklipp snapshotA.apply() message “” color Red global snapshot “hello” Blue
Walkthrough: created nested child, apply -
@zachklipp snapshotA.apply() message “” color Red global snapshot “hello” Blue
Walkthrough: created nested child, apply -
@zachklipp snapshotA.apply() message “” color Red global snapshot “hello” Blue
Walkthrough: created nested child, apply -
@zachklipp snapshotA.apply() message “” color Red global snapshot “hello” Blue
Walkthrough: created nested child, apply -
@zachklipp snapshotA.apply() message “” color Red global snapshot “hello” Blue
Walkthrough: created nested child, apply -
@zachklipp snapshotA.apply() message “” color Red global snapshot “hello” Blue
Walkthrough: created nested child, apply -
@zachklipp message “” color Red global snapshot “hello” Blue merged!
Walkthrough: created nested child, apply -
@zachklipp MutableState message “” “hello” interface MutableState<T> : State<T> {
override var value: T operator fun component1(): T operator fun component2(): (T) -> Unit } -
@zachklipp SnapshotMutableState MutableState message “” “hello” interface SnapshotMutableState<T> : MutableState<T>
{ val policy: SnapshotMutationPolicy<T> } interface MutableState<T> : State<T> { -
@zachklipp 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> { -
@zachklipp SnapshotMutableStateImpl StateObject message “” “hello” internal class SnapshotMutableStateImpl<T>( val
firstStateRecord: StateRecord fun prependStateRecord(value: StateRecord) fun mergeRecords( previous: StateRecord, current: StateRecord, applied: StateRecord ): StateRecord? = null } interface SnapshotMutableState<T> : MutableState<T> { -
@zachklipp message “” “hello” interface StateObject { value: T, override
val policy: SnapshotMutationPolicy<T> ) : StateObject, SnapshotMutableState<T> { override var value: T get() = TODO("???") set(value) { TODO("???") } } SnapshotMutableStateImpl StateObject : -
@zachklipp message “” “hello” interface StateObject { 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 : -
@zachklipp 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("???") } } -
@zachklipp 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("???") } } } -
@zachklipp 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("???") } } } -
@zachklipp 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 } } } -
@zachklipp 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 } } } -
@zachklipp 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 } } } -
@zachklipp 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 } } } -
@zachklipp 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 } } } -
@zachklipp 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 } } } -
@zachklipp 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 } } } -
@zachklipp internal class SnapshotMutableStateImpl<T>( val firstStateRecord: StateRecord fun prependStateRecord(value: StateRecord)
fun mergeRecords( previous: StateRecord, current: StateRecord, applied: StateRecord ): StateRecord? = null } -
@zachklipp internal class SnapshotMutableStateImpl<T>( value: T, override val policy: SnapshotMutationPolicy<T>
) : StateObject, SnapshotMutableState<T> { override fun mergeRecords( previous: StateRecord, current: StateRecord, applied: StateRecord ): StateRecord? { } } -
@zachklipp 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 } } } } -
@zachklipp Custom state objects class Range { var start: Int
= 0 var end: Int = 0 } -
@zachklipp 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 } } } -
@zachklipp 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 } } } -
@zachklipp 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 } } } -
@zachklipp 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 } } } -
@zachklipp 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 } } } -
@zachklipp 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 } } } -
@zachklipp Custom state objects class Range : StateObject { override
fun mergeRecords( previous: StateRecord, current: StateRecord, applied: StateRecord ): StateRecord? { } } -
@zachklipp 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 } } } -
@zachklipp 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 } } } -
@zachklipp 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 } } } -
@zachklipp 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]) } } } } -
@zachklipp 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]) } } } } -
@zachklipp 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]) } } } } -
@zachklipp 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]) } } } } -
@zachklipp 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]) } } } } -
@zachklipp Persistence fun observeUserAsState(name: String): State<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 -
@zachklipp Persistence Read fun observeUserAsState(name: String): State<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 } -
@zachklipp Persistence Read fun observeUserAsState(name: String): State<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 } -
@zachklipp Persistence Read fun observeUserAsState(name: String): State<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 } -
@zachklipp Persistence Read fun observeUserAsState(name: String): State<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 } -
@zachklipp 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() } } } } -
@zachklipp 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() } } } } -
@zachklipp 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() } } } } -
@zachklipp 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() } } } } -
@zachklipp 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() } } } } -
@zachklipp 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() } } } } -
@zachklipp 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) } ) } } -
@zachklipp 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) } ) } } -
Further reading: More info about implementing custom StateObjects: Slides: Whitepaper
on multiversion concurrency control: @zachklipp -
Questions? twitter.com/ @zachklipp More info about implementing custom StateObjects: Slides:
Whitepaper on multiversion concurrency control: