Thread
#compose
    Arsen

    Arsen

    1 year ago
    How to manage state for structures like "File System" (Nodes with list of themselves)?
    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.
    Dominaezzz

    Dominaezzz

    1 year ago
    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.
    Arsen

    Arsen

    1 year ago
    Let's consider File Explorer: user can CRUD folders and achieve any level of nesting
    ProjectViewTree from intellij is a good example
    Dominaezzz

    Dominaezzz

    1 year ago
    Ah, I see. Just make the properties mutable and call it a day.
    Arsen

    Arsen

    1 year ago
    What's about recomposition? it could happen from different threads, what if i mutate my tree in background during another recomposition?
    Dominaezzz

    Dominaezzz

    1 year ago
    The compose's job to worry about. Mutate the tree as you wish.
    Adam Powell

    Adam Powell

    1 year ago
    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
    Arsen

    Arsen

    1 year ago
    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
    Arkadii Ivanov

    Arkadii Ivanov

    1 year ago
    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.
    Arsen

    Arsen

    1 year ago
    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
    Arkadii Ivanov

    Arkadii Ivanov

    1 year ago
    The recursion knows about parents.
    Arsen

    Arsen

    1 year ago
    so each modification leads to traversal of whole tree (in worst case)
    Arkadii Ivanov

    Arkadii Ivanov

    1 year ago
    It depends on how the tree is organized. If it is a search tree, then you can copy a node in O(log(n))
    Arsen

    Arsen

    1 year ago
    i mean to find specific node which is modified
    Adam Powell

    Adam Powell

    1 year ago
    find for what purpose?
    Arsen

    Arsen

    1 year ago
    cuz child doesnt knows about parent
    Arkadii Ivanov

    Arkadii Ivanov

    1 year ago
    You can derive your implementation from a deep-copy algorithm https://stackoverflow.com/questions/3918811/copy-binary-tree-in-order
    Adam Powell

    Adam Powell

    1 year ago
    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:
    Arsen

    Arsen

    1 year ago
    @Arkadii Ivanov list of childs isn't binary
    Adam Powell

    Adam Powell

    1 year ago
    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
    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.
    Rick Regan

    Rick Regan

    1 year ago
    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)?
    Adam Powell

    Adam Powell

    1 year ago
    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.
    Rick Regan

    Rick Regan

    1 year ago
    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.)
    Arsen

    Arsen

    1 year ago
    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
    Adam Powell

    Adam Powell

    1 year ago
    Try using
    key(item) {}
    in your loop to associate item identity with each one rather than just the index order
    Arsen

    Arsen

    1 year ago
    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.
    Adam Powell

    Adam Powell

    1 year ago
    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
    Arsen

    Arsen

    1 year ago
    @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