diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index fe63bb6..6d0ee1c 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/iosApp.xml b/.idea/runConfigurations/iosApp.xml
new file mode 100644
index 0000000..296ddc1
--- /dev/null
+++ b/.idea/runConfigurations/iosApp.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 72e814c..067e602 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,20 @@
# 🌳 JsonTree
-   
+   
-JsonTree is an Android library to display JSON data in Compose with syntax highlighting and more.
+JsonTree is a Compose Multiplatform library to display JSON data in Compose with syntax highlighting and more.
-
+
+
+
## About
-JsonTree is an Android library to display formatted JSON data in Compose.
+JsonTree is an Compose Multiplatform library to display formatted JSON data in Compose.
Users can expand/collapse objects and arrays, which can also display additional info like item counts and item indices for arrays.
JsonTree offers several customizations for visual appearance like syntax highlighting, text style and icons.
@@ -20,9 +22,10 @@ JsonTree offers several customizations for visual appearance like syntax highlig
See `maven-central` tag at the top for the latest version.
-```groovy
+```kotlin
dependencies {
- implementation 'com.sebastianneubauer.jsontree:jsontree:latest-version'
+ // add this to commonMain in a multiplatform project
+ implementation("com.sebastianneubauer.jsontree:jsontree:latest-version")
}
```
@@ -65,7 +68,7 @@ JsonTree(
// The size of the arrow icon
iconSize = 20.dp,
// The TextStyle to use for the json tree
- textStyle = TextStyle(...),
+ textStyle = TextStyle(/*...*/),
// If true, then array items will show their index.
showIndices = false,
// If true, then arrays and objects will show the amount of child items when collapsed.
@@ -74,7 +77,7 @@ JsonTree(
// until there are none or multiple on a level.
expandSingleChildren = false,
// A callback method which is called when the provided json data can't be parsed.
- onError = { throwable -> /* Do something */ }
+ onError = { throwable: Throwable -> /* Do something */ }
)
```
diff --git a/app/.gitignore b/app/.gitignore
deleted file mode 100644
index 42afabf..0000000
--- a/app/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
deleted file mode 100644
index 8c1fb25..0000000
--- a/app/build.gradle
+++ /dev/null
@@ -1,69 +0,0 @@
-plugins {
- alias libs.plugins.android.application
- alias libs.plugins.kotlin.android
- alias(libs.plugins.compose.compiler)
-}
-
-android {
- namespace="com.sebastianneubauer.jsontreedemo"
- compileSdk libs.versions.android.target.get().toInteger()
-
- defaultConfig {
- applicationId "com.sebastianneubauer.jsontreedemo"
- minSdk libs.versions.android.min.get().toInteger()
- targetSdk libs.versions.android.target.get().toInteger()
- versionCode 1
- versionName "1.0"
-
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- vectorDrawables {
- useSupportLibrary true
- }
- }
-
- signingConfigs {
- debug {}
- }
-
- buildTypes {
- release {
- minifyEnabled true
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- //for testing only, don't do this in your app
- signingConfig signingConfigs.debug
- }
- debug {
- applicationIdSuffix ".debug"
- signingConfig signingConfigs.debug
- }
- }
- buildFeatures {
- compose true
- }
- packagingOptions {
- resources {
- excludes += '/META-INF/{AL2.0,LGPL2.1}'
- }
- }
-}
-
-composeCompiler {
- enableStrongSkippingMode = true
-}
-
-kotlin {
- jvmToolchain {
- languageVersion.set(JavaLanguageVersion.of(libs.versions.java.toolchain.get()))
- }
-}
-
-dependencies {
- implementation platform(libs.androidx.compose.bom)
- implementation libs.androidx.activity.compose
- implementation libs.androidx.compose.ui
- implementation libs.androidx.compose.material
- implementation libs.androidx.compose.ui.tooling
- implementation libs.androidx.compose.ui.tooling.preview
-
- implementation project(":jsontree")
-}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
deleted file mode 100644
index 481bb43..0000000
--- a/app/proguard-rules.pro
+++ /dev/null
@@ -1,21 +0,0 @@
-# Add project specific ProGuard rules here.
-# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
-
-# Uncomment this to preserve the line number information for
-# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
-
-# If you keep the line number information, uncomment this to
-# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/main/java/com/sebastianneubauer/jsontreedemo/MainActivity.kt b/app/src/main/java/com/sebastianneubauer/jsontreedemo/MainActivity.kt
deleted file mode 100644
index 42a5e8f..0000000
--- a/app/src/main/java/com/sebastianneubauer/jsontreedemo/MainActivity.kt
+++ /dev/null
@@ -1,208 +0,0 @@
-package com.sebastianneubauer.jsontreedemo
-
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.background
-import androidx.compose.foundation.horizontalScroll
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ExperimentalLayoutApi
-import androidx.compose.foundation.layout.FlowRow
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.pager.HorizontalPager
-import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.material3.Button
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import com.sebastianneubauer.jsontree.TreeColors
-import com.sebastianneubauer.jsontree.JsonTree
-import com.sebastianneubauer.jsontree.TreeState
-import com.sebastianneubauer.jsontree.defaultDarkColors
-import com.sebastianneubauer.jsontree.defaultLightColors
-import com.sebastianneubauer.jsontreedemo.ui.theme.JsonTreeTheme
-import java.lang.IllegalStateException
-
-internal class MainActivity : ComponentActivity() {
-
- @OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- setContent {
- JsonTreeTheme {
- Column(
- modifier = Modifier.fillMaxSize()
- ) {
- var errorMessage: String? by remember { mutableStateOf(null) }
- var json: String by remember { mutableStateOf(simpleJson) }
- var colors: TreeColors by remember { mutableStateOf(defaultLightColors) }
- var initialState: TreeState by remember { mutableStateOf(TreeState.FIRST_ITEM_EXPANDED) }
- var showIndices: Boolean by remember { mutableStateOf(true) }
- var showItemCount: Boolean by remember { mutableStateOf(true) }
- var expandSingleChildren: Boolean by remember { mutableStateOf(true) }
-
- Spacer(modifier = Modifier.height(16.dp))
-
- Text(
- modifier = Modifier.padding(horizontal = 16.dp),
- text = "🌳 JsonTree",
- style = MaterialTheme.typography.headlineMedium,
- color = Color.Black
- )
-
- Spacer(modifier = Modifier.height(48.dp))
-
- FlowRow {
- Button(
- modifier = Modifier.padding(horizontal = 8.dp),
- onClick = {
- errorMessage = null
- json = when (json) {
- emptyJson -> simpleJson
- simpleJson -> complexJson
- complexJson -> invalidJson
- invalidJson -> emptyJson
- else -> throw IllegalStateException("No JSON selected!")
- }
- }
- ) {
- Text(
- text = when (json) {
- simpleJson -> "Simple Json"
- emptyJson -> "Empty Json"
- complexJson -> "Complex Json"
- invalidJson -> "Invalid Json"
- else -> throw IllegalStateException("No JSON selected!")
- }
- )
- }
-
- Button(
- modifier = Modifier.padding(horizontal = 8.dp),
- onClick = {
- val newState = when(initialState) {
- TreeState.EXPANDED -> TreeState.COLLAPSED
- TreeState.COLLAPSED -> TreeState.FIRST_ITEM_EXPANDED
- TreeState.FIRST_ITEM_EXPANDED -> TreeState.EXPANDED
- }
- initialState = newState
- }
- ) {
- Text(text = initialState.name)
- }
-
- Button(
- modifier = Modifier.padding(horizontal = 8.dp),
- onClick = { showIndices = !showIndices }
- ) {
- Text(text = if(showIndices) "Hide indices" else "Show indices")
- }
-
- Button(
- modifier = Modifier.padding(horizontal = 8.dp),
- onClick = { showItemCount = !showItemCount }
- ) {
- Text(text = if(showItemCount) "Hide item count" else "Show item count")
- }
-
- Button(
- modifier = Modifier.padding(horizontal = 8.dp),
- onClick = {
- colors = if(colors == defaultLightColors) defaultDarkColors else defaultLightColors
- }
- ) {
- Text(text = if(colors == defaultLightColors) "Light" else "Dark")
- }
-
- Button(
- modifier = Modifier.padding(horizontal = 8.dp),
- onClick = { expandSingleChildren = !expandSingleChildren }
- ) {
- Text(text = if(expandSingleChildren) "Expand children" else "Don't expand children")
- }
- }
-
- val pagerState = rememberPagerState(
- initialPage = 0,
- pageCount = { 3 }
- )
-
- //Pager to test leaving composition
- HorizontalPager(
- modifier = Modifier
- .fillMaxWidth()
- .weight(1F),
- state = pagerState,
- verticalAlignment = Alignment.Top
- ) { pageIndex ->
- when (pageIndex) {
- 0 -> {
- val error = errorMessage
- if(error!= null) {
- Text(
- text = error,
- color = Color.Black
- )
- } else {
- JsonTree(
- modifier = Modifier
- .fillMaxSize()
- .horizontalScroll(rememberScrollState())
- .background(
- if (colors == defaultLightColors) Color.White else Color.Black
- ),
- contentPadding = PaddingValues(16.dp),
- json = json,
- onLoading = {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
- ) {
- Text(
- text = "Loading...",
- color = if (colors == defaultLightColors) Color.Black else Color.White
- )
- }
- },
- initialState = initialState,
- colors = colors,
- showIndices = showIndices,
- showItemCount = showItemCount,
- expandSingleChildren = expandSingleChildren,
- onError = { errorMessage = it.localizedMessage },
- )
- }
- }
- 1 -> {
- Text(text = "Page 1")
- }
- 2 -> {
- Text(text = "Page 2")
- }
- }
- }
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
deleted file mode 100644
index c209e78..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
deleted file mode 100644
index b2dfe3d..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
deleted file mode 100644
index 4f0f1d6..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
deleted file mode 100644
index 62b611d..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
deleted file mode 100644
index 948a307..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
deleted file mode 100644
index 1b9a695..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
deleted file mode 100644
index 28d4b77..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9287f50..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
deleted file mode 100644
index aa7d642..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9126ae3..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
deleted file mode 100644
index f8c6127..0000000
--- a/app/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
- #FFBB86FC
- #FF6200EE
- #FF3700B3
- #FF03DAC5
- #FF018786
- #FF000000
- #FFFFFFFF
-
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
deleted file mode 100644
index c2aab59..0000000
--- a/app/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
- JsonTree
-
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
deleted file mode 100644
index 02a6fd1..0000000
--- a/app/src/main/res/values/themes.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index b9aa328..0000000
--- a/build.gradle
+++ /dev/null
@@ -1,63 +0,0 @@
-buildscript {
- repositories {
- mavenCentral()
- }
-}
-
-plugins {
- alias libs.plugins.android.application apply false
- alias libs.plugins.android.library apply false
- alias libs.plugins.kotlin.android apply false
- alias(libs.plugins.compose.compiler) apply false
- alias libs.plugins.publish apply false
- alias libs.plugins.api.validator
- alias libs.plugins.detekt
-}
-
-apiValidation {
- ignoredProjects += ["app"]
-}
-
-def projectSource = file(projectDir)
-def configFile = files("$rootDir/detekt/config.yml")
-def baselineFile = file("$rootDir/detekt/baseline.xml")
-def kotlinFiles = "**/*.kt"
-def appModuleFiles = "**/app/**"
-def resourceFiles = "**/resources/**"
-def buildFiles = "**/build/**"
-
-tasks.register("detektAll", io.gitlab.arturbosch.detekt.Detekt) {
- def autoFix = project.hasProperty('detektAutoFix')
-
- description = "Custom DETEKT task for all modules"
- parallel = true
- ignoreFailures = false
- autoCorrect = autoFix
- buildUponDefaultConfig = true
- setSource(projectSource)
- config.setFrom(configFile)
- baseline.set(baselineFile)
- include(kotlinFiles)
- exclude(appModuleFiles, resourceFiles, buildFiles)
- reports {
- html.enabled = true
- xml.enabled = false
- txt.enabled = false
- }
-}
-
-tasks.register("detektGenerateBaseline", io.gitlab.arturbosch.detekt.DetektCreateBaselineTask) {
- description = "Custom DETEKT task to build baseline for all modules"
- parallel = true
- ignoreFailures = false
- buildUponDefaultConfig = true
- setSource(projectSource)
- baseline.set(baselineFile)
- config.setFrom(configFile)
- include(kotlinFiles)
- exclude(appModuleFiles, resourceFiles, buildFiles)
-}
-
-dependencies {
- detektPlugins libs.detekt.formatting
-}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..5c0c311
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,76 @@
+import io.gitlab.arturbosch.detekt.Detekt
+
+buildscript {
+ repositories {
+ mavenCentral()
+ }
+ dependencies {
+ classpath(libs.atomicfu)
+ }
+}
+
+plugins {
+ alias(libs.plugins.android.application).apply(false)
+ alias(libs.plugins.android.library).apply(false)
+ alias(libs.plugins.kotlinMultiplatform).apply(false)
+ alias(libs.plugins.compose).apply(false)
+ alias(libs.plugins.compose.compiler).apply(false)
+ alias(libs.plugins.publish).apply(false)
+ alias(libs.plugins.api.validator)
+ alias(libs.plugins.detekt)
+}
+
+apiValidation {
+ ignoredProjects.add("sample")
+}
+
+val projectSource = file(projectDir)
+val configFiles = files("$rootDir/detekt/config.yml")
+val baselineFile = File("$rootDir/detekt/baseline.xml")
+val kotlinFiles = "**/*.kt"
+val sampleModuleFiles = "**/sample/**"
+val resourceFiles = "**/resources/**"
+val buildFiles = "**/build/**"
+
+tasks.register("detektAll") {
+ val autoFix = project.hasProperty("detektAutoFix")
+
+ description = "Custom DETEKT task for all modules"
+ parallel = true
+ ignoreFailures = false
+ autoCorrect = autoFix
+ buildUponDefaultConfig = true
+ setSource(projectSource)
+ config.setFrom(configFiles)
+ baseline = baselineFile
+ reports {
+ html.required = true
+ xml.required = false
+ txt.required = false
+ }
+}
+
+tasks.register("detektGenerateBaseline") {
+ description = "Custom DETEKT task to build baseline for all modules"
+ parallel = true
+ ignoreFailures = false
+ buildUponDefaultConfig = true
+ setSource(projectSource)
+ baseline.set(baselineFile)
+ config.setFrom(configFiles)
+ include(kotlinFiles)
+ exclude(sampleModuleFiles, resourceFiles, buildFiles)
+}
+
+
+tasks.withType().configureEach {
+ include(kotlinFiles)
+ exclude(sampleModuleFiles, resourceFiles, buildFiles)
+}
+
+dependencies {
+ detektPlugins(libs.detekt.formatting)
+}
+
+
+apply(plugin = "kotlinx-atomicfu")
diff --git a/common-android.gradle b/common-android.gradle
deleted file mode 100644
index ff19a36..0000000
--- a/common-android.gradle
+++ /dev/null
@@ -1,24 +0,0 @@
-android {
- namespace = 'com.sebastianneubauer.jsontree'
-
- compileSdk libs.versions.android.target.get().toInteger()
-
- defaultConfig {
- minSdk libs.versions.android.min.get().toInteger()
- targetSdk libs.versions.android.target.get().toInteger()
-
- aarMetadata {
- minCompileSdk = libs.versions.android.min.get().toInteger()
- }
- }
-
- buildFeatures {
- buildConfig = false
- }
-}
-
-kotlin{
- jvmToolchain {
- languageVersion.set(JavaLanguageVersion.of(libs.versions.java.toolchain.get()))
- }
-}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index a81301d..d2f55a8 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,49 +1,46 @@
[versions]
-android-min = "21"
-android-target = "34"
+android-minSdk = "21"
+android-compileSdk = "34"
java-toolchain = "17"
-gradle-plugin = "8.4.1"
+compose = "1.6.11"
+agp = "8.4.2"
kotlin = "2.0.0"
-publish = "0.28.0"
-api-validator = "0.14.0"
+publish = "0.29.0"
+nexus-publish = "2.0.0"
+api-validator = "0.16.3"
+atomicfuGradlePlugin = "0.22.0"
-androidx-activity-compose = "1.9.0"
-androidx-compose-bom = "2024.05.00"
+androidx-activity-compose = "1.9.1"
-kotlinx-serialization-json = "1.7.0"
-coroutines = "1.8.1"
+kotlinx-serialization-json = "1.7.1"
+kotlinx-coroutines = "1.8.1"
junit = "4.13.2"
detekt = "1.23.6"
[libraries]
-androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidx-compose-bom" }
-androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
-androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
-androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
-androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }
-androidx-compose-material = { group = "androidx.compose.material3", name = "material3" }
-androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime" }
+kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" }
-
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
+kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
+kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
+kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
+kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
-testing-junit = { module = "junit:junit", version.ref = "junit" }
-testing-compose-ui-junit = { group = "androidx.compose.ui", name = "ui-test-junit4" }
-testing-compose-ui-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
-testing-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
publish = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "publish" }
+atomicfu = { module = "org.jetbrains.kotlinx:atomicfu-gradle-plugin", version.ref = "atomicfuGradlePlugin" }
[plugins]
-android-application = { id = "com.android.application", version.ref = "gradle-plugin" }
-android-library = { id = "com.android.library", version.ref = "gradle-plugin" }
-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+android-library = { id = "com.android.library", version.ref = "agp" }
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
+compose = { id = "org.jetbrains.compose", version.ref = "compose" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
publish = { id = "com.vanniktech.maven.publish", version.ref = "publish" }
api-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "api-validator" }
diff --git a/iosApp/Configuration/Config.xcconfig b/iosApp/Configuration/Config.xcconfig
new file mode 100644
index 0000000..76674b4
--- /dev/null
+++ b/iosApp/Configuration/Config.xcconfig
@@ -0,0 +1,3 @@
+TEAM_ID=
+BUNDLE_ID=com.sebastianneubauer.jsontreesample.KotlinProject
+APP_NAME=KotlinProject
\ No newline at end of file
diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..42f290d
--- /dev/null
+++ b/iosApp/iosApp.xcodeproj/project.pbxproj
@@ -0,0 +1,393 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 56;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; };
+ 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; };
+ 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };
+ 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
+ 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; };
+ 7555FF7B242A565900829871 /* KotlinProject.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KotlinProject.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
+ 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ B92378962B6B1156000C7307 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 058557D7273AAEEB004C7B11 /* Preview Content */ = {
+ isa = PBXGroup;
+ children = (
+ 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */,
+ );
+ path = "Preview Content";
+ sourceTree = "";
+ };
+ 42799AB246E5F90AF97AA0EF /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ 7555FF72242A565900829871 = {
+ isa = PBXGroup;
+ children = (
+ AB1DB47929225F7C00F7AF9C /* Configuration */,
+ 7555FF7D242A565900829871 /* iosApp */,
+ 7555FF7C242A565900829871 /* Products */,
+ 42799AB246E5F90AF97AA0EF /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 7555FF7C242A565900829871 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 7555FF7B242A565900829871 /* KotlinProject.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 7555FF7D242A565900829871 /* iosApp */ = {
+ isa = PBXGroup;
+ children = (
+ 058557BA273AAA24004C7B11 /* Assets.xcassets */,
+ 7555FF82242A565900829871 /* ContentView.swift */,
+ 7555FF8C242A565B00829871 /* Info.plist */,
+ 2152FB032600AC8F00CF470E /* iOSApp.swift */,
+ 058557D7273AAEEB004C7B11 /* Preview Content */,
+ );
+ path = iosApp;
+ sourceTree = "";
+ };
+ AB1DB47929225F7C00F7AF9C /* Configuration */ = {
+ isa = PBXGroup;
+ children = (
+ AB3632DC29227652001CCB65 /* Config.xcconfig */,
+ );
+ path = Configuration;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 7555FF7A242A565900829871 /* iosApp */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */;
+ buildPhases = (
+ F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */,
+ 7555FF77242A565900829871 /* Sources */,
+ B92378962B6B1156000C7307 /* Frameworks */,
+ 7555FF79242A565900829871 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = iosApp;
+ packageProductDependencies = (
+ );
+ productName = iosApp;
+ productReference = 7555FF7B242A565900829871 /* KotlinProject.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 7555FF73242A565900829871 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = YES;
+ LastSwiftUpdateCheck = 1130;
+ LastUpgradeCheck = 1540;
+ ORGANIZATIONNAME = orgName;
+ TargetAttributes = {
+ 7555FF7A242A565900829871 = {
+ CreatedOnToolsVersion = 11.3.1;
+ };
+ };
+ };
+ buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */;
+ compatibilityVersion = "Xcode 14.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 7555FF72242A565900829871;
+ packageReferences = (
+ );
+ productRefGroup = 7555FF7C242A565900829871 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 7555FF7A242A565900829871 /* iosApp */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 7555FF79242A565900829871 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */,
+ 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ name = "Compile Kotlin Framework";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :sample:embedAndSignAppleFrameworkForXcode\n";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 7555FF77242A565900829871 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */,
+ 7555FF83242A565900829871 /* ContentView.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ 7555FFA3242A565B00829871 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.3;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 7555FFA4242A565B00829871 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.3;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 7555FFA6242A565B00829871 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
+ DEVELOPMENT_TEAM = "${TEAM_ID}";
+ ENABLE_PREVIEWS = YES;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../sample/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
+ );
+ INFOPLIST_FILE = iosApp/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.3;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";
+ PRODUCT_NAME = "${APP_NAME}";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 7555FFA7242A565B00829871 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
+ DEVELOPMENT_TEAM = "${TEAM_ID}";
+ ENABLE_PREVIEWS = YES;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../sample/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
+ );
+ INFOPLIST_FILE = iosApp/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.3;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";
+ PRODUCT_NAME = "${APP_NAME}";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 7555FFA3242A565B00829871 /* Debug */,
+ 7555FFA4242A565B00829871 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 7555FFA6242A565B00829871 /* Debug */,
+ 7555FFA7242A565B00829871 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 7555FF73242A565900829871 /* Project object */;
+}
\ No newline at end of file
diff --git a/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..ee7e3ca
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
\ No newline at end of file
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..8edf56e
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,14 @@
+{
+ "images" : [
+ {
+ "filename" : "app-icon-1024.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png
new file mode 100644
index 0000000..53fc536
Binary files /dev/null and b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png differ
diff --git a/iosApp/iosApp/Assets.xcassets/Contents.json b/iosApp/iosApp/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..4aa7c53
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
\ No newline at end of file
diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift
new file mode 100644
index 0000000..ab75edd
--- /dev/null
+++ b/iosApp/iosApp/ContentView.swift
@@ -0,0 +1,21 @@
+import UIKit
+import SwiftUI
+import Sample
+
+struct ComposeView: UIViewControllerRepresentable {
+ func makeUIViewController(context: Context) -> UIViewController {
+ MainKt.MainViewController()
+ }
+
+ func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
+}
+
+struct ContentView: View {
+ var body: some View {
+ ComposeView()
+ .ignoresSafeArea(.keyboard) // Compose has own keyboard handler
+ }
+}
+
+
+
diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist
new file mode 100644
index 0000000..412e378
--- /dev/null
+++ b/iosApp/iosApp/Info.plist
@@ -0,0 +1,50 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ LSRequiresIPhoneOS
+
+ CADisableMinimumFrameDurationOnPhone
+
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+
+ UILaunchScreen
+
+ UIRequiredDeviceCapabilities
+
+ armv7
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+
+
diff --git a/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 0000000..4aa7c53
--- /dev/null
+++ b/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
\ No newline at end of file
diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift
new file mode 100644
index 0000000..d83dca6
--- /dev/null
+++ b/iosApp/iosApp/iOSApp.swift
@@ -0,0 +1,10 @@
+import SwiftUI
+
+@main
+struct iOSApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ }
+ }
+}
\ No newline at end of file
diff --git a/jsontree/api/android/jsontree.api b/jsontree/api/android/jsontree.api
new file mode 100644
index 0000000..ab9660f
--- /dev/null
+++ b/jsontree/api/android/jsontree.api
@@ -0,0 +1,44 @@
+public final class com/sebastianneubauer/jsontree/JsonTreeKt {
+ public static final fun JsonTree-xKBSf-U (Ljava/lang/String;Lkotlin/jvm/functions/Function2;Landroidx/compose/ui/Modifier;Lcom/sebastianneubauer/jsontree/TreeState;Landroidx/compose/foundation/layout/PaddingValues;Lcom/sebastianneubauer/jsontree/TreeColors;Landroidx/compose/ui/graphics/vector/ImageVector;FLandroidx/compose/ui/text/TextStyle;ZZZLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;III)V
+}
+
+public final class com/sebastianneubauer/jsontree/TreeColors {
+ public static final field $stable I
+ public synthetic fun (JJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1-0d7_KjU ()J
+ public final fun component2-0d7_KjU ()J
+ public final fun component3-0d7_KjU ()J
+ public final fun component4-0d7_KjU ()J
+ public final fun component5-0d7_KjU ()J
+ public final fun component6-0d7_KjU ()J
+ public final fun component7-0d7_KjU ()J
+ public final fun component8-0d7_KjU ()J
+ public final fun copy-FD3wquc (JJJJJJJJ)Lcom/sebastianneubauer/jsontree/TreeColors;
+ public static synthetic fun copy-FD3wquc$default (Lcom/sebastianneubauer/jsontree/TreeColors;JJJJJJJJILjava/lang/Object;)Lcom/sebastianneubauer/jsontree/TreeColors;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getBooleanValueColor-0d7_KjU ()J
+ public final fun getIconColor-0d7_KjU ()J
+ public final fun getIndexColor-0d7_KjU ()J
+ public final fun getKeyColor-0d7_KjU ()J
+ public final fun getNullValueColor-0d7_KjU ()J
+ public final fun getNumberValueColor-0d7_KjU ()J
+ public final fun getStringValueColor-0d7_KjU ()J
+ public final fun getSymbolColor-0d7_KjU ()J
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final class com/sebastianneubauer/jsontree/TreeColorsKt {
+ public static final fun getDefaultDarkColors ()Lcom/sebastianneubauer/jsontree/TreeColors;
+ public static final fun getDefaultLightColors ()Lcom/sebastianneubauer/jsontree/TreeColors;
+}
+
+public final class com/sebastianneubauer/jsontree/TreeState : java/lang/Enum {
+ public static final field COLLAPSED Lcom/sebastianneubauer/jsontree/TreeState;
+ public static final field EXPANDED Lcom/sebastianneubauer/jsontree/TreeState;
+ public static final field FIRST_ITEM_EXPANDED Lcom/sebastianneubauer/jsontree/TreeState;
+ public static fun getEntries ()Lkotlin/enums/EnumEntries;
+ public static fun valueOf (Ljava/lang/String;)Lcom/sebastianneubauer/jsontree/TreeState;
+ public static fun values ()[Lcom/sebastianneubauer/jsontree/TreeState;
+}
+
diff --git a/jsontree/api/jvm/jsontree.api b/jsontree/api/jvm/jsontree.api
new file mode 100644
index 0000000..ab9660f
--- /dev/null
+++ b/jsontree/api/jvm/jsontree.api
@@ -0,0 +1,44 @@
+public final class com/sebastianneubauer/jsontree/JsonTreeKt {
+ public static final fun JsonTree-xKBSf-U (Ljava/lang/String;Lkotlin/jvm/functions/Function2;Landroidx/compose/ui/Modifier;Lcom/sebastianneubauer/jsontree/TreeState;Landroidx/compose/foundation/layout/PaddingValues;Lcom/sebastianneubauer/jsontree/TreeColors;Landroidx/compose/ui/graphics/vector/ImageVector;FLandroidx/compose/ui/text/TextStyle;ZZZLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;III)V
+}
+
+public final class com/sebastianneubauer/jsontree/TreeColors {
+ public static final field $stable I
+ public synthetic fun (JJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1-0d7_KjU ()J
+ public final fun component2-0d7_KjU ()J
+ public final fun component3-0d7_KjU ()J
+ public final fun component4-0d7_KjU ()J
+ public final fun component5-0d7_KjU ()J
+ public final fun component6-0d7_KjU ()J
+ public final fun component7-0d7_KjU ()J
+ public final fun component8-0d7_KjU ()J
+ public final fun copy-FD3wquc (JJJJJJJJ)Lcom/sebastianneubauer/jsontree/TreeColors;
+ public static synthetic fun copy-FD3wquc$default (Lcom/sebastianneubauer/jsontree/TreeColors;JJJJJJJJILjava/lang/Object;)Lcom/sebastianneubauer/jsontree/TreeColors;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getBooleanValueColor-0d7_KjU ()J
+ public final fun getIconColor-0d7_KjU ()J
+ public final fun getIndexColor-0d7_KjU ()J
+ public final fun getKeyColor-0d7_KjU ()J
+ public final fun getNullValueColor-0d7_KjU ()J
+ public final fun getNumberValueColor-0d7_KjU ()J
+ public final fun getStringValueColor-0d7_KjU ()J
+ public final fun getSymbolColor-0d7_KjU ()J
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final class com/sebastianneubauer/jsontree/TreeColorsKt {
+ public static final fun getDefaultDarkColors ()Lcom/sebastianneubauer/jsontree/TreeColors;
+ public static final fun getDefaultLightColors ()Lcom/sebastianneubauer/jsontree/TreeColors;
+}
+
+public final class com/sebastianneubauer/jsontree/TreeState : java/lang/Enum {
+ public static final field COLLAPSED Lcom/sebastianneubauer/jsontree/TreeState;
+ public static final field EXPANDED Lcom/sebastianneubauer/jsontree/TreeState;
+ public static final field FIRST_ITEM_EXPANDED Lcom/sebastianneubauer/jsontree/TreeState;
+ public static fun getEntries ()Lkotlin/enums/EnumEntries;
+ public static fun valueOf (Ljava/lang/String;)Lcom/sebastianneubauer/jsontree/TreeState;
+ public static fun values ()[Lcom/sebastianneubauer/jsontree/TreeState;
+}
+
diff --git a/jsontree/build.gradle b/jsontree/build.gradle
deleted file mode 100644
index 574e71b..0000000
--- a/jsontree/build.gradle
+++ /dev/null
@@ -1,41 +0,0 @@
-plugins {
- alias libs.plugins.android.library
- alias libs.plugins.kotlin.android
- alias(libs.plugins.compose.compiler)
- alias libs.plugins.publish
-}
-
-apply from: "$rootProject.projectDir/common-android.gradle"
-
-android {
- kotlinOptions {
- freeCompilerArgs += ["-Xexplicit-api=strict", "-Xjvm-default=all", "-opt-in=kotlin.RequiresOptIn"]
- }
- buildFeatures {
- compose = true
- }
- defaultConfig {
- testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
- }
-}
-
-composeCompiler {
- enableStrongSkippingMode = true
-}
-
-dependencies {
- implementation platform(libs.androidx.compose.bom)
- implementation libs.androidx.compose.ui
- implementation libs.androidx.compose.ui.tooling
- implementation libs.androidx.compose.foundation
- implementation libs.androidx.compose.material
- implementation libs.androidx.compose.runtime
-
- implementation libs.kotlinx.serialization.json
-
- testImplementation libs.testing.junit
- testImplementation libs.testing.coroutines
- testImplementation platform(libs.androidx.compose.bom)
- androidTestImplementation libs.testing.compose.ui.junit
- debugImplementation libs.testing.compose.ui.manifest
-}
\ No newline at end of file
diff --git a/jsontree/build.gradle.kts b/jsontree/build.gradle.kts
new file mode 100644
index 0000000..6e57616
--- /dev/null
+++ b/jsontree/build.gradle.kts
@@ -0,0 +1,114 @@
+import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree
+
+plugins {
+ alias(libs.plugins.kotlinMultiplatform)
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.compose)
+ alias(libs.plugins.compose.compiler)
+ id("kotlinx-atomicfu")
+ alias(libs.plugins.publish)
+}
+
+kotlin {
+ jvm()
+ androidTarget {
+ publishLibraryVariants("release")
+ @OptIn(ExperimentalKotlinGradlePluginApi::class)
+ instrumentedTestVariant.sourceSetTree.set(KotlinSourceSetTree.test)
+ @OptIn(ExperimentalKotlinGradlePluginApi::class)
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_1_8)
+ }
+
+ @OptIn(ExperimentalKotlinGradlePluginApi::class)
+ instrumentedTestVariant {
+ sourceSetTree.set(KotlinSourceSetTree.test)
+ dependencies {
+ implementation(libs.kotlinx.coroutines.test)
+ }
+ }
+ }
+ iosX64()
+ iosArm64()
+ iosSimulatorArm64()
+
+ @OptIn(ExperimentalKotlinGradlePluginApi::class)
+ compilerOptions {
+ freeCompilerArgs.addAll("-Xexplicit-api=strict", "-Xjvm-default=all", "-opt-in=kotlin.RequiresOptIn")
+ }
+
+ jvmToolchain {
+ languageVersion.set(JavaLanguageVersion.of(libs.versions.java.toolchain.get()))
+ }
+
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation(compose.runtime)
+ implementation(compose.foundation)
+ implementation(compose.material3)
+ implementation(compose.components.resources)
+ implementation(compose.components.uiToolingPreview)
+ implementation(libs.kotlinx.coroutines.core)
+
+ implementation (libs.kotlinx.serialization.json)
+
+ }
+ }
+
+ val commonTest by getting {
+ dependencies {
+ implementation(libs.kotlin.test)
+ implementation(libs.kotlinx.coroutines.test)
+
+ @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
+ implementation(compose.uiTest)
+ }
+ }
+
+ val androidMain by getting {
+ dependencies {
+ implementation(libs.kotlinx.coroutines.android)
+ }
+ }
+
+ val jvmMain by getting {
+ dependencies {
+ implementation(libs.kotlinx.coroutines.swing)
+ implementation(compose.desktop.currentOs)
+ }
+ }
+
+ val jvmTest by getting {
+ dependencies {
+ implementation(compose.desktop.uiTestJUnit4)
+ implementation(compose.desktop.currentOs)
+ }
+ }
+ }
+}
+
+
+android {
+ namespace = "com.sebastianneubauer.jsontree"
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
+ buildFeatures {
+ buildConfig = false
+ compose = true
+ }
+ defaultConfig {
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ minSdk = libs.versions.android.minSdk.get().toInt()
+ aarMetadata {
+ minCompileSdk = libs.versions.android.minSdk.get().toInt()
+ }
+ }
+}
+
+composeCompiler {
+ enableStrongSkippingMode = true
+}
+
+apply(plugin = "kotlinx-atomicfu")
\ No newline at end of file
diff --git a/jsontree/proguard-rules.pro b/jsontree/proguard-rules.pro
index 481bb43..ff59496 100644
--- a/jsontree/proguard-rules.pro
+++ b/jsontree/proguard-rules.pro
@@ -1,6 +1,6 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
+# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
diff --git a/jsontree/src/androidTest/java/com/sebastianneubauer/jsontree/JsonTreeTest.kt b/jsontree/src/androidTest/java/com/sebastianneubauer/jsontree/JsonTreeTest.kt
deleted file mode 100644
index 0992ed3..0000000
--- a/jsontree/src/androidTest/java/com/sebastianneubauer/jsontree/JsonTreeTest.kt
+++ /dev/null
@@ -1,455 +0,0 @@
-package com.sebastianneubauer.jsontree
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.material3.Button
-import androidx.compose.material3.Text
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTestApi
-import androidx.compose.ui.test.assertCountEquals
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.hasTestTag
-import androidx.compose.ui.test.hasText
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onAllNodesWithText
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.test.performClick
-import org.junit.Rule
-import org.junit.Test
-
-@OptIn(ExperimentalTestApi::class)
-internal class JsonTreeTest {
-
- @JvmField
- @Rule
- val composeTestRule = createComposeRule()
-
- private fun setJson(
- json: String,
- initialState: TreeState = TreeState.FIRST_ITEM_EXPANDED,
- ) {
- composeTestRule.setContent {
- JsonTree(
- json = json,
- initialState = initialState,
- onLoading = {}
- )
- }
- }
-
- @Test
- fun initial_state_is_first_item_expanded() {
- setJson(nestedJson)
-
- composeTestRule.onNodeWithText("\"topLevelObject\": { 2 items },").assertIsDisplayed()
- composeTestRule.onNodeWithText("\"topLevelArray\": [ 2 items ],").assertIsDisplayed()
- composeTestRule.onNodeWithText("\"emptyObject\": { 0 items }").assertIsDisplayed()
-
- composeTestRule.onNodeWithText("\"string\": \"stringValue\",").assertDoesNotExist()
- composeTestRule.onNodeWithText("\"hello\",").assertDoesNotExist()
- }
-
- @Test
- fun initial_state_is_collapsed() {
- setJson(nestedJson, initialState = TreeState.COLLAPSED)
-
- composeTestRule.onNodeWithText("{ 3 items }").assertIsDisplayed()
- composeTestRule.onNodeWithText("\"topLevelObject\"", substring = true).assertDoesNotExist()
- }
-
- @Test
- fun initial_state_is_expanded() {
- setJson(nestedJson, initialState = TreeState.EXPANDED)
-
- // fix flakiness by waiting until first item appears
- composeTestRule.waitUntilExactlyOneExists(hasText("\"topLevelObject\": {"))
-
- // every collapsable is expanded
- composeTestRule.onNodeWithText("\"topLevelObject\": {").assertIsDisplayed()
- composeTestRule.onNodeWithText("\"string\": \"stringValue\",").assertIsDisplayed()
-
- composeTestRule.onNodeWithText("\"nestedObject\": {").assertIsDisplayed()
- composeTestRule.onNodeWithText("\"int\": 42,").assertIsDisplayed()
-
- composeTestRule.onNodeWithText("\"nestedArray\": [").assertIsDisplayed()
- composeTestRule.onNodeWithText("\"nestedArrayValue\",").assertIsDisplayed()
- composeTestRule.onNodeWithText("\"nestedArrayValue\"").assertIsDisplayed()
-
- composeTestRule.onNodeWithText("\"arrayOfObjects\": [").assertIsDisplayed()
- composeTestRule.onNodeWithText("\"anotherString\": \"anotherStringValue\"").assertIsDisplayed()
- composeTestRule.onNodeWithText("\"anotherInt\": 52").assertIsDisplayed()
-
- composeTestRule.onNodeWithText("\"topLevelArray\": [").assertIsDisplayed()
- composeTestRule.onNodeWithText("\"hello\",").assertIsDisplayed()
- composeTestRule.onNodeWithText("\"emptyObject\": {").assertIsDisplayed()
- }
-
- @Test
- fun click_on_collapsed_object_or_array_expands_it() {
- setJson(nestedJson)
- // fix flakiness by waiting until first item appears
- composeTestRule.waitUntilExactlyOneExists(hasText("\"topLevelObject\": { 2 items },"))
-
- composeTestRule.onNodeWithText("\"topLevelObject\": { 2 items },").performClick()
- composeTestRule.onNodeWithText("\"topLevelObject\": {").assertIsDisplayed()
- composeTestRule.onNodeWithText("\"string\": \"stringValue\",").assertIsDisplayed()
- composeTestRule.onNodeWithText("\"nestedObject\": { 3 items }").assertIsDisplayed()
-
- composeTestRule.onNodeWithText("\"topLevelArray\": [ 2 items ],").performClick()
- composeTestRule.onNodeWithText("\"topLevelArray\": [").assertIsDisplayed()
- composeTestRule.onNodeWithText("\"hello\",").assertIsDisplayed()
- composeTestRule.onNodeWithText("\"world\"").assertIsDisplayed()
- }
-
- @Test
- fun click_on_expanded_object_or_array_collapses_it() {
- setJson(nestedJson, initialState = TreeState.EXPANDED)
-
- composeTestRule.onNodeWithText("\"arrayOfObjects\": [").performClick()
- composeTestRule.onNodeWithText("\"arrayOfObjects\": [ 2 items ]").assertIsDisplayed()
-
- composeTestRule.onNodeWithText("\"nestedObject\": {").performClick()
- composeTestRule.onNodeWithText("\"nestedObject\": { 3 items }").assertIsDisplayed()
-
- composeTestRule.onNodeWithText("\"topLevelObject\": {").performClick()
- composeTestRule.onNodeWithText("\"topLevelObject\": { 2 items },").assertIsDisplayed()
- }
-
- @Test
- fun array_of_arrays_is_rendered_correctly() {
- setJson(arrayOfArraysJson)
- // fix flakiness by waiting until first item appears
- composeTestRule.waitUntilExactlyOneExists(hasText("\"array\": [ 2 items ]"))
-
- composeTestRule.onNodeWithText("\"array\": [ 2 items ]").assertIsDisplayed()
- composeTestRule.onNodeWithText("\"array\": [ 2 items ]").performClick()
- composeTestRule.onNodeWithText("\"array\": [").assertIsDisplayed()
- composeTestRule.onNodeWithText("[ 1 item ],").assertIsDisplayed()
- composeTestRule.onNodeWithText("[ 2 items ]").assertIsDisplayed()
-
- composeTestRule.onNodeWithText("[ 1 item ],").performClick()
- composeTestRule.onNodeWithText("[ 2 items ]").performClick()
-
- composeTestRule.onNodeWithText("\"stringValue\"").assertIsDisplayed()
- composeTestRule.onNodeWithText("],").assertIsDisplayed()
- composeTestRule.onNodeWithText("42,").assertIsDisplayed()
- composeTestRule.onNodeWithText("52").assertIsDisplayed()
- }
-
- @Test
- fun root_array_is_rendered_correctly() {
- setJson(rootArrayJson)
-
- composeTestRule.assertRootArrayIsDisplayed()
- }
-
- @Test
- fun root_string_is_rendered_correctly() {
- setJson(rootStringJson)
-
- composeTestRule.assertRootStringIsDisplayed()
- }
-
- @Test
- fun empty_object_is_rendered_correctly() {
- setJson(EMPTY_OBJECT_JSON)
-
- composeTestRule.assertEmptyObjectIsDisplayed()
-
- composeTestRule.onNodeWithText("{").performClick()
- composeTestRule.onNodeWithText("{ 0 items }").assertIsDisplayed()
- }
-
- @Test
- fun invalid_json_shows_an_error() {
- composeTestRule.setContent {
- Box {
- var errorMessage: String? by remember { mutableStateOf(null) }
-
- JsonTree(
- modifier = Modifier.testTag("jsonTree"),
- json = INVALID_JSON,
- onError = { throwable -> errorMessage = throwable.localizedMessage },
- onLoading = {}
- )
-
- errorMessage?.let {
- Text(
- modifier = Modifier.testTag("errorText"),
- text = it
- )
- }
- }
- }
-
- composeTestRule.onNodeWithTag("errorText").assertIsDisplayed()
- }
-
- @Test
- fun changing_json_is_handled_correctly() {
- composeTestRule.setContent {
- var jsonString: String by remember { mutableStateOf(rootStringJson) }
-
- Column {
- JsonTree(json = jsonString, onLoading = {})
-
- Button(
- modifier = Modifier.testTag("button"),
- onClick = { jsonString = rootArrayJson }
- ) {}
- }
- }
-
- composeTestRule.assertRootStringIsDisplayed()
-
- composeTestRule.onNodeWithTag("button").performClick()
-
- composeTestRule.assertRootArrayIsDisplayed()
- }
-
- @Test
- fun changing_json_while_collapsed_is_handled_correctly() {
- composeTestRule.setContent {
- var jsonString: String by remember { mutableStateOf(nestedJson) }
-
- Column {
- JsonTree(
- json = jsonString,
- initialState = TreeState.COLLAPSED,
- onLoading = {}
- )
-
- Button(
- modifier = Modifier.testTag("button"),
- onClick = { jsonString = rootArrayJson }
- ) {}
- }
- }
-
- composeTestRule.onNodeWithText("{ 3 items }").assertIsDisplayed()
-
- composeTestRule.onNodeWithTag("button").performClick()
-
- composeTestRule.onNodeWithText("[ 1 item ]").assertIsDisplayed()
- }
-
- @Test
- fun changing_json_from_invalid_to_valid_is_handled_correctly() {
- composeTestRule.setContent {
- var jsonString: String by remember { mutableStateOf(INVALID_JSON) }
-
- Column {
- JsonTree(
- modifier = Modifier.testTag("jsonTree"),
- json = jsonString,
- onLoading = {}
- )
-
- Button(
- modifier = Modifier.testTag("button"),
- onClick = { jsonString = rootStringJson }
- ) {}
- }
- }
-
- composeTestRule.onNodeWithTag("button").performClick()
- composeTestRule.assertRootStringIsDisplayed()
- }
-
- @Test
- fun changing_json_from_valid_to_invalid_is_handled_correctly() {
- composeTestRule.setContent {
- var jsonString: String by remember { mutableStateOf(rootStringJson) }
- var errorMessage: String? by remember { mutableStateOf(null) }
-
- Column {
- JsonTree(
- modifier = Modifier.testTag("jsonTree"),
- json = jsonString,
- onError = { throwable -> errorMessage = throwable.localizedMessage },
- onLoading = {}
- )
-
- Button(
- modifier = Modifier.testTag("button"),
- onClick = { jsonString = INVALID_JSON }
- ) {}
-
- errorMessage?.let {
- Text(
- modifier = Modifier.testTag("errorText"),
- text = it
- )
- }
- }
- }
-
- // fix flakiness by waiting until first item appears
- composeTestRule.waitUntilExactlyOneExists(hasTestTag("jsonTree"))
-
- composeTestRule.onNodeWithTag("jsonTree").assertIsDisplayed()
- composeTestRule.onNodeWithTag("errorText").assertDoesNotExist()
-
- composeTestRule.onNodeWithTag("button").performClick()
-
- composeTestRule.onNodeWithTag("errorText").assertIsDisplayed()
- }
-
- @Test
- fun changing_initial_state_is_handled_correctly() {
- composeTestRule.setContent {
- var initalState: TreeState by remember { mutableStateOf(TreeState.EXPANDED) }
-
- Column {
- JsonTree(
- json = arrayOfArraysJson,
- initialState = initalState,
- onLoading = {}
- )
-
- Button(
- modifier = Modifier.testTag("button"),
- onClick = {
- initalState = when (initalState) {
- TreeState.EXPANDED -> TreeState.COLLAPSED
- TreeState.COLLAPSED -> TreeState.FIRST_ITEM_EXPANDED
- TreeState.FIRST_ITEM_EXPANDED -> TreeState.EXPANDED
- }
- }
- ) {}
- }
- }
-
- composeTestRule.onNodeWithText("\"array\": [").assertIsDisplayed()
- composeTestRule.onNodeWithText("\"stringValue\"").assertIsDisplayed()
- composeTestRule.onNodeWithText("42,").assertIsDisplayed()
- composeTestRule.onNodeWithText("52").assertIsDisplayed()
-
- composeTestRule.onNodeWithTag("button").performClick()
-
- composeTestRule.onNodeWithText("{ 1 item }").assertIsDisplayed()
-
- composeTestRule.onNodeWithTag("button").performClick()
-
- composeTestRule.onNodeWithText("\"array\": [ 2 items ]").assertIsDisplayed()
- }
-
- @Test
- fun showing_and_hiding_indices_is_handled_correctly() {
- composeTestRule.setContent {
- var showIndices: Boolean by remember { mutableStateOf(true) }
-
- Column {
- JsonTree(
- json = arrayOfArraysJson,
- onLoading = {},
- showIndices = showIndices,
- initialState = TreeState.EXPANDED
- )
-
- Button(
- modifier = Modifier.testTag("button"),
- onClick = { showIndices = !showIndices }
- ) {}
- }
- }
-
- composeTestRule.onNodeWithText("0: [").assertIsDisplayed()
- composeTestRule.onNodeWithText("0: \"stringValue\"").assertIsDisplayed()
- composeTestRule.onNodeWithText("1: [").assertIsDisplayed()
- composeTestRule.onNodeWithText("0: 42,").assertIsDisplayed()
- composeTestRule.onNodeWithText("1: 52").assertIsDisplayed()
-
- composeTestRule.onNodeWithTag("button").performClick()
-
- composeTestRule.onAllNodesWithText("[").assertCountEquals(2)
- composeTestRule.onNodeWithText("\"stringValue\"").assertIsDisplayed()
- composeTestRule.onNodeWithText("42,").assertIsDisplayed()
- composeTestRule.onNodeWithText("52").assertIsDisplayed()
- }
-
- @Test
- fun showing_and_hiding_item_count_is_handled_correctly() {
- composeTestRule.setContent {
- var showItemCount: Boolean by remember { mutableStateOf(true) }
-
- Column {
- JsonTree(
- json = rootArrayJson,
- onLoading = {},
- showItemCount = showItemCount,
- initialState = TreeState.COLLAPSED
- )
-
- Button(
- modifier = Modifier.testTag("button"),
- onClick = { showItemCount = !showItemCount }
- ) {}
- }
- }
-
- composeTestRule.onNodeWithText("[ 1 item ]").assertIsDisplayed()
-
- composeTestRule.onNodeWithTag("button").performClick()
-
- composeTestRule.onNodeWithText("[ ... ]").assertIsDisplayed()
-
- composeTestRule.onNodeWithTag("button").performClick()
-
- composeTestRule.onNodeWithText("[ 1 item ]").assertIsDisplayed()
- }
-
- @Test
- fun expanding_single_children_is_handled_correctly() {
- composeTestRule.setContent {
- JsonTree(
- json = arrayOfArraysJson,
- initialState = TreeState.COLLAPSED,
- onLoading = {},
- expandSingleChildren = true
- )
- }
-
- composeTestRule.onNodeWithText("{ 1 item }").performClick()
- // shows nested array
- composeTestRule.onNodeWithText("\"array\": [").assertIsDisplayed()
- // shows collapsed contents of nested array
- composeTestRule.onNodeWithText("[ 1 item ],").assertIsDisplayed()
- composeTestRule.onNodeWithText("[ 2 items ]").assertIsDisplayed()
-
- // collapse root
- composeTestRule.onNodeWithText("{").performClick()
- composeTestRule.onNodeWithText("{ 1 item }").assertIsDisplayed()
- }
-
- @Test
- fun not_expanding_single_children_is_handled_correctly() {
- composeTestRule.setContent {
- JsonTree(
- json = arrayOfArraysJson,
- initialState = TreeState.COLLAPSED,
- onLoading = {},
- expandSingleChildren = false
- )
- }
-
- composeTestRule.onNodeWithText("{ 1 item }").performClick()
-
- composeTestRule.waitUntilExactlyOneExists(hasText("\"array\": [ 2 items ]"))
- composeTestRule.onNodeWithText("\"array\": [ 2 items ]").assertIsDisplayed()
- composeTestRule.onNodeWithText("\"array\": [ 2 items ]").performClick()
-
- composeTestRule.onNodeWithText("[ 1 item ],").assertIsDisplayed()
- composeTestRule.onNodeWithText("[ 2 items ]").assertIsDisplayed()
-
- // collapse root
- composeTestRule.onNodeWithText("{").performClick()
- composeTestRule.onNodeWithText("{ 1 item }").assertIsDisplayed()
- }
-}
diff --git a/jsontree/src/main/res/drawable/jsontree_arrow_right.xml b/jsontree/src/commonMain/composeResources/drawable/jsontree_arrow_right.xml
similarity index 71%
rename from jsontree/src/main/res/drawable/jsontree_arrow_right.xml
rename to jsontree/src/commonMain/composeResources/drawable/jsontree_arrow_right.xml
index e3b8f37..4272a0e 100644
--- a/jsontree/src/main/res/drawable/jsontree_arrow_right.xml
+++ b/jsontree/src/commonMain/composeResources/drawable/jsontree_arrow_right.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/jsontree/src/main/res/values/plurals.xml b/jsontree/src/commonMain/composeResources/values/plurals.xml
similarity index 51%
rename from jsontree/src/main/res/values/plurals.xml
rename to jsontree/src/commonMain/composeResources/values/plurals.xml
index cf8b5d3..a4ca7f9 100644
--- a/jsontree/src/main/res/values/plurals.xml
+++ b/jsontree/src/commonMain/composeResources/values/plurals.xml
@@ -1,7 +1,7 @@
- - \u0020%d item\u0020
- - \u0020%d items\u0020
+ - \u0020%1$d item\u0020
+ - \u0020%1$d items\u0020
\ No newline at end of file
diff --git a/jsontree/src/main/java/com/sebastianneubauer/jsontree/AnnotatedText.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/AnnotatedText.kt
similarity index 93%
rename from jsontree/src/main/java/com/sebastianneubauer/jsontree/AnnotatedText.kt
rename to jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/AnnotatedText.kt
index 4811913..451c8cd 100644
--- a/jsontree/src/main/java/com/sebastianneubauer/jsontree/AnnotatedText.kt
+++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/AnnotatedText.kt
@@ -2,18 +2,20 @@ package com.sebastianneubauer.jsontree
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
-import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import com.sebastianneubauer.jsontree.JsonTreeElement.ParentType
+import jsontree.jsontree.generated.resources.Res
+import jsontree.jsontree.generated.resources.jsontree_collapsable_items
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.doubleOrNull
import kotlinx.serialization.json.floatOrNull
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.longOrNull
+import org.jetbrains.compose.resources.pluralStringResource
@Composable
internal fun rememberCollapsableText(
@@ -27,7 +29,7 @@ internal fun rememberCollapsableText(
showItemCount: Boolean,
parentType: ParentType,
): AnnotatedString {
- val itemCount = pluralStringResource(R.plurals.jsontree_collapsable_items, childItemCount, childItemCount)
+ val itemCount = pluralStringResource(Res.plurals.jsontree_collapsable_items, childItemCount, childItemCount)
return remember(key, state, colors, isLastItem, itemCount, type, showIndices, showItemCount) {
val openBracket = if (type == CollapsableType.OBJECT) "{" else "["
diff --git a/jsontree/src/main/java/com/sebastianneubauer/jsontree/CollapsableType.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/CollapsableType.kt
similarity index 100%
rename from jsontree/src/main/java/com/sebastianneubauer/jsontree/CollapsableType.kt
rename to jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/CollapsableType.kt
diff --git a/jsontree/src/main/java/com/sebastianneubauer/jsontree/JsonTree.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTree.kt
similarity index 97%
rename from jsontree/src/main/java/com/sebastianneubauer/jsontree/JsonTree.kt
rename to jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTree.kt
index 7b7b4fd..2f1c4a3 100644
--- a/jsontree/src/main/java/com/sebastianneubauer/jsontree/JsonTree.kt
+++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTree.kt
@@ -21,14 +21,16 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.times
+import jsontree.jsontree.generated.resources.Res
+import jsontree.jsontree.generated.resources.jsontree_arrow_right
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import org.jetbrains.compose.resources.vectorResource
/**
* Renders JSON data as a formatted tree with collapsable objects and arrays.
@@ -59,7 +61,7 @@ public fun JsonTree(
initialState: TreeState = TreeState.FIRST_ITEM_EXPANDED,
contentPadding: PaddingValues = PaddingValues(0.dp),
colors: TreeColors = defaultLightColors,
- icon: ImageVector = ImageVector.vectorResource(R.drawable.jsontree_arrow_right),
+ icon: ImageVector = vectorResource(Res.drawable.jsontree_arrow_right),
iconSize: Dp = 20.dp,
textStyle: TextStyle = LocalTextStyle.current,
showIndices: Boolean = false,
diff --git a/jsontree/src/main/java/com/sebastianneubauer/jsontree/JsonTreeElement.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeElement.kt
similarity index 100%
rename from jsontree/src/main/java/com/sebastianneubauer/jsontree/JsonTreeElement.kt
rename to jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeElement.kt
diff --git a/jsontree/src/main/java/com/sebastianneubauer/jsontree/JsonTreeParser.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeParser.kt
similarity index 97%
rename from jsontree/src/main/java/com/sebastianneubauer/jsontree/JsonTreeParser.kt
rename to jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeParser.kt
index afe8367..d82c722 100644
--- a/jsontree/src/main/java/com/sebastianneubauer/jsontree/JsonTreeParser.kt
+++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeParser.kt
@@ -11,6 +11,7 @@ import com.sebastianneubauer.jsontree.JsonTreeParserState.Loading
import com.sebastianneubauer.jsontree.JsonTreeParserState.Parsing.Error
import com.sebastianneubauer.jsontree.JsonTreeParserState.Parsing.Parsed
import com.sebastianneubauer.jsontree.JsonTreeParserState.Ready
+import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
@@ -20,7 +21,6 @@ import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
-import java.util.concurrent.atomic.AtomicLong
internal class JsonTreeParser(
private val json: String,
@@ -42,7 +42,7 @@ internal class JsonTreeParser(
Ready(
list = parsingState.jsonElement
.toJsonTree(
- idGenerator = AtomicLong(),
+ idGenerator = AtomicLongWrapper(),
state = initialState,
level = 0,
key = null,
@@ -193,7 +193,7 @@ internal class JsonTreeParser(
}
private fun JsonElement.toJsonTree(
- idGenerator: AtomicLong,
+ idGenerator: AtomicLongWrapper,
state: TreeState,
level: Int,
key: String?,
@@ -297,3 +297,10 @@ internal class JsonTreeParser(
return list
}
}
+
+internal class AtomicLongWrapper {
+ private val atomicLong = atomic(0L)
+ fun incrementAndGet(): Long {
+ return atomicLong.incrementAndGet()
+ }
+}
diff --git a/jsontree/src/main/java/com/sebastianneubauer/jsontree/JsonTreeParserState.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeParserState.kt
similarity index 100%
rename from jsontree/src/main/java/com/sebastianneubauer/jsontree/JsonTreeParserState.kt
rename to jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeParserState.kt
diff --git a/jsontree/src/main/java/com/sebastianneubauer/jsontree/TreeColors.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/TreeColors.kt
similarity index 100%
rename from jsontree/src/main/java/com/sebastianneubauer/jsontree/TreeColors.kt
rename to jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/TreeColors.kt
diff --git a/jsontree/src/main/java/com/sebastianneubauer/jsontree/TreeState.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/TreeState.kt
similarity index 100%
rename from jsontree/src/main/java/com/sebastianneubauer/jsontree/TreeState.kt
rename to jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/TreeState.kt
diff --git a/jsontree/src/test/java/com/sebastianneubauer/jsontree/JsonTreeParserTest.kt b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/JsonTreeParserTest.kt
similarity index 76%
rename from jsontree/src/test/java/com/sebastianneubauer/jsontree/JsonTreeParserTest.kt
rename to jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/JsonTreeParserTest.kt
index 3cf8a14..c18cdbc 100644
--- a/jsontree/src/test/java/com/sebastianneubauer/jsontree/JsonTreeParserTest.kt
+++ b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/JsonTreeParserTest.kt
@@ -3,14 +3,15 @@ package com.sebastianneubauer.jsontree
import com.sebastianneubauer.jsontree.JsonTreeParserState.Parsing.Error
import com.sebastianneubauer.jsontree.JsonTreeParserState.Ready
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestResult
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.JsonPrimitive
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
-import org.junit.Test
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
-internal class JsonTreeParserTest {
+public class JsonTreeParserTest {
@OptIn(ExperimentalCoroutinesApi::class)
private suspend fun underTest(json: String, initialState: TreeState): JsonTreeParser {
@@ -22,14 +23,14 @@ internal class JsonTreeParserTest {
}
@Test
- fun `invalid json - shows error state`() = runTest {
+ public fun `invalid json - shows error state`(): TestResult = runTest {
val underTest = underTest(INVALID_JSON, TreeState.COLLAPSED)
// produces Error state with any throwable
assertTrue(underTest.state.value is Error)
}
@Test
- fun `empty json - collapsed - shows collapsed empty json`() = runTest {
+ public fun `empty json - collapsed - shows collapsed empty json`(): TestResult = runTest {
val underTest = underTest(EMPTY_OBJECT_JSON, TreeState.COLLAPSED)
assertEquals(
Ready(
@@ -50,7 +51,7 @@ internal class JsonTreeParserTest {
}
@Test
- fun `empty json - first item expanded - shows expanded empty json`() = runTest {
+ public fun `empty json - first item expanded - shows expanded empty json`(): TestResult = runTest {
val underTest = underTest(EMPTY_OBJECT_JSON, TreeState.FIRST_ITEM_EXPANDED)
assertEquals(
Ready(
@@ -77,7 +78,7 @@ internal class JsonTreeParserTest {
}
@Test
- fun `empty json - expanded - shows expanded empty json`() = runTest {
+ public fun `empty json - expanded - shows expanded empty json`(): TestResult = runTest {
val underTest = underTest(EMPTY_OBJECT_JSON, TreeState.EXPANDED)
assertEquals(
Ready(
@@ -104,7 +105,7 @@ internal class JsonTreeParserTest {
}
@Test
- fun `root string json - collapsed - shows root string`() = runTest {
+ public fun `root string json - collapsed - shows root string`(): TestResult = runTest {
val underTest = underTest(rootStringJson, TreeState.COLLAPSED)
assertEquals(
Ready(
@@ -124,7 +125,7 @@ internal class JsonTreeParserTest {
}
@Test
- fun `root string json - expanded - shows root string`() = runTest {
+ public fun `root string json - expanded - shows root string`(): TestResult = runTest {
val underTest = underTest(rootStringJson, TreeState.EXPANDED)
assertEquals(
Ready(
@@ -144,7 +145,7 @@ internal class JsonTreeParserTest {
}
@Test
- fun `root string json - first item expanded - shows root string`() = runTest {
+ public fun `root string json - first item expanded - shows root string`(): TestResult = runTest {
val underTest = underTest(rootStringJson, TreeState.FIRST_ITEM_EXPANDED)
assertEquals(
Ready(
@@ -164,7 +165,7 @@ internal class JsonTreeParserTest {
}
@Test
- fun `root array json - collapsed - shows collapsed array`() = runTest {
+ public fun `root array json - collapsed - shows collapsed array`(): TestResult = runTest {
val underTest = underTest(rootArrayJson, TreeState.COLLAPSED)
assertEquals(
Ready(
@@ -194,7 +195,7 @@ internal class JsonTreeParserTest {
}
@Test
- fun `root array json - first item expanded - shows expanded array`() = runTest {
+ public fun `root array json - first item expanded - shows expanded array`(): TestResult = runTest {
val underTest = underTest(rootArrayJson, TreeState.FIRST_ITEM_EXPANDED)
assertEquals(
Ready(
@@ -238,7 +239,7 @@ internal class JsonTreeParserTest {
}
@Test
- fun `root array json - expanded - shows expanded array`() = runTest {
+ public fun `root array json - expanded - shows expanded array`(): TestResult = runTest {
val underTest = underTest(rootArrayJson, TreeState.EXPANDED)
assertEquals(
Ready(
@@ -282,7 +283,7 @@ internal class JsonTreeParserTest {
}
@Test
- fun `nested json - collapsed - shows collapsed json`() = runTest {
+ public fun `nested json - collapsed - shows collapsed json`(): TestResult = runTest {
val underTest = underTest(nestedJson, TreeState.COLLAPSED)
assertEquals(
Ready(list = listOf(jsonTreeElement())),
@@ -291,12 +292,15 @@ internal class JsonTreeParserTest {
}
@Test
- fun `nested json - first item expanded - shows first item expanded`() = runTest {
+ public fun `nested json - first item expanded - shows first item expanded`(): TestResult = runTest {
val underTest = underTest(nestedJson, TreeState.FIRST_ITEM_EXPANDED)
assertEquals(
Ready(
list = listOf(
- jsonTreeElement(state = TreeState.FIRST_ITEM_EXPANDED, childrenState = TreeState.COLLAPSED),
+ jsonTreeElement(
+ state = TreeState.FIRST_ITEM_EXPANDED,
+ childrenState = TreeState.COLLAPSED
+ ),
array(),
JsonTreeElement.EndBracket(
id = "8-b",
@@ -311,7 +315,7 @@ internal class JsonTreeParserTest {
}
@Test
- fun `nested json - expanded - shows expanded json`() = runTest {
+ public fun `nested json - expanded - shows expanded json`(): TestResult = runTest {
val underTest = underTest(nestedJson, TreeState.EXPANDED)
assertEquals(
Ready(
@@ -361,130 +365,146 @@ internal class JsonTreeParserTest {
}
@Test
- fun `nested json - expandSingleChildren is false - expands and collapses correctly`() = runTest {
- val underTest = underTest(nestedJson, TreeState.COLLAPSED)
- assertEquals(
- Ready(list = listOf(jsonTreeElement())),
- underTest.state.value
- )
-
- // expand root
- underTest.expandOrCollapseItem(jsonTreeElement(), expandSingleChildren = false)
-
- assertEquals(
- Ready(
- list = listOf(
- jsonTreeElement(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED),
- array(),
- JsonTreeElement.EndBracket(
- id = "8-b",
- level = 0,
- isLastItem = true,
- type = JsonTreeElement.EndBracket.Type.OBJECT
- ),
- )
- ),
- underTest.state.value
- )
-
- // expand array
- underTest.expandOrCollapseItem(item = array(), expandSingleChildren = false)
-
- assertEquals(
- Ready(
- list = listOf(
- jsonTreeElement(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED),
- array(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED),
- nestedArray1(),
- nestedArray2(),
- JsonTreeElement.EndBracket(
- id = "7-b",
- level = 1,
- isLastItem = true,
- type = JsonTreeElement.EndBracket.Type.ARRAY
- ),
- JsonTreeElement.EndBracket(
- id = "8-b",
- level = 0,
- isLastItem = true,
- type = JsonTreeElement.EndBracket.Type.OBJECT
- ),
- )
- ),
- underTest.state.value
- )
-
- // expand nestedArray2
- underTest.expandOrCollapseItem(item = nestedArray2(), expandSingleChildren = false)
-
- assertEquals(
- Ready(
- list = listOf(
- jsonTreeElement(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED),
- array(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED),
- nestedArray1(),
- nestedArray2(state = TreeState.EXPANDED),
- numberPrimitive1,
- numberPrimitive2,
- JsonTreeElement.EndBracket(
- id = "6-b",
- level = 2,
- isLastItem = true,
- type = JsonTreeElement.EndBracket.Type.ARRAY
- ),
- JsonTreeElement.EndBracket(
- id = "7-b",
- level = 1,
- isLastItem = true,
- type = JsonTreeElement.EndBracket.Type.ARRAY
- ),
- JsonTreeElement.EndBracket(
- id = "8-b",
- level = 0,
- isLastItem = true,
- type = JsonTreeElement.EndBracket.Type.OBJECT
- ),
- )
- ),
- underTest.state.value
- )
-
- // collapse array
- underTest.expandOrCollapseItem(
- item = array(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED),
- expandSingleChildren = false
- )
-
- assertEquals(
- Ready(
- list = listOf(
- jsonTreeElement(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED),
- array(),
- JsonTreeElement.EndBracket(
- id = "8-b",
- level = 0,
- isLastItem = true,
- type = JsonTreeElement.EndBracket.Type.OBJECT
- ),
- )
- ),
- underTest.state.value
- )
-
- // collapse root
- underTest.expandOrCollapseItem(
- item = jsonTreeElement(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED),
- expandSingleChildren = false
- )
-
- assertEquals(
- Ready(list = listOf(jsonTreeElement())),
- underTest.state.value
- )
- }
+ public fun `nested json - expandSingleChildren is false - expands and collapses correctly`(): TestResult =
+ runTest {
+ val underTest = underTest(nestedJson, TreeState.COLLAPSED)
+ assertEquals(
+ Ready(list = listOf(jsonTreeElement())),
+ underTest.state.value
+ )
+
+ // expand root
+ underTest.expandOrCollapseItem(jsonTreeElement(), expandSingleChildren = false)
+
+ assertEquals(
+ Ready(
+ list = listOf(
+ jsonTreeElement(
+ state = TreeState.EXPANDED,
+ childrenState = TreeState.COLLAPSED
+ ),
+ array(),
+ JsonTreeElement.EndBracket(
+ id = "8-b",
+ level = 0,
+ isLastItem = true,
+ type = JsonTreeElement.EndBracket.Type.OBJECT
+ ),
+ )
+ ),
+ underTest.state.value
+ )
+
+ // expand array
+ underTest.expandOrCollapseItem(item = array(), expandSingleChildren = false)
+
+ assertEquals(
+ Ready(
+ list = listOf(
+ jsonTreeElement(
+ state = TreeState.EXPANDED,
+ childrenState = TreeState.COLLAPSED
+ ),
+ array(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED),
+ nestedArray1(),
+ nestedArray2(),
+ JsonTreeElement.EndBracket(
+ id = "7-b",
+ level = 1,
+ isLastItem = true,
+ type = JsonTreeElement.EndBracket.Type.ARRAY
+ ),
+ JsonTreeElement.EndBracket(
+ id = "8-b",
+ level = 0,
+ isLastItem = true,
+ type = JsonTreeElement.EndBracket.Type.OBJECT
+ ),
+ )
+ ),
+ underTest.state.value
+ )
+
+ // expand nestedArray2
+ underTest.expandOrCollapseItem(item = nestedArray2(), expandSingleChildren = false)
+
+ assertEquals(
+ Ready(
+ list = listOf(
+ jsonTreeElement(
+ state = TreeState.EXPANDED,
+ childrenState = TreeState.COLLAPSED
+ ),
+ array(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED),
+ nestedArray1(),
+ nestedArray2(state = TreeState.EXPANDED),
+ numberPrimitive1,
+ numberPrimitive2,
+ JsonTreeElement.EndBracket(
+ id = "6-b",
+ level = 2,
+ isLastItem = true,
+ type = JsonTreeElement.EndBracket.Type.ARRAY
+ ),
+ JsonTreeElement.EndBracket(
+ id = "7-b",
+ level = 1,
+ isLastItem = true,
+ type = JsonTreeElement.EndBracket.Type.ARRAY
+ ),
+ JsonTreeElement.EndBracket(
+ id = "8-b",
+ level = 0,
+ isLastItem = true,
+ type = JsonTreeElement.EndBracket.Type.OBJECT
+ ),
+ )
+ ),
+ underTest.state.value
+ )
+
+ // collapse array
+ underTest.expandOrCollapseItem(
+ item = array(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED),
+ expandSingleChildren = false
+ )
+
+ assertEquals(
+ Ready(
+ list = listOf(
+ jsonTreeElement(
+ state = TreeState.EXPANDED,
+ childrenState = TreeState.COLLAPSED
+ ),
+ array(),
+ JsonTreeElement.EndBracket(
+ id = "8-b",
+ level = 0,
+ isLastItem = true,
+ type = JsonTreeElement.EndBracket.Type.OBJECT
+ ),
+ )
+ ),
+ underTest.state.value
+ )
+
+ // collapse root
+ underTest.expandOrCollapseItem(
+ item = jsonTreeElement(
+ state = TreeState.EXPANDED,
+ childrenState = TreeState.COLLAPSED
+ ),
+ expandSingleChildren = false
+ )
+
+ assertEquals(
+ Ready(list = listOf(jsonTreeElement())),
+ underTest.state.value
+ )
+ }
@Test
- fun `nested json - expandSingleChildren is true - expands and collapses correctly`() = runTest {
+ public fun `nested json - expandSingleChildren is true - expands and collapses correctly`(): TestResult = runTest {
val underTest = underTest(nestedJson, TreeState.COLLAPSED)
assertEquals(
Ready(list = listOf(jsonTreeElement())),
diff --git a/jsontree/src/androidTest/java/com/sebastianneubauer/jsontree/TestData.kt b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/TestData.kt
similarity index 81%
rename from jsontree/src/androidTest/java/com/sebastianneubauer/jsontree/TestData.kt
rename to jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/TestData.kt
index 46c34f4..dc37ae7 100644
--- a/jsontree/src/androidTest/java/com/sebastianneubauer/jsontree/TestData.kt
+++ b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/TestData.kt
@@ -1,6 +1,22 @@
package com.sebastianneubauer.jsontree
internal val nestedJson = """
+ {
+ "array": [
+ [
+ {
+ "string": "aString"
+ }
+ ],
+ [
+ 42,
+ 52
+ ]
+ ]
+ }
+""".trimIndent()
+
+internal val nestedJson2 = """
{
"topLevelObject": {
"string": "stringValue",
diff --git a/jsontree/src/androidTest/java/com/sebastianneubauer/jsontree/Assertions.kt b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/ui/Assertions.kt
similarity index 52%
rename from jsontree/src/androidTest/java/com/sebastianneubauer/jsontree/Assertions.kt
rename to jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/ui/Assertions.kt
index 9b09c27..3403f94 100644
--- a/jsontree/src/androidTest/java/com/sebastianneubauer/jsontree/Assertions.kt
+++ b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/ui/Assertions.kt
@@ -1,20 +1,23 @@
-package com.sebastianneubauer.jsontree
+@file:OptIn(ExperimentalTestApi::class)
+package com.sebastianneubauer.jsontree.ui
+
+import androidx.compose.ui.test.ComposeUiTest
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onNodeWithText
-internal fun ComposeTestRule.assertRootArrayIsDisplayed() {
+internal fun ComposeUiTest.assertRootArrayIsDisplayed() {
onNodeWithText("[").assertIsDisplayed()
onNodeWithText("\"stringValue\"").assertIsDisplayed()
onNodeWithText("]").assertIsDisplayed()
}
-internal fun ComposeTestRule.assertRootStringIsDisplayed() {
+internal fun ComposeUiTest.assertRootStringIsDisplayed() {
onNodeWithText("\"stringValue\"").assertIsDisplayed()
}
-internal fun ComposeTestRule.assertEmptyObjectIsDisplayed() {
+internal fun ComposeUiTest.assertEmptyObjectIsDisplayed() {
onNodeWithText("{").assertIsDisplayed()
onNodeWithText("}").assertIsDisplayed()
}
diff --git a/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/ui/JsonTreeTest.kt b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/ui/JsonTreeTest.kt
new file mode 100644
index 0000000..624960c
--- /dev/null
+++ b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/ui/JsonTreeTest.kt
@@ -0,0 +1,470 @@
+@file:OptIn(ExperimentalTestApi::class)
+
+package com.sebastianneubauer.jsontree.ui
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ComposeUiTest
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assertCountEquals
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.onAllNodesWithText
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.runComposeUiTest
+import androidx.compose.ui.test.waitUntilExactlyOneExists
+import com.sebastianneubauer.jsontree.EMPTY_OBJECT_JSON
+import com.sebastianneubauer.jsontree.INVALID_JSON
+import com.sebastianneubauer.jsontree.JsonTree
+import com.sebastianneubauer.jsontree.TreeState
+import com.sebastianneubauer.jsontree.arrayOfArraysJson
+import com.sebastianneubauer.jsontree.nestedJson2
+import com.sebastianneubauer.jsontree.rootArrayJson
+import com.sebastianneubauer.jsontree.rootStringJson
+import kotlin.test.Test
+
+public class JsonTreeTest {
+
+ private fun ComposeUiTest.setJson(
+ json: String,
+ initialState: TreeState = TreeState.FIRST_ITEM_EXPANDED,
+ ) {
+ setContent {
+ JsonTree(
+ json = json,
+ initialState = initialState,
+ onLoading = {},
+ )
+ }
+ }
+
+ @Test
+ public fun initial_state_is_first_item_expanded(): Unit = runComposeUiTest {
+ setJson(nestedJson2)
+
+ waitForIdle()
+ onNodeWithText("\"topLevelObject\": { 2 items },").assertIsDisplayed()
+ onNodeWithText("\"topLevelArray\": [ 2 items ],").assertIsDisplayed()
+ onNodeWithText("\"emptyObject\": { 0 items }").assertIsDisplayed()
+
+ onNodeWithText("\"string\": \"stringValue\",").assertDoesNotExist()
+ onNodeWithText("\"hello\",").assertDoesNotExist()
+ }
+
+ @Test
+ public fun initial_state_is_collapsed(): Unit = runComposeUiTest {
+ setJson(nestedJson2, initialState = TreeState.COLLAPSED)
+
+ waitUntilExactlyOneExists(hasText("{ 3 items }"))
+
+ onNodeWithText("{ 3 items }").assertIsDisplayed()
+ onNodeWithText("\"topLevelObject\"", substring = true).assertDoesNotExist()
+ }
+
+ @Test
+ public fun initial_state_is_expanded(): Unit = runComposeUiTest {
+ setJson(nestedJson2, initialState = TreeState.EXPANDED)
+
+ // fix flakiness by waiting until first item appears
+ waitUntilExactlyOneExists(hasText("\"topLevelObject\": {"))
+
+ // every collapsable is expanded
+ onNodeWithText("\"topLevelObject\": {").assertIsDisplayed()
+ onNodeWithText("\"string\": \"stringValue\",").assertIsDisplayed()
+
+ onNodeWithText("\"nestedObject\": {").assertIsDisplayed()
+ onNodeWithText("\"int\": 42,").assertIsDisplayed()
+
+ onNodeWithText("\"nestedArray\": [").assertIsDisplayed()
+ onNodeWithText("\"nestedArrayValue\",").assertIsDisplayed()
+ onNodeWithText("\"nestedArrayValue\"").assertIsDisplayed()
+
+ onNodeWithText("\"arrayOfObjects\": [").assertIsDisplayed()
+ onNodeWithText("\"anotherString\": \"anotherStringValue\"").assertIsDisplayed()
+ onNodeWithText("\"anotherInt\": 52").assertIsDisplayed()
+
+ onNodeWithText("\"topLevelArray\": [").assertIsDisplayed()
+ onNodeWithText("\"hello\",").assertIsDisplayed()
+ onNodeWithText("\"emptyObject\": {").assertIsDisplayed()
+ }
+
+ @Test
+ public fun click_on_collapsed_object_or_array_expands_it(): Unit = runComposeUiTest {
+ setJson(nestedJson2)
+ // fix flakiness by waiting until first item appears
+ waitUntilExactlyOneExists(hasText("\"topLevelObject\": { 2 items },"))
+
+ onNodeWithText("\"topLevelObject\": { 2 items },").performClick()
+ onNodeWithText("\"topLevelObject\": {").assertIsDisplayed()
+ onNodeWithText("\"string\": \"stringValue\",").assertIsDisplayed()
+ onNodeWithText("\"nestedObject\": { 3 items }").assertIsDisplayed()
+
+ onNodeWithText("\"topLevelArray\": [ 2 items ],").performClick()
+ onNodeWithText("\"topLevelArray\": [").assertIsDisplayed()
+ onNodeWithText("\"hello\",").assertIsDisplayed()
+ onNodeWithText("\"world\"").assertIsDisplayed()
+ }
+
+ @Test
+ public fun click_on_expanded_object_or_array_collapses_it(): Unit = runComposeUiTest {
+ setJson(nestedJson2, initialState = TreeState.EXPANDED)
+
+ waitForIdle()
+
+ onNodeWithText("\"arrayOfObjects\": [").performClick()
+ waitForIdle()
+ onNodeWithText("\"arrayOfObjects\": [ 2 items ]").assertIsDisplayed()
+
+ onNodeWithText("\"nestedObject\": {").performClick()
+ waitForIdle()
+ onNodeWithText("\"nestedObject\": { 3 items }").assertIsDisplayed()
+
+ onNodeWithText("\"topLevelObject\": {").performClick()
+ waitForIdle()
+ onNodeWithText("\"topLevelObject\": { 2 items },").assertIsDisplayed()
+ }
+
+ @Test
+ public fun array_of_arrays_is_rendered_correctly(): Unit = runComposeUiTest {
+ setJson(arrayOfArraysJson)
+ // fix flakiness by waiting until first item appears
+ waitUntilExactlyOneExists(hasText("\"array\": [ 2 items ]"))
+
+ onNodeWithText("\"array\": [ 2 items ]").assertIsDisplayed()
+ onNodeWithText("\"array\": [ 2 items ]").performClick()
+
+ waitUntilExactlyOneExists(hasText("\"array\": ["))
+ onNodeWithText("\"array\": [").assertIsDisplayed()
+ onNodeWithText("[ 1 item ],").assertIsDisplayed()
+ onNodeWithText("[ 2 items ]").assertIsDisplayed()
+
+ onNodeWithText("[ 1 item ],").performClick()
+ onNodeWithText("[ 2 items ]").performClick()
+
+ onNodeWithText("\"stringValue\"").assertIsDisplayed()
+ onNodeWithText("],").assertIsDisplayed()
+ onNodeWithText("42,").assertIsDisplayed()
+ onNodeWithText("52").assertIsDisplayed()
+ }
+
+ @Test
+ public fun root_array_is_rendered_correctly(): Unit = runComposeUiTest {
+ setJson(rootArrayJson)
+
+ assertRootArrayIsDisplayed()
+ }
+
+ @Test
+ public fun root_string_is_rendered_correctly(): Unit = runComposeUiTest {
+ setJson(rootStringJson)
+
+ assertRootStringIsDisplayed()
+ }
+
+ @Test
+ public fun empty_object_is_rendered_correctly(): Unit = runComposeUiTest {
+ setJson(EMPTY_OBJECT_JSON)
+ waitForIdle()
+ assertEmptyObjectIsDisplayed()
+
+ onNodeWithText("{").performClick()
+ onNodeWithText("{ 0 items }").assertIsDisplayed()
+ }
+
+ @Test
+ public fun invalid_json_shows_an_error(): Unit = runComposeUiTest {
+ setContent {
+ Box {
+ var errorMessage: String? by remember { mutableStateOf(null) }
+
+ JsonTree(
+ modifier = Modifier.testTag("jsonTree"),
+ json = INVALID_JSON,
+ onError = { throwable -> errorMessage = throwable.message },
+ onLoading = {},
+ )
+
+ errorMessage?.let {
+ Text(
+ modifier = Modifier.testTag("errorText"),
+ text = it
+ )
+ }
+ }
+ }
+
+ onNodeWithTag("errorText").assertIsDisplayed()
+ }
+
+ @Test
+ public fun changing_json_is_handled_correctly(): Unit = runComposeUiTest {
+ setContent {
+ var jsonString: String by remember { mutableStateOf(rootStringJson) }
+
+ Column {
+ JsonTree(json = jsonString, onLoading = {})
+
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = { jsonString = rootArrayJson }
+ ) {}
+ }
+ }
+
+ assertRootStringIsDisplayed()
+
+ onNodeWithTag("button").performClick()
+
+ assertRootArrayIsDisplayed()
+ }
+
+ @Test
+ public fun changing_json_while_collapsed_is_handled_correctly(): Unit = runComposeUiTest {
+ setContent {
+ var jsonString: String by remember { mutableStateOf(nestedJson2) }
+
+ Column {
+ JsonTree(
+ json = jsonString,
+ initialState = TreeState.COLLAPSED,
+ onLoading = {},
+ )
+
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = { jsonString = rootArrayJson }
+ ) {}
+ }
+ }
+
+ onNodeWithText("{ 3 items }").assertIsDisplayed()
+
+ onNodeWithTag("button").performClick()
+
+ onNodeWithText("[ 1 item ]").assertIsDisplayed()
+ }
+
+ @Test
+ public fun changing_json_from_invalid_to_valid_is_handled_correctly(): Unit = runComposeUiTest {
+ setContent {
+ var jsonString: String by remember { mutableStateOf(INVALID_JSON) }
+
+ Column {
+ JsonTree(
+ modifier = Modifier.testTag("jsonTree"),
+ json = jsonString,
+ onLoading = {},
+ )
+
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = { jsonString = rootStringJson }
+ ) {}
+ }
+ }
+
+ onNodeWithTag("button").performClick()
+ assertRootStringIsDisplayed()
+ }
+
+ @Test
+ public fun changing_json_from_valid_to_invalid_is_handled_correctly(): Unit = runComposeUiTest {
+ setContent {
+ var jsonString: String by remember { mutableStateOf(rootStringJson) }
+ var errorMessage: String? by remember { mutableStateOf(null) }
+
+ Column {
+ JsonTree(
+ modifier = Modifier.testTag("jsonTree"),
+ json = jsonString,
+ onError = { throwable -> errorMessage = throwable.message },
+ onLoading = {}
+ )
+
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = { jsonString = INVALID_JSON }
+ ) {}
+
+ errorMessage?.let {
+ Text(
+ modifier = Modifier.testTag("errorText"),
+ text = it
+ )
+ }
+ }
+ }
+
+ // fix flakiness by waiting until first item appears
+ waitUntilExactlyOneExists(hasTestTag("jsonTree"))
+
+ onNodeWithTag("jsonTree").assertIsDisplayed()
+ onNodeWithTag("errorText").assertDoesNotExist()
+
+ onNodeWithTag("button").performClick()
+
+ onNodeWithTag("errorText").assertIsDisplayed()
+ }
+
+ @Test
+ public fun changing_initial_state_is_handled_correctly(): Unit = runComposeUiTest {
+ setContent {
+ var initalState: TreeState by remember { mutableStateOf(TreeState.EXPANDED) }
+
+ Column {
+ JsonTree(json = arrayOfArraysJson, initialState = initalState, onLoading = {})
+
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = {
+ initalState = when (initalState) {
+ TreeState.EXPANDED -> TreeState.COLLAPSED
+ TreeState.COLLAPSED -> TreeState.FIRST_ITEM_EXPANDED
+ TreeState.FIRST_ITEM_EXPANDED -> TreeState.EXPANDED
+ }
+ }
+ ) {}
+ }
+ }
+
+ onNodeWithText("\"array\": [").assertIsDisplayed()
+ onNodeWithText("\"stringValue\"").assertIsDisplayed()
+ onNodeWithText("42,").assertIsDisplayed()
+ onNodeWithText("52").assertIsDisplayed()
+
+ onNodeWithTag("button").performClick()
+
+ onNodeWithText("{ 1 item }").assertIsDisplayed()
+
+ onNodeWithTag("button").performClick()
+
+ onNodeWithText("\"array\": [ 2 items ]").assertIsDisplayed()
+ }
+
+ @Test
+ public fun showing_and_hiding_indices_is_handled_correctly(): Unit = runComposeUiTest {
+ setContent {
+ var showIndices: Boolean by remember { mutableStateOf(true) }
+
+ Column {
+ JsonTree(
+ json = arrayOfArraysJson,
+ onLoading = {},
+ showIndices = showIndices,
+ initialState = TreeState.EXPANDED
+ )
+
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = { showIndices = !showIndices }
+ ) {}
+ }
+ }
+
+ onNodeWithText("0: [").assertIsDisplayed()
+ onNodeWithText("0: \"stringValue\"").assertIsDisplayed()
+ onNodeWithText("1: [").assertIsDisplayed()
+ onNodeWithText("0: 42,").assertIsDisplayed()
+ onNodeWithText("1: 52").assertIsDisplayed()
+
+ onNodeWithTag("button").performClick()
+
+ onAllNodesWithText("[").assertCountEquals(2)
+ onNodeWithText("\"stringValue\"").assertIsDisplayed()
+ onNodeWithText("42,").assertIsDisplayed()
+ onNodeWithText("52").assertIsDisplayed()
+ }
+
+ @Test
+ public fun showing_and_hiding_item_count_is_handled_correctly(): Unit = runComposeUiTest {
+ setContent {
+ var showItemCount: Boolean by remember { mutableStateOf(true) }
+
+ Column {
+ JsonTree(
+ json = rootArrayJson,
+ onLoading = {},
+ showItemCount = showItemCount,
+ initialState = TreeState.COLLAPSED
+ )
+
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = { showItemCount = !showItemCount }
+ ) {}
+ }
+ }
+
+ onNodeWithText("[ 1 item ]").assertIsDisplayed()
+
+ onNodeWithTag("button").performClick()
+
+ onNodeWithText("[ ... ]").assertIsDisplayed()
+
+ onNodeWithTag("button").performClick()
+
+ onNodeWithText("[ 1 item ]").assertIsDisplayed()
+ }
+
+ @Test
+ public fun expanding_single_children_is_handled_correctly(): Unit = runComposeUiTest {
+ setContent {
+ JsonTree(
+ json = arrayOfArraysJson,
+ initialState = TreeState.COLLAPSED,
+ onLoading = {},
+ expandSingleChildren = true
+ )
+ }
+ waitUntilExactlyOneExists(hasText("{ 1 item }"))
+ onNodeWithText("{ 1 item }").performClick()
+ waitUntilExactlyOneExists(hasText("\"array\": ["))
+ // shows nested array
+ onNodeWithText("\"array\": [").assertIsDisplayed()
+ // shows collapsed contents of nested array
+ onNodeWithText("[ 1 item ],").assertIsDisplayed()
+ onNodeWithText("[ 2 items ]").assertIsDisplayed()
+
+ // collapse root
+ onNodeWithText("{").performClick()
+ onNodeWithText("{ 1 item }").assertIsDisplayed()
+ }
+
+ @Test
+ public fun not_expanding_single_children_is_handled_correctly(): Unit = runComposeUiTest {
+ setContent {
+ JsonTree(
+ json = arrayOfArraysJson,
+ initialState = TreeState.COLLAPSED,
+ onLoading = {},
+ expandSingleChildren = false
+ )
+ }
+ waitUntilExactlyOneExists(hasText("{ 1 item }"))
+ onNodeWithText("{ 1 item }").performClick()
+
+ waitUntilExactlyOneExists(hasText("\"array\": [ 2 items ]"))
+ onNodeWithText("\"array\": [ 2 items ]").assertIsDisplayed()
+ onNodeWithText("\"array\": [ 2 items ]").performClick()
+
+ waitForIdle()
+ onNodeWithText("[ 1 item ],").assertIsDisplayed()
+ onNodeWithText("[ 2 items ]").assertIsDisplayed()
+
+ waitForIdle()
+ // collapse root
+ onNodeWithText("{").performClick()
+ onNodeWithText("{ 1 item }").assertIsDisplayed()
+ }
+}
diff --git a/jsontree/src/test/java/com/sebastianneubauer/jsontree/TestData.kt b/jsontree/src/test/java/com/sebastianneubauer/jsontree/TestData.kt
deleted file mode 100644
index 58df75a..0000000
--- a/jsontree/src/test/java/com/sebastianneubauer/jsontree/TestData.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.sebastianneubauer.jsontree
-
-internal val nestedJson = """
- {
- "array": [
- [
- {
- "string": "aString"
- }
- ],
- [
- 42,
- 52
- ]
- ]
- }
-""".trimIndent()
-
-internal val rootArrayJson = """
- [
- "stringValue"
- ]
-""".trimIndent()
-
-internal val rootStringJson = """
- "stringValue"
-""".trimIndent()
-
-internal const val EMPTY_OBJECT_JSON = "{}"
-
-internal const val INVALID_JSON = ""
diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts
new file mode 100644
index 0000000..a9b1c7d
--- /dev/null
+++ b/sample/build.gradle.kts
@@ -0,0 +1,107 @@
+import org.jetbrains.compose.desktop.application.dsl.TargetFormat
+
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlinMultiplatform)
+ alias(libs.plugins.compose)
+ alias(libs.plugins.compose.compiler)
+}
+
+group = "com.sebastianneubauer.jsontreesample"
+version = "1.0"
+
+kotlin {
+ androidTarget()
+
+ jvm()
+
+ listOf(
+ iosX64(),
+ iosArm64(),
+ iosSimulatorArm64()
+ ).forEach {
+ it.binaries.framework {
+ baseName = "Sample"
+ isStatic = true
+ }
+ }
+
+ sourceSets {
+ all {
+ languageSettings {
+ optIn("org.jetbrains.compose.resources.ExperimentalResourceApi")
+ }
+ }
+ commonMain.dependencies {
+ implementation(compose.runtime)
+ implementation(compose.foundation)
+ implementation(compose.material3)
+ implementation(compose.components.resources)
+ implementation(compose.components.uiToolingPreview)
+ implementation(libs.kotlinx.serialization.json)
+ implementation(project(":jsontree"))
+ }
+
+ commonTest.dependencies {
+ implementation(kotlin("test"))
+ }
+
+ androidMain.dependencies {
+ implementation(compose.uiTooling)
+ implementation(libs.androidx.activity.compose)
+ }
+
+ jvmMain.dependencies {
+ implementation(compose.desktop.currentOs)
+ }
+
+
+ }
+
+ //https://kotlinlang.org/docs/native-objc-interop.html#export-of-kdoc-comments-to-generated-objective-c-headers
+ targets.withType {
+ compilations["main"].compilerOptions.options.freeCompilerArgs.add("-Xexport-kdoc")
+ }
+
+}
+
+
+
+android {
+ namespace = "com.sebastianneubauer.jsontreesample"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 24
+ }
+ sourceSets["main"].apply {
+ manifest.srcFile("src/androidMain/AndroidManifest.xml")
+ res.srcDirs("src/androidMain/res")
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.11"
+ }
+}
+
+compose.desktop {
+ application {
+ mainClass = "MainKt"
+ buildTypes.release {
+ proguard {
+ configurationFiles.from("compose-desktop.pro")
+ }
+ }
+ nativeDistributions {
+ targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
+ packageName = "com.sebastianneubauer.jsontreesample"
+ packageVersion = "1.0.0"
+ }
+ }
+}
diff --git a/sample/compose-desktop.pro b/sample/compose-desktop.pro
new file mode 100644
index 0000000..c00d74f
--- /dev/null
+++ b/sample/compose-desktop.pro
@@ -0,0 +1 @@
+-keep class org.sqlite.** { *; }
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/sample/src/androidMain/AndroidManifest.xml
similarity index 62%
rename from app/src/main/AndroidManifest.xml
rename to sample/src/androidMain/AndroidManifest.xml
index 573c488..756d3b5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/sample/src/androidMain/AndroidManifest.xml
@@ -1,7 +1,7 @@
-
+
+ android:theme="@android:style/Theme.Material.Light.NoActionBar">
+ android:launchMode="singleInstance"
+ android:windowSoftInputMode="adjustPan">
-
diff --git a/sample/src/androidMain/kotlin/com/sebastianneubauer/jsontreesample/MainActivity.kt b/sample/src/androidMain/kotlin/com/sebastianneubauer/jsontreesample/MainActivity.kt
new file mode 100644
index 0000000..f85c22a
--- /dev/null
+++ b/sample/src/androidMain/kotlin/com/sebastianneubauer/jsontreesample/MainActivity.kt
@@ -0,0 +1,14 @@
+package com.sebastianneubauer.jsontreesample
+
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+
+class MainActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: android.os.Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent { App() }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/sample/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml
similarity index 100%
rename from app/src/main/res/drawable-v24/ic_launcher_foreground.xml
rename to sample/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/sample/src/androidMain/res/drawable/ic_launcher_background.xml
similarity index 99%
rename from app/src/main/res/drawable/ic_launcher_background.xml
rename to sample/src/androidMain/res/drawable/ic_launcher_background.xml
index 07d5da9..e93e11a 100644
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ b/sample/src/androidMain/res/drawable/ic_launcher_background.xml
@@ -167,4 +167,4 @@
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
-
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/sample/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml
similarity index 100%
rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
rename to sample/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/sample/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml
similarity index 100%
rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
rename to sample/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml
diff --git a/sample/src/androidMain/res/mipmap-hdpi/ic_launcher.png b/sample/src/androidMain/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a571e60
Binary files /dev/null and b/sample/src/androidMain/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/sample/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png b/sample/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..61da551
Binary files /dev/null and b/sample/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/sample/src/androidMain/res/mipmap-mdpi/ic_launcher.png b/sample/src/androidMain/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c41dd28
Binary files /dev/null and b/sample/src/androidMain/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/sample/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png b/sample/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..db5080a
Binary files /dev/null and b/sample/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/sample/src/androidMain/res/mipmap-xhdpi/ic_launcher.png b/sample/src/androidMain/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..6dba46d
Binary files /dev/null and b/sample/src/androidMain/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/sample/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png b/sample/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..da31a87
Binary files /dev/null and b/sample/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png b/sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..15ac681
Binary files /dev/null and b/sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png b/sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..b216f2d
Binary files /dev/null and b/sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png b/sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..f25a419
Binary files /dev/null and b/sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png b/sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..e96783c
Binary files /dev/null and b/sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/sample/src/androidMain/res/values/strings.xml b/sample/src/androidMain/res/values/strings.xml
new file mode 100644
index 0000000..93ac338
--- /dev/null
+++ b/sample/src/androidMain/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ JsonTree Sample
+
\ No newline at end of file
diff --git a/sample/src/commonMain/kotlin/App.kt b/sample/src/commonMain/kotlin/App.kt
new file mode 100644
index 0000000..2ef6c25
--- /dev/null
+++ b/sample/src/commonMain/kotlin/App.kt
@@ -0,0 +1,199 @@
+package com.sebastianneubauer.jsontreesample
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawingPadding
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.material3.Button
+import androidx.compose.material3.CenterAlignedTopAppBar
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import com.sebastianneubauer.jsontree.JsonTree
+import com.sebastianneubauer.jsontree.TreeColors
+import com.sebastianneubauer.jsontree.TreeState
+import com.sebastianneubauer.jsontree.defaultDarkColors
+import com.sebastianneubauer.jsontree.defaultLightColors
+import com.sebastianneubauer.jsontreesample.ui.theme.JsonTreeTheme
+import org.jetbrains.compose.ui.tooling.preview.Preview
+
+
+@Composable
+internal fun App() = JsonTreeTheme {
+ MainScreen()
+}
+
+@OptIn(
+ ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, ExperimentalLayoutApi::class
+)
+@Composable
+fun MainScreen() {
+
+ Scaffold(
+ modifier = Modifier.safeDrawingPadding(),
+ topBar = {
+ CenterAlignedTopAppBar(
+ title = {
+ Text(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ text = "🌳 JsonTree",
+ style = MaterialTheme.typography.headlineMedium,
+ color = Color.Black
+ )
+ },
+ )
+ },
+ contentWindowInsets = WindowInsets(top = 60.dp),
+ ) { padding ->
+ Column(
+ modifier = Modifier.fillMaxSize().padding(padding)
+ ) {
+ var errorMessage: String? by remember { mutableStateOf(null) }
+ var json: String by remember { mutableStateOf(simpleJson) }
+ var colors: TreeColors by remember { mutableStateOf(defaultLightColors) }
+ var initialState: TreeState by remember { mutableStateOf(TreeState.FIRST_ITEM_EXPANDED) }
+ var showIndices: Boolean by remember { mutableStateOf(true) }
+ var showItemCount: Boolean by remember { mutableStateOf(true) }
+ var expandSingleChildren: Boolean by remember { mutableStateOf(true) }
+
+ FlowRow {
+ Button(modifier = Modifier.padding(horizontal = 8.dp), onClick = {
+ errorMessage = null
+ json = when (json) {
+ emptyJson -> simpleJson
+ simpleJson -> complexJson
+ complexJson -> invalidJson
+ invalidJson -> emptyJson
+ else -> throw IllegalStateException("No JSON selected!")
+ }
+ }) {
+ Text(
+ text = when (json) {
+ simpleJson -> "Simple Json"
+ emptyJson -> "Empty Json"
+ complexJson -> "Complex Json"
+ invalidJson -> "Invalid Json"
+ else -> throw IllegalStateException("No JSON selected!")
+ }
+ )
+ }
+
+ Button(modifier = Modifier.padding(horizontal = 8.dp), onClick = {
+ val newState = when (initialState) {
+ TreeState.EXPANDED -> TreeState.COLLAPSED
+ TreeState.COLLAPSED -> TreeState.FIRST_ITEM_EXPANDED
+ TreeState.FIRST_ITEM_EXPANDED -> TreeState.EXPANDED
+ }
+ initialState = newState
+ }) {
+ Text(text = initialState.name)
+ }
+
+ Button(modifier = Modifier.padding(horizontal = 8.dp),
+ onClick = { showIndices = !showIndices }) {
+ Text(text = if (showIndices) "Hide indices" else "Show indices")
+ }
+
+ Button(modifier = Modifier.padding(horizontal = 8.dp),
+ onClick = { showItemCount = !showItemCount }) {
+ Text(text = if (showItemCount) "Hide item count" else "Show item count")
+ }
+
+ Button(modifier = Modifier.padding(horizontal = 8.dp), onClick = {
+ colors =
+ if (colors == defaultLightColors) defaultDarkColors else defaultLightColors
+ }) {
+ Text(text = if (colors == defaultLightColors) "Light" else "Dark")
+ }
+
+ Button(modifier = Modifier.padding(horizontal = 8.dp),
+ onClick = { expandSingleChildren = !expandSingleChildren }) {
+ Text(text = if (expandSingleChildren) "Expand children" else "Don't expand children")
+ }
+ }
+
+ val pagerState = rememberPagerState(initialPage = 0, pageCount = { 3 })
+
+ //Pager to test leaving composition
+ HorizontalPager(
+ modifier = Modifier.fillMaxWidth().weight(1F),
+ state = pagerState,
+ verticalAlignment = Alignment.Top
+ ) { pageIndex ->
+ when (pageIndex) {
+ 0 -> {
+ val error = errorMessage
+ if (error != null) {
+ Text(
+ text = error, color = Color.Black
+ )
+ } else {
+ JsonTree(
+ modifier = Modifier.fillMaxSize()
+ .horizontalScroll(rememberScrollState()).background(
+ if (colors == defaultLightColors) Color.White else Color.Black
+ ),
+ contentPadding = PaddingValues(16.dp),
+ json = json,
+ onLoading = {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = "Loading...",
+ color = if (colors == defaultLightColors) Color.Black else Color.White
+ )
+ }
+ },
+ initialState = initialState,
+ colors = colors,
+ showIndices = showIndices,
+ showItemCount = showItemCount,
+ expandSingleChildren = expandSingleChildren,
+ onError = { errorMessage = it.message },
+ )
+ }
+ }
+
+ 1 -> {
+ Text(text = "Page 1")
+ }
+
+ 2 -> {
+ Text(text = "Page 2")
+ }
+ }
+ }
+ }
+ }
+}
+
+
+@Preview
+@Composable
+fun PreviewMainScreen() = JsonTreeTheme {
+ MainScreen()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sebastianneubauer/jsontreedemo/JsonStrings.kt b/sample/src/commonMain/kotlin/JsonStrings.kt
similarity index 99%
rename from app/src/main/java/com/sebastianneubauer/jsontreedemo/JsonStrings.kt
rename to sample/src/commonMain/kotlin/JsonStrings.kt
index d235a9a..5b74cdd 100644
--- a/app/src/main/java/com/sebastianneubauer/jsontreedemo/JsonStrings.kt
+++ b/sample/src/commonMain/kotlin/JsonStrings.kt
@@ -1,4 +1,4 @@
-package com.sebastianneubauer.jsontreedemo
+package com.sebastianneubauer.jsontreesample
internal val complexJson = """
{
diff --git a/app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Color.kt b/sample/src/commonMain/kotlin/ui/theme/Color.kt
similarity index 79%
rename from app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Color.kt
rename to sample/src/commonMain/kotlin/ui/theme/Color.kt
index 672857d..4d63bba 100644
--- a/app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Color.kt
+++ b/sample/src/commonMain/kotlin/ui/theme/Color.kt
@@ -1,4 +1,4 @@
-package com.sebastianneubauer.jsontreedemo.ui.theme
+package com.sebastianneubauer.jsontreesample.ui.theme
import androidx.compose.ui.graphics.Color
diff --git a/app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Shape.kt b/sample/src/commonMain/kotlin/ui/theme/Shape.kt
similarity index 83%
rename from app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Shape.kt
rename to sample/src/commonMain/kotlin/ui/theme/Shape.kt
index 9bed189..12a9092 100644
--- a/app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Shape.kt
+++ b/sample/src/commonMain/kotlin/ui/theme/Shape.kt
@@ -1,4 +1,4 @@
-package com.sebastianneubauer.jsontreedemo.ui.theme
+package com.sebastianneubauer.jsontreesample.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes
diff --git a/app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Theme.kt b/sample/src/commonMain/kotlin/ui/theme/Theme.kt
similarity index 95%
rename from app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Theme.kt
rename to sample/src/commonMain/kotlin/ui/theme/Theme.kt
index 5f399c0..3c35e58 100644
--- a/app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Theme.kt
+++ b/sample/src/commonMain/kotlin/ui/theme/Theme.kt
@@ -1,4 +1,4 @@
-package com.sebastianneubauer.jsontreedemo.ui.theme
+package com.sebastianneubauer.jsontreesample.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
diff --git a/app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Type.kt b/sample/src/commonMain/kotlin/ui/theme/Type.kt
similarity index 93%
rename from app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Type.kt
rename to sample/src/commonMain/kotlin/ui/theme/Type.kt
index 5bf373a..032cf12 100644
--- a/app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Type.kt
+++ b/sample/src/commonMain/kotlin/ui/theme/Type.kt
@@ -1,4 +1,4 @@
-package com.sebastianneubauer.jsontreedemo.ui.theme
+package com.sebastianneubauer.jsontreesample.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
diff --git a/sample/src/iosMain/kotlin/main.kt b/sample/src/iosMain/kotlin/main.kt
new file mode 100644
index 0000000..e4d28f7
--- /dev/null
+++ b/sample/src/iosMain/kotlin/main.kt
@@ -0,0 +1,5 @@
+import androidx.compose.ui.window.ComposeUIViewController
+import com.sebastianneubauer.jsontreesample.App
+import platform.UIKit.UIViewController
+
+fun MainViewController(): UIViewController = ComposeUIViewController { App() }
diff --git a/sample/src/jvmMain/kotlin/main.kt b/sample/src/jvmMain/kotlin/main.kt
new file mode 100644
index 0000000..8ebcf1a
--- /dev/null
+++ b/sample/src/jvmMain/kotlin/main.kt
@@ -0,0 +1,17 @@
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Window
+import androidx.compose.ui.window.application
+import androidx.compose.ui.window.rememberWindowState
+import com.sebastianneubauer.jsontreesample.App
+import java.awt.Dimension
+
+fun main() = application {
+ Window(
+ title = "JsonTree Sample",
+ state = rememberWindowState(width = 800.dp, height = 600.dp),
+ onCloseRequest = ::exitApplication,
+ ) {
+ window.minimumSize = Dimension(350, 600)
+ App()
+ }
+}
\ No newline at end of file
diff --git a/screenshots/jsonTree.png b/screenshots/jsonTree-android-dark.png
similarity index 100%
rename from screenshots/jsonTree.png
rename to screenshots/jsonTree-android-dark.png
diff --git a/screenshots/jsonTree-macos.png b/screenshots/jsonTree-macos.png
new file mode 100644
index 0000000..adfa4b5
Binary files /dev/null and b/screenshots/jsonTree-macos.png differ
diff --git a/settings.gradle b/settings.gradle.kts
similarity index 88%
rename from settings.gradle
rename to settings.gradle.kts
index 3818b8c..1e7ddb7 100644
--- a/settings.gradle
+++ b/settings.gradle.kts
@@ -13,5 +13,5 @@ dependencyResolutionManagement {
}
}
rootProject.name = "JsonTree"
-include ':app'
-include ':jsontree'
+include(":jsontree")
+include(":sample")