dingyi222666 / TreeView

An Android TreeView with RecyclerView
Apache License 2.0
105 stars 9 forks source link

How to build a tree from dynamic data? #4

Closed lijingdoc closed 1 year ago

lijingdoc commented 1 year ago

I have dynamic data, I want to build a tree from it. Let me put an example.

I have my own Item

data class Item(
    val name: String,
    val childItems: List<Item>
)

I use dynamic Item. For example:

val item = Item(
    "head", listOf(
        Item("1", listOf(
            Item("11", listOf()),
            Item("12", listOf())
        )),
        Item("2", listOf(
            Item("21", listOf()),
            Item("22", listOf())
        ))
    )
)

How can I create tree for this dynamic item?

private fun createTree(item: Item): Tree<DataSource<Item>> {
    val dataCreator: CreateDataScope<Item> = { name, item ->
        Item("", listOf())
    }

    return buildTree(dataCreator) {
        fun getSubBranches(item: =Item): List<Unit> {
            val branches: MutableList<Unit> = mutableListOf()

            item.childItems.forEach {
                branches.add(
                    Branch(it.name, item) {
                        getSubBranches(it)
                    }
                )
            }
            return branches
        }
        getSubBranches(item)
    }
}

I tried the above code, but I am getting 1 dimension tree and there are no subbranches. How can I approach it? Thank you!

dingyi222666 commented 1 year ago

For dynamic data, we recommend using TreeNodeGenerator

lijingdoc commented 1 year ago

Is there an example for this? @dingyi222666

dingyi222666 commented 1 year ago

Like this. If you don't understand it for now, it's okay, I may improve the DataSource api in the coming days.

lijingdoc commented 1 year ago

Thanks, @dingyi222666 I hope to get new update asap.

dingyi222666 commented 1 year ago

Yes, but I still recommend using TreeNodeGenerator to generate the nodes. When I update the API for DataSource I may also update the example using TreeNodeGenerator.

lijingdoc commented 1 year ago

Ok, got, thanks

dingyi222666 commented 1 year ago

No, it may not be necessary to update the API of the DataSource. If this data does not need to be updated later, then this will work.

val item = Item(
    "head", listOf(
        Item(
            "1", listOf(
                Item("11", listOf()),
                Item("12", listOf())
            )
        ),
        Item(
            "2", listOf(
                Item("21", listOf()),
                Item("22", listOf())
            )
        )
    )
)

fun DataSourceScope<Item>.getSubBranches(item: Item): List<Unit> {
    val branches: MutableList<Unit> = mutableListOf()

    item.childItems.forEach {
        branches.add(
            Branch(it.name, item) {
                getSubBranches(it)
            }
        )
    }
    return branches
}

// The dataCreator is not needed because the data is already provided in Branch(name:String,data,T). 
// The dataCreator is called only when the set data is null.
return buildTree {
    getSubBranches(item)
}
dingyi222666 commented 1 year ago

This is an example of using TreeNodeGenerator.

private fun createTree(): Tree<Item> {

    val item = Item(
        "head", listOf(
            Item(
                "1", listOf(
                    Item("11", listOf()),
                    Item("12", listOf())
                )
            ),
            Item(
                "2", listOf(
                    Item("21", listOf()),
                    Item("22", listOf())
                )
            )
        )
    )

    return Tree.createTree<Item>().apply {
        generator = ItemTreeNodeGenerator(item)
        initTree()
    }
}

inner class ItemTreeNodeGenerator(
    private val rootItem: Item
) : TreeNodeGenerator<Item> {

    override suspend fun fetchNodeChildData(targetNode: TreeNode<Item>): Set<Item> {
        return targetNode.requireData().childItems.toSet()
    }

    override fun createNode(
        parentNode: TreeNode<Item>,
        currentData: Item,
        tree: AbstractTree<Item>
    ): TreeNode<Item> {
        return TreeNode(
            data = currentData,
            depth = parentNode.depth + 1,
            name = currentData.name,
            id = tree.generateId(),
            hasChild = currentData.childItems.isNotEmpty(),
            // It should be taken from the Item
            isChild = currentData.childItems.isNotEmpty(),
            expand = false
        )
    }

    override fun createRootNode(): TreeNode<Item> {
        return TreeNode(
            data = rootItem,
            depth = 0,
            name = rootItem.name,
            id = Tree.ROOT_NODE_ID,
            hasChild = true,
            isChild = true
        )
    }
}
lijingdoc commented 1 year ago

@dingyi222666 thanks for your kind help It works well.

lijingdoc commented 1 year ago

@dingyi222666 I have one more question.

If there are several root nodes, then how can I implement it? For example:

val items: List<Item> = 
    listOf(
        Item(
            "root1", listOf(
                Item("1", listOf(
                    Item("11", listOf()),
                    Item("12", listOf())
                )),
                Item("2", listOf(
                    Item("21", listOf()),
                    Item("22", listOf())
                ))
            )
        ),
        Item(
            "root2", listOf(
                Item("1", listOf(
                    Item("11", listOf()),
                    Item("12", listOf())
                )),
                Item("2", listOf(
                    Item("21", listOf()),
                    Item("22", listOf())
                ))
            )
        )
    )
dingyi222666 commented 1 year ago

If you are using TreeNodeGenerator, this will work.

private fun createTree(): Tree<Item> {
    val items =
        listOf(
            Item(
                "root1", listOf(
                    Item("1", listOf(
                        Item("11", listOf()),
                        Item("12", listOf())
                    )),
                    Item("2", listOf(
                        Item("21", listOf()),
                        Item("22", listOf())
                    ))
                )
            ),
            Item(
                "root2", listOf(
                    Item("1", listOf(
                        Item("11", listOf()),
                        Item("12", listOf())
                    )),
                    Item("2", listOf(
                        Item("21", listOf()),
                        Item("22", listOf())
                    ))
                )
            )
        )

    val rootItem = Item("root_not_show", items)

    return Tree.createTree<Item>().apply {
        generator = ItemTreeNodeGenerator(rootItem)
        initTree()
    }
}

inner class ItemTreeNodeGenerator(
    private val rootItem: Item
) : TreeNodeGenerator<Item> {
    override suspend fun fetchNodeChildData(targetNode: TreeNode<Item>): Set<Item> {
        return targetNode.requireData().childItems.toSet()
    }

    override fun createNode(
        parentNode: TreeNode<Item>,
        currentData: Item,
        tree: AbstractTree<Item>
    ): TreeNode<Item> {
        return TreeNode(
            data = currentData,
            depth = parentNode.depth + 1,
            name = currentData.name,
            id = tree.generateId(),
            hasChild = currentData.childItems.isNotEmpty(),
            // It should be taken from the Item
            isChild = currentData.childItems.isNotEmpty(),
            expand = false
        )
    }

    override fun createRootNode(): TreeNode<Item> {
        return TreeNode(
            data = rootItem,
            // Set to -1 to not show the root node
            depth = -1,
            name = rootItem.name,
            id = Tree.ROOT_NODE_ID,
            hasChild = true,
            isChild = true
        )
    }

}
dingyi222666 commented 1 year ago

If you use buildTree DSL, this will work.


private fun createTree(): Tree<DataSource<Item>> {
    val items =
        listOf(
            Item(
                "root1", listOf(
                    Item("1", listOf(
                        Item("11", listOf()),
                        Item("12", listOf())
                    )),
                    Item("2", listOf(
                        Item("21", listOf()),
                        Item("22", listOf())
                    ))
                )
            ),
            Item(
                "root2", listOf(
                    Item("1", listOf(
                        Item("11", listOf()),
                        Item("12", listOf())
                    )),
                    Item("2", listOf(
                        Item("21", listOf()),
                        Item("22", listOf())
                    ))
                )
            )
        )

    fun DataSourceScope<Item>.getSubBranches(items:List<Item>) {
        items.forEach {
            Branch(it.name, it) {
                getSubBranches(it.childItems)
            }

        }
    }

    return buildTree {
        getSubBranches(items)
    }
}
lijingdoc commented 1 year ago

Thank you, @dingyi222666 It works well.

BlueCatSoftware commented 1 year ago

@dingyi222666 Now the problem is how do i update the data of the TreeView, if i'm using the TreeNodeGenerator and still maintaining the hierarchy of the Tree

dingyi222666 commented 1 year ago

@dingyi222666 Now the problem is how do i update the data of the TreeView, if i'm using the TreeNodeGenerator and still maintaining the hierarchy of the Tree

tree.refresh()?

BlueCatSoftware commented 1 year ago

@dingyi222666 Now the problem is how do i update the data of the TreeView, if i'm using the TreeNodeGenerator and still maintaining the hierarchy of the Tree

tree.refresh()?

that doesn't work, i'm loading a folder from the device, with the functionality of creating, delete, rename etc, which i have added already and it works fine, but calling tree.refresh() seems not work though i'm calling it from a ViewBinder, which the constructor has the Tree instance

dingyi222666 commented 1 year ago

@dingyi222666 Now the problem is how do i update the data of the TreeView, if i'm using the TreeNodeGenerator and still maintaining the hierarchy of the Tree

tree.refresh()?

that doesn't work, i'm loading a folder from the device, with the functionality of creating, delete, rename etc, which i have added already and it works fine, but calling tree.refresh() seems not work though i'm calling it from a ViewBinder, which the constructor has the Tree instance

Can you show me how your TreeNodeGenerator is implemented?

BlueCatSoftware commented 1 year ago

@dingyi222666 here is the link , usage

dingyi222666 commented 1 year ago

@dingyi222666 Now the problem is how do i update the data of the TreeView, if i'm using the TreeNodeGenerator and still maintaining the hierarchy of the Tree

tree.refresh()?

that doesn't work, i'm loading a folder from the device, with the functionality of creating, delete, rename etc, which i have added already and it works fine, but calling tree.refresh() seems not work though i'm calling it from a ViewBinder, which the constructor has the Tree instance

No, it should call treeView.refresh(). tree.refresh may only refresh the tree data, and not synchronize it to the treeView.

BlueCatSoftware commented 1 year ago

@dingyi222666 non of the treeView.refresh() and treeView.refresh(false, node) works or do anything here, it was called from the ViewBinder here is the link

the treeView.refresh() used to work when i was using kotlin dsl thingy, since when i switched to TreeNodeGenerator it doesn't work, i think the bug is from the library

dingyi222666 commented 1 year ago

@BlueCatSoftware You also need to update your FileSet data.

The data flow should look like this

raw data -> node generator -> tree -> treeView

BlueCatSoftware commented 1 year ago

@BlueCatSoftware You also need to update your FileSet data.

The data flow should look like this

raw data -> node generator -> tree -> treeView

private suspend fun refreshTreeView(treeView: TreeView<FileSet>, node: TreeNode<FileSet>){
        val rootItem = FileSet(project.root,
            transverseTree(project.root) as MutableSet<FileSet>
        )
        val generator = FileTreeNodeGenerator(rootItem)
        val tree = Tree.createTree<FileSet>().apply {
            this.generator = generator
            initTree()
        }
        treeView.tree = tree
        treeView.refresh(false, node)
    }

this works but doesn't retain the tree hierarchy, it literally reloads and collapses the tree.

At this point an example will definitely help

dingyi222666 commented 1 year ago

@BlueCatSoftware See https://github.com/dingyi222666/UnLuacTool/blob/main/app/src/main/java/com/dingyi/unluactool/ui/editor/main/FileViewerFragment.kt#L361

BlueCatSoftware commented 1 year ago

@BlueCatSoftware See https://github.com/dingyi222666/UnLuacTool/blob/main/app/src/main/java/com/dingyi/unluactool/ui/editor/main/FileViewerFragment.kt#L361

yeah i am aware of this method, it fetches the data. i'm still confused, the problem is there's no method to update the node's data. i know how to fetch nodes data but doesn't know how to manipulate it. I'm still very confused with the updating

dingyi222666 commented 1 year ago

@BlueCatSoftware The FileSet class should have a parent attribute that points to the parent file. When updating, it needs to get the parent’s FileSet and refresh its child files.

Actually, I recommend you to use File directly, so that the node generator only needs File.listFile.

BlueCatSoftware commented 1 year ago

@BlueCatSoftware The FileSet class should have a parent attribute that points to the parent file. When updating, it needs to get the parent’s FileSet and refresh its child files.

Actually, I recommend you to use File directly, so that the node generator only needs File.listFile.

i get your point, here is my problem UPDATING, how do i exactly update. The big question is the UPDATING, what am i swapping or editing ?, is it the generator or what exactly ?

Sorry for too much question lol.

dingyi222666 commented 1 year ago

@BlueCatSoftware I don't think you fully understand what I'm saying.

Let me give you a sample code.


class FileTreeNodeGenerator(val rootItem: FileSet) : TreeNodeGenerator<FileSet> {

    override fun createNode(
        parentNode: TreeNode<FileSet>,
        currentData: FileSet,
        tree: AbstractTree<FileSet>
    ): TreeNode<FileSet> {
        return TreeNode(
            currentData,
            parentNode.depth + 1,
            currentData.file.name,
            tree.generateId(),
            currentData.subDir.isNotEmpty(),
            currentData.subDir.isNotEmpty(),
            false
        )
    }

    override suspend fun fetchNodeChildData(targetNode: TreeNode<FileSet>): Set<FileSet> = withContext(Dispatcher.IO) {
        // refresh data here
        val fileSet = targetNode.requireData()
        fileSet.subDir.clear()
        for (file in dir.listFiles()) {
              fileSet.subDir.add(FileSet(fileSet))
        }
        fileSet.subDir.toSet()
    }

    override fun createRootNode(): TreeNode<FileSet> {
        return TreeNode(
            data = rootItem,
            depth = 0,
            name = rootItem.file.name,
            id = Tree.ROOT_NODE_ID,
            hasChild = true,
            isChild = true
        )
    }
}

data class FileSet(val file: File, val subDir: MutableSet<FileSet> = mutableSetOf())
BlueCatSoftware commented 1 year ago

@dingyi222666 oh this, i'm gonna try it out. Thanks by the way

BlueCatSoftware commented 1 year ago
override suspend fun fetchNodeChildData(targetNode: TreeNode<FileSet>): Set<FileSet> =
        withContext(Dispatchers.IO) {
            val set = targetNode.requireData().subDir
            set.clear()
            val files = targetNode.requireData().file.listFiles() ?: return@withContext set
            for (file in files) {
                when {
                    file.isFile -> set.add(FileSet(file))
                    file.isDirectory -> {
                        val tempSet = mutableSetOf<FileSet>().apply {
                            addAll(transverseTree(file))
                        }
                        set.add(FileSet(file, subDir = tempSet))
                    }
                }
            }
            return@withContext set
        }

this definitely worked for me, i didn't know treeView.refresh() calls fetchNodeChildData() internally though. what if the data is dynamic, e.g you are not loading data from the device file storage, but rather you want to insert data based on user's interaction?

dingyi222666 commented 1 year ago
override suspend fun fetchNodeChildData(targetNode: TreeNode<FileSet>): Set<FileSet> =
        withContext(Dispatchers.IO) {
            val set = targetNode.requireData().subDir
            set.clear()
            val files = targetNode.requireData().file.listFiles() ?: return@withContext set
            for (file in files) {
                when {
                    file.isFile -> set.add(FileSet(file))
                    file.isDirectory -> {
                        val tempSet = mutableSetOf<FileSet>().apply {
                            addAll(transverseTree(file))
                        }
                        set.add(FileSet(file, subDir = tempSet))
                    }
                }
            }
            return@withContext set
        }

this definitely worked for me, i didn't know treeView.refresh() calls fetchNodeChildData() internally though. what if the data is dynamic, e.g you are not loading data from the device file storage, but rather you want to insert data based on user's interaction?

Let’s take another look at this data flow: raw data -> node generator -> tree -> treeView. If your raw data comes from multiple sources, then you need to provide your raw data externally. To implement your example, you need to provide raw data (which is FileSet). This data needs to be refreshed outside the node generator. I’m afraid I can’t provide any more code examples, I think you need to think more about how to implement it, this is actually a simple problem.

dingyi222666 commented 1 year ago

@BlueCatSoftware See here

I'm soon to release version 1.2.0, which will improve some things, so feel free to update and use it.