Bonsai A batteries-included Tree View for Jetpack Compose

Overview

Maven metadata URL Android API kotlin ktlint License MIT


Bonsai

A batteries-included Tree View for Jetpack Compose

Features

Roadmap

  • iOS support

Usage

Start by creating a new tree with rememberTree(). Use SimpleLeafNode and SimpleBranchNode to create leaf and branch nodes, respectively. Call the Bonsai() composable to render the tree.

listOf( SimpleLeafNode( content = "A child node", parent = node ), ) } ) ) ) Bonsai(tree) }">
@Composable
fun BonsaiExample() {
    val tree = rememberTree(
        nodes = listOf(
            SimpleLeafNode(
                content = "A leaf node"
            ),
            SimpleBranchNode(
                content = "A branch node",
                children = { node ->
                    listOf(
                        SimpleLeafNode(
                            content = "A child node",
                            parent = node
                        ),
                    )
                }
            )
        )
    )

    Bonsai(tree)
}

Output:

Take a look at the sample app for a working example.

File System integration

Import cafe.adriel.bonsai:bonsai-file-system module to use it.

val tree = rememberTree<Path>(
    nodes = fileSystemNodes(
        // Also works with java.nio.file.Path and okio.Path
        rootPath = File(path), 
        // To show or not the root directory in the tree
        selfInclude = true 
    )
)

Bonsai(
    tree = tree,
    // Custom style
    style = FileSystemBonsaiStyle()
)

Output:

JSON integration

Import cafe.adriel.bonsai:bonsai-json module to use it.

val tree = rememberTree<Path>(
    // Sample JSON from https://rickandmortyapi.com/api/character
    nodes = jsonNodes(json)
)

Bonsai(
    tree = tree,
    // Custom style
    style = JsonBonsaiStyle()
)

Output:

DSL

Looking for a simpler and less verbose way to create a tree? Here's a handy DSL for you.

val tree = Tree<String> {
    Branch("bonsai") {
        Branch("bonsai-core") {
            Branch("build")
            Branch("src") {
                Branch("androidMain") {
                    Leaf("AndroidManifest.xml")
                }
                Branch("commonMain")
            }
            Leaf("build.gradle")
        }
        Branch("bonsai-file-system")
        Branch("sample")
        Leaf(".gitignore")
        Leaf("build.gradle")
        Leaf("README.md")
    }
}

Output:

Expanding & Collapsing

Easy control the expanded/collapsed state of your Tree:

  • toggleExpansion(node)
  • collapseAll() / expandAll()
  • collapseRoot() / expandRoot()
  • collapseFrom(minLevel) / expandUntil(maxLevel)
  • collapseNode(node) / expandNode(node)

Selecting

Selected/Unselected state is also pretty simple to control:

  • selectedNodes (observable state backed by SnapshotStateList)
  • toggleSelection(node)
  • selectNode(node) / unselectNode(node)
  • clearSelection()

Click handling

Its also possible to set custom click behaviors for your Tree. Control single, double and long clicks by using the expand and select APIs.

Bonsai(
    tree = tree,
    onClick = { node ->
        tree.clearSelection()
        tree.toggleExpansion(node)
    },
    onDoubleClick = { node -> /* ... */ },
    onLongClick = { node -> /* ... */ }
)

Styling

Change your Tree appearance as you wish. Take a look at BonsaiStyle class for all available customizations.

Bonsai(
    tree = tree,
    style = BonsaiStyle(
        expandTransition = slideIn(),
        collapseTransition = slideOut(),
        toggleIconRotationDegrees = 0f,
        toggleIcon = { node ->
            rememberVectorPainter(
                if (node is BranchNode && node.isExpanded) Icons.Outlined.UnfoldLess
                else Icons.Outlined.UnfoldMore
            )
        },
        nodeIconSize = 18.dp,
        nodeShape = CutCornerShape(percent = 20),
        nodeCollapsedIcon = { rememberVectorPainter(Icons.Outlined.Circle) },
        nodeExpandedIcon = { rememberVectorPainter(Icons.Outlined.Adjust) },
        nodeNameTextStyle = MaterialTheme.typography.overline
    )
)

Output:

Custom nodes

Need a deeper customization? Just extend the SimpleLeafNode and SimpleBranchNode classes, or even the Node interface, to fit your needs.

class CustomLeafNode : SimpleLeafNode<CustomClass>(/* setup */) {

    @Composable
    override fun BonsaiScope.NodeIcon() {
        // Custom leaf node icon
    }

    @Composable
    override fun BonsaiScope.NodeName() {
        // Custom leaf node name
    }
}

class CustomBranchNode : SimpleBranchNode<CustomClass>(/* setup */) {

    @Composable
    override fun BonsaiScope.NodeIcon() {
        // Custom branch node icon
    }

    @Composable
    override fun BonsaiScope.NodeName() {
        // Custom branch node name
    }
}

Import to your project

Add the desired dependencies to your module's build.gradle:

implementation "cafe.adriel.bonsai:bonsai-core:${latest-version}"
implementation "cafe.adriel.bonsai:bonsai-file-system:${latest-version}"
implementation "cafe.adriel.bonsai:bonsai-json:${latest-version}"

Current version: Maven metadata URL

Comments
  • Consider supporting a composable tree builder

    Consider supporting a composable tree builder

    Compose vs Plain Old DSL

    The current DSL for building trees looks a lot like compose code:

    Branch {
      Leaf()
      Branch {
        Leaf()
      }
      Leaf()
    }
    

    Compose is, in fact, a tree-building library. The core runtime knows nothing about UI, it can be used to build any type of tree. For example, the rememberVectorPainter composable has a content parameter that emits not UI nodes, but vector primitives (think SVG elements).

    Since one of the main tasks of this library is to express a tree structure, Compose itself might be a good fit to use for the DSL. Some of the advantages over a simpler, non-compose approach are:

    • Effects - tree nodes that need to load data asynchronously when expanded can do so by performing the load in an effect that is only composed when expanded.
    • State – nodes can store their own state in the composition. Comes in handy with the above.
    • Donut-hole skipping – If a node's children can change over time, using Compose for the DSL will ensure only the nodes that change are updated.

    There's one other benefit of using Compose, which is that it makes it really easy to make better use of the LazyColumn that is actually used to structure the tree UI.

    Tree flattening

    To get the full benefit of a LazyColumn, every expanded node in the tree (everything that has a "row" in the lazy column) should have its own lazy item so the column can effectively recycle and reuse bits of UI that aren't on the screen. To do that, you need to flatten a tree structure into a single list that can be processed by the LazyColumn.

    This also happens to be one of the core jobs of Compose. It takes the call graph of Composable functions (a tree) and flattens them into a single array-like structure (the slot table). So we can take advantage of that to flatten a tree for the LazyColumn.

    Proof-of-concept

    I've thrown together a quick sketch of how this could look. I didn't try too hard to make it match all the exact APIs of this library, but just wanted to show how a custom subcomposition could be used and consumed by the LazyColumn.

    The main entry point to the API in this demo is the rememberLazyTree function:

    val tree = rememberLazyTree {
      Leaf({ /* UI */ })
      Branch({ /* UI */ }) {
        Leaf({ /* UI */ })
      }
      Leaf({ /* UI */ })
    }
    

    Then, you can take the tree object returned by that function and pass it to a LazyColumn:

    LazyColumn {
      treeItems(tree) { content ->
        // Wrap content with the indent and expand/collapse UI
        …
        content()
    }
    

    This is probably something you'd want to hide from the public API of this library, but it shows how flexible and generic this approach can be (it could work in lazy rows, or even grids, I guess).

    Code

    Usage demo
    @Composable
    fun LazyTreeDemo() {
        // Create a simple tree structure just to demo.
        // The lambda to rememberLazyTree is a composable lambda, but instead of emitting UI directly,
        // it emits tree nodes. And each tree node takes a composable function that _can_ emit the UI
        // for that node.
        val tree = rememberLazyTree {
            // Note that the child lambdas for Branch nodes aren't until the node is expanded, and they
            // are removed from the composition when it's collapsed.
            Branch({ Text("Root") }) {
                Leaf { Text("Header") }
                Branch({ Text("Second child") }) {
                    Leaf { Text("Second-level leaf") }
                }
                Leaf { Text("Footer") }
            }
        }
    
        // This column will host the UI for the tree created above.
        LazyColumn {
            item {
                Text("This is a lazy tree.")
            }
    
            // Add a flattened representation of the expanded nodes from the tree to the column.
            // The composable lambda here will be used to wrap each node in the tree.
            treeItems(tree) { content ->
                Row(verticalAlignment = CenterVertically) {
                    // Indent each item proportional to its depth. This should match up with the size
                    // of the toggle button probably, it doesn't right now to keep the code simpler.
                    Spacer(Modifier.width(depth * 48.dp))
    
                    // Only show the expand/collapse toggle button for branch nodes. These properties
                    // and the setExpanded function are from the LazyItemTreeScope.
                    if (isExpandable) {
                        IconToggleButton(
                            checked = isExpanded,
                            onCheckedChange = ::setExpanded
                        ) {
                            val angle by animateFloatAsState(if (isExpanded) 0f else -90f)
                            Icon(
                                Icons.Default.ArrowDropDown,
                                contentDescription = if (isExpanded) "Collapse node" else "Expand node",
                                modifier = Modifier.graphicsLayer { rotationZ = angle }
                            )
                        }
                        Spacer(Modifier.width(8.dp))
                    }
    
                    // Compose the node's actual UI in the rest of the row.
                    content()
                }
            }
        }
    }
    

    Note the @UiComposable and @TreeNodeComposable annotations that document to the reader, and tell the compiler, what type of nodes can be emitted by each composable function.

    Also, I omitted the kotlin DSL annotations from the TreeBuilderScope type, but you'd probably want to have those on there to make sure that children can't accidentally refer to the wrong scope and get the wrong depth.

    Implementation
    import androidx.compose.animation.core.animateFloatAsState
    import androidx.compose.foundation.layout.Row
    import androidx.compose.foundation.layout.Spacer
    import androidx.compose.foundation.layout.width
    import androidx.compose.foundation.lazy.LazyColumn
    import androidx.compose.foundation.lazy.LazyListScope
    import androidx.compose.foundation.lazy.items
    import androidx.compose.material.Icon
    import androidx.compose.material.IconToggleButton
    import androidx.compose.material.Text
    import androidx.compose.material.icons.Icons
    import androidx.compose.material.icons.filled.ArrowDropDown
    import androidx.compose.runtime.AbstractApplier
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.ComposableTargetMarker
    import androidx.compose.runtime.ComposeNode
    import androidx.compose.runtime.Composition
    import androidx.compose.runtime.Stable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableStateListOf
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.rememberCompositionContext
    import androidx.compose.runtime.saveable.rememberSaveable
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Alignment.Companion.CenterVertically
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.UiComposable
    import androidx.compose.ui.graphics.graphicsLayer
    import androidx.compose.ui.unit.dp
    import androidx.compose.ui.unit.times
    
    /**
     * The receiver type of [TreeBuilderComposable] composable functions. Carries information about the
     * current depth of the tree. This could also be done via composition locals, but since this is just
     * a regular function parameter it's a lot cheaper. It's an API cleanliness/efficiency tradeoff.
     */
    @JvmInline
    value class TreeBuilderScope(val depth: Int)
    
    /**
     * Indicates that a composable function can only emit [Branch] and [Leaf] nodes, not UI.
     */
    @ComposableTargetMarker
    @Retention(AnnotationRetention.BINARY)
    @Target(
        AnnotationTarget.FUNCTION,
        AnnotationTarget.PROPERTY_GETTER,
        AnnotationTarget.TYPE,
        AnnotationTarget.TYPE_PARAMETER,
    )
    annotation class TreeBuilderComposable
    
    /**
     * Represents a tree structure that can be displayed inside a lazy list via [treeItems].
     * Created by [rememberLazyTree].
     */
    @Stable
    class LazyTreeContent internal constructor(internal val roots: List<TreeNode>)
    
    /**
     * Creates a subcomposition that will compose [content] to determine the contents of the tree
     * to show in a lazy list via [treeItems].
     *
     * @param content A composable function that, instead of emitting UI directly, emits [Branch] and
     * [Leaf] nodes that define the structure of a tree.
     */
    @Composable
    fun rememberLazyTree(
        content: @Composable @TreeBuilderComposable TreeBuilderScope.() -> Unit
    ): LazyTreeContent {
        val applier = remember { TreeApplier() }
        val compositionContext = rememberCompositionContext()
        val composition = remember(applier, compositionContext) {
            Composition(applier, compositionContext)
        }
        composition.setContent { TreeBuilderScope(0).content() }
        return remember(applier) { LazyTreeContent(applier.children) }
    }
    
    /**
     * Contains a number of properties about the current node that can be used to indicate the state of
     * the node in its UI.
     */
    interface LazyTreeItemScope {
        /** The depth of the node in the tree. */
        val depth: Int
    
        /** Whether the node can be expanded. */
        val isExpandable: Boolean
    
        /**
         * Whether the node is currently expanded and showing its children. Never true if
         * [isExpandable] is false
         */
        val isExpanded: Boolean
    
        /**
         * Requests a change to the value of [isExpanded].
         */
        fun setExpanded(expanded: Boolean)
    }
    
    /**
     * Adds the tree items from [content] to the lazy list.
     *
     * @param content The model of the tree to display, as created by [rememberLazyTree].
     * @param rowContent A wrapper composable that can add UI like branch toggle buttons and indentation
     * around each node, as required.
     */
    fun LazyListScope.treeItems(
        content: LazyTreeContent,
        rowContent: @Composable LazyTreeItemScope.(
            content: @Composable LazyTreeItemScope.() -> Unit
        ) -> Unit
    ) {
        // The compose runtime has already flattened the tree into a flat list, so we can just add that
        // list to the lazy one. This roots property is a snapshot state list, so whenever the tree
        // structure changes the lazy list will automatically update.
        items(content.roots) { item ->
            // TreeNode implements the LazyTreeItemScope interface itself.
            item.rowContent(item.content)
        }
    }
    
    /**
     * Emits a branch node of the tree. Branch nodes are expandable and, when expanded, run their
     * [children] function to determine the structure of the subtree beneath them.
     *
     * This composable function can only be called from inside a [rememberLazyTree] composition, not
     * from regular UI composables.
     *
     * This overload manages the expanded/collapsed state internally. There's another overload if you
     * want to hoist it and manage it yourself.
     *
     * @param content A Compose UI composable function that emits the UI for the node. This function
     * can emit things like text, buttons, and anything else that can exist in a regular Compose UI.
     * @param children A composable function that can make other [Branch] and [Leaf] calls to define
     * the structure of the subtree below this node. It can _not_ emit UI composables like text.
     */
    @Composable
    @TreeBuilderComposable
    fun TreeBuilderScope.Branch(
        content: @Composable @UiComposable LazyTreeItemScope.() -> Unit,
        children: @Composable @TreeBuilderComposable TreeBuilderScope.() -> Unit = {}
    ) {
        var expanded by rememberSaveable { mutableStateOf(false) }
        Branch(
            expanded = expanded,
            onToggleExpanded = { expanded = it },
            content = content,
            children = children
        )
    }
    
    /**
     * Emits a branch node of the tree. Branch nodes are expandable and, when expanded, run their
     * [children] function to determine the structure of the subtree beneath them.
     *
     * This composable function can only be called from inside a [rememberLazyTree] composition, not
     * from regular UI composables.
     *
     * @param expanded If true, the node will be shown with a UI indicator that it's expanded, and the
     * [children] function will be ran to compose its children. If false, [children] will not be
     * composed, and the UI will indicate that the node is collapsed.
     * @param onToggleExpanded Called when the node is expanded and collapsed. This callback should
     * update some state that causes [expanded] to be passed as the new value on the next composition.
     * @param content A Compose UI composable function that emits the UI for the node. This function
     * can emit things like text, buttons, and anything else that can exist in a regular Compose UI.
     * @param children A composable function that can make other [Branch] and [Leaf] calls to define
     * the structure of the subtree below this node. It can _not_ emit UI composables like text.
     */
    @Composable
    @TreeBuilderComposable
    fun TreeBuilderScope.Branch(
        expanded: Boolean,
        onToggleExpanded: (Boolean) -> Unit,
        content: @Composable @UiComposable LazyTreeItemScope.() -> Unit,
        children: @Composable @TreeBuilderComposable TreeBuilderScope.() -> Unit = {}
    ) {
        ComposeNode<TreeNode, TreeApplier>(
            factory = { TreeNode(depth, isExpandable = true) },
            update = {
                set(content) { this.content = it }
                set(expanded) { this._isExpanded = it }
                set(onToggleExpanded) { this.onToggleExpanded = onToggleExpanded }
            }
        )
    
        if (expanded) {
            // Typically the children of a ComposeNode are passed as the children parameter to the
            // ComposeNode function. That has the effect of making any nodes they emit children of this
            // ComposeNode. However, in this case, we don't want to emit a tree structure, we want to
            // emit a flattened tree where the children of a node show up as its siblings.
            // In other words, we're making the emitted structure mirror the structure of the slot table
            // itself.
            TreeBuilderScope(depth + 1).children()
        }
    }
    
    /**
     * Emits a leaf node of the tree. Leaf nodes are not expandable and never have children.
     *
     * This composable function can only be called from inside a [rememberLazyTree] composition, not
     * from regular UI composables.
     *
     * @param content A Compose UI composable function that emits the UI for the node. This function
     * can emit things like text, buttons, and anything else that can exist in a regular Compose UI.
     */
    @Composable
    @TreeBuilderComposable
    fun TreeBuilderScope.Leaf(content: @Composable @UiComposable LazyTreeItemScope.() -> Unit) {
        ComposeNode<TreeNode, TreeApplier>(
            factory = { TreeNode(depth, isExpandable = false) },
            update = {
                // Leaf nodes aren't expandable so we don't need to set any of the other properties.
                set(content) { this.content = it }
            }
        )
    }
    
    /**
     * Represents information for the UI about each node of the tree. These nodes are emitted by
     * [ComposeNode] calls in the composable functions above that define the tree-building DSL, and are
     * collected by the [TreeApplier] class.
     *
     * This is also internal to the library. The public API for creating these nodes is the [Branch] and
     * [Leaf] composable functions.
     *
     * @param depth The depth of the node in the tree, used to calculate indentation in layout.
     * @param isExpandable Whether an expansion toggle should be shown for this node. If true, it might
     * have children, but that might not be known until the children are actually requested.
     */
    internal class TreeNode(
        override val depth: Int,
        override val isExpandable: Boolean
    ) : LazyTreeItemScope {
        /**
         * Backing property for [isExpanded] so that the setter doesn't clash with the [setExpanded]
         * function.
         */
        @Suppress("PropertyName")
        var _isExpanded: Boolean by mutableStateOf(false)
        override val isExpanded: Boolean get() = _isExpanded
    
        override fun setExpanded(expanded: Boolean) {
            onToggleExpanded(expanded)
        }
    
        var onToggleExpanded: (Boolean) -> Unit by mutableStateOf({})
        var content: @Composable @UiComposable LazyTreeItemScope.() -> Unit by mutableStateOf({})
    
        override fun toString(): String = "TreeNode(" +
            "depth=$depth, " +
            "isExpandable=$isExpandable, " +
            "isExpanded=$isExpanded" +
            ")"
    }
    
    /**
     * This class is not part of this library's public API. It's used by the Compose runtime to convert
     * [ComposeNode] calls to the actual structure of emitted nodes. Typically that's a tree, but in
     * this case, since we are flattening a tree, we just build a flat list of [TreeNode]s.
     * The root "node" for the applier is simply null, and every method includes an assertion to make
     * sure that nothing is trying to create a tree.
     */
    private class TreeApplier : AbstractApplier<TreeNode?>(null) {
        /**
         * This is a mutable _state_ list so that the output from the tree-building subcomposition can
         * be observed by the [LazyColumn]'s layout.
         */
        val children = mutableStateListOf<TreeNode>()
    
        override fun insertTopDown(index: Int, instance: TreeNode?) {
            checkNotNull(instance)
            check(current == null)
            children.add(index, instance)
        }
    
        override fun insertBottomUp(index: Int, instance: TreeNode?) {
            // Only this or insertTopDown should be implemented. In our case it doesn't matter since
            // we're building a list, not a tree, so either one works and I just picked topDown
            // arbitrarily.
        }
    
        override fun remove(index: Int, count: Int) {
            check(current == null)
            children.removeRange(index, index + count)
        }
    
        override fun move(from: Int, to: Int, count: Int) {
            check(current == null)
            // This move helper function is defined in AbstractApplier, but only for lists of the exact
            // type of the nodes of this applier, so we have to cast our list type to get access to it.
            @Suppress("UNCHECKED_CAST")
            (children as MutableList<TreeNode?>).move(from, to, count)
        }
    
        override fun onClear() {
            check(current == null)
            children.clear()
        }
    }
    
    enhancement 
    opened by zach-klippenstein 5
  • CompositionLocal LocalLayoutDirection not present (1.2.0)

    CompositionLocal LocalLayoutDirection not present (1.2.0)

    Adrielcafe, muito obrigado pelo seu trabalho. Tenho o seguinte erro quando aplico o exemplo da home page do bonsái, a árvore animal e os ícones, mas não sei qual é o problema. No meu caso, é simplesmente um projeto novo compose desktop:

    Exception in thread "main" java.lang.IllegalStateException: CompositionLocal LocalLayoutDirection not present
    	at androidx.compose.ui.platform.CompositionLocalsKt.noLocalProvidedFor(CompositionLocals.kt:188)
    	at androidx.compose.ui.platform.CompositionLocalsKt.access$noLocalProvidedFor(CompositionLocals.kt:1)
    	at androidx.compose.ui.platform.CompositionLocalsKt$LocalLayoutDirection$1.invoke(CompositionLocals.kt:119)
    	at androidx.compose.ui.platform.CompositionLocalsKt$LocalLayoutDirection$1.invoke(CompositionLocals.kt:118)
    	at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74)
    	at androidx.compose.runtime.LazyValueHolder.getCurrent(ValueHolders.kt:29)
    	at androidx.compose.runtime.LazyValueHolder.getValue(ValueHolders.kt:31)
    	at androidx.compose.runtime.ComposerImpl.resolveCompositionLocal(Composer.kt:1776)
    	at androidx.compose.runtime.ComposerImpl.consume(Composer.kt:1746)
    	at androidx.compose.foundation.lazy.LazyListKt.LazyList(LazyList.kt:310)
    	at androidx.compose.foundation.lazy.LazyDslKt.LazyColumn(LazyDsl.kt:250)
    	at cafe.adriel.bonsai.core.BonsaiKt.Bonsai(Bonsai.kt:92)
    	at com.gioia.radio.MainKt.BonsaiExample(Main.kt:58)
    	at com.gioia.radio.ComposableSingletons$MainKt$lambda-13$1.invoke(Main.kt:62)
    	at com.gioia.radio.ComposableSingletons$MainKt$lambda-13$1.invoke(Main.kt:61)
    	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:116)
    	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
    	at androidx.compose.ui.window.Application_desktopKt$application$1$1.invoke(Application.desktop.kt:116)
    	at androidx.compose.ui.window.Application_desktopKt$application$1$1.invoke(Application.desktop.kt:115)
    	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:116)
    	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
    	at androidx.compose.ui.window.Application_desktopKt$awaitApplication$2$1$2$1$1.invoke(Application.desktop.kt:227)
    	at androidx.compose.ui.window.Application_desktopKt$awaitApplication$2$1$2$1$1.invoke(Application.desktop.kt:226)
    	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
    	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
    	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
    	at androidx.compose.ui.window.Application_desktopKt$awaitApplication$2$1$2$1.invoke(Application.desktop.kt:222)
    	at androidx.compose.ui.window.Application_desktopKt$awaitApplication$2$1$2$1.invoke(Application.desktop.kt:220)
    	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
    	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
    	at androidx.compose.runtime.ActualJvm_jvmKt.invokeComposable(ActualJvm.jvm.kt:72)
    	at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:2582)
    	at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:2571)
    	at androidx.compose.runtime.SnapshotStateKt__DerivedStateKt.observeDerivedStateRecalculations(DerivedState.kt:247)
    	at androidx.compose.runtime.SnapshotStateKt.observeDerivedStateRecalculations(Unknown Source)
    	at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:2571)
    	at androidx.compose.runtime.ComposerImpl.composeContent$runtime(Composer.kt:2522)
    	at androidx.compose.runtime.CompositionImpl.composeContent(Composition.kt:478)
    	at androidx.compose.runtime.Recomposer.composeInitial$runtime(Recomposer.kt:748)
    	at androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:433)
    	at androidx.compose.ui.window.Application_desktopKt$awaitApplication$2$1$2.invokeSuspend(Application.desktop.kt:220)
    	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    	at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:313)
    	at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:770)
    	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:721)
    	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:715)
    	at java.base/java.security.AccessController.doPrivileged(Native Method)
    	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
    	at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:740)
    	at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
    	at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
    	at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
    	at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
    	at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
    	at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)
    

    Process finished with exit code 1

    Meu código é o seguinte:

    @Composable
    private fun EmojiIcon(emoji: String) {
        Text(emoji)
    }
    
    @Composable
    fun BonsaiExample() {
        val tree = Tree<String> {
            Branch("Mammalia") {
                Branch("Carnivora") {
                    Branch("Canidae") {
                        Branch("Canis") {
                            Leaf("Wolf", customIcon = { EmojiIcon("🐺") })
                            Leaf("Dog", customIcon = { EmojiIcon("🐶") })
                        }
                    }
                    Branch("Felidae") {
                        Branch("Felis") {
                            Leaf("Cat", customIcon = { EmojiIcon("🐱") })
                        }
                        Branch("Panthera") {
                            Leaf("Lion", customIcon = { EmojiIcon("🦁") })
                        }
                    }
                }
            }
        }
    
        Bonsai(tree)
    }
    
    fun main() = application{
        BonsaiExample()
    }
    

    Em build.gradle, além das outras coisas: `plugins { kotlin("jvm") version "1.5.31" id("org.jetbrains.compose") version "1.0.0" } tasks.withType { kotlinOptions.jvmTarget = "11"

    } `

    Saludações.

    opened by lucas-gio 2
  • Is there a way to disable horizontal scrolling?

    Is there a way to disable horizontal scrolling?

    I want to disable the horizontal scrolling of the LazyColumn.

    Update - For now I have directly included the bonsai-core lib locally in the app with required changes.

    opened by Yash-Garg 0
  • How do I use it recursively like with a `LazyColumn`?

    How do I use it recursively like with a `LazyColumn`?

    I have a custom model class ContentTreeItem which has a parent folder and children list. Furthermore, which can also contain multiple sub folder and files.

    I am unable to understand how do I implement it with Bonsai? My LazyColumn implementation is below:

    @Composable
    fun ContentTreeView(nodes: List<ContentTreeItem>) {
        val expandedItems = remember { mutableStateListOf<ContentTreeItem>() }
        LazyColumn {
            nodes(
                nodes,
                isExpanded = { expandedItems.contains(it) },
                toggleExpanded = {
                    if (expandedItems.contains(it)) {
                        expandedItems.remove(it)
                    } else {
                        expandedItems.add(it)
                    }
                },
            )
        }
    }
    
    fun LazyListScope.nodes(
        nodes: List<ContentTreeItem>,
        isExpanded: (ContentTreeItem) -> Boolean,
        toggleExpanded: (ContentTreeItem) -> Unit,
    ) {
        nodes.forEach { node ->
            node(
                node,
                isExpanded = isExpanded,
                toggleExpanded = toggleExpanded,
            )
        }
    }
    
    fun LazyListScope.node(
        node: ContentTreeItem,
        isExpanded: (ContentTreeItem) -> Boolean,
        toggleExpanded: (ContentTreeItem) -> Unit,
    ) {
        item {
            Text(
                node.name,
                modifier = Modifier
                    .fillMaxSize()
                    .clickable { toggleExpanded(node) }
                    .padding(16.dp)
            )
        }
        if (isExpanded(node)) {
            node.children?.reversed()?.let { childNodes ->
                nodes(
                    childNodes,
                    isExpanded = isExpanded,
                    toggleExpanded = toggleExpanded,
                )
            }
        }
    }
    

    The list of files are from an API so I cannot check if it's a Directory or a File.

    opened by Yash-Garg 0
  • Memory Leak possibly due to no proper `disposing` of `Composition`

    Memory Leak possibly due to no proper `disposing` of `Composition`

    I am facing a memory leak when using this library. I think it's probably due to the Composition used in bonsai/core/tree/Tree.kt.

    There is a method for disposing the composition (composition.dispose()) which isn't getting called. It would be great if you can have a look at it.

    Tried adding myComposeView.disposeComposition() in onStop() of Fragment, but got no luck.

    Docs Reference

    Leakcanary Stacktrace

    D/LeakCanary: ​
    D/LeakCanary: ====================================
    D/LeakCanary: HEAP ANALYSIS RESULT
    D/LeakCanary: ====================================
    D/LeakCanary: 3 APPLICATION LEAKS
    D/LeakCanary:D/LeakCanary: References underlined with "~~~" are likely causes.
    D/LeakCanary: Learn more at https://squ.re/leaks.
    D/LeakCanary:D/LeakCanary: 2794 bytes retained by leaking objects
    D/LeakCanary: Signature: 294c14f86ef1f535d2d18c00e3aefe49abd3e220
    D/LeakCanary: ┬───
    D/LeakCanary: │ GC Root: Input or output parameters in native code
    D/LeakCanary: │
    D/LeakCanary: ├─ dalvik.system.PathClassLoader instance
    D/LeakCanary: │    Leaking: NO (SnapshotKt↓ is not leaking and A ClassLoader is never leaking)
    D/LeakCanary: │    ↓ ClassLoader.runtimeInternalObjects
    D/LeakCanary: ├─ java.lang.Object[] array
    D/LeakCanary: │    Leaking: NO (SnapshotKt↓ is not leaking)
    D/LeakCanary: │    ↓ Object[6667]
    D/LeakCanary: ├─ androidx.compose.runtime.snapshots.SnapshotKt class
    D/LeakCanary: │    Leaking: NO (Recomposer↓ is not leaking and a class is never leaking)
    D/LeakCanary: │    ↓ static SnapshotKt.applyObservers
    D/LeakCanary: ├─ java.util.ArrayList instance
    D/LeakCanary: │    Leaking: NO (Recomposer↓ is not leaking)
    D/LeakCanary: │    ↓ ArrayList[0]
    D/LeakCanary: ├─ androidx.compose.runtime.Recomposer$recompositionRunner$2$unregisterApplyObserver$1 instance
    D/LeakCanary: │    Leaking: NO (Recomposer↓ is not leaking)
    D/LeakCanary: │    Anonymous subclass of kotlin.jvm.internal.Lambda
    D/LeakCanary: │    ↓ Recomposer$recompositionRunner$2$unregisterApplyObserver$1.this$0
    D/LeakCanary: ├─ androidx.compose.runtime.Recomposer instance
    D/LeakCanary: │    Leaking: NO (CompositionImpl↓ is not leaking and Recomposer is in state Idle)
    D/LeakCanary: │    ↓ Recomposer.knownCompositions
    D/LeakCanary: ├─ java.util.ArrayList instance
    D/LeakCanary: │    Leaking: NO (CompositionImpl↓ is not leaking)
    D/LeakCanary: │    ↓ ArrayList[0]
    D/LeakCanary: ├─ androidx.compose.runtime.CompositionImpl instance
    D/LeakCanary: │    Leaking: NO (Composition not disposed)
    D/LeakCanary: │    ↓ CompositionImpl.composer
    D/LeakCanary: │                      ~~~~~~~~
    D/LeakCanary: ├─ androidx.compose.runtime.ComposerImpl instance
    D/LeakCanary: │    Leaking: UNKNOWN
    D/LeakCanary: │    Retaining 9.1 kB in 48 objects
    D/LeakCanary: │    ↓ ComposerImpl.parentProvider
    D/LeakCanary: │                   ~~~~~~~~~~~~~~
    D/LeakCanary: ├─ androidx.compose.runtime.external.kotlinx.collections.immutable.implementations.immutableMap.PersistentHashMap
    D/LeakCanary: │  instance
    D/LeakCanary: │    Leaking: UNKNOWN
    D/LeakCanary: │    Retaining 320 B in 8 objects
    D/LeakCanary: │    ↓ PersistentHashMap.node
    D/LeakCanary: │                        ~~~~
    D/LeakCanary: ├─ androidx.compose.runtime.external.kotlinx.collections.immutable.implementations.immutableMap.TrieNode instance
    D/LeakCanary: │    Leaking: UNKNOWN
    D/LeakCanary: │    Retaining 296 B in 7 objects
    D/LeakCanary: │    ↓ TrieNode.buffer
    D/LeakCanary: │               ~~~~~~
    D/LeakCanary: ├─ java.lang.Object[] array
    D/LeakCanary: │    Leaking: UNKNOWN
    D/LeakCanary: │    Retaining 272 B in 6 objects
    D/LeakCanary: │    ↓ Object[45]
    D/LeakCanary: │            ~~~~
    D/LeakCanary: ├─ androidx.compose.runtime.StaticValueHolder instance
    D/LeakCanary: │    Leaking: UNKNOWN
    D/LeakCanary: │    Retaining 12 B in 1 objects
    D/LeakCanary: │    ↓ StaticValueHolder.value
    D/LeakCanary: │                        ~~~~~
    D/LeakCanary: ├─ androidx.fragment.app.FragmentViewLifecycleOwner instance
    D/LeakCanary: │    Leaking: UNKNOWN
    D/LeakCanary: │    Retaining 173 B in 6 objects
    D/LeakCanary: │    ↓ FragmentViewLifecycleOwner.mFragment
    D/LeakCanary: │                                 ~~~~~~~~~
    D/LeakCanary: ╰→ dev.yashgarg.qbit.ui.torrent.tabs.TorrentFilesFragment instance
    D/LeakCanary: ​     Leaking: YES (ObjectWatcher was watching this because dev.yashgarg.qbit.ui.torrent.tabs.TorrentFilesFragment
    D/LeakCanary: ​     received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
    D/LeakCanary: ​     Retaining 2.8 kB in 105 objects
    D/LeakCanary: ​     key = 729161f9-e2c4-4211-a526-d0a54992cd9e
    D/LeakCanary: ​     watchDurationMillis = 5484
    D/LeakCanary: ​     retainedDurationMillis = 483
    D/LeakCanary:D/LeakCanary: 210 bytes retained by leaking objects
    D/LeakCanary: Signature: cd1c7c9324750e1908bfb93e73a7daaccc8ddf30
    D/LeakCanary: ┬───
    D/LeakCanary: │ GC Root: Input or output parameters in native code
    D/LeakCanary: │
    D/LeakCanary: ├─ dalvik.system.PathClassLoader instance
    D/LeakCanary: │    Leaking: NO (SnapshotKt↓ is not leaking and A ClassLoader is never leaking)
    D/LeakCanary: │    ↓ ClassLoader.runtimeInternalObjects
    D/LeakCanary: ├─ java.lang.Object[] array
    D/LeakCanary: │    Leaking: NO (SnapshotKt↓ is not leaking)
    D/LeakCanary: │    ↓ Object[6667]
    D/LeakCanary: ├─ androidx.compose.runtime.snapshots.SnapshotKt class
    D/LeakCanary: │    Leaking: NO (Recomposer↓ is not leaking and a class is never leaking)
    D/LeakCanary: │    ↓ static SnapshotKt.applyObservers
    D/LeakCanary: ├─ java.util.ArrayList instance
    D/LeakCanary: │    Leaking: NO (Recomposer↓ is not leaking)
    D/LeakCanary: │    ↓ ArrayList[0]
    D/LeakCanary: ├─ androidx.compose.runtime.Recomposer$recompositionRunner$2$unregisterApplyObserver$1 instance
    D/LeakCanary: │    Leaking: NO (Recomposer↓ is not leaking)
    D/LeakCanary: │    Anonymous subclass of kotlin.jvm.internal.Lambda
    D/LeakCanary: │    ↓ Recomposer$recompositionRunner$2$unregisterApplyObserver$1.this$0
    D/LeakCanary: ├─ androidx.compose.runtime.Recomposer instance
    D/LeakCanary: │    Leaking: NO (CompositionImpl↓ is not leaking and Recomposer is in state Idle)
    D/LeakCanary: │    ↓ Recomposer.knownCompositions
    D/LeakCanary: ├─ java.util.ArrayList instance
    D/LeakCanary: │    Leaking: NO (CompositionImpl↓ is not leaking)
    D/LeakCanary: │    ↓ ArrayList[0]
    D/LeakCanary: ├─ androidx.compose.runtime.CompositionImpl instance
    D/LeakCanary: │    Leaking: NO (Composition not disposed)
    D/LeakCanary: │    ↓ CompositionImpl.slotTable
    D/LeakCanary: │                      ~~~~~~~~~
    D/LeakCanary: ├─ androidx.compose.runtime.SlotTable instance
    D/LeakCanary: │    Leaking: UNKNOWN
    D/LeakCanary: │    Retaining 345 B in 17 objects
    D/LeakCanary: │    ↓ SlotTable.slots
    D/LeakCanary: │                ~~~~~
    D/LeakCanary: ├─ java.lang.Object[] array
    D/LeakCanary: │    Leaking: UNKNOWN
    D/LeakCanary: │    Retaining 6.4 kB in 206 objects
    D/LeakCanary: │    ↓ Object[32]
    D/LeakCanary: │            ~~~~
    D/LeakCanary: ├─ androidx.compose.ui.platform.DisposableSaveableStateRegistry instance
    D/LeakCanary: │    Leaking: UNKNOWN
    D/LeakCanary: │    Retaining 37 B in 2 objects
    D/LeakCanary: │    ↓ DisposableSaveableStateRegistry.onDispose
    D/LeakCanary: │                                      ~~~~~~~~~
    D/LeakCanary: ├─ androidx.compose.ui.platform.DisposableSaveableStateRegistry_androidKt$DisposableSaveableStateRegistry$1 instance
    D/LeakCanary: │    Leaking: UNKNOWN
    D/LeakCanary: │    Retaining 21 B in 1 objects
    D/LeakCanary: │    Anonymous subclass of kotlin.jvm.internal.Lambda
    D/LeakCanary: │    ↓ DisposableSaveableStateRegistry_androidKt$DisposableSaveableStateRegistry$1.$androidxRegistry
    D/LeakCanary: │                                                                                  ~~~~~~~~~~~~~~~~~
    D/LeakCanary: ├─ androidx.savedstate.SavedStateRegistry instance
    D/LeakCanary: │    Leaking: UNKNOWN
    D/LeakCanary: │    Retaining 506 B in 19 objects
    D/LeakCanary: │    ↓ SavedStateRegistry.components
    D/LeakCanary: │                         ~~~~~~~~~~
    D/LeakCanary: ├─ androidx.arch.core.internal.SafeIterableMap instance
    D/LeakCanary: │    Leaking: UNKNOWN
    D/LeakCanary: │    Retaining 483 B in 18 objects
    D/LeakCanary: │    ↓ SafeIterableMap["androidx.lifecycle.internal.SavedStateHandlesProvider"]
    D/LeakCanary: │                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    D/LeakCanary: ├─ androidx.lifecycle.SavedStateHandlesProvider instance
    D/LeakCanary: │    Leaking: UNKNOWN
    D/LeakCanary: │    Retaining 251 B in 10 objects
    D/LeakCanary: │    ↓ SavedStateHandlesProvider.viewModel$delegate
    D/LeakCanary: │                                ~~~~~~~~~~~~~~~~~~
    D/LeakCanary: ├─ kotlin.SynchronizedLazyImpl instance
    D/LeakCanary: │    Leaking: UNKNOWN
    D/LeakCanary: │    Retaining 230 B in 9 objects
    D/LeakCanary: │    ↓ SynchronizedLazyImpl._value
    D/LeakCanary: │                           ~~~~~~
    D/LeakCanary: ╰→ androidx.lifecycle.SavedStateHandlesVM instance
    D/LeakCanary: ​     Leaking: YES (ObjectWatcher was watching this because androidx.lifecycle.SavedStateHandlesVM received
    D/LeakCanary: ​     ViewModel#onCleared() callback)
    D/LeakCanary: ​     Retaining 210 B in 8 objects
    D/LeakCanary: ​     key = 375b335d-00db-4565-8c6d-0848ea7ca599
    D/LeakCanary: ​     watchDurationMillis = 5485
    D/LeakCanary: ​     retainedDurationMillis = 483
    D/LeakCanary:D/LeakCanary: 15324 bytes retained by leaking objects
    D/LeakCanary: Signature: 3074046c89ba82b393e6e7ba1f02c8a330683dbd
    D/LeakCanary: ┬───
    D/LeakCanary: │ GC Root: Input or output parameters in native code
    D/LeakCanary: │
    D/LeakCanary: ├─ dalvik.system.PathClassLoader instance
    D/LeakCanary: │    Leaking: NO (SnapshotKt↓ is not leaking and A ClassLoader is never leaking)
    D/LeakCanary: │    ↓ ClassLoader.runtimeInternalObjects
    D/LeakCanary: ├─ java.lang.Object[] array
    D/LeakCanary: │    Leaking: NO (SnapshotKt↓ is not leaking)
    D/LeakCanary: │    ↓ Object[6667]
    D/LeakCanary: ├─ androidx.compose.runtime.snapshots.SnapshotKt class
    D/LeakCanary: │    Leaking: NO (Recomposer↓ is not leaking and a class is never leaking)
    D/LeakCanary: │    ↓ static SnapshotKt.applyObservers
    D/LeakCanary: ├─ java.util.ArrayList instance
    D/LeakCanary: │    Leaking: NO (Recomposer↓ is not leaking)
    D/LeakCanary: │    ↓ ArrayList[0]
    D/LeakCanary: ├─ androidx.compose.runtime.Recomposer$recompositionRunner$2$unregisterApplyObserver$1 instance
    D/LeakCanary: │    Leaking: NO (Recomposer↓ is not leaking)
    D/LeakCanary: │    Anonymous subclass of kotlin.jvm.internal.Lambda
    D/LeakCanary: │    ↓ Recomposer$recompositionRunner$2$unregisterApplyObserver$1.this$0
    D/LeakCanary: ├─ androidx.compose.runtime.Recomposer instance
    D/LeakCanary: │    Leaking: NO (CompositionImpl↓ is not leaking and Recomposer is in state Idle)
    D/LeakCanary: │    ↓ Recomposer.knownCompositions
    D/LeakCanary: ├─ java.util.ArrayList instance
    D/LeakCanary: │    Leaking: NO (CompositionImpl↓ is not leaking)
    D/LeakCanary: │    ↓ ArrayList[0]
    D/LeakCanary: ├─ androidx.compose.runtime.CompositionImpl instance
    D/LeakCanary: │    Leaking: NO (Composition not disposed)
    D/LeakCanary: │    ↓ CompositionImpl.parent
    D/LeakCanary: │                      ~~~~~~
    D/LeakCanary: ├─ androidx.compose.runtime.ComposerImpl$CompositionContextImpl instance
    D/LeakCanary: │    Leaking: UNKNOWN
    D/LeakCanary: │    Retaining 190 B in 6 objects
    D/LeakCanary: │    ↓ ComposerImpl$CompositionContextImpl.this$0
    D/LeakCanary: │                                          ~~~~~~
    D/LeakCanary: ├─ androidx.compose.runtime.ComposerImpl instance
    D/LeakCanary: │    Leaking: UNKNOWN
    D/LeakCanary: │    Retaining 17.2 kB in 54 objects
    D/LeakCanary: │    ↓ ComposerImpl.composition
    D/LeakCanary: │                   ~~~~~~~~~~~
    D/LeakCanary: ├─ androidx.compose.runtime.CompositionImpl instance
    D/LeakCanary: │    Leaking: YES (Composition disposed)
    D/LeakCanary: │    Retaining 15.3 kB in 388 objects
    D/LeakCanary: │    ↓ CompositionImpl.observations
    D/LeakCanary: ├─ androidx.compose.runtime.collection.IdentityScopeMap instance
    D/LeakCanary: │    Leaking: YES (CompositionImpl↑ is leaking)
    D/LeakCanary: │    Retaining 13.2 kB in 356 objects
    D/LeakCanary: │    ↓ IdentityScopeMap.scopeSets
    D/LeakCanary: ├─ androidx.compose.runtime.collection.IdentityArraySet[] array
    D/LeakCanary: │    Leaking: YES (CompositionImpl↑ is leaking)
    D/LeakCanary: │    Retaining 10.5 kB in 252 objects
    D/LeakCanary: │    ↓ IdentityArraySet[4]
    D/LeakCanary: ├─ androidx.compose.runtime.collection.IdentityArraySet instance
    D/LeakCanary: │    Leaking: YES (CompositionImpl↑ is leaking)
    D/LeakCanary: │    Retaining 148 B in 5 objects
    D/LeakCanary: │    ↓ IdentityArraySet.values
    D/LeakCanary: ├─ java.lang.Object[] array
    D/LeakCanary: │    Leaking: YES (CompositionImpl↑ is leaking)
    D/LeakCanary: │    Retaining 132 B in 4 objects
    D/LeakCanary: │    ↓ Object[0]
    D/LeakCanary: ├─ androidx.compose.runtime.RecomposeScopeImpl instance
    D/LeakCanary: │    Leaking: YES (CompositionImpl↑ is leaking)
    D/LeakCanary: │    Retaining 68 B in 3 objects
    D/LeakCanary: │    ↓ RecomposeScopeImpl.block
    D/LeakCanary: ├─ androidx.compose.ui.platform.ComposeView$Content$1 instance
    D/LeakCanary: │    Leaking: YES (CompositionImpl↑ is leaking)
    D/LeakCanary: │    Retaining 20 B in 1 objects
    D/LeakCanary: │    Anonymous subclass of kotlin.jvm.internal.Lambda
    D/LeakCanary: │    ↓ ComposeView$Content$1.$tmp1_rcvr
    D/LeakCanary: ╰→ androidx.compose.ui.platform.ComposeView instance
    D/LeakCanary: ​     Leaking: YES (ObjectWatcher was watching this because dev.yashgarg.qbit.ui.torrent.tabs.TorrentFilesFragment
    D/LeakCanary: ​     received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
    D/LeakCanary: ​     Retaining 2.4 kB in 39 objects
    D/LeakCanary: ​     key = d15cfab8-cfee-4de6-82f2-9e5e4d4f6d02
    D/LeakCanary: ​     watchDurationMillis = 5489
    D/LeakCanary: ​     retainedDurationMillis = 488
    D/LeakCanary: ​     View not part of a window view hierarchy
    D/LeakCanary: ​     View.mAttachInfo is null (view detached)
    D/LeakCanary: ​     View.mID = R.id.filesComposeView
    D/LeakCanary: ​     View.mWindowAttachCount = 1
    D/LeakCanary: ​     mContext instance of dev.yashgarg.qbit.MainActivity with mDestroyed = false
    D/LeakCanary: ====================================
    
    opened by Yash-Garg 0
  • Unable to make customName content use full width

    Unable to make customName content use full width

    Branch("Carnivora", customName = { Text("Full Width", modifier = Modifier.fillMaxWidth().background(Color.Magenta)) }

    That doesn't work. It's still minimum width. Please help :)

    opened by ferreus 1
  • java.lang.ClassNotFoundException: androidx.compose.material.icons.filled.ChevronRightKt in compose desktop

    java.lang.ClassNotFoundException: androidx.compose.material.icons.filled.ChevronRightKt in compose desktop

    When bonsai is rendering the tree, an class not found exception is thrown. After investigate, the solution is to add the dependency (gradle): var icons = "1.1.1" implementation("org.jetbrains.compose.material:material-icons-extended-desktop:$icons")

    I dont know which is better: transitive dependency or documentate this special case of compose desktop for add this in build gradle/maven.

    opened by lucas-gio 1
Releases(1.2.0)
  • 1.2.0(Apr 21, 2022)

    BREAKING CHANGES

    • The library was rewritten to use a custom composition (#1)
    • Removed rememberTree<T>(), SimpleLeafNode<T> and SimpleBranchNode<T> in favor of the DSL
    • The DSL is now the default way to build a tree (it uses the custom composition)
    • Added customIcon and customName params on Leaf<T>() and Branch<T>()
    • Replaced fileSystemNodes() with FileSystemTree()
    • Replaced jsonNodes() with JsonTree()
    • Disabled expand/collapse transitions for now (doesn't work yet with custom composition)
    Source code(tar.gz)
    Source code(zip)
  • 1.1.0(Apr 15, 2022)

  • 1.0.0(Apr 14, 2022)

    First release with a bunch of cool features:

    • Multiplatform: Android, Desktop
    • State-aware: changes in the tree will trigger recomposition
    • Unlimited levels
    • File system integration
    • Built-in DSL
    • Expandable
    • Selectable
    • Clickable
    • Styleable
    • Extendable
    Source code(tar.gz)
    Source code(zip)
Owner
Adriel Café
In a love-hate relationship with Android for a decade
Adriel Café
Example Jetpack Compose Android App, that uses the newest mechanisms, like StateFlow, SharedFlow, etc. to manage states and handle events. ViewModel, UI and Screenshot tests included :)

AndroidMVIExample Example Jetpack Compose Android App, that uses the newest mechanisms, like StateFlow, SharedFlow, etc. to manage states and handle e

Patryk Kosieradzki 55 Nov 18, 2022
OTPView is a view made in Jetpack compose. It is highly customisable and can be used to show OTP view with different length and shapes.

OTPView OTPView is a highly costumizable OTP view made in the Jetpack compose UI. Usage: CircleOtpView is a sample composable that calls the OtpView w

kunalsale 17 Aug 4, 2022
Jetpack Compose Boids | Flocking Insect 🐜. bird or Fish simulation using Jetpack Compose Desktop 🚀, using Canvas API 🎨

?? ?? ?? Compose flocking Ants(boids) ?? ?? ?? Jetpack compose Boids | Flocking Insect. bird or Fish simulation using Jetpack Compose Desktop ?? , usi

Chetan Gupta 38 Sep 25, 2022
A collection of animations, compositions, UIs using Jetpack Compose. You can say Jetpack Compose cookbook or play-ground if you want!

Why Not Compose! A collection of animations, compositions, UIs using Jetpack Compose. You can say Jetpack Compose cookbook or play-ground if you want!

Md. Mahmudul Hasan Shohag 186 Jan 1, 2023
Learn Jetpack Compose for Android by Examples. Learn how to use Jetpack Compose for Android App Development. Android’s modern toolkit for building native UI.

Learn Jetpack Compose for Android by Examples. Learn how to use Jetpack Compose for Android App Development. Android’s modern toolkit for building native UI.

MindOrks 382 Jan 5, 2023
This is a sample app(For beginners - App #2) built using Jetpack Compose. It demonstrates the concept of State Hoisting in Jetpack Compose.

JetBMICalculator This is a sample app(For beginners - App #2) built using Jetpack Compose. It demonstrates the concept of State Hoisting in Jetpack Co

BHAVNA THACKER 3 Dec 31, 2022
Jetpack-Compose-Demo - Instagram Profile UI using Jetpack Compose

Jetpack-Compose-Demo Instagram Profile UI using Jetpack Compose

omar 1 Aug 11, 2022
Jetpack-compose-animations-examples - Cool animations implemented with Jetpack compose

Jetpack-compose-animations-examples This repository consists of 4 animations: St

Canopas Software 180 Jan 2, 2023
Compose-navigation - Set of utils to help with integrating Jetpack Compose and Jetpack's Navigation

Jetpack Compose Navigation Set of utils to help with integrating Jetpack Compose

Adam Kobus 5 Apr 5, 2022
Jetpack-compose-uis - A collection of some UIs using Jetpack Compose. built using Katalog

Jetpack Compose UIs This is a collection of some UIs using Jetpack Compose. It i

Mori Atsushi 3 Dec 15, 2022
A simple authentication application using Jetpack compose to illustrate signin and sign up using Mvvm, Kotlin and jetpack compose

Authentication A simple authentication application using Jetpack compose to illustrate signin and sign up using Mvvm, Kotlin and jetpack compose Scree

Felix Kariuki 5 Dec 29, 2022
🎺 Orchestra is a collection of Android custom view compatible libraries for Jetpack Compose.

Orchestra ?? Jetpack Compose compatible libraries using Balloon, ColorPickerView, PowerSpinner. Balloon Add below codes to your root build.gradle file

Jaewoong Eum 408 Jan 4, 2023
An MVI project setup using Jetpack compose. This would be a good place to start learning Model View Intent (MVI) architecture for Android.

Compose-MVI An MVI project setup using Jetpack compose. This would be a good place to start learning Model View Intent (MVI) architecture for Android.

null 6 Jul 28, 2022
ComposeCreditCardView - Jetpack Compose Credit Card View Library

ComposeCreditCardView Jetpack Compose Credit Card View Library Screenshots ??  

Umut Soysal 5 Sep 19, 2022
Jetpack Compose Timeline View

Simple Timeline View, wrote via Kotlin Jetpack Compose.

Yunus Emre OCAK 5 Dec 11, 2022
Partial port of https://github.com/davemorrissey/subsampling-scale-image-view library to Jetpack Compose.

ComposeSubsamplingScaleImage Early preview (expect bugs) dependencies { implementation 'com.github.K1rakishou:ComposeSubsamplingScaleImage:fab4ae38c

Dmitry 13 Dec 21, 2022
An application that i developed with a aim of learning Jetpack compose and many other jetpack libraries

An application that i developed with a aim of learning Jetpack compose and many other jetpack libraries, The application make use of jikan Api which displays a list of animations,there more details and even trailers of the animations.

Odhiambo Brandy 10 Nov 23, 2022
A Kotlin library to use Jetpack Compose in Android and iOS. Allow to write UI for both in Kotin. Still experimental as many compose features are not yet available.

Multiplatform Compose A Kotlin library to use Jetpack Compose in Android and iOS. Allow to write UI for both in Kotin. Still experimental as many comp

Clément Beffa 548 Jan 7, 2023
K5-compose is a sketchy port of p5.js for Jetpack Compose

k5-compose k5-compose is a sketchy port of P5.js for Jetpack Compose Desktop. This library provides you a playground to play with your sketches so you

Nikhil Chaudhari 176 Nov 22, 2022