diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5b1f98e..ef8c6b4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 09bc313..40b796f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ All notable changes to this project are documented here. The format is based on ## [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 ### Fixed diff --git a/README.md b/README.md index a747d10..bd5f736 100644 --- a/README.md +++ b/README.md @@ -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`. @@ -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") ``` + +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`, diff --git a/api/tree-structure.api b/api/android/tree-structure.api similarity index 100% rename from api/tree-structure.api rename to api/android/tree-structure.api diff --git a/api/jvm/tree-structure.api b/api/jvm/tree-structure.api new file mode 100644 index 0000000..6212ff1 --- /dev/null +++ b/api/jvm/tree-structure.api @@ -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 (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 (Ljava/lang/Object;Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;)V + public synthetic fun (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 ()V + public fun (Ljava/lang/String;)V + public fun (Ljava/lang/String;Ljava/lang/Throwable;)V + public synthetic fun (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 (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 (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 (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; +} + diff --git a/build.gradle.kts b/build.gradle.kts index fcf6e83..289ff1e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,6 +7,11 @@ 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" @@ -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 + } +} diff --git a/gradle.properties b/gradle.properties index 279da73..74a5878 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,3 +2,5 @@ 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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4c863bc..456771a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1af9e09..df97d72 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/samples-android/build.gradle.kts b/samples-android/build.gradle.kts new file mode 100644 index 0000000..44c70f6 --- /dev/null +++ b/samples-android/build.gradle.kts @@ -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) +} diff --git a/samples-android/src/main/AndroidManifest.xml b/samples-android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9af720d --- /dev/null +++ b/samples-android/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/samples-android/src/main/kotlin/com/github/adriankuta/treestructure/sample/MainActivity.kt b/samples-android/src/main/kotlin/com/github/adriankuta/treestructure/sample/MainActivity.kt new file mode 100644 index 0000000..76ea000 --- /dev/null +++ b/samples-android/src/main/kotlin/com/github/adriankuta/treestructure/sample/MainActivity.kt @@ -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 = 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 = {}, + ) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index dee42cf..6476c63 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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") diff --git a/tree-structure-compose/api/android/tree-structure-compose.api b/tree-structure-compose/api/android/tree-structure-compose.api new file mode 100644 index 0000000..9515e1a --- /dev/null +++ b/tree-structure-compose/api/android/tree-structure-compose.api @@ -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 +} + diff --git a/tree-structure-compose/api/jvm/tree-structure-compose.api b/tree-structure-compose/api/jvm/tree-structure-compose.api new file mode 100644 index 0000000..9515e1a --- /dev/null +++ b/tree-structure-compose/api/jvm/tree-structure-compose.api @@ -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 +} + diff --git a/tree-structure-compose/api/tree-structure-compose.api b/tree-structure-compose/api/tree-structure-compose.api deleted file mode 100644 index 49d41d5..0000000 --- a/tree-structure-compose/api/tree-structure-compose.api +++ /dev/null @@ -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 -} - diff --git a/tree-structure-compose/build.gradle.kts b/tree-structure-compose/build.gradle.kts index 9027215..631f908 100644 --- a/tree-structure-compose/build.gradle.kts +++ b/tree-structure-compose/build.gradle.kts @@ -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 + } +} diff --git a/tree-structure-compose/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/compose/LazyTree.kt b/tree-structure-compose/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/compose/LazyTree.kt index 8b6cead..bb59fe5 100644 --- a/tree-structure-compose/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/compose/LazyTree.kt +++ b/tree-structure-compose/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/compose/LazyTree.kt @@ -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 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 LazyTree( + root: TreeNode, + 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. diff --git a/tree-structure-compose/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/compose/TreeNodeRow.kt b/tree-structure-compose/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/compose/TreeNodeRow.kt new file mode 100644 index 0000000..b3edd77 --- /dev/null +++ b/tree-structure-compose/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/compose/TreeNodeRow.kt @@ -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 TreeNodeRow( + node: TreeNode, + 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)) + } +}