Hi everyone, I have a question that has puzzled me...
# compose
s
Hi everyone, I have a question that has puzzled me for a long time, and I can't find the answer in Google search or source code. What happens if I implement a custom StateObject but don't conform to the StateRecord protocol? That is, all reads and writes of values aren't reads or writes of the StateRecord itself, but rather reads and writes an internal value? I can still use the StateObject's readable and writable methods to notify Compose of value reads and updates, thus triggering re-composition. However, the feature of snapshot is lost, and the state cannot be restored to its historical recorded value. My example code:
Copy code
class MyStateObject(private var value: Int): StateObject {

    private var stateRecord = MyRecord()

    override val firstStateRecord: StateRecord
        get() = stateRecord

    override fun prependStateRecord(value: StateRecord) {
        stateRecord = value as MyRecord
    }

    var intValue: Int
        get() {
            stateRecord.readable(this)
            // Do not read the value from the record, always use the "latest" value
            return this@MyStateObject.value
        }
        set(value) {
            stateRecord.writable(this) {
                // Instead of writing the value to the specified "current" record, the "latest" value is written
                this@MyStateObject.value = value
            }
        }

    // This Record does not actually do anything and does not record anything.
    private class MyRecord: StateRecord() {

        override fun assign(value: StateRecord) {
        }

        override fun create(): StateRecord {
            return MyRecord()
        }
    }
}
I'm wondering where in Compose does snapshots or similar functionality use this internally? Will my implementation encounter any real-world issues, such as animations? (My code doesn't use snapshots.) The reason for this is that I have a very large list of data, and only one copy of it exists in memory. I can't have any copies or snapshots. Whenever I read it, I only get the latest data, and the list is constantly changing. Therefore, I can't use SnapshotStateList.
z
You’ll get read and write notifications, so invalidation will work as long as all writes go through that object (ie recomposing when it changes), but you won’t get any snapshot isolation so it won’t be safe to write from composition and you may get weird behavior if the value changes during composition.
> Whenever I read it, I only get the latest data, and the list is constantly changing. Therefore, I can't use SnapshotStateList. I don’t understand why this means you can’t use SnapshotStateList for the underlying list. Unless the real reason is it’s owned by a module that doesn’t know about compose/snapshot state. This sounds like potentially a great use case for a persistent data structure, which would play much nicer with compose (and is what SnapshotStateList actually uses internally).
For more info on how the snapshot system works, see this.
For how state records are used, this talk in particular.
❤️ 1
Other stuff I haven’t thought about might break too, now or in the future, since you’re violating the state object contract.
s
@Zach Klippenstein (he/him) [MOD] Thank you very much for clarifying this. Regarding the situation you described, after further debugging Compose, I discovered that the only place Compose uses snapshots is in the following code in Recomposer:
Copy code
private inline fun <T> composing(
    composition: ControlledComposition,
    modifiedValues: MutableScatterSet<Any>?,
    block: () -> T
): T {
    val snapshot = Snapshot.takeMutableSnapshot(
        readObserverOf(composition), writeObserverOf(composition, modifiedValues)
    )
    try {
        return snapshot.enter(block)
    } finally {
        applyAndCheck(snapshot)
    }
}
From this code, it seems that if I'm not modifying state multithreaded, theoretically, the behavior of not saving historical snapshots is equivalent to saving snapshots. This is because Recomposer always uses the latest snapshot, applies changes after modification, and throws an exception if the apply fails. In other words, Compose currently doesn't support concurrent (unmergeable) state modifications during the recomposition process. I'm not sure whether snapshots are used in animations or other scenarios.
z
the only place Compose uses snapshots is in the following code in Recomposer
That is perhaps currently true of the compose runtime, but not of compose UI, which also uses them for layout, draw, graphicslayer, etc. And it is not a good idea to rely on specific implementation details over the documented contract because if you do that, when the implementation details inevitably change, your code will break. Fighting a framework will always be a bad time, there are other solutions to your problem that actually do work and also comply with the snapshot contract.