How to manage state for structures like "File System" (Nodes with list of themselves)? ```data class...
a
How to manage state for structures like "File System" (Nodes with list of themselves)?
Copy code
data class Node(
    val name: String,
    val children: List<Node>
)
I want to follow UDF + single source of truth. Level of depth is unknown (assume it's unlimited). How to follow immutability for updates (add/remove node) in this case if i should at all. Maybe it worth to try mutable state, but idk how to cook mutable state with compose and don't break recomposition.
🤔 1
d
Without information about how want to use this class, it's hard to say. There are so many ways you can go about it, the "best" comes with context/usecase.
a
Let's consider File Explorer: user can CRUD folders and achieve any level of nesting
ProjectViewTree from intellij is a good example
d
Ah, I see. Just make the properties mutable and call it a day.
a
What's about recomposition? it could happen from different threads, what if i mutate my tree in background during another recomposition?
d
The compose's job to worry about. Mutate the tree as you wish.
a
Compose isn't going to worry about it unless you use a
mutableStateListOf()
to back that children list
And once you declare mutable properties you should avoid
data class
2
a
Ok, let's kick off singsle source. Can i traverse composition to build model from remembered mutableStates* data?
but in this case i lose ability to drive state outside of composition
unless abuse LocalCompositionOf
a
I would go with immutable structures. The task to update the tree is beyond Compose. It is a generic question how to update such a tree. Kinda go down the tree, find a node, shallow-copy the node itself and all its parents. Finally assign the new root to the MutableState.
a
but it's the main problem, i looked at lens from FP but didn't found anything about rucursion
node itself doesn't know about parent
a
The recursion knows about parents.
a
so each modification leads to traversal of whole tree (in worst case)
a
It depends on how the tree is organized. If it is a search tree, then you can copy a node in O(log(n))
a
i mean to find specific node which is modified
a
find for what purpose?
a
cuz child doesnt knows about parent
a
You can derive your implementation from a deep-copy algorithm https://stackoverflow.com/questions/3918811/copy-binary-tree-in-order
☝️ 1
a
backing up for a moment, I see several implied conclusions about these approaches that seem to follow from incomplete premises. I'll note a few things about compose and this space in general that I'm taking for granted in this conversation:
a
@Arkadii Ivanov list of childs isn't binary
a
Snapshot state, i.e. "mutable" state as represented by the containers created the by
mutableState[List|Map]Of
functions, is under the hood implemented by process-wide immutable data structures. If you take a snapshot, (which compose does when it performs recomposition,) then all snapshot state objects in the process are consistent with one another in that snapshot. Think of each snapshot state object as a pointer into a big immutable data structure: the snapshot itself.
So while implementing shared-structure copies of data structures to represent changes to that structure by hand gets cumbersome, snapshots more or less do all of that for you and make it look like mutating plain old mutable containers.
UDF/single source of truth as concepts generally speak to controlling who can produce new values for a piece of data. With immutable data structures this means a single emitter of new root values that refer to other immutable data. With snapshots this means controlling who has a reference to an object as a
MutableFoo
as opposed to just a
Foo
that only exposes read access
The general tactic @Arkadii Ivanov is referring to for persistent data structure mutation works regardless of whether a tree is limited to binary or N-ary. An implementation more or less looks like
node.copy(children = children.mapNotNull { changedOrExistingValueOrNullIfRemoved(it) })
plus or minus some optimizations for fully unmodified nodes
If you go with manual immutable data here, compose will skip recomposition if your nodes are stable and it the node is equal to the old value.
List<T>
is not assumed to be stable, so if you want to make this promise about your nodes you should mark the node class as
@Stable
👍 1
`data class`'s implementation of
hashCode
is particularly inefficient if you find yourself using it, as it will end up traversing the whole subtree every time it's called. You'll probably want to implement your own that caches, and the usefulness of `data class`'s conveniences will dry up pretty quickly.
r
Adam, this is probably related to what you're saying or is what you're saying but I wanted to verify: if you update multiple mutableState values from within one callback, those are all done before the next recompose (in other words, the updates are atomic with respect to the next UI update)?
a
it's going to sound like I'm hedging a bit with words here since the global transaction gets a bit interesting sometimes. 🙂 to a first approximation, yes.
If you want to 100% ensure several changes are snapshot-atomic with one another, you can wrap those changes in a call to
Snapshot.withMutableSnapshot {}
. That defines a single isolated snapshot transaction.
If you're on the main thread using Compose UI though, the global transaction management more or less does this for you.
Whenever you write a snapshot object in the global transaction (i.e. not in an explicit snapshot) then compose UI schedules a call to
Snapshot.sendApplyNotifications()
, which commits the current global transaction state and may in turn schedule recomposition
if you're running in a callback on the main thread this can't happen until your callback returns, so yes, changes made in that callback will be atomic with regard to that snapshot commit.
r
I was just typing that question about callbacks on the main thread and you just answered it 🙂 (BTW, these conversations are very helpful, and I appreciate your and the team's time.)
👍 3
a
Would be nice to make sample in github with tree-like composition. I will try do that later
I'm on the way of implementing sample, but get stuck with confusing "remove node" behavior (with Compose memoization to be precise): When i remove item, it goes away from composition, but his children moves to next sibling https://github.com/CeH9/ComposeTreeSample/blob/master/src/main/kotlin/main.kt
😕 1
a
Try using
key(item) {}
in your loop to associate item identity with each one rather than just the index order
👍 1
a
I thought keys works as part of LazyList only 😮
now "remove" works fine
Is it possible to traverse composables (Nodes) from root and get their SnapshotStateList's?
Usecase: User build some tree structure and clicks "Save". Since SSOT moved to internal snapshot of composable we need some way to convert it to custom data model.
a
yes, this is generally what the state hoisting pattern is about. Instead of storing the children list in a
remember {}
in the node composable, store it in the
NodeModel
Treat the
@Composable fun
as a visitor of your data, which leaves your data as accessible as you like
👍 1
a
@Adam Powell hi, can u please review my updated sample. I'll highlight important pieces: • NodeUiModel • UI State (model for component/screen) to render by view (composition) • Rendering state (collectAsState) I'm not sure how Models/State should look like: data class or regular one or marked as @Stable and so on