feat: add Android target and default TreeNodeRow to tree-structure-compose (#39) (#48)

Add the `android` target to the Compose Multiplatform library and a sensible
default renderer, so the largest Compose audience is a first-class consumer.

- Core `tree-structure` and `tree-structure-compose` now build and publish an
  `-android` variant (compileSdk 35, minSdk 21). The core needs it too: its
  inline `tree { }` DSL is built per target, and Android consumers (JVM 11/17)
  cannot inline a JVM-21 build.
- `TreeNodeRow`: a foundation-only default node row (clickable, indented, with a
  `▾`/`▸` marker, no Material dependency), plus a no-content `LazyTree(root,
  label = …)` overload that uses it. The existing lambda overload is unchanged.
- New `samples` Android app module demonstrating `LazyTree` + `@Preview`
  (not published; excluded from API validation).
- Toolchain: Gradle wrapper 8.5 -> 8.10.2, Android Gradle Plugin 8.7.2.
- binary-compatibility-validator now emits per-target dumps (api/jvm, api/android)
  for the two multi-JVM-target modules; CI assembles the Android outputs.
This commit is contained in:
2026-06-08 21:43:05 +02:00
committed by GitHub
parent 1fce412815
commit dc59476b10
19 changed files with 490 additions and 11 deletions

View File

@@ -21,9 +21,9 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- name: JVM / JS / Wasm / Native + API check - name: JVM / JS / Wasm / Native / Android + API check
os: ubuntu-latest os: ubuntu-latest
tasks: jvmTest jsNodeTest wasmJsNodeTest nativeTest apiCheck :samples:test :samples:run tasks: jvmTest jsNodeTest wasmJsNodeTest nativeTest :assembleRelease :tree-structure-compose:assembleRelease :samples-android:assembleDebug :samples:test :samples:run apiCheck
- name: iOS - name: iOS
os: macos-latest os: macos-latest
tasks: iosSimulatorArm64Test tasks: iosSimulatorArm64Test

View File

@@ -6,6 +6,19 @@ All notable changes to this project are documented here. The format is based on
## [Unreleased] ## [Unreleased]
### Added
- **Android target.** The core `tree-structure` module and `tree-structure-compose` now publish an
`-android` variant (`minSdk` 21), so both can be consumed directly in Android projects.
- `tree-structure-compose`: a foundation-only `TreeNodeRow` composable — a sensible default node row
(clickable, indented, with a `▾`/`▸` marker and no Material dependency) — plus a no-content
`LazyTree(root, label = …)` overload that renders each node with it. The existing lambda overload
is unchanged.
- A runnable Android sample app in the new `samples` module, with `@Preview` composables.
### Changed
- Upgraded the Gradle wrapper to 8.10.2 and introduced Android Gradle Plugin 8.7.2 (`compileSdk` 35).
No source or behavior changes to existing targets.
## [4.1.1] - 2026-06-08 ## [4.1.1] - 2026-06-08
### Fixed ### Fixed

View File

@@ -16,7 +16,7 @@ usually a better fit.
## Features ## Features
- Kotlin Multiplatform: JVM, JS, Wasm, iOS, and a native host target. - Kotlin Multiplatform: JVM, Android, JS, Wasm, iOS, and a native host target.
- Build trees with a `tree { child(...) }` DSL or node by node with `addChild`. - Build trees with a `tree { child(...) }` DSL or node by node with `addChild`.
- Pre-order, post-order, and level-order traversal, as iterators or lazy `Sequence`s. - Pre-order, post-order, and level-order traversal, as iterators or lazy `Sequence`s.
- Navigation: `root()`, `ancestors()`, `siblings()`, `leaves()`, `descendants()`, `isLeaf`, `degree`. - Navigation: `root()`, `ancestors()`, `siblings()`, `leaves()`, `descendants()`, `isLeaf`, `degree`.
@@ -181,12 +181,23 @@ root.asFlow(TreeNodeIterators.LevelOrder).map { it.value }
### Compose UI (`tree-structure-compose`) ### Compose UI (`tree-structure-compose`)
A `LazyTree` composable for Compose Multiplatform (JVM/desktop, iOS, Wasm). Only the visible nodes A `LazyTree` composable for Compose Multiplatform (JVM/desktop, Android, iOS, Wasm). Only the visible
are composed, and you decide how each node looks: nodes are composed.
```kotlin ```kotlin
implementation("com.github.adriankuta:tree-structure-compose:4.1.1") implementation("com.github.adriankuta:tree-structure-compose:4.1.1")
``` ```
For the common case, the no-content overload renders each node with the built-in `TreeNodeRow`
(a clickable, indented row with a `▾`/`▸` marker — foundation-only, no Material dependency):
```kotlin
LazyTree(root) // sensible default
LazyTree(root, label = { it.name }) // map a node's value to its text
```
Or supply your own row for full control:
```kotlin ```kotlin
LazyTree(root) { node, depth, expanded, toggle -> LazyTree(root) { node, depth, expanded, toggle ->
Row(Modifier.padding(start = (depth * 16).dp).clickable(onClick = toggle)) { Row(Modifier.padding(start = (depth * 16).dp).clickable(onClick = toggle)) {
@@ -196,6 +207,8 @@ LazyTree(root) { node, depth, expanded, toggle ->
} }
``` ```
A runnable Android demo lives in the [`samples-android`](samples-android) module.
### Immutable (`tree-structure-immutable`) ### Immutable (`tree-structure-immutable`)
A persistent `ImmutableTreeNode` with structural sharing. Every operation (`addChild`, A persistent `ImmutableTreeNode` with structural sharing. Every operation (`addChild`,

142
api/jvm/tree-structure.api Normal file
View File

@@ -0,0 +1,142 @@
public abstract interface class com/github/adriankuta/datastructure/tree/ChildDeclarationInterface {
public abstract synthetic fun child (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
}
public final class com/github/adriankuta/datastructure/tree/ChildDeclarationInterface$DefaultImpls {
public static synthetic fun child$default (Lcom/github/adriankuta/datastructure/tree/ChildDeclarationInterface;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
}
public final class com/github/adriankuta/datastructure/tree/TreeConnectors {
public static final field Companion Lcom/github/adriankuta/datastructure/tree/TreeConnectors$Companion;
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/github/adriankuta/datastructure/tree/TreeConnectors;
public static synthetic fun copy$default (Lcom/github/adriankuta/datastructure/tree/TreeConnectors;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/github/adriankuta/datastructure/tree/TreeConnectors;
public fun equals (Ljava/lang/Object;)Z
public final fun getBranch ()Ljava/lang/String;
public final fun getEmpty ()Ljava/lang/String;
public final fun getLastBranch ()Ljava/lang/String;
public final fun getVertical ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class com/github/adriankuta/datastructure/tree/TreeConnectors$Companion {
public final fun getAscii ()Lcom/github/adriankuta/datastructure/tree/TreeConnectors;
public final fun getDefault ()Lcom/github/adriankuta/datastructure/tree/TreeConnectors;
}
public class com/github/adriankuta/datastructure/tree/TreeNode : com/github/adriankuta/datastructure/tree/ChildDeclarationInterface, java/lang/Iterable, kotlin/jvm/internal/markers/KMappedMarker {
public fun <init> (Ljava/lang/Object;Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;)V
public synthetic fun <init> (Ljava/lang/Object;Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun addChild (Lcom/github/adriankuta/datastructure/tree/TreeNode;)V
public final fun addChildren ([Lcom/github/adriankuta/datastructure/tree/TreeNode;)V
public synthetic fun child (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public final fun clear ()V
public final fun depth ()I
public final fun detach ()Z
public final fun getChildren ()Ljava/util/List;
public final fun getParent ()Lcom/github/adriankuta/datastructure/tree/TreeNode;
public final fun getTreeIterator ()Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;
public final fun getValue ()Ljava/lang/Object;
public final fun height ()I
public final fun insertChild (ILcom/github/adriankuta/datastructure/tree/TreeNode;)V
public final fun isRoot ()Z
public fun iterator ()Ljava/util/Iterator;
public final fun iterator (Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;)Ljava/util/Iterator;
public final fun moveChild (Lcom/github/adriankuta/datastructure/tree/TreeNode;I)Z
public final fun nodeCount ()I
public final fun path (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List;
public final fun prettyString ()Ljava/lang/String;
public final fun removeChild (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Z
public final fun removeChildAt (I)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public final fun replaceChild (ILcom/github/adriankuta/datastructure/tree/TreeNode;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public final fun sortChildren (Ljava/util/Comparator;)V
public fun toString ()Ljava/lang/String;
}
public final class com/github/adriankuta/datastructure/tree/TreeNodeFunctionalExtKt {
public static final fun allNodes (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lkotlin/jvm/functions/Function1;)Z
public static final fun anyNode (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lkotlin/jvm/functions/Function1;)Z
public static final fun countNodes (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lkotlin/jvm/functions/Function1;)I
public static final fun deepCopy (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public static final fun filterNodes (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lkotlin/jvm/functions/Function1;)Ljava/util/List;
public static final fun findNode (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lkotlin/jvm/functions/Function1;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public static final fun foldNodes (Lcom/github/adriankuta/datastructure/tree/TreeNode;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
public static final fun mapValues (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lkotlin/jvm/functions/Function1;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public static final fun structurallyEquals (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/TreeNode;)Z
}
public final class com/github/adriankuta/datastructure/tree/TreeNodeNavigationExtKt {
public static final fun ancestors (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List;
public static final fun descendants (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List;
public static final fun getDegree (Lcom/github/adriankuta/datastructure/tree/TreeNode;)I
public static final fun isLeaf (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Z
public static final fun leaves (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List;
public static final fun root (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public static final fun siblings (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List;
}
public final class com/github/adriankuta/datastructure/tree/TreeNodePrettyPrintExtKt {
public static final fun prettyString (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/TreeConnectors;Lkotlin/jvm/functions/Function3;)Ljava/lang/String;
public static synthetic fun prettyString$default (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/TreeConnectors;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Ljava/lang/String;
}
public final class com/github/adriankuta/datastructure/tree/TreeNodeQueryExtKt {
public static final fun contains (Lcom/github/adriankuta/datastructure/tree/TreeNode;Ljava/lang/Object;)Z
public static final fun distance (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/lang/Integer;
public static final fun lowestCommonAncestor (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public static final fun pathBetween (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List;
}
public final class com/github/adriankuta/datastructure/tree/TreeNodeSequenceExtKt {
public static final fun asSequence (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;)Lkotlin/sequences/Sequence;
public static synthetic fun asSequence$default (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;ILjava/lang/Object;)Lkotlin/sequences/Sequence;
public static final fun levelOrderSequence (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lkotlin/sequences/Sequence;
public static final fun postOrderSequence (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lkotlin/sequences/Sequence;
public static final fun preOrderSequence (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lkotlin/sequences/Sequence;
}
public final class com/github/adriankuta/datastructure/tree/exceptions/TreeNodeException : java/lang/RuntimeException {
public fun <init> ()V
public fun <init> (Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public final class com/github/adriankuta/datastructure/tree/iterators/LevelOrderTreeIterator : java/util/Iterator, kotlin/jvm/internal/markers/KMappedMarker {
public fun <init> (Lcom/github/adriankuta/datastructure/tree/TreeNode;)V
public fun hasNext ()Z
public fun next ()Lcom/github/adriankuta/datastructure/tree/TreeNode;
public synthetic fun next ()Ljava/lang/Object;
public fun remove ()V
}
public final class com/github/adriankuta/datastructure/tree/iterators/PostOrderTreeIterator : java/util/Iterator, kotlin/jvm/internal/markers/KMappedMarker {
public fun <init> (Lcom/github/adriankuta/datastructure/tree/TreeNode;)V
public fun hasNext ()Z
public fun next ()Lcom/github/adriankuta/datastructure/tree/TreeNode;
public synthetic fun next ()Ljava/lang/Object;
public fun remove ()V
}
public final class com/github/adriankuta/datastructure/tree/iterators/PreOrderTreeIterator : java/util/Iterator, kotlin/jvm/internal/markers/KMappedMarker {
public fun <init> (Lcom/github/adriankuta/datastructure/tree/TreeNode;)V
public fun hasNext ()Z
public fun next ()Lcom/github/adriankuta/datastructure/tree/TreeNode;
public synthetic fun next ()Ljava/lang/Object;
public fun remove ()V
}
public final class com/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators : java/lang/Enum {
public static final field LevelOrder Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;
public static final field PostOrder Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;
public static final field PreOrder Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;
public static fun values ()[Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;
}

View File

@@ -7,6 +7,11 @@ plugins {
alias(libs.plugins.binaryCompatibilityValidator) alias(libs.plugins.binaryCompatibilityValidator)
alias(libs.plugins.kover) alias(libs.plugins.kover)
signing signing
alias(libs.plugins.androidLibrary)
// Loaded once here (with a known version) so the Android application/library variants can be
// applied across subprojects without the "already on the classpath with an unknown version" clash.
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.kotlinAndroid) apply false
} }
val PUBLISH_GROUP_ID = "com.github.adriankuta" val PUBLISH_GROUP_ID = "com.github.adriankuta"
@@ -57,11 +62,13 @@ mavenPublishing {
repositories { repositories {
mavenCentral() mavenCentral()
google()
} }
apiValidation { apiValidation {
// :samples is a dev-facing examples module, not a published artifact, so it has no .api dump. // Neither sample module is a published artifact, so neither has a .api dump.
ignoredProjects.add("samples") ignoredProjects.add("samples")
ignoredProjects.add("samples-android")
} }
dependencies { dependencies {
@@ -94,6 +101,15 @@ kotlin {
jvm() jvm()
androidTarget {
publishLibraryVariants("release")
// Build the Android variant at JVM 17 so Android consumers (JVM 11/17) can inline the
// library's inline DSL (`tree { }`) — they cannot inline the default JVM-21 bytecode.
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
js(IR) { js(IR) {
browser() browser()
nodejs() nodejs()
@@ -126,3 +142,15 @@ kotlin {
} }
} }
} }
android {
namespace = "com.github.adriankuta.datastructure.tree"
compileSdk = libs.versions.androidCompileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.androidMinSdk.get().toInt()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}

View File

@@ -2,3 +2,5 @@ kotlin.code.style=official
# Dokka Gradle Plugin v2 (https://kotl.in/dokka-gradle-migration) # Dokka Gradle Plugin v2 (https://kotl.in/dokka-gradle-migration)
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
# Android: Compose pulls AndroidX artifacts, which AGP requires this flag to consume.
android.useAndroidX=true

View File

@@ -1,5 +1,9 @@
[versions] [versions]
kotlin = "2.1.0" kotlin = "2.1.0"
agp = "8.7.2"
androidCompileSdk = "35"
androidMinSdk = "21"
activityCompose = "1.9.3"
dokka = "2.2.0" dokka = "2.2.0"
mavenPublish = "0.34.0" mavenPublish = "0.34.0"
binaryCompatibilityValidator = "0.16.3" binaryCompatibilityValidator = "0.16.3"
@@ -11,7 +15,10 @@ composeMultiplatform = "1.7.3"
[plugins] [plugins]
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
androidApplication = { id = "com.android.application", version.ref = "agp" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" } mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" }
binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidator" } binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidator" }
@@ -24,3 +31,4 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" } kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@@ -0,0 +1,48 @@
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.kotlinAndroid)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
repositories {
google()
mavenCentral()
}
kotlin {
jvmToolchain(21)
// Match the Kotlin bytecode target to the Java level in android.compileOptions below.
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
android {
namespace = "com.github.adriankuta.treestructure.sample"
compileSdk = libs.versions.androidCompileSdk.get().toInt()
defaultConfig {
applicationId = "com.github.adriankuta.treestructure.sample"
minSdk = libs.versions.androidMinSdk.get().toInt()
targetSdk = libs.versions.androidCompileSdk.get().toInt()
versionCode = 1
versionName = rootProject.version.toString()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
dependencies {
implementation(project(":"))
implementation(project(":tree-structure-compose"))
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.components.uiToolingPreview)
implementation(libs.androidx.activity.compose)
debugImplementation(compose.uiTooling)
}

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:label="Tree Sample"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,73 @@
package com.github.adriankuta.treestructure.sample
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.github.adriankuta.datastructure.tree.TreeNode
import com.github.adriankuta.datastructure.tree.compose.LazyTree
import com.github.adriankuta.datastructure.tree.compose.TreeNodeRow
import com.github.adriankuta.datastructure.tree.tree
import org.jetbrains.compose.ui.tooling.preview.Preview
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
TreeSampleScreen()
}
}
}
}
}
/** Renders the [sampleTree] with the library's default [TreeNodeRow] via the one-line [LazyTree]. */
@Composable
fun TreeSampleScreen() {
LazyTree(sampleTree())
}
private fun sampleTree(): TreeNode<String> = tree("World") {
child("North America") {
child("USA")
child("Canada")
}
child("Europe") {
child("Poland")
child("Germany")
child("Spain")
}
child("Asia") {
child("Japan")
child("India")
}
}
@Preview
@Composable
private fun TreeSampleScreenPreview() {
MaterialTheme {
Surface {
TreeSampleScreen()
}
}
}
@Preview
@Composable
private fun TreeNodeRowPreview() {
MaterialTheme {
TreeNodeRow(
node = TreeNode("Europe").apply { addChild(TreeNode("Poland")) },
depth = 1,
expanded = true,
toggle = {},
)
}
}

View File

@@ -1,3 +1,11 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
rootProject.name = "tree-structure" rootProject.name = "tree-structure"
include(":tree-structure-serialization") include(":tree-structure-serialization")
@@ -5,3 +13,4 @@ include(":tree-structure-coroutines")
include(":tree-structure-compose") include(":tree-structure-compose")
include(":tree-structure-immutable") include(":tree-structure-immutable")
include(":samples") include(":samples")
include(":samples-android")

View File

@@ -0,0 +1,9 @@
public final class com/github/adriankuta/datastructure/tree/compose/LazyTreeKt {
public static final fun LazyTree (Lcom/github/adriankuta/datastructure/tree/TreeNode;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function6;Landroidx/compose/runtime/Composer;II)V
public static final fun LazyTree-hGBTI10 (Lcom/github/adriankuta/datastructure/tree/TreeNode;Landroidx/compose/ui/Modifier;ZFLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
}
public final class com/github/adriankuta/datastructure/tree/compose/TreeNodeRowKt {
public static final fun TreeNodeRow-PfoAEA0 (Lcom/github/adriankuta/datastructure/tree/TreeNode;IZLkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;FLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
}

View File

@@ -0,0 +1,9 @@
public final class com/github/adriankuta/datastructure/tree/compose/LazyTreeKt {
public static final fun LazyTree (Lcom/github/adriankuta/datastructure/tree/TreeNode;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function6;Landroidx/compose/runtime/Composer;II)V
public static final fun LazyTree-hGBTI10 (Lcom/github/adriankuta/datastructure/tree/TreeNode;Landroidx/compose/ui/Modifier;ZFLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
}
public final class com/github/adriankuta/datastructure/tree/compose/TreeNodeRowKt {
public static final fun TreeNodeRow-PfoAEA0 (Lcom/github/adriankuta/datastructure/tree/TreeNode;IZLkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;FLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
}

View File

@@ -1,4 +0,0 @@
public final class com/github/adriankuta/datastructure/tree/compose/LazyTreeKt {
public static final fun LazyTree (Lcom/github/adriankuta/datastructure/tree/TreeNode;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function6;Landroidx/compose/runtime/Composer;II)V
}

View File

@@ -2,6 +2,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins { plugins {
alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler) alias(libs.plugins.composeCompiler)
alias(libs.plugins.dokka) alias(libs.plugins.dokka)
@@ -68,6 +69,14 @@ kotlin {
jvm() jvm()
androidTarget {
publishLibraryVariants("release")
// Match the Android variant's Kotlin bytecode to the Java level set in compileOptions.
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
@OptIn(ExperimentalWasmDsl::class) @OptIn(ExperimentalWasmDsl::class)
wasmJs { wasmJs {
browser() browser()
@@ -85,3 +94,15 @@ kotlin {
} }
} }
} }
android {
namespace = "com.github.adriankuta.datastructure.tree.compose"
compileSdk = libs.versions.androidCompileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.androidMinSdk.get().toInt()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}

View File

@@ -5,6 +5,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.github.adriankuta.datastructure.tree.TreeNode import com.github.adriankuta.datastructure.tree.TreeNode
/** /**
@@ -49,6 +51,35 @@ public fun <T> LazyTree(
} }
} }
/**
* Convenience overload of [LazyTree] that renders each node with the built-in [TreeNodeRow], so the
* common case is a single call:
*
* ```
* LazyTree(root)
* ```
*
* Use the overload that takes a `nodeContent` lambda when you need full control over a node's look.
*
* @param root the root of the tree to display.
* @param modifier the [Modifier] applied to the underlying [LazyColumn].
* @param initiallyExpanded whether nodes start expanded.
* @param indent the horizontal indentation applied per depth level.
* @param label maps a node's value to the text shown. Defaults to `toString()`.
*/
@Composable
public fun <T> LazyTree(
root: TreeNode<T>,
modifier: Modifier = Modifier,
initiallyExpanded: Boolean = true,
indent: Dp = 16.dp,
label: (T) -> String = { it.toString() },
) {
LazyTree(root, modifier, initiallyExpanded) { node, depth, expanded, toggle ->
TreeNodeRow(node, depth, expanded, toggle, indent = indent, label = label)
}
}
/** /**
* Flattens the tree into the list of currently-visible `(node, depth)` pairs in pre-order, skipping * Flattens the tree into the list of currently-visible `(node, depth)` pairs in pre-order, skipping
* the subtrees of collapsed nodes. Iterative, so it is safe on deep trees. * the subtrees of collapsed nodes. Iterative, so it is safe on deep trees.

View File

@@ -0,0 +1,59 @@
package com.github.adriankuta.datastructure.tree.compose
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.github.adriankuta.datastructure.tree.TreeNode
/**
* A sensible default row for a single node in a [LazyTree]. The whole row is clickable to expand or
* collapse, indentation reflects [depth], and a `▾`/`▸` marker precedes non-leaf nodes.
*
* It is intentionally foundation-only (no Material), so using it does not pull a theming dependency
* into your app. For full control over a node's appearance, use the `LazyTree` overload that takes a
* `nodeContent` lambda instead.
*
* ```
* LazyTree(root) { node, depth, expanded, toggle ->
* TreeNodeRow(node, depth, expanded, toggle)
* }
* ```
*
* @param node the node to render.
* @param depth the node's depth in the tree (root = 0), used for indentation.
* @param expanded whether the node is currently expanded.
* @param toggle flips this node's expansion state; invoked when the row is clicked.
* @param modifier the [Modifier] applied to the row.
* @param indent the horizontal indentation applied per depth level.
* @param label maps the node's value to the text shown. Defaults to `toString()`.
*/
@Composable
public fun <T> TreeNodeRow(
node: TreeNode<T>,
depth: Int,
expanded: Boolean,
toggle: () -> Unit,
modifier: Modifier = Modifier,
indent: Dp = 16.dp,
label: (T) -> String = { it.toString() },
) {
val marker = when {
node.children.isEmpty() -> ""
expanded -> ""
else -> ""
}
Row(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = toggle)
.padding(start = indent * depth, top = 8.dp, bottom = 8.dp, end = 8.dp),
) {
BasicText(text = marker + label(node.value))
}
}