3 Commits

Author SHA1 Message Date
Adrian Kuta
0fe6535258 Release 4.2.0
Some checks failed
Test / JVM / JS / Wasm / Native / Android + API check (push) Has been cancelled
Test / iOS (push) Has been cancelled
- PUBLISH_VERSION 4.1.1 -> 4.2.0
- CHANGELOG: promote [Unreleased] -> [4.2.0] (2026-06-08) + compare link
- README: bump install snippets to 4.2.0

Adds the Android target for tree-structure and tree-structure-compose,
the default TreeNodeRow composable + LazyTree(label=...) overload, and a
runnable Android sample. All additive, no breaking changes.
2026-06-08 21:58:23 +02:00
Adrian Kuta
28c1690f96 fix: raise Gradle daemon heap/Metaspace for the Android (AGP) build
The Android target added in #48 brought AGP onto the build's classpath
without setting org.gradle.jvmargs, so the Gradle daemon ran on its
defaults (512m heap / 384m Metaspace). AGP loads enough classes that,
combined with the KMP matrix, binary-compatibility-validator, Kover and
Dokka in a single build, the daemon exhausts Metaspace and is killed
mid-build (GC thrashing / out of Metaspace). The
'JVM / JS / Wasm / Native / Android + API check' job therefore failed
deterministically on master, and the publish job builds the same targets.

Set org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g. Verified with a
cold full-matrix build locally.
2026-06-08 21:58:23 +02:00
Adrian Kuta
dc59476b10 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.
2026-06-08 19:43:05 +00:00
19 changed files with 504 additions and 21 deletions

View File

@@ -21,9 +21,9 @@ jobs:
fail-fast: false
matrix:
include:
- name: JVM / JS / Wasm / Native + API check
- name: JVM / JS / Wasm / Native / Android + API check
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
os: macos-latest
tasks: iosSimulatorArm64Test

View File

@@ -4,7 +4,20 @@ All notable changes to this project are documented here. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [4.2.0] - 2026-06-08
### 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
@@ -99,7 +112,7 @@ A breaking release that cleans up the core API and enforces an explicit public s
## [3.1.3]
- iOS targets and Maven Central (Sonatype Central Portal) publishing.
[Unreleased]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v4.1.1...HEAD
[4.2.0]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v4.1.1...v4.2.0
[4.1.1]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v4.1.0...v4.1.1
[4.1.0]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v4.0.0...v4.1.0
[4.0.0]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v3.4.0...v4.0.0

View File

@@ -16,7 +16,7 @@ usually a better fit.
## 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`.
- Pre-order, post-order, and level-order traversal, as iterators or lazy `Sequence`s.
- Navigation: `root()`, `ancestors()`, `siblings()`, `leaves()`, `descendants()`, `isLeaf`, `degree`.
@@ -30,14 +30,14 @@ Gradle (Kotlin DSL):
```kotlin
// commonMain for KMP projects, or any sourceSet/module where you need it
dependencies {
implementation("com.github.adriankuta:tree-structure:4.1.1") // latest version is on the badge above
implementation("com.github.adriankuta:tree-structure:4.2.0") // latest version is on the badge above
}
```
Gradle (Groovy):
```groovy
dependencies {
implementation "com.github.adriankuta:tree-structure:4.1.1"
implementation "com.github.adriankuta:tree-structure:4.2.0"
}
```
@@ -46,7 +46,7 @@ Maven:
<dependency>
<groupId>com.github.adriankuta</groupId>
<artifactId>tree-structure</artifactId>
<version>4.1.1</version>
<version>4.2.0</version>
</dependency>
```
@@ -160,7 +160,7 @@ that depends on the core.
`@Serializable` directly. Convert to and from the acyclic `TreeNodeDto` instead.
```kotlin
implementation("com.github.adriankuta:tree-structure-serialization:4.1.1")
implementation("com.github.adriankuta:tree-structure-serialization:4.2.0")
```
```kotlin
val json = Json.encodeToString(root.toDto())
@@ -172,7 +172,7 @@ val restored = Json.decodeFromString<TreeNodeDto<String>>(json).toTreeNode()
Traverse a tree as a cold `Flow`, which is handy inside coroutine and `ViewModel` pipelines.
```kotlin
implementation("com.github.adriankuta:tree-structure-coroutines:4.1.1")
implementation("com.github.adriankuta:tree-structure-coroutines:4.2.0")
```
```kotlin
root.preOrderFlow().collect { println(it.value) }
@@ -181,12 +181,23 @@ root.asFlow(TreeNodeIterators.LevelOrder).map { it.value }
### Compose UI (`tree-structure-compose`)
A `LazyTree` composable for Compose Multiplatform (JVM/desktop, iOS, Wasm). Only the visible nodes
are composed, and you decide how each node looks:
A `LazyTree` composable for Compose Multiplatform (JVM/desktop, Android, iOS, Wasm). Only the visible
nodes are composed.
```kotlin
implementation("com.github.adriankuta:tree-structure-compose:4.1.1")
implementation("com.github.adriankuta:tree-structure-compose:4.2.0")
```
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
LazyTree(root) { node, depth, expanded, 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`)
A persistent `ImmutableTreeNode` with structural sharing. Every operation (`addChild`,
@@ -204,7 +217,7 @@ subtrees are reused, so updates are cheap and old roots stay valid. Backed by
`kotlinx.collections.immutable`.
```kotlin
implementation("com.github.adriankuta:tree-structure-immutable:4.1.1")
implementation("com.github.adriankuta:tree-structure-immutable:4.2.0")
```
```kotlin
val root = ImmutableTreeNode("World").addChild(ImmutableTreeNode("Europe"))

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,11 +7,16 @@ plugins {
alias(libs.plugins.binaryCompatibilityValidator)
alias(libs.plugins.kover)
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_ARTIFACT_ID = "tree-structure" // base artifact; KMP will add -jvm, -ios*, etc.
val PUBLISH_VERSION = "4.1.1"
val PUBLISH_VERSION = "4.2.0"
val snapshot: String? by project
@@ -57,11 +62,13 @@ mavenPublishing {
repositories {
mavenCentral()
google()
}
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-android")
}
dependencies {
@@ -94,6 +101,15 @@ kotlin {
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) {
browser()
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,9 @@ kotlin.code.style=official
# Dokka Gradle Plugin v2 (https://kotl.in/dokka-gradle-migration)
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
# Android: Compose pulls AndroidX artifacts, which AGP requires this flag to consume.
android.useAndroidX=true
# AGP loads many classes into the Gradle daemon's Metaspace; combined with the KMP
# matrix, binary-compatibility-validator, Kover and Dokka in a single build, the
# default 512m heap / 384m Metaspace is exhausted (daemon OOM). Raise both.
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -Dfile.encoding=UTF-8

View File

@@ -1,5 +1,9 @@
[versions]
kotlin = "2.1.0"
agp = "8.7.2"
androidCompileSdk = "35"
androidMinSdk = "21"
activityCompose = "1.9.3"
dokka = "2.2.0"
mavenPublish = "0.34.0"
binaryCompatibilityValidator = "0.16.3"
@@ -11,7 +15,10 @@ composeMultiplatform = "1.7.3"
[plugins]
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" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
androidApplication = { id = "com.android.application", version.ref = "agp" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" }
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-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" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
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
validateDistributionUrl=true
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"
include(":tree-structure-serialization")
@@ -5,3 +13,4 @@ include(":tree-structure-coroutines")
include(":tree-structure-compose")
include(":tree-structure-immutable")
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 {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.dokka)
@@ -68,6 +69,14 @@ kotlin {
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)
wasmJs {
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.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
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
* 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))
}
}