How to build a tree using LazyColumn in Jetpack Compose?
Asked Answered
D

1

7

In my jetpack-compose app, I'm building a comment tree, where the top level, and the leaves, are lists, that would be best to use LazyColumn.

This is of the form:

List<CommentNode>
...
CommentNode: {
  content: String
  children: List<CommentNode>
}
@Composable
fun Nodes(nodes: List<CommentNode>) {
  LazyColumn {
    items(nodes) { node -> 
      Node(node)
    }
  }
}

@Composable
fun Node(node: CommentNode) {
  LazyColumn {
    item {
      Text(node.content)
    }
    item {
      Nodes(node.children)
    }
  }
}

On the top level, LazyColumn works, but it seems I have to use Column for the leaves, otherwise I get an unexplained crash:

03-29 14:36:38.792  1658  6241 W ActivityTaskManager:   Force finishing activity com.jerboa/.MainActivity 
03-29 14:36:38.902  1658  3033 I WindowManager: WIN DEATH: Window{d3b902b u0 com.jerboa/com.jerboa.MainActivity}                      
03-29 14:36:38.902  1658  3033 W InputManager-JNI: Input channel object 'd3b902b com.jerboa/com.jerboa.MainActivity (client)' was disposed without first being removed with the input manager!

Has anyone had any luck building a variable length tree in jetpack compose?

Dreamworld answered 29/3, 2022 at 18:44 Comment(1)
Added a composable exampleDreamworld
L
14

I don't think you really need to place one LazyColumn into an other one - each of them gonna have it's own scroll logic.

Instead you can place item for each node recursively. To do this, declare your function on LazyListScope. These are no longer views, since the views will be inside item. And I think the lowercase naming would be correct here.

@Composable
fun View(nodes: List<CommentNode>) {
    val expandedItems = remember { mutableStateListOf<CommentNode>() }
    LazyColumn {
        nodes(
            nodes,
            isExpanded = {
                expandedItems.contains(it)
            },
            toggleExpanded = {
                if (expandedItems.contains(it)) {
                    expandedItems.remove(it)
                } else {
                    expandedItems.add(it)
                }
            },
        )
    }
}

fun LazyListScope.nodes(
    nodes: List<CommentNode>,
    isExpanded: (CommentNode) -> Boolean,
    toggleExpanded: (CommentNode) -> Unit,
) {
    nodes.forEach { node ->
        node(
            node,
            isExpanded = isExpanded,
            toggleExpanded = toggleExpanded,
        )
    }
}

fun LazyListScope.node(
    node: CommentNode,
    isExpanded: (CommentNode) -> Boolean,
    toggleExpanded: (CommentNode) -> Unit,
) {
    item {
        Text(
            node.content,
            Modifier.clickable {
                toggleExpanded(node)
            }
        )
    }
    if (isExpanded(node)) {
        nodes(
            node.children,
            isExpanded = isExpanded,
            toggleExpanded = toggleExpanded,
        )
    }
}
Lytton answered 1/4, 2022 at 16:2 Comment(8)
This is very close, but I can't use it, because I need to be able to hide leaves of the tree, and this requires a remember, and a if (expanded) { nodes(nodes.children) } that is only usable on Composable function.Dreamworld
@thouliha remember should not be used inside item, as it will be cleared after the item is scrolled off the screen. It should be stored outside the LazyColumn, see the updated answer how you can do that.Lytton
I'm now using this in my codebase, it worked great. Thanks so much!Dreamworld
Thanks a lot @PhilDukhov, I used this as my tree imlementation starting point!Dantzler
This looks like a good start to getting a working tree component - something I wonder about it though is - do I lose any of the benefits of using LazyColumn by using item() instead of items()? If I have a mega-tree with a million children inside some node, how is this expected to perform? Should I instead be using items() over the children?Dumbhead
@Dumbhead First of all, I would try it and see how it works. But yes, according to source code, it looks like you may face some issues, as adding single item and multiple items has same complexity, which means that when you add them by one you're getting extra N complexity. I don't know how you can improve it - maybe if your data has a certain structure, you can take that into account in the algo.Lytton
@PhilDukhov I hit the issue with nested lazy columns immediately and it wrecked my plans, LOL. It might be possible to get around that, but I gave up on that approach immediately. What I'm doing now is building a list of the currently expanded part of the tree, and putting that inside a single LazyColumn. When expanding the tree, I have to add 1,000,000 elements to the list, but that runs fast enough as to be unnoticeable. I haven't managed to get collapsing the tree to work correctly yet but it seems like this is a viable way to go about writing a large tree.Dumbhead
It turns out adding 1,000,000 elements is fast but removing 1,000,000 elements is extremely slow because Compose is calling removeAt individually for each index. So it's just something about their list implementation in general, which if I replaced it with a performant implementation, might be totally fine to use for this.Dumbhead

© 2022 - 2024 — McMap. All rights reserved.