diff --git a/artifacts.json b/artifacts.json index 3890a37a8..0128a04ea 100644 --- a/artifacts.json +++ b/artifacts.json @@ -171,22 +171,67 @@ "publicationName": "maven" }, { - "gradlePath": ":workflow-ui:compose", + "gradlePath": ":workflow-ui:compose-tooling", "group": "com.squareup.workflow1", - "artifactId": "workflow-ui-compose", - "description": "Workflow UI Compose", + "artifactId": "workflow-ui-compose-tooling", + "description": "Workflow UI Compose Tooling", "packaging": "aar", "javaVersion": 8, "publicationName": "maven" }, { - "gradlePath": ":workflow-ui:compose-tooling", + "gradlePath": ":workflow-ui:core", "group": "com.squareup.workflow1", - "artifactId": "workflow-ui-compose-tooling", - "description": "Workflow UI Compose Tooling", - "packaging": "aar", + "artifactId": "workflow-ui-core-iosarm64", + "description": "Workflow UI Core", + "packaging": "klib", "javaVersion": 8, - "publicationName": "maven" + "publicationName": "iosArm64" + }, + { + "gradlePath": ":workflow-ui:core", + "group": "com.squareup.workflow1", + "artifactId": "workflow-ui-core-iossimulatorarm64", + "description": "Workflow UI Core", + "packaging": "klib", + "javaVersion": 8, + "publicationName": "iosSimulatorArm64" + }, + { + "gradlePath": ":workflow-ui:core", + "group": "com.squareup.workflow1", + "artifactId": "workflow-ui-core-iosx64", + "description": "Workflow UI Core", + "packaging": "klib", + "javaVersion": 8, + "publicationName": "iosX64" + }, + { + "gradlePath": ":workflow-ui:core", + "group": "com.squareup.workflow1", + "artifactId": "workflow-ui-core-js", + "description": "Workflow UI Core", + "packaging": "klib", + "javaVersion": 8, + "publicationName": "js" + }, + { + "gradlePath": ":workflow-ui:core", + "group": "com.squareup.workflow1", + "artifactId": "workflow-ui-core-jvm", + "description": "Workflow UI Core", + "packaging": "jar", + "javaVersion": 8, + "publicationName": "jvm" + }, + { + "gradlePath": ":workflow-ui:core", + "group": "com.squareup.workflow1", + "artifactId": "workflow-ui-core", + "description": "Workflow UI Core", + "packaging": "jar", + "javaVersion": 8, + "publicationName": "kotlinMultiplatform" }, { "gradlePath": ":workflow-ui:core-android", diff --git a/benchmarks/performance-poetry/complex-poetry/build.gradle.kts b/benchmarks/performance-poetry/complex-poetry/build.gradle.kts index a228c7cfc..874960cd9 100644 --- a/benchmarks/performance-poetry/complex-poetry/build.gradle.kts +++ b/benchmarks/performance-poetry/complex-poetry/build.gradle.kts @@ -64,7 +64,7 @@ dependencies { api(project(":workflow-core")) api(project(":workflow-runtime")) api(project(":workflow-ui:core-android")) - api(project(":workflow-ui:core-common")) + api(project(":workflow-ui:core")) debugImplementation(libs.squareup.leakcanary.android) diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index 97f2d7155..11f864471 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -1,6 +1,6 @@ plugins { // Hardcoded as this is upstream of the version catalog. Keep this in sync with that. - kotlin("jvm") version "1.9.10" apply false + kotlin("jvm") version "1.9.24" apply false } dependencyResolutionManagement { diff --git a/build-logic/src/main/java/com/squareup/workflow1/buildsrc/AndroidSampleAppPlugin.kt b/build-logic/src/main/java/com/squareup/workflow1/buildsrc/AndroidSampleAppPlugin.kt index eef6fcb5f..90cc1f958 100644 --- a/build-logic/src/main/java/com/squareup/workflow1/buildsrc/AndroidSampleAppPlugin.kt +++ b/build-logic/src/main/java/com/squareup/workflow1/buildsrc/AndroidSampleAppPlugin.kt @@ -2,7 +2,7 @@ package com.squareup.workflow1.buildsrc import com.android.build.gradle.TestedExtension import com.android.build.gradle.internal.dsl.BaseAppModuleExtension -import com.rickbusarow.kgx.dependency +import com.rickbusarow.kgx.library import com.rickbusarow.kgx.libsCatalog import com.squareup.workflow1.buildsrc.internal.implementation import com.squareup.workflow1.buildsrc.internal.invoke @@ -31,8 +31,8 @@ class AndroidSampleAppPlugin : Plugin { implementation(target.project(":workflow-runtime")) implementation(target.project(":workflow-config:config-android")) - implementation(target.libsCatalog.dependency("androidx-appcompat")) - implementation(target.libsCatalog.dependency("timber")) + implementation(target.libsCatalog.library("androidx-appcompat")) + implementation(target.libsCatalog.library("timber")) } } } diff --git a/build-logic/src/main/java/com/squareup/workflow1/buildsrc/AndroidUiTestsPlugin.kt b/build-logic/src/main/java/com/squareup/workflow1/buildsrc/AndroidUiTestsPlugin.kt index 9f7b996e1..4ce59d3e7 100644 --- a/build-logic/src/main/java/com/squareup/workflow1/buildsrc/AndroidUiTestsPlugin.kt +++ b/build-logic/src/main/java/com/squareup/workflow1/buildsrc/AndroidUiTestsPlugin.kt @@ -1,7 +1,7 @@ package com.squareup.workflow1.buildsrc import com.android.build.gradle.TestedExtension -import com.rickbusarow.kgx.dependency +import com.rickbusarow.kgx.library import com.rickbusarow.kgx.libsCatalog import com.squareup.workflow1.buildsrc.internal.androidTestImplementation import com.squareup.workflow1.buildsrc.internal.invoke @@ -28,9 +28,9 @@ class AndroidUiTestsPlugin : Plugin { target.dependencies { androidTestImplementation(target.project(":workflow-ui:internal-testing-android")) - androidTestImplementation(target.libsCatalog.dependency("androidx-test-espresso-core")) - androidTestImplementation(target.libsCatalog.dependency("androidx-test-junit")) - androidTestImplementation(target.libsCatalog.dependency("squareup-leakcanary-instrumentation")) + androidTestImplementation(target.libsCatalog.library("androidx-test-espresso-core")) + androidTestImplementation(target.libsCatalog.library("androidx-test-junit")) + androidTestImplementation(target.libsCatalog.library("squareup-leakcanary-instrumentation")) } } } diff --git a/build-logic/src/main/java/com/squareup/workflow1/buildsrc/ComposeUiTestsPlugin.kt b/build-logic/src/main/java/com/squareup/workflow1/buildsrc/ComposeUiTestsPlugin.kt index 113a34b47..08d8a2f42 100644 --- a/build-logic/src/main/java/com/squareup/workflow1/buildsrc/ComposeUiTestsPlugin.kt +++ b/build-logic/src/main/java/com/squareup/workflow1/buildsrc/ComposeUiTestsPlugin.kt @@ -1,6 +1,6 @@ package com.squareup.workflow1.buildsrc -import com.rickbusarow.kgx.dependency +import com.rickbusarow.kgx.library import com.rickbusarow.kgx.libsCatalog import com.squareup.workflow1.buildsrc.internal.androidTestImplementation import com.squareup.workflow1.buildsrc.internal.invoke @@ -15,7 +15,7 @@ class ComposeUiTestsPlugin : Plugin { target.dependencies { androidTestImplementation(target.project(":workflow-ui:internal-testing-compose")) - androidTestImplementation(target.libsCatalog.dependency("androidx-compose-ui-test-junit4")) + androidTestImplementation(target.libsCatalog.library("androidx-compose-ui-test-junit4")) } } } diff --git a/build-logic/src/main/java/com/squareup/workflow1/buildsrc/KotlinMultiplatformExtensions.kt b/build-logic/src/main/java/com/squareup/workflow1/buildsrc/KotlinMultiplatformExtensions.kt index 1d1427803..8a669825d 100644 --- a/build-logic/src/main/java/com/squareup/workflow1/buildsrc/KotlinMultiplatformExtensions.kt +++ b/build-logic/src/main/java/com/squareup/workflow1/buildsrc/KotlinMultiplatformExtensions.kt @@ -1,17 +1,12 @@ package com.squareup.workflow1.buildsrc -import org.gradle.api.Project import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTargetWithSimulatorTests +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget -fun KotlinMultiplatformExtension.iosWithSimulatorArm64(target: Project) { - ios() - iosSimulatorArm64() - - sourceSets.getByName("iosSimulatorArm64Main") { - it.dependsOn(sourceSets.getByName("iosMain")) - } - sourceSets.getByName("iosSimulatorArm64Test") { - it.dependsOn(sourceSets.getByName("iosTest")) - } +fun KotlinMultiplatformExtension.iosTargets(): List { + return listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ) } diff --git a/build.gradle.kts b/build.gradle.kts index ada8fa044..f7f0a65b5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,6 +27,7 @@ plugins { id("artifacts-check") id("dependency-guard") alias(libs.plugins.ktlint) + id("com.autonomousapps.dependency-analysis") version "1.32.0" } shardConnectedCheckTasks(project) diff --git a/dependencies/classpath.txt b/dependencies/classpath.txt index 9b8cbf21f..8357f9ab6 100644 --- a/dependencies/classpath.txt +++ b/dependencies/classpath.txt @@ -41,6 +41,11 @@ com.android.tools:sdk-common:31.1.2 com.android.tools:sdklib:31.1.2 com.android:signflinger:8.1.2 com.android:zipflinger:8.1.2 +com.autonomousapps.dependency-analysis:com.autonomousapps.dependency-analysis.gradle.plugin:1.32.0 +com.autonomousapps:antlr:4.10.1.6 +com.autonomousapps:asm-relocated:9.6.0.1 +com.autonomousapps:dependency-analysis-gradle-plugin:1.32.0 +com.autonomousapps:graph-support:0.2 com.dropbox.dependency-guard:dependency-guard:0.4.3 com.fasterxml.jackson.core:jackson-annotations:2.12.7 com.fasterxml.jackson.core:jackson-core:2.12.7 @@ -50,6 +55,7 @@ com.fasterxml.jackson.module:jackson-module-jaxb-annotations:2.12.7 com.fasterxml.jackson.module:jackson-module-kotlin:2.12.7 com.fasterxml.jackson:jackson-bom:2.12.7 com.fasterxml.woodstox:woodstox-core:6.2.4 +com.github.ben-manes.caffeine:caffeine:3.1.8 com.google.android:annotations:4.1.1.4 com.google.api.grpc:proto-google-common-protos:2.0.1 com.google.auto.value:auto-value-annotations:1.6.2 @@ -57,11 +63,13 @@ com.google.code.findbugs:jsr305:3.0.2 com.google.code.gson:gson:2.8.9 com.google.crypto.tink:tink:1.7.0 com.google.dagger:dagger:2.28.3 -com.google.devtools.ksp:symbol-processing-gradle-plugin:1.9.10-1.0.13 -com.google.errorprone:error_prone_annotations:2.11.0 +com.google.devtools.ksp:symbol-processing-api:1.9.24-1.0.20 +com.google.devtools.ksp:symbol-processing-common-deps:1.9.24-1.0.20 +com.google.devtools.ksp:symbol-processing-gradle-plugin:1.9.24-1.0.20 +com.google.errorprone:error_prone_annotations:2.26.1 com.google.flatbuffers:flatbuffers-java:1.12.0 -com.google.guava:failureaccess:1.0.1 -com.google.guava:guava:31.1-jre +com.google.guava:failureaccess:1.0.2 +com.google.guava:guava:33.1.0-jre com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava com.google.j2objc:j2objc-annotations:1.3 com.google.jimfs:jimfs:1.1 @@ -75,14 +83,18 @@ com.rickbusarow.kgx:names:0.1.12 com.rickbusarow.ktlint:com.rickbusarow.ktlint.gradle.plugin:0.2.2 com.rickbusarow.ktlint:ktlint-gradle-plugin:0.2.2 com.squareup.moshi:moshi-adapters:1.15.0 -com.squareup.moshi:moshi:1.15.0 +com.squareup.moshi:moshi-kotlin:1.15.1 +com.squareup.moshi:moshi:1.15.1 com.squareup.okhttp3:okhttp:4.12.0 -com.squareup.okio:okio-jvm:3.6.0 -com.squareup.okio:okio:3.6.0 +com.squareup.okio:okio-bom:3.9.0 +com.squareup.okio:okio-jvm:3.9.0 +com.squareup.okio:okio:3.9.0 com.squareup.retrofit2:converter-moshi:2.9.0 com.squareup.retrofit2:retrofit:2.9.0 com.squareup:javapoet:1.10.0 com.squareup:javawriter:2.5.0 +com.squareup:kotlinpoet-jvm:1.15.1 +com.squareup:kotlinpoet:1.15.1 com.sun.activation:javax.activation:1.2.0 com.sun.istack:istack-commons-runtime:3.0.8 com.sun.xml.fastinfoset:FastInfoset:1.2.16 @@ -91,6 +103,8 @@ com.vanniktech:nexus:0.27.0 commons-codec:commons-codec:1.11 commons-io:commons-io:2.4 commons-logging:commons-logging:1.2 +dev.zacsweers.moshix:moshi-sealed-reflect:0.25.1 +dev.zacsweers.moshix:moshi-sealed-runtime:0.25.1 io.github.java-diff-utils:java-diff-utils:4.12 io.grpc:grpc-api:1.45.1 io.grpc:grpc-context:1.45.1 @@ -126,50 +140,50 @@ org.apache.httpcomponents:httpmime:4.5.6 org.bitbucket.b_c:jose4j:0.7.0 org.bouncycastle:bcpkix-jdk15on:1.67 org.bouncycastle:bcprov-jdk15on:1.67 -org.checkerframework:checker-qual:3.12.0 +org.checkerframework:checker-qual:3.42.0 org.codehaus.mojo:animal-sniffer-annotations:1.19 org.codehaus.woodstox:stax2-api:4.2.1 org.glassfish.jaxb:jaxb-runtime:2.3.2 org.glassfish.jaxb:txw2:2.3.2 org.jdom:jdom2:2.0.6 -org.jetbrains.dokka:dokka-core:1.9.10 -org.jetbrains.dokka:dokka-gradle-plugin:1.9.10 +org.jetbrains.dokka:dokka-core:1.9.20 +org.jetbrains.dokka:dokka-gradle-plugin:1.9.20 org.jetbrains.intellij.deps:trove4j:1.0.20200330 -org.jetbrains.kotlin:kotlin-android-extensions:1.9.10 -org.jetbrains.kotlin:kotlin-bom:1.9.10 -org.jetbrains.kotlin:kotlin-build-tools-api:1.9.10 -org.jetbrains.kotlin:kotlin-compiler-embeddable:1.9.10 -org.jetbrains.kotlin:kotlin-compiler-runner:1.9.10 -org.jetbrains.kotlin:kotlin-daemon-client:1.9.10 -org.jetbrains.kotlin:kotlin-daemon-embeddable:1.9.10 -org.jetbrains.kotlin:kotlin-gradle-plugin-annotations:1.9.10 -org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.9.10 -org.jetbrains.kotlin:kotlin-gradle-plugin-idea-proto:1.9.10 -org.jetbrains.kotlin:kotlin-gradle-plugin-idea:1.9.10 -org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.9.10 -org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10 -org.jetbrains.kotlin:kotlin-gradle-plugins-bom:1.9.10 -org.jetbrains.kotlin:kotlin-klib-commonizer-api:1.9.10 -org.jetbrains.kotlin:kotlin-native-utils:1.9.10 -org.jetbrains.kotlin:kotlin-project-model:1.9.10 +org.jetbrains.kotlin:kotlin-android-extensions:1.9.24 +org.jetbrains.kotlin:kotlin-bom:1.9.24 +org.jetbrains.kotlin:kotlin-build-tools-api:1.9.24 +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.9.24 +org.jetbrains.kotlin:kotlin-compiler-runner:1.9.24 +org.jetbrains.kotlin:kotlin-daemon-client:1.9.24 +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.9.24 +org.jetbrains.kotlin:kotlin-gradle-plugin-annotations:1.9.24 +org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.9.24 +org.jetbrains.kotlin:kotlin-gradle-plugin-idea-proto:1.9.24 +org.jetbrains.kotlin:kotlin-gradle-plugin-idea:1.9.24 +org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.9.24 +org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24 +org.jetbrains.kotlin:kotlin-gradle-plugins-bom:1.9.24 +org.jetbrains.kotlin:kotlin-klib-commonizer-api:1.9.24 +org.jetbrains.kotlin:kotlin-native-utils:1.9.24 +org.jetbrains.kotlin:kotlin-project-model:1.9.24 org.jetbrains.kotlin:kotlin-reflect:1.9.10 -org.jetbrains.kotlin:kotlin-scripting-common:1.9.10 -org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.9.10 -org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.9.10 -org.jetbrains.kotlin:kotlin-scripting-jvm:1.9.10 -org.jetbrains.kotlin:kotlin-serialization:1.9.10 +org.jetbrains.kotlin:kotlin-scripting-common:1.9.24 +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.9.24 +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.9.24 +org.jetbrains.kotlin:kotlin-scripting-jvm:1.9.24 +org.jetbrains.kotlin:kotlin-serialization:1.9.24 org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.24 org.jetbrains.kotlin:kotlin-stdlib:1.9.10 -org.jetbrains.kotlin:kotlin-tooling-core:1.9.10 -org.jetbrains.kotlin:kotlin-util-io:1.9.10 -org.jetbrains.kotlin:kotlin-util-klib:1.9.10 +org.jetbrains.kotlin:kotlin-tooling-core:1.9.24 +org.jetbrains.kotlin:kotlin-util-io:1.9.24 +org.jetbrains.kotlin:kotlin-util-klib:1.9.24 org.jetbrains.kotlinx:binary-compatibility-validator:0.13.2 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.3 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.3 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3 -org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.6.2 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 +org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.9.0 org.jetbrains:annotations:13.0 org.jvnet.staxex:stax-ex:1.8.1 org.ow2.asm:asm-analysis:9.2 diff --git a/gradle.properties b/gradle.properties index a896b2f4c..536e03907 100644 --- a/gradle.properties +++ b/gradle.properties @@ -30,3 +30,7 @@ SONATYPE_STAGING_PROFILE=com.squareup # we're only supporting IR for now. # For details see https://kotlinlang.org/docs/js-ir-compiler.html kotlin.js.compiler=ir + +# Compose Multiplatform +org.jetbrains.compose.experimental.jscanvas.enabled=true +dependency.analysis.print.build.health=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5ac906084..cbdecb03a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,14 +13,14 @@ androidx-activity = "1.6.1" androidx-appcompat = "1.6.1" androidx-benchmark = "1.2.3" androidx-cardview = "1.0.0" -androidx-compose-compiler = "1.5.3" +androidx-compose-compiler = "1.5.14" # see https://developer.android.com/jetpack/compose/bom/bom-mapping -androidx-compose-bom = "2023.01.00" +androidx-compose-bom = "2024.05.00" androidx-constraintlayout = "2.1.4" androidx-core = "1.12.0" androidx-fragment = "1.3.6" androidx-gridlayout = "1.0.0" -androidx-lifecycle = "2.6.1" +androidx-lifecycle = "2.8.0" androidx-navigation = "2.4.0-alpha09" androidx-paging = "3.0.1" androidx-profileinstaller = "1.2.0-alpha02" @@ -29,7 +29,7 @@ androidx-room = "2.4.0-alpha04" androidx-savedstate = "1.2.1" androidx-startup = "1.1.0" androidx-test = "1.5.0" -androidx-test-espresso = "3.5.1" +androidx-test-espresso = "3.5.0" androidx-test-junit-ext = "1.1.5" androidx-test-runner = "1.5.2" androidx-test-truth-ext = "1.5.0" @@ -37,23 +37,24 @@ androidx-tracing = "1.1.0" androidx-transition = "1.4.1" detekt = "1.19.0" -dokka = "1.9.10" +dokka = "1.9.20" dependencyGuard = "0.4.3" google-accompanist = "0.18.0" google-dagger = "2.40.5" -google-ksp = "1.9.10-1.0.13" +google-ksp = "1.9.24-1.0.20" google-material = "1.4.0" groovy = "3.0.9" jUnit = "4.13.2" java-diff-utils = "4.12" javaParser = "3.24.0" +jetbrains-compose = "1.6.11" kgx = "0.1.12" kotest = "5.1.0" # Keep this in sync with what is hard-coded in build-logic/settings.gradle.kts as that is upstream # of loading the library versions from this file but should be the same. -kotlin = "1.9.10" +kotlin = "1.9.24" kotlinx-binary-compatibility = "0.13.2" kotlinx-coroutines = "1.7.3" @@ -91,6 +92,7 @@ vanniktech-publish = "0.27.0" [plugins] +jetbrains-compose-plugin = { id = "org.jetbrains.compose", version.ref = "jetbrains-compose" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } @@ -132,6 +134,7 @@ androidx-compose-ui = { module = "androidx.compose.ui:ui" } androidx-compose-ui-geometry = { module = "androidx.compose.ui:ui-geometry" } androidx-compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } +androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } @@ -189,6 +192,7 @@ hamcrest = "org.hamcrest:hamcrest-core:2.2" java-diff-utils = { module = "io.github.java-diff-utils:java-diff-utils", version.ref = "java-diff-utils" } jetbrains-annotations = "org.jetbrains:annotations:24.0.1" +jetbrains-lifecycle-runtime-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } junit = { module = "junit:junit", version.ref = "jUnit" } diff --git a/internal-testing-utils/dependencies/runtimeClasspath.txt b/internal-testing-utils/dependencies/runtimeClasspath.txt index a689dbdb0..0b32e703e 100644 --- a/internal-testing-utils/dependencies/runtimeClasspath.txt +++ b/internal-testing-utils/dependencies/runtimeClasspath.txt @@ -1,6 +1,5 @@ -org.jetbrains.kotlin:kotlin-bom:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlin:kotlin-bom:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib:1.9.24 org.jetbrains:annotations:13.0 diff --git a/samples/compose-samples/README.md b/samples/compose-samples/README.md index 2fb59c6b5..f5915d784 100644 --- a/samples/compose-samples/README.md +++ b/samples/compose-samples/README.md @@ -2,3 +2,13 @@ This module is named "compose-samples" because the binary validation tool seems to refuse to look at the `:workflow-ui:compose` module if this one is also named `compose`. + +## iOS + +1. To run the iOS target you need to be on a Mac and have Xcode installed. +1. Then install the Kotlin Multiplatform plugin for your intellij IDE +1. Finally go to run configurations and add a new iOS Application configuration and select the iOS project file in ./iosApp + +[To enable iOS debugging](https://appkickstarter.com/blog/debug-an-ios-kotlin-multiplatform-app-from-android-studio/) open Settings -> Advanced Settings -> Enable experimental Multiplatform IDE features + +[BackHandler implementation for iOS here](https://exyte.com/blog/jetpack-compose-multiplatform) diff --git a/samples/compose-samples/build.gradle.kts b/samples/compose-samples/build.gradle.kts index 4b230b5db..3f13fcd05 100644 --- a/samples/compose-samples/build.gradle.kts +++ b/samples/compose-samples/build.gradle.kts @@ -1,60 +1,171 @@ +import com.squareup.workflow1.buildsrc.iosTargets +import org.gradle.api.JavaVersion.VERSION_1_9 +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.dsl.KotlinTargetContainerWithPresetFunctions +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree + plugins { + alias(libs.plugins.jetbrains.compose.plugin) + id("kotlin-multiplatform") id("com.android.application") - id("kotlin-android") - id("android-sample-app") + id("android-defaults") id("android-ui-tests") id("compose-ui-tests") } +kotlin { + val targets = project.findProperty("workflow.targets") ?: "kmp" + + listOf( + "ios" to { + iosTargets().forEach { iosTarget -> + // This allows us to import ComposeApp into the iOS project + iosTarget.binaries.framework { + baseName = "ComposeApp" + isStatic = true + } + } + }, + "jvm" to { jvm() }, + "js" to { js(IR).browser() }, + "android" to { androidTargetWithTesting() }, + ).forEach { (target, action) -> + if (targets == "kmp" || targets == target) { + action() + } + } + + sourceSets { + commonMain.dependencies { + implementation(compose.components.uiToolingPreview) + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.ui) + + implementation(project(":workflow-core")) + implementation(project(":workflow-runtime")) + implementation(project(":workflow-ui:compose")) + implementation(project(":workflow-ui:core")) + } + + androidMain.dependencies { + implementation(project.dependencies.platform(libs.androidx.compose.bom)) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.activity.core) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.compose.foundation.layout) + implementation(libs.androidx.compose.material) + implementation(libs.androidx.compose.runtime) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.geometry) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.lifecycle.viewmodel.savedstate) + implementation(libs.androidx.viewbinding) + implementation(libs.kotlin.common) + // For the LayoutInspector. + implementation(libs.kotlin.reflect) + + implementation(project(":workflow-config:config-android")) + implementation(project(":workflow-ui:compose-tooling")) + implementation(project(":workflow-ui:core-android")) + } + + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + + val iosMain by creating { + dependsOn(commonMain.get()) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + + val nonAndroidMain by creating { + dependsOn(commonMain.get()) + iosMain.dependsOn(this) + appleMain.get().dependsOn(this) + jsMain.get().dependsOn(this) + jvmMain.get().dependsOn(this) + } + + // Currently just used to make a noop BackHandler + val nonMobileMain by creating { + appleMain.get().dependsOn(this) + jsMain.get().dependsOn(this) + jvmMain.get().dependsOn(this) + dependsOn(commonMain.get()) + } + } +} + +/** + * All of these are needed due needing Java 9 whenever databinding is enabled and the release + * flag is set. See [com.android.build.gradle.tasks.JavaCompileUtils::checkReleaseFlag]. Setting + * the language version here fixes the issue. + * TODO: Figure out how to disable the release flag for the sample app + */ +tasks.withType { + kotlinOptions.jvmTarget = "9" +} + android { - defaultConfig { - applicationId = "com.squareup.sample.compose" + compileOptions { + sourceCompatibility = VERSION_1_9 + targetCompatibility = VERSION_1_9 } + buildFeatures { + viewBinding = true + // This is needed for the layout inspector to work in Kotlin 2.0.0 even though we need it for + // the current Kotlin version we should leave this here when we upgrade to 2.0.0 compose = true } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() + + defaultConfig { + applicationId = "com.squareup.sample.compose" } + + // In Kotlin 2.0.0 this isn't strictly necessary but it is necessary for the layout inspector to + // work in Kotlin 2.0.0 + composeOptions.kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() namespace = "com.squareup.sample.compose" + + dependencies { + debugImplementation(compose.uiTooling) + } } -dependencies { - val composeBom = platform(libs.androidx.compose.bom) - - androidTestImplementation(libs.androidx.activity.core) - androidTestImplementation(composeBom) - androidTestImplementation(libs.androidx.compose.ui) - androidTestImplementation(libs.androidx.compose.ui.test.junit4) - androidTestImplementation(libs.androidx.test.core) - androidTestImplementation(libs.androidx.test.truth) - androidTestImplementation(libs.kotlin.test.jdk) - - androidTestImplementation(project(":workflow-runtime")) - - debugImplementation(libs.squareup.leakcanary.android) - - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.activity.core) - implementation(composeBom) - implementation(libs.androidx.compose.foundation) - implementation(libs.androidx.compose.foundation.layout) - implementation(libs.androidx.compose.material) - implementation(libs.androidx.compose.runtime) - implementation(libs.androidx.compose.ui) - implementation(libs.androidx.compose.ui.geometry) - implementation(libs.androidx.compose.ui.graphics) - implementation(libs.androidx.compose.ui.tooling) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.androidx.lifecycle.viewmodel.ktx) - implementation(libs.androidx.lifecycle.viewmodel.savedstate) - implementation(libs.androidx.viewbinding) - implementation(libs.kotlin.common) - // For the LayoutInspector. - implementation(libs.kotlin.reflect) - - implementation(project(":workflow-ui:compose")) - implementation(project(":workflow-ui:compose-tooling")) - implementation(project(":workflow-ui:core-android")) - implementation(project(":workflow-ui:core-common")) +fun KotlinTargetContainerWithPresetFunctions.androidTargetWithTesting() { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "9" + } + } + @OptIn(ExperimentalKotlinGradlePluginApi::class) + instrumentedTestVariant { + sourceSetTree.set(KotlinSourceSetTree.test) + + dependencies { + androidTestImplementation(platform(libs.androidx.compose.bom)) + + androidTestImplementation(libs.androidx.activity.core) + androidTestImplementation(libs.androidx.compose.ui) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + androidTestImplementation(libs.androidx.test.core) + androidTestImplementation(libs.androidx.test.truth) + androidTestImplementation(libs.kotlin.test.jdk) + + androidTestImplementation(project(":workflow-runtime")) + + debugImplementation(libs.squareup.leakcanary.android) + } + } + } } diff --git a/samples/compose-samples/iosApp/Configuration/Config.xcconfig b/samples/compose-samples/iosApp/Configuration/Config.xcconfig new file mode 100644 index 000000000..176ea0167 --- /dev/null +++ b/samples/compose-samples/iosApp/Configuration/Config.xcconfig @@ -0,0 +1,3 @@ +TEAM_ID= +BUNDLE_ID=com.squareup.sample.compose +APP_NAME=Workflow Compose Samples diff --git a/samples/compose-samples/iosApp/iosApp.xcodeproj/project.pbxproj b/samples/compose-samples/iosApp/iosApp.xcodeproj/project.pbxproj new file mode 100644 index 000000000..3dc199bcf --- /dev/null +++ b/samples/compose-samples/iosApp/iosApp.xcodeproj/project.pbxproj @@ -0,0 +1,409 @@ +// !$*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 */; }; + 86B843592C2F435300048ED6 /* ContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86B843582C2F435300048ED6 /* ContainerViewController.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 /* Workflow Compose Samples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Workflow Compose Samples.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 = ""; }; + 86B843582C2F435300048ED6 /* ContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerViewController.swift; 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 /* Workflow Compose Samples.app */, + ); + name = Products; + sourceTree = ""; + }; + 7555FF7D242A565900829871 /* iosApp */ = { + isa = PBXGroup; + children = ( + 058557BA273AAA24004C7B11 /* Assets.xcassets */, + 7555FF82242A565900829871 /* ContentView.swift */, + 86B843582C2F435300048ED6 /* ContainerViewController.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 /* Workflow Compose Samples.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\necho 'SRCROOT: ' + $SRCROOT \necho $(SRCROOT)/../compose-samples/\ncd \"$SRCROOT/..\"\ncd \"../..\"\n./gradlew :samples:compose-samples:embedAndSignAppleFrameworkForXcode\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7555FF77242A565900829871 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, + 86B843592C2F435300048ED6 /* ContainerViewController.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)", + "$(SRCROOT)/../build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + ); + INFOPLIST_FILE = iosApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.3; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + ComposeApp, + ); + 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)", + "$(SRCROOT)/../build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + ); + INFOPLIST_FILE = iosApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.3; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + ComposeApp, + ); + 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 */; +} diff --git a/samples/compose-samples/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/samples/compose-samples/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..ee7e3ca03 --- /dev/null +++ b/samples/compose-samples/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/samples/compose-samples/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/samples/compose-samples/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..8edf56e7a --- /dev/null +++ b/samples/compose-samples/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/samples/compose-samples/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png b/samples/compose-samples/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png new file mode 100644 index 000000000..53fc536fb Binary files /dev/null and b/samples/compose-samples/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png differ diff --git a/samples/compose-samples/iosApp/iosApp/Assets.xcassets/Contents.json b/samples/compose-samples/iosApp/iosApp/Assets.xcassets/Contents.json new file mode 100644 index 000000000..4aa7c5350 --- /dev/null +++ b/samples/compose-samples/iosApp/iosApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/samples/compose-samples/iosApp/iosApp/ContainerViewController.swift b/samples/compose-samples/iosApp/iosApp/ContainerViewController.swift new file mode 100644 index 000000000..0a8b490f8 --- /dev/null +++ b/samples/compose-samples/iosApp/iosApp/ContainerViewController.swift @@ -0,0 +1,25 @@ +import SwiftUI + +class ContainerViewController: UIViewController { + private let onTouchDown: (CGPoint) -> Void + + init(child: UIViewController, onTouchDown: @escaping (CGPoint) -> Void) { + self.onTouchDown = onTouchDown + super.init(nibName: nil, bundle: nil) + addChild(child) + child.view.frame = view.frame + view.addSubview(child.view) + child.didMove(toParent: self) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + if let startPoint = touches.first?.location(in: nil) { + onTouchDown(startPoint) + } + } +} diff --git a/samples/compose-samples/iosApp/iosApp/ContentView.swift b/samples/compose-samples/iosApp/iosApp/ContentView.swift new file mode 100644 index 000000000..fb201e6a8 --- /dev/null +++ b/samples/compose-samples/iosApp/iosApp/ContentView.swift @@ -0,0 +1,62 @@ +import UIKit +import SwiftUI +import ComposeApp + +struct ComposeView: UIViewControllerRepresentable { + var onSwipe: () -> Void + + func makeUIViewController(context: Context) -> UIViewController { + let mainViewController = MainViewControllerKt.MainViewController() + + let containerController = ContainerViewController(child: mainViewController) { + context.coordinator.startPoint = $0 + } + + let swipeGestureRecognizer = UISwipeGestureRecognizer( + target: + context.coordinator, action: #selector(Coordinator.handleSwipe) + ) + swipeGestureRecognizer.direction = .right + swipeGestureRecognizer.numberOfTouchesRequired = 1 + containerController.view.addGestureRecognizer(swipeGestureRecognizer) + return containerController + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} + + func makeCoordinator() -> Coordinator { + Coordinator(onSwipe: onSwipe) + } + + class Coordinator: NSObject, UIGestureRecognizerDelegate { + var onSwipe: () -> Void + var startPoint: CGPoint? + + init(onSwipe: @escaping () -> Void) { + self.onSwipe = onSwipe + } + + @objc func handleSwipe(_ gesture: UISwipeGestureRecognizer) { + if gesture.state == .ended, let startPoint = startPoint, startPoint.x < 50 { + onSwipe() + } + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + true + } + } +} + +struct ContentView: View { + var body: some View { + ComposeView { + onBackGesture() + } + .ignoresSafeArea(.keyboard) + } +} + +public func onBackGesture() { + MainViewControllerKt.onBackGesture() +} diff --git a/samples/compose-samples/iosApp/iosApp/Info.plist b/samples/compose-samples/iosApp/iosApp/Info.plist new file mode 100644 index 000000000..412e37812 --- /dev/null +++ b/samples/compose-samples/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/samples/compose-samples/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/samples/compose-samples/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 000000000..4aa7c5350 --- /dev/null +++ b/samples/compose-samples/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/samples/compose-samples/iosApp/iosApp/iOSApp.swift b/samples/compose-samples/iosApp/iosApp/iOSApp.swift new file mode 100644 index 000000000..0648e8602 --- /dev/null +++ b/samples/compose-samples/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/samples/compose-samples/src/main/AndroidManifest.xml b/samples/compose-samples/src/androidMain/AndroidManifest.xml similarity index 100% rename from samples/compose-samples/src/main/AndroidManifest.xml rename to samples/compose-samples/src/androidMain/AndroidManifest.xml diff --git a/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/DefaultsAndroid.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/DefaultsAndroid.kt new file mode 100644 index 000000000..0c9c32018 --- /dev/null +++ b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/DefaultsAndroid.kt @@ -0,0 +1,21 @@ +package com.squareup.sample.compose + +import androidx.compose.runtime.Composable +import com.squareup.workflow1.RuntimeConfig +import com.squareup.workflow1.WorkflowExperimentalRuntime +import com.squareup.workflow1.config.AndroidRuntimeConfigTools +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.compose.withComposeInteropSupport + +@OptIn(WorkflowExperimentalRuntime::class) +actual fun defaultRuntimeConfig(): RuntimeConfig = + AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig() + +@OptIn(WorkflowUiExperimentalApi::class) +actual fun defaultViewEnvironment(): ViewEnvironment = + ViewEnvironment.EMPTY.withComposeInteropSupport() + +@Composable +actual fun BackHandler(isEnabled: Boolean, onBack: () -> Unit) = + androidx.activity.compose.BackHandler(isEnabled, onBack) diff --git a/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/hellocompose/AppAndroid.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/hellocompose/AppAndroid.kt new file mode 100644 index 000000000..be7eaa139 --- /dev/null +++ b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/hellocompose/AppAndroid.kt @@ -0,0 +1,10 @@ +package com.squareup.sample.compose.hellocompose + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview + +@Preview(showBackground = true) +@Composable +private fun AppPreview() { + App() +} diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocompose/HelloComposeActivity.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/hellocompose/HelloComposeActivity.kt similarity index 100% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocompose/HelloComposeActivity.kt rename to samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/hellocompose/HelloComposeActivity.kt diff --git a/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/hellocompose/HelloComposeScreenPreview.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/hellocompose/HelloComposeScreenPreview.kt new file mode 100644 index 000000000..2a795eec1 --- /dev/null +++ b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/hellocompose/HelloComposeScreenPreview.kt @@ -0,0 +1,16 @@ +package com.squareup.sample.compose.hellocompose + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.compose.tooling.Preview + +@OptIn(WorkflowUiExperimentalApi::class) +@Preview(heightDp = 150, showBackground = true) +@Composable +private fun HelloPreview() { + HelloComposeScreen( + "Hello!", + onClick = {} + ).Preview() +} diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposebinding/HelloBindingActivity.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/hellocomposebinding/HelloBindingActivity.kt similarity index 76% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposebinding/HelloBindingActivity.kt rename to samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/hellocomposebinding/HelloBindingActivity.kt index f20954ba3..c00bbbd36 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposebinding/HelloBindingActivity.kt +++ b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/hellocomposebinding/HelloBindingActivity.kt @@ -5,7 +5,6 @@ package com.squareup.sample.compose.hellocomposebinding import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.compose.material.MaterialTheme import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -13,25 +12,12 @@ import com.squareup.workflow1.WorkflowExperimentalRuntime import com.squareup.workflow1.config.AndroidRuntimeConfigTools import com.squareup.workflow1.mapRendering import com.squareup.workflow1.ui.Screen -import com.squareup.workflow1.ui.ViewEnvironment -import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowLayout import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.compose.withComposeInteropSupport -import com.squareup.workflow1.ui.compose.withCompositionRoot -import com.squareup.workflow1.ui.plus import com.squareup.workflow1.ui.renderWorkflowIn import com.squareup.workflow1.ui.withEnvironment import kotlinx.coroutines.flow.StateFlow -@OptIn(WorkflowUiExperimentalApi::class) -private val viewEnvironment = - (ViewEnvironment.EMPTY + ViewRegistry(HelloBinding)) - .withCompositionRoot { content -> - MaterialTheme(content = content) - } - .withComposeInteropSupport() - /** * Demonstrates how to create and display a view factory with * [screenComposableFactory][com.squareup.workflow1.ui.compose.ScreenComposableFactory]. diff --git a/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/hellocomposebinding/HelloBindingPreview.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/hellocomposebinding/HelloBindingPreview.kt new file mode 100644 index 000000000..cfa8aebbe --- /dev/null +++ b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/hellocomposebinding/HelloBindingPreview.kt @@ -0,0 +1,14 @@ +package com.squareup.sample.compose.hellocomposebinding + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.squareup.sample.compose.hellocomposebinding.HelloWorkflow.Rendering +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.compose.tooling.Preview + +@OptIn(WorkflowUiExperimentalApi::class) +@Preview(heightDp = 150, showBackground = true) +@Composable +fun DrawHelloRenderingPreview() { + HelloBinding.Preview(Rendering("Hello!", onClick = {})) +} diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflowActivity.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflowActivity.kt similarity index 100% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflowActivity.kt rename to samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflowActivity.kt diff --git a/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflowPreview.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflowPreview.kt new file mode 100644 index 000000000..9a562f64f --- /dev/null +++ b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflowPreview.kt @@ -0,0 +1,22 @@ +package com.squareup.sample.compose.hellocomposeworkflow + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.tooling.preview.Preview +import com.squareup.sample.compose.defaultRuntimeConfig +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.compose.WorkflowRendering +import com.squareup.workflow1.ui.compose.renderAsState + +@OptIn(WorkflowUiExperimentalApi::class) +@Preview(showBackground = true) +@Composable +fun HelloComposeWorkflowPreview() { + val rendering by HelloComposeWorkflow.renderAsState( + props = "hello", + onOutput = {}, + runtimeConfig = defaultRuntimeConfig() + ) + WorkflowRendering(rendering, ViewEnvironment.EMPTY) +} diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingActivity.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/inlinerendering/InlineRenderingActivity.kt similarity index 82% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingActivity.kt rename to samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/inlinerendering/InlineRenderingActivity.kt index 8386aaa99..59007075c 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingActivity.kt +++ b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/inlinerendering/InlineRenderingActivity.kt @@ -1,5 +1,3 @@ -@file:OptIn(WorkflowExperimentalRuntime::class) - package com.squareup.sample.compose.inlinerendering import android.os.Bundle @@ -8,14 +6,14 @@ import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.squareup.sample.compose.defaultRuntimeConfig +import com.squareup.sample.compose.defaultViewEnvironment import com.squareup.workflow1.WorkflowExperimentalRuntime import com.squareup.workflow1.config.AndroidRuntimeConfigTools import com.squareup.workflow1.mapRendering import com.squareup.workflow1.ui.Screen -import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowLayout import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.compose.withComposeInteropSupport import com.squareup.workflow1.ui.renderWorkflowIn import com.squareup.workflow1.ui.withEnvironment import kotlinx.coroutines.flow.StateFlow @@ -39,11 +37,11 @@ class InlineRenderingActivity : AppCompatActivity() { val renderings: StateFlow by lazy { renderWorkflowIn( workflow = InlineRenderingWorkflow.mapRendering { - it.withEnvironment(ViewEnvironment.EMPTY.withComposeInteropSupport()) + it.withEnvironment(defaultViewEnvironment()) }, scope = viewModelScope, savedStateHandle = savedState, - runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig() + runtimeConfig = defaultRuntimeConfig() ) } } diff --git a/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/inlinerendering/InlineRenderingWorkflowPreview.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/inlinerendering/InlineRenderingWorkflowPreview.kt new file mode 100644 index 000000000..346110398 --- /dev/null +++ b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/inlinerendering/InlineRenderingWorkflowPreview.kt @@ -0,0 +1,10 @@ +package com.squareup.sample.compose.inlinerendering + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview + +@Preview(showBackground = true) +@Composable +internal fun InlineRenderingWorkflowPreview() { + InlineRenderingWorkflowRendering() +} diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/launcher/SampleLauncherActivity.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/launcher/SampleLauncherActivity.kt similarity index 100% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/launcher/SampleLauncherActivity.kt rename to samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/launcher/SampleLauncherActivity.kt diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/launcher/SampleLauncherApp.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/launcher/SampleLauncherApp.kt similarity index 100% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/launcher/SampleLauncherApp.kt rename to samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/launcher/SampleLauncherApp.kt diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/launcher/Samples.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/launcher/Samples.kt similarity index 100% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/launcher/Samples.kt rename to samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/launcher/Samples.kt diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/LegacyRunner.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/nestedrenderings/LegacyRunner.kt similarity index 100% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/LegacyRunner.kt rename to samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/nestedrenderings/LegacyRunner.kt diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/NestedRenderingsActivity.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/nestedrenderings/NestedRenderingsActivity.kt similarity index 100% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/NestedRenderingsActivity.kt rename to samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/nestedrenderings/NestedRenderingsActivity.kt diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/RecursiveViewFactory.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/nestedrenderings/RecursiveViewFactory.kt similarity index 100% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/RecursiveViewFactory.kt rename to samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/nestedrenderings/RecursiveViewFactory.kt diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/RecursiveWorkflow.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/nestedrenderings/RecursiveWorkflow.kt similarity index 100% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/RecursiveWorkflow.kt rename to samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/nestedrenderings/RecursiveWorkflow.kt diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/StringRendering.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/nestedrenderings/StringRendering.kt similarity index 100% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/StringRendering.kt rename to samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/nestedrenderings/StringRendering.kt diff --git a/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/preview/PreviewActivity.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/preview/PreviewActivity.kt new file mode 100644 index 000000000..2f2a4c915 --- /dev/null +++ b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/preview/PreviewActivity.kt @@ -0,0 +1,34 @@ +@file:OptIn(WorkflowUiExperimentalApi::class) + +package com.squareup.sample.compose.preview + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.compose.tooling.Preview + +class PreviewActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + PreviewApp() + } + } +} + + +@Preview +@Composable +fun PreviewApp() { + MaterialTheme { + Surface { + previewContactRendering.Preview() + } + } +} diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/textinput/App.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/textinput/AppAndroid.kt similarity index 76% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/textinput/App.kt rename to samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/textinput/AppAndroid.kt index 5533b1251..825dd5277 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/textinput/App.kt +++ b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/textinput/AppAndroid.kt @@ -1,4 +1,4 @@ -@file:OptIn(WorkflowUiExperimentalApi::class, WorkflowExperimentalRuntime::class) +@file:OptIn(WorkflowUiExperimentalApi::class) package com.squareup.sample.compose.textinput @@ -6,8 +6,7 @@ import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.tooling.preview.Preview -import com.squareup.workflow1.WorkflowExperimentalRuntime -import com.squareup.workflow1.config.AndroidRuntimeConfigTools +import com.squareup.sample.compose.defaultRuntimeConfig import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi @@ -22,7 +21,7 @@ private val viewEnvironment = ViewEnvironment.EMPTY + ViewRegistry(TextInputView val rendering by TextInputWorkflow.renderAsState( props = Unit, onOutput = {}, - runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig() + runtimeConfig = defaultRuntimeConfig() ) WorkflowRendering(rendering, viewEnvironment) } diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/textinput/TextInputActivity.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/textinput/TextInputActivity.kt similarity index 100% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/textinput/TextInputActivity.kt rename to samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/textinput/TextInputActivity.kt diff --git a/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/textinput/TextInputViewFactoryPreview.kt b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/textinput/TextInputViewFactoryPreview.kt new file mode 100644 index 000000000..e68fa472c --- /dev/null +++ b/samples/compose-samples/src/androidMain/kotlin/com/squareup/sample/compose/textinput/TextInputViewFactoryPreview.kt @@ -0,0 +1,20 @@ +package com.squareup.sample.compose.textinput + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.squareup.sample.compose.textinput.TextInputWorkflow.Rendering +import com.squareup.workflow1.ui.TextController +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.compose.tooling.Preview + +@OptIn(WorkflowUiExperimentalApi::class) +@Preview(showBackground = true) +@Composable +private fun TextInputViewFactoryPreview() { + TextInputViewFactory.Preview( + Rendering( + textController = TextController("Hello world"), + onSwapText = {} + ) + ) +} diff --git a/samples/compose-samples/src/main/res/layout/legacy_view.xml b/samples/compose-samples/src/androidMain/res/layout/legacy_view.xml similarity index 100% rename from samples/compose-samples/src/main/res/layout/legacy_view.xml rename to samples/compose-samples/src/androidMain/res/layout/legacy_view.xml diff --git a/samples/compose-samples/src/main/res/values/dimens.xml b/samples/compose-samples/src/androidMain/res/values/dimens.xml similarity index 100% rename from samples/compose-samples/src/main/res/values/dimens.xml rename to samples/compose-samples/src/androidMain/res/values/dimens.xml diff --git a/samples/compose-samples/src/main/res/values/strings.xml b/samples/compose-samples/src/androidMain/res/values/strings.xml similarity index 100% rename from samples/compose-samples/src/main/res/values/strings.xml rename to samples/compose-samples/src/androidMain/res/values/strings.xml diff --git a/samples/compose-samples/src/main/res/values/styles.xml b/samples/compose-samples/src/androidMain/res/values/styles.xml similarity index 100% rename from samples/compose-samples/src/main/res/values/styles.xml rename to samples/compose-samples/src/androidMain/res/values/styles.xml diff --git a/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/Defaults.kt b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/Defaults.kt new file mode 100644 index 000000000..787a3ad79 --- /dev/null +++ b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/Defaults.kt @@ -0,0 +1,14 @@ +package com.squareup.sample.compose + +import androidx.compose.runtime.Composable +import com.squareup.workflow1.RuntimeConfig +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +expect fun defaultRuntimeConfig(): RuntimeConfig + +@OptIn(WorkflowUiExperimentalApi::class) +expect fun defaultViewEnvironment(): ViewEnvironment + +@Composable +expect fun BackHandler(isEnabled: Boolean = true, onBack: () -> Unit) diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocompose/App.kt b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocompose/App.kt similarity index 60% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocompose/App.kt rename to samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocompose/App.kt index 9a4706258..493fa23e4 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocompose/App.kt +++ b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocompose/App.kt @@ -1,4 +1,4 @@ -@file:OptIn(WorkflowUiExperimentalApi::class, WorkflowExperimentalRuntime::class) +@file:OptIn(WorkflowUiExperimentalApi::class) package com.squareup.sample.compose.hellocompose @@ -9,23 +9,20 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.squareup.workflow1.WorkflowExperimentalRuntime -import com.squareup.workflow1.config.AndroidRuntimeConfigTools -import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.sample.compose.defaultRuntimeConfig +import com.squareup.sample.compose.defaultViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.compose.WorkflowRendering import com.squareup.workflow1.ui.compose.renderAsState -import com.squareup.workflow1.ui.compose.withComposeInteropSupport -private val viewEnvironment = ViewEnvironment.EMPTY.withComposeInteropSupport() +private val viewEnvironment = defaultViewEnvironment() @Composable fun App() { MaterialTheme { val rendering by HelloComposeWorkflow.renderAsState( props = Unit, - runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig(), + runtimeConfig = defaultRuntimeConfig(), onOutput = {} ) WorkflowRendering( @@ -39,9 +36,3 @@ private val viewEnvironment = ViewEnvironment.EMPTY.withComposeInteropSupport() ) } } - -@Preview(showBackground = true) -@Composable -private fun AppPreview() { - App() -} diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocompose/HelloComposeScreen.kt b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocompose/HelloComposeScreen.kt similarity index 74% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocompose/HelloComposeScreen.kt rename to samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocompose/HelloComposeScreen.kt index 88e8f8b87..066968fcd 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocompose/HelloComposeScreen.kt +++ b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocompose/HelloComposeScreen.kt @@ -7,11 +7,9 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.compose.ComposeScreen -import com.squareup.workflow1.ui.compose.tooling.Preview @OptIn(WorkflowUiExperimentalApi::class) data class HelloComposeScreen( @@ -28,13 +26,3 @@ data class HelloComposeScreen( ) } } - -@OptIn(WorkflowUiExperimentalApi::class) -@Preview(heightDp = 150, showBackground = true) -@Composable -private fun HelloPreview() { - HelloComposeScreen( - "Hello!", - onClick = {} - ).Preview() -} diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocompose/HelloComposeWorkflow.kt b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocompose/HelloComposeWorkflow.kt similarity index 100% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocompose/HelloComposeWorkflow.kt rename to samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocompose/HelloComposeWorkflow.kt diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposebinding/HelloBinding.kt b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocomposebinding/HelloBinding.kt similarity index 68% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposebinding/HelloBinding.kt rename to samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocomposebinding/HelloBinding.kt index aa3f65e67..4ad66e697 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposebinding/HelloBinding.kt +++ b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocomposebinding/HelloBinding.kt @@ -4,13 +4,10 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.Text -import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview import com.squareup.sample.compose.hellocomposebinding.HelloWorkflow.Rendering import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.compose.ScreenComposableFactory -import com.squareup.workflow1.ui.compose.tooling.Preview @OptIn(WorkflowUiExperimentalApi::class) val HelloBinding = ScreenComposableFactory { rendering, _ -> @@ -22,10 +19,3 @@ val HelloBinding = ScreenComposableFactory { rendering, _ -> .wrapContentSize() ) } - -@OptIn(WorkflowUiExperimentalApi::class) -@Preview(heightDp = 150, showBackground = true) -@Composable -fun DrawHelloRenderingPreview() { - HelloBinding.Preview(Rendering("Hello!", onClick = {})) -} diff --git a/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocomposebinding/HelloBindingViewEnvironment.kt b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocomposebinding/HelloBindingViewEnvironment.kt new file mode 100644 index 000000000..1cac0652e --- /dev/null +++ b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocomposebinding/HelloBindingViewEnvironment.kt @@ -0,0 +1,15 @@ +package com.squareup.sample.compose.hellocomposebinding + +import androidx.compose.material.MaterialTheme +import com.squareup.sample.compose.defaultViewEnvironment +import com.squareup.workflow1.ui.ViewRegistry +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.compose.withCompositionRoot +import com.squareup.workflow1.ui.plus + +@OptIn(WorkflowUiExperimentalApi::class) +val viewEnvironment = + (defaultViewEnvironment() + ViewRegistry(HelloBinding)) + .withCompositionRoot { content -> + MaterialTheme(content = content) + } diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposebinding/HelloWorkflow.kt b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocomposebinding/HelloWorkflow.kt similarity index 100% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposebinding/HelloWorkflow.kt rename to samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocomposebinding/HelloWorkflow.kt diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/ComposeWorkflow.kt b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocomposeworkflow/ComposeWorkflow.kt similarity index 100% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/ComposeWorkflow.kt rename to samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocomposeworkflow/ComposeWorkflow.kt diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/ComposeWorkflowImpl.kt b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocomposeworkflow/ComposeWorkflowImpl.kt similarity index 100% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/ComposeWorkflowImpl.kt rename to samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocomposeworkflow/ComposeWorkflowImpl.kt diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflow.kt b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflow.kt similarity index 64% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflow.kt rename to samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflow.kt index ac91f5afb..e30b40411 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflow.kt +++ b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflow.kt @@ -1,5 +1,3 @@ -@file:OptIn(WorkflowExperimentalRuntime::class) - package com.squareup.sample.compose.hellocomposeworkflow import androidx.compose.foundation.clickable @@ -8,18 +6,12 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview import com.squareup.sample.compose.hellocomposeworkflow.HelloComposeWorkflow.Toggle import com.squareup.workflow1.Sink -import com.squareup.workflow1.WorkflowExperimentalRuntime -import com.squareup.workflow1.config.AndroidRuntimeConfigTools import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.compose.WorkflowRendering -import com.squareup.workflow1.ui.compose.renderAsState /** * A [ComposeWorkflow] that is used by [HelloWorkflow] to render the screen. @@ -47,15 +39,3 @@ object HelloComposeWorkflow : ComposeWorkflow() { } } } - -@OptIn(WorkflowUiExperimentalApi::class) -@Preview(showBackground = true) -@Composable -fun HelloComposeWorkflowPreview() { - val rendering by HelloComposeWorkflow.renderAsState( - props = "hello", - onOutput = {}, - runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig() - ) - WorkflowRendering(rendering, ViewEnvironment.EMPTY) -} diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/HelloWorkflow.kt b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocomposeworkflow/HelloWorkflow.kt similarity index 100% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/HelloWorkflow.kt rename to samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/hellocomposeworkflow/HelloWorkflow.kt diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingWorkflow.kt b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/inlinerendering/InlineRenderingWorkflow.kt similarity index 83% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingWorkflow.kt rename to samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/inlinerendering/InlineRenderingWorkflow.kt index 3e77d96fb..09a966d00 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingWorkflow.kt +++ b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/inlinerendering/InlineRenderingWorkflow.kt @@ -1,4 +1,4 @@ -@file:OptIn(WorkflowUiExperimentalApi::class, WorkflowExperimentalRuntime::class) +@file:OptIn(WorkflowUiExperimentalApi::class) package com.squareup.sample.compose.inlinerendering @@ -15,11 +15,9 @@ import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.tooling.preview.Preview +import com.squareup.sample.compose.defaultRuntimeConfig import com.squareup.workflow1.Snapshot import com.squareup.workflow1.StatefulWorkflow -import com.squareup.workflow1.WorkflowExperimentalRuntime -import com.squareup.workflow1.config.AndroidRuntimeConfigTools import com.squareup.workflow1.parse import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ViewEnvironment @@ -58,17 +56,11 @@ fun InlineRenderingWorkflowRendering() { val rendering by InlineRenderingWorkflow.renderAsState( props = Unit, onOutput = {}, - runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig() + runtimeConfig = defaultRuntimeConfig() ) WorkflowRendering(rendering, ViewEnvironment.EMPTY) } -@Preview(showBackground = true) -@Composable -internal fun InlineRenderingWorkflowPreview() { - InlineRenderingWorkflowRendering() -} - @OptIn(ExperimentalAnimationApi::class) @Composable private fun AnimatedCounter( diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/preview/PreviewActivity.kt b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/preview/PreviewApp.kt similarity index 78% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/preview/PreviewActivity.kt rename to samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/preview/PreviewApp.kt index 1606fc23b..1bd310d74 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/preview/PreviewActivity.kt +++ b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/preview/PreviewApp.kt @@ -2,9 +2,6 @@ package com.squareup.sample.compose.preview -import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy @@ -13,47 +10,16 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.padding import androidx.compose.material.Card import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.compose.ComposeScreen import com.squareup.workflow1.ui.compose.WorkflowRendering -import com.squareup.workflow1.ui.compose.tooling.Preview - -class PreviewActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - PreviewApp() - } - } -} - -val previewContactRendering = ContactRendering( - name = "Dim Tonnelly", - details = ContactDetailsRendering( - phoneNumber = "555-555-5555", - address = "1234 Apgar Lane" - ) -) - -@Preview -@Composable -fun PreviewApp() { - MaterialTheme { - Surface { - previewContactRendering.Preview() - } - } -} data class ContactRendering( val name: String, @@ -95,3 +61,11 @@ private fun ContactDetails( } } } + +val previewContactRendering = ContactRendering( + name = "Dim Tonnelly", + details = ContactDetailsRendering( + phoneNumber = "555-555-5555", + address = "1234 Apgar Lane" + ) +) diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/textinput/TextInputViewFactory.kt b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/textinput/TextInputViewFactory.kt similarity index 77% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/textinput/TextInputViewFactory.kt rename to samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/textinput/TextInputViewFactory.kt index 979dedf2e..6b4cb4710 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/textinput/TextInputViewFactory.kt +++ b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/textinput/TextInputViewFactory.kt @@ -9,19 +9,15 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.Button import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text -import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.squareup.sample.compose.textinput.TextInputWorkflow.Rendering -import com.squareup.workflow1.ui.TextController import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.compose.ScreenComposableFactory import com.squareup.workflow1.ui.compose.asMutableState -import com.squareup.workflow1.ui.compose.tooling.Preview @OptIn(WorkflowUiExperimentalApi::class) val TextInputViewFactory = ScreenComposableFactory { rendering, _ -> @@ -47,15 +43,3 @@ val TextInputViewFactory = ScreenComposableFactory { rendering, _ -> } } } - -@OptIn(WorkflowUiExperimentalApi::class) -@Preview(showBackground = true) -@Composable -private fun TextInputViewFactoryPreview() { - TextInputViewFactory.Preview( - Rendering( - textController = TextController("Hello world"), - onSwapText = {} - ) - ) -} diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/textinput/TextInputWorkflow.kt b/samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/textinput/TextInputWorkflow.kt similarity index 100% rename from samples/compose-samples/src/main/java/com/squareup/sample/compose/textinput/TextInputWorkflow.kt rename to samples/compose-samples/src/commonMain/kotlin/com/squareup/sample/compose/textinput/TextInputWorkflow.kt diff --git a/samples/compose-samples/src/iosMain/kotlin/com/squareup/sample/compose/DefaultsIos.kt b/samples/compose-samples/src/iosMain/kotlin/com/squareup/sample/compose/DefaultsIos.kt new file mode 100644 index 000000000..1790cdf0d --- /dev/null +++ b/samples/compose-samples/src/iosMain/kotlin/com/squareup/sample/compose/DefaultsIos.kt @@ -0,0 +1,16 @@ +package com.squareup.sample.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import store + +@Composable +actual fun BackHandler(isEnabled: Boolean, onBack: () -> Unit) { + LaunchedEffect(isEnabled) { + if (isEnabled) { + store.events.collect { + onBack() + } + } + } +} diff --git a/samples/compose-samples/src/iosMain/kotlin/com/squareup/sample/compose/MainViewController.kt b/samples/compose-samples/src/iosMain/kotlin/com/squareup/sample/compose/MainViewController.kt new file mode 100644 index 000000000..b5a2d9063 --- /dev/null +++ b/samples/compose-samples/src/iosMain/kotlin/com/squareup/sample/compose/MainViewController.kt @@ -0,0 +1,37 @@ +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.window.ComposeUIViewController +import com.squareup.sample.compose.defaultViewEnvironment +import com.squareup.sample.compose.launcher.SampleWorkflow +import com.squareup.sample.compose.states.Action +import com.squareup.sample.compose.states.Store +import com.squareup.sample.compose.states.createStore +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.compose.WorkflowRendering +import com.squareup.workflow1.ui.compose.renderAsState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.withContext + +fun MainViewController() = ComposeUIViewController { App() } + +val store: Store = CoroutineScope(SupervisorJob()).createStore() + +@OptIn(WorkflowUiExperimentalApi::class) +@Composable +fun App() { + MaterialTheme { + val rendering by SampleWorkflow.renderAsState(Unit) {} + + WorkflowRendering( + rendering, + defaultViewEnvironment() + ) + } +} + +fun onBackGesture() { + store.send(Action.OnBackPressed) +} diff --git a/samples/compose-samples/src/iosMain/kotlin/com/squareup/sample/compose/inlinerendering/InlineRenderingApp.kt b/samples/compose-samples/src/iosMain/kotlin/com/squareup/sample/compose/inlinerendering/InlineRenderingApp.kt new file mode 100644 index 000000000..a0cc88754 --- /dev/null +++ b/samples/compose-samples/src/iosMain/kotlin/com/squareup/sample/compose/inlinerendering/InlineRenderingApp.kt @@ -0,0 +1,15 @@ +package com.squareup.sample.compose.inlinerendering + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import com.squareup.sample.compose.defaultViewEnvironment +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.compose.WorkflowRendering +import com.squareup.workflow1.ui.compose.renderAsState + +@OptIn(WorkflowUiExperimentalApi::class) +@Composable +fun InlineRenderingApp() { + val rendering by InlineRenderingWorkflow.renderAsState(Unit) {} + WorkflowRendering(rendering, defaultViewEnvironment()) +} diff --git a/samples/compose-samples/src/iosMain/kotlin/com/squareup/sample/compose/launcher/SampleScreen.kt b/samples/compose-samples/src/iosMain/kotlin/com/squareup/sample/compose/launcher/SampleScreen.kt new file mode 100644 index 000000000..cd5d076f5 --- /dev/null +++ b/samples/compose-samples/src/iosMain/kotlin/com/squareup/sample/compose/launcher/SampleScreen.kt @@ -0,0 +1,44 @@ +@file:OptIn(WorkflowUiExperimentalApi::class) + +package com.squareup.sample.compose.launcher + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Card +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.compose.ComposeScreen + +data class SampleScreen(val onSampleClicked: (Sample) -> Unit) : ComposeScreen { + @Composable + override fun Content(viewEnvironment: ViewEnvironment) { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxSize().padding(16.dp) + ) { + items(samples) { sample -> + Card( + modifier = Modifier + .fillMaxWidth() + .clickable { onSampleClicked(sample) } + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text(sample.name, style = MaterialTheme.typography.h6) + Text(sample.description, style = MaterialTheme.typography.body1) + } + } + } + } + } +} diff --git a/samples/compose-samples/src/iosMain/kotlin/com/squareup/sample/compose/launcher/SampleWorkflow.kt b/samples/compose-samples/src/iosMain/kotlin/com/squareup/sample/compose/launcher/SampleWorkflow.kt new file mode 100644 index 000000000..8c4ab92de --- /dev/null +++ b/samples/compose-samples/src/iosMain/kotlin/com/squareup/sample/compose/launcher/SampleWorkflow.kt @@ -0,0 +1,60 @@ +@file:OptIn(WorkflowUiExperimentalApi::class) + +package com.squareup.sample.compose.launcher + +import androidx.compose.runtime.Composable +import com.squareup.sample.compose.BackHandler +import com.squareup.sample.compose.hellocomposebinding.HelloWorkflow +import com.squareup.sample.compose.hellocomposebinding.viewEnvironment +import com.squareup.sample.compose.hellocomposeworkflow.HelloComposeWorkflow +import com.squareup.workflow1.Snapshot +import com.squareup.workflow1.StatefulWorkflow +import com.squareup.workflow1.Workflow +import com.squareup.workflow1.WorkflowAction.Companion.noAction +import com.squareup.workflow1.mapRendering +import com.squareup.workflow1.renderChild +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.compose.ComposeScreen +import com.squareup.workflow1.ui.compose.WorkflowRendering +import com.squareup.workflow1.ui.withEnvironment + +object SampleWorkflow : StatefulWorkflow() { + override fun render( + renderProps: Unit, + renderState: Sample?, + context: RenderContext + ): Screen { + val screen = when (val workflow = renderState?.workflow) { + null -> SampleScreen(context.eventHandler { sample -> state = sample }) + is HelloComposeWorkflow -> context.renderChild( + child = workflow, + props = "Hello", + ) { noAction() } + + is HelloWorkflow -> context.renderChild( + child = workflow.mapRendering { it.withEnvironment(viewEnvironment) } + ) { noAction() } + + else -> context.renderChild(child = workflow as Workflow) + } + + return BackHandlerScreen(screen, onBack = context.eventHandler { state = null }) + } + + override fun initialState( + props: Unit, + snapshot: Snapshot? + ) = null + + override fun snapshotState(state: Sample?): Snapshot? = null +} + +data class BackHandlerScreen(val screen: Screen, val onBack: () -> Unit) : ComposeScreen { + @Composable + override fun Content(viewEnvironment: ViewEnvironment) { + BackHandler(onBack = onBack) + WorkflowRendering(screen, viewEnvironment) + } +} diff --git a/samples/compose-samples/src/iosMain/kotlin/com/squareup/sample/compose/launcher/Samples.kt b/samples/compose-samples/src/iosMain/kotlin/com/squareup/sample/compose/launcher/Samples.kt new file mode 100644 index 000000000..eef5f6348 --- /dev/null +++ b/samples/compose-samples/src/iosMain/kotlin/com/squareup/sample/compose/launcher/Samples.kt @@ -0,0 +1,52 @@ +package com.squareup.sample.compose.launcher + +import com.squareup.sample.compose.inlinerendering.InlineRenderingWorkflow +import com.squareup.workflow1.Workflow +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +@OptIn(WorkflowUiExperimentalApi::class) +val samples = listOf( + Sample( + "Compose Workflow", + "Demonstrates a special implementation of Workflow that lets the workflow define " + + "its own composable content inline.", + com.squareup.sample.compose.hellocomposeworkflow.HelloWorkflow + ), + Sample( + "Hello Compose", + "A pure Compose app that launches its root Workflow from inside Compose.", + com.squareup.sample.compose.hellocompose.HelloComposeWorkflow + ), + Sample( + "Hello Compose Binding", + "Binds a Screen to a UI factory using ScreenComposableFactory().", + com.squareup.sample.compose.hellocomposebinding.HelloWorkflow + ), + + // TODO: Migrate :workflow-ui:compose-tooling to be multiplatform to display this sample + // since both of these samples utilize the special preview view environments + // Sample( + // "Text Input", + // "Demonstrates a workflow that drives a TextField.", + // TextInputWorkflow + // ), + // + // + // Sample( + // "ViewFactory Preview", + // "Demonstrates displaying @Previews of ViewFactories.", + // ComposeRendering(previewContactRendering) + // ), + Sample( + "Inline ComposeRendering", + "Demonstrates a workflow that returns an anonymous ComposeRendering.", + InlineRenderingWorkflow + ) +) + +data class Sample @OptIn(WorkflowUiExperimentalApi::class) constructor( + val name: String, + val description: String, + val workflow: Workflow<*, *, Screen> +) diff --git a/samples/compose-samples/src/iosMain/kotlin/com/squareup/sample/compose/states/Store.kt b/samples/compose-samples/src/iosMain/kotlin/com/squareup/sample/compose/states/Store.kt new file mode 100644 index 000000000..d477f548a --- /dev/null +++ b/samples/compose-samples/src/iosMain/kotlin/com/squareup/sample/compose/states/Store.kt @@ -0,0 +1,30 @@ +package com.squareup.sample.compose.states + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch + +sealed interface Action { + data object OnBackPressed : Action +} + +interface Store { + fun send(action: Action) + val events: SharedFlow +} + +fun CoroutineScope.createStore(): Store { + val events = MutableSharedFlow() + + return object : Store { + override fun send(action: Action) { + launch { + events.emit(action) + } + } + + override val events: SharedFlow = events.asSharedFlow() + } +} diff --git a/samples/compose-samples/src/nonAndroidMain/kotlin/com/squareup/sample/compose/DefaultsNonAndroid.kt b/samples/compose-samples/src/nonAndroidMain/kotlin/com/squareup/sample/compose/DefaultsNonAndroid.kt new file mode 100644 index 000000000..dcc7476cd --- /dev/null +++ b/samples/compose-samples/src/nonAndroidMain/kotlin/com/squareup/sample/compose/DefaultsNonAndroid.kt @@ -0,0 +1,11 @@ +package com.squareup.sample.compose + +import com.squareup.workflow1.RuntimeConfig +import com.squareup.workflow1.RuntimeConfigOptions +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +actual fun defaultRuntimeConfig(): RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG + +@OptIn(WorkflowUiExperimentalApi::class) +actual fun defaultViewEnvironment(): ViewEnvironment = ViewEnvironment.EMPTY diff --git a/samples/compose-samples/src/nonMobileMain/kotlin/com/squareup/sample/compose/DefaultsNonMobile.kt b/samples/compose-samples/src/nonMobileMain/kotlin/com/squareup/sample/compose/DefaultsNonMobile.kt new file mode 100644 index 000000000..05925923f --- /dev/null +++ b/samples/compose-samples/src/nonMobileMain/kotlin/com/squareup/sample/compose/DefaultsNonMobile.kt @@ -0,0 +1,6 @@ +package com.squareup.sample.compose + +import androidx.compose.runtime.Composable + +@Composable +actual fun BackHandler(isEnabled: Boolean, onBack: () -> Unit) = Unit diff --git a/samples/containers/android/build.gradle.kts b/samples/containers/android/build.gradle.kts index bfa449036..93b33534e 100644 --- a/samples/containers/android/build.gradle.kts +++ b/samples/containers/android/build.gradle.kts @@ -27,7 +27,7 @@ dependencies { api(project(":samples:containers:common")) api(project(":workflow-ui:core-android")) - api(project(":workflow-ui:core-common")) + api(project(":workflow-ui:core")) implementation(libs.androidx.appcompat) implementation(libs.androidx.core) diff --git a/samples/containers/common/build.gradle.kts b/samples/containers/common/build.gradle.kts index 6616f0991..8a75c69aa 100644 --- a/samples/containers/common/build.gradle.kts +++ b/samples/containers/common/build.gradle.kts @@ -3,7 +3,7 @@ plugins { } dependencies { - api(project(":workflow-ui:core-common")) + api(project(":workflow-ui:core")) implementation(libs.kotlin.jdk6) diff --git a/samples/containers/hello-back-button/build.gradle.kts b/samples/containers/hello-back-button/build.gradle.kts index 6cd6a7118..21218a84f 100644 --- a/samples/containers/hello-back-button/build.gradle.kts +++ b/samples/containers/hello-back-button/build.gradle.kts @@ -20,5 +20,5 @@ dependencies { implementation(project(":samples:containers:android")) implementation(project(":workflow-ui:core-android")) - implementation(project(":workflow-ui:core-common")) + implementation(project(":workflow-ui:core")) } diff --git a/samples/containers/poetry/build.gradle.kts b/samples/containers/poetry/build.gradle.kts index e362d69a1..3872cc39e 100644 --- a/samples/containers/poetry/build.gradle.kts +++ b/samples/containers/poetry/build.gradle.kts @@ -15,7 +15,7 @@ dependencies { api(project(":samples:containers:common")) api(project(":workflow-core")) api(project(":workflow-ui:core-android")) - api(project(":workflow-ui:core-common")) + api(project(":workflow-ui:core")) implementation(libs.androidx.appcompat) implementation(libs.androidx.recyclerview) diff --git a/samples/dungeon/app/build.gradle.kts b/samples/dungeon/app/build.gradle.kts index 0c15f339a..624250147 100644 --- a/samples/dungeon/app/build.gradle.kts +++ b/samples/dungeon/app/build.gradle.kts @@ -49,7 +49,7 @@ dependencies { implementation(project(":samples:dungeon:timemachine-shakeable")) implementation(project(":workflow-tracing")) implementation(project(":workflow-ui:core-android")) - implementation(project(":workflow-ui:core-common")) + implementation(project(":workflow-ui:core")) testImplementation(libs.junit) testImplementation(libs.truth) diff --git a/samples/dungeon/common/build.gradle.kts b/samples/dungeon/common/build.gradle.kts index c635fbdd4..e85a53c80 100644 --- a/samples/dungeon/common/build.gradle.kts +++ b/samples/dungeon/common/build.gradle.kts @@ -7,7 +7,7 @@ dependencies { api(libs.squareup.okio) api(project(":workflow-core")) - api(project(":workflow-ui:core-common")) + api(project(":workflow-ui:core")) implementation(libs.kotlin.jdk8) implementation(libs.kotlinx.coroutines.core) diff --git a/samples/dungeon/timemachine-shakeable/build.gradle.kts b/samples/dungeon/timemachine-shakeable/build.gradle.kts index be1aeab47..4535fc332 100644 --- a/samples/dungeon/timemachine-shakeable/build.gradle.kts +++ b/samples/dungeon/timemachine-shakeable/build.gradle.kts @@ -14,7 +14,7 @@ dependencies { api(project(":samples:dungeon:timemachine")) api(project(":workflow-core")) api(project(":workflow-ui:core-android")) - api(project(":workflow-ui:core-common")) + api(project(":workflow-ui:core")) implementation(libs.androidx.appcompat) implementation(libs.androidx.constraintlayout) diff --git a/samples/hello-workflow-fragment/build.gradle.kts b/samples/hello-workflow-fragment/build.gradle.kts index 720f031b7..d060e07d2 100644 --- a/samples/hello-workflow-fragment/build.gradle.kts +++ b/samples/hello-workflow-fragment/build.gradle.kts @@ -20,5 +20,5 @@ dependencies { implementation(libs.androidx.lifecycle.viewmodel.savedstate) implementation(project(":workflow-ui:core-android")) - implementation(project(":workflow-ui:core-common")) + implementation(project(":workflow-ui:core")) } diff --git a/samples/hello-workflow/build.gradle.kts b/samples/hello-workflow/build.gradle.kts index 8a584863d..65f1e4710 100644 --- a/samples/hello-workflow/build.gradle.kts +++ b/samples/hello-workflow/build.gradle.kts @@ -21,5 +21,5 @@ dependencies { implementation(libs.androidx.viewbinding) implementation(project(":workflow-ui:core-android")) - implementation(project(":workflow-ui:core-common")) + implementation(project(":workflow-ui:core")) } diff --git a/samples/nested-overlays/build.gradle.kts b/samples/nested-overlays/build.gradle.kts index d3ecfb610..d618ed695 100644 --- a/samples/nested-overlays/build.gradle.kts +++ b/samples/nested-overlays/build.gradle.kts @@ -21,5 +21,5 @@ dependencies { implementation(libs.androidx.viewbinding) implementation(project(":workflow-ui:core-android")) - implementation(project(":workflow-ui:core-common")) + implementation(project(":workflow-ui:core")) } diff --git a/samples/stub-visibility/build.gradle.kts b/samples/stub-visibility/build.gradle.kts index 5faafe000..ff54d305b 100644 --- a/samples/stub-visibility/build.gradle.kts +++ b/samples/stub-visibility/build.gradle.kts @@ -21,5 +21,5 @@ dependencies { implementation(libs.androidx.viewbinding) implementation(project(":workflow-ui:core-android")) - implementation(project(":workflow-ui:core-common")) + implementation(project(":workflow-ui:core")) } diff --git a/samples/tictactoe/app/build.gradle.kts b/samples/tictactoe/app/build.gradle.kts index 3bf58b7d7..b0a330407 100644 --- a/samples/tictactoe/app/build.gradle.kts +++ b/samples/tictactoe/app/build.gradle.kts @@ -41,5 +41,5 @@ dependencies { implementation(project(":samples:tictactoe:common")) implementation(project(":workflow-tracing")) implementation(project(":workflow-ui:core-android")) - implementation(project(":workflow-ui:core-common")) + implementation(project(":workflow-ui:core")) } diff --git a/samples/tictactoe/common/build.gradle.kts b/samples/tictactoe/common/build.gradle.kts index ca3b3ad1c..58e325675 100644 --- a/samples/tictactoe/common/build.gradle.kts +++ b/samples/tictactoe/common/build.gradle.kts @@ -7,7 +7,7 @@ dependencies { api(libs.squareup.okio) api(project(":workflow-core")) - api(project(":workflow-ui:core-common")) + api(project(":workflow-ui:core")) implementation(libs.kotlin.jdk6) implementation(libs.kotlinx.coroutines.core) diff --git a/samples/todo-android/app/build.gradle.kts b/samples/todo-android/app/build.gradle.kts index c6b5b76b8..4c9196178 100644 --- a/samples/todo-android/app/build.gradle.kts +++ b/samples/todo-android/app/build.gradle.kts @@ -31,7 +31,7 @@ dependencies { implementation(project(":workflow-core")) implementation(project(":workflow-tracing")) implementation(project(":workflow-ui:core-android")) - implementation(project(":workflow-ui:core-common")) + implementation(project(":workflow-ui:core")) testImplementation(libs.junit) testImplementation(libs.truth) diff --git a/settings.gradle.kts b/settings.gradle.kts index 0cc8d2b26..f0d7719eb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -69,6 +69,7 @@ include( ":workflow-tracing", ":workflow-ui:compose", ":workflow-ui:compose-tooling", + ":workflow-ui:core", ":workflow-ui:core-common", ":workflow-ui:core-android", ":workflow-ui:internal-testing-android", diff --git a/trace-encoder/dependencies/runtimeClasspath.txt b/trace-encoder/dependencies/runtimeClasspath.txt index 1562a659c..44200e3e2 100644 --- a/trace-encoder/dependencies/runtimeClasspath.txt +++ b/trace-encoder/dependencies/runtimeClasspath.txt @@ -2,11 +2,11 @@ com.squareup.moshi:moshi-adapters:1.15.0 com.squareup.moshi:moshi:1.15.0 com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlin:kotlin-bom:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib:1.9.24 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 diff --git a/workflow-config/config-android/dependencies/releaseRuntimeClasspath.txt b/workflow-config/config-android/dependencies/releaseRuntimeClasspath.txt index d8cb7353d..3d72a2ba1 100644 --- a/workflow-config/config-android/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-config/config-android/dependencies/releaseRuntimeClasspath.txt @@ -1,10 +1,10 @@ com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlin:kotlin-bom:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib:1.9.24 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 diff --git a/workflow-config/config-jvm/dependencies/runtimeClasspath.txt b/workflow-config/config-jvm/dependencies/runtimeClasspath.txt index d8cb7353d..3d72a2ba1 100644 --- a/workflow-config/config-jvm/dependencies/runtimeClasspath.txt +++ b/workflow-config/config-jvm/dependencies/runtimeClasspath.txt @@ -1,10 +1,10 @@ com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlin:kotlin-bom:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib:1.9.24 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 diff --git a/workflow-core/build.gradle.kts b/workflow-core/build.gradle.kts index e03ba149f..ffc274f23 100644 --- a/workflow-core/build.gradle.kts +++ b/workflow-core/build.gradle.kts @@ -1,4 +1,4 @@ -import com.squareup.workflow1.buildsrc.iosWithSimulatorArm64 +import com.squareup.workflow1.buildsrc.iosTargets plugins { id("kotlin-multiplatform") @@ -6,9 +6,19 @@ plugins { } kotlin { + targets.all { + compilations.all { + compileTaskProvider.configure { + compilerOptions { + freeCompilerArgs.add("-Xexpect-actual-classes") + } + } + } + } + val targets = project.findProperty("workflow.targets") ?: "kmp" if (targets == "kmp" || targets == "ios") { - iosWithSimulatorArm64(project) + iosTargets() } if (targets == "kmp" || targets == "jvm") { jvm { withJava() } diff --git a/workflow-core/dependencies/jsRuntimeClasspath.txt b/workflow-core/dependencies/jsRuntimeClasspath.txt index 6dd90ac24..a5e6dd8ce 100644 --- a/workflow-core/dependencies/jsRuntimeClasspath.txt +++ b/workflow-core/dependencies/jsRuntimeClasspath.txt @@ -1,11 +1,10 @@ com.squareup.okio:okio-js:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.9.10 -org.jetbrains.kotlin:kotlin-dom-api-compat:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-js:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlin:kotlin-bom:1.9.24 +org.jetbrains.kotlin:kotlin-dom-api-compat:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-js:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib:1.9.24 org.jetbrains.kotlin:kotlinx-atomicfu-runtime:1.8.20 org.jetbrains.kotlinx:atomicfu-js:0.21.0 org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -org.jetbrains:annotations:13.0 diff --git a/workflow-core/dependencies/jvmRuntimeClasspath.txt b/workflow-core/dependencies/jvmRuntimeClasspath.txt index 3ced6a669..9bb8a4666 100644 --- a/workflow-core/dependencies/jvmRuntimeClasspath.txt +++ b/workflow-core/dependencies/jvmRuntimeClasspath.txt @@ -1,9 +1,9 @@ com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlin:kotlin-bom:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib:1.9.24 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 diff --git a/workflow-core/dependencies/runtimeClasspath.txt b/workflow-core/dependencies/runtimeClasspath.txt index d8cb7353d..3d72a2ba1 100644 --- a/workflow-core/dependencies/runtimeClasspath.txt +++ b/workflow-core/dependencies/runtimeClasspath.txt @@ -1,10 +1,10 @@ com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlin:kotlin-bom:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib:1.9.24 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 diff --git a/workflow-runtime/build.gradle.kts b/workflow-runtime/build.gradle.kts index 6d4ea4339..fde010973 100644 --- a/workflow-runtime/build.gradle.kts +++ b/workflow-runtime/build.gradle.kts @@ -1,4 +1,4 @@ -import com.squareup.workflow1.buildsrc.iosWithSimulatorArm64 +import com.squareup.workflow1.buildsrc.iosTargets plugins { id("kotlin-multiplatform") @@ -8,7 +8,7 @@ plugins { kotlin { val targets = project.findProperty("workflow.targets") ?: "kmp" if (targets == "kmp" || targets == "ios") { - iosWithSimulatorArm64(project) + iosTargets() } if (targets == "kmp" || targets == "jvm") { jvm {} diff --git a/workflow-runtime/dependencies/jsRuntimeClasspath.txt b/workflow-runtime/dependencies/jsRuntimeClasspath.txt index 6dd90ac24..a5e6dd8ce 100644 --- a/workflow-runtime/dependencies/jsRuntimeClasspath.txt +++ b/workflow-runtime/dependencies/jsRuntimeClasspath.txt @@ -1,11 +1,10 @@ com.squareup.okio:okio-js:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.9.10 -org.jetbrains.kotlin:kotlin-dom-api-compat:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-js:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlin:kotlin-bom:1.9.24 +org.jetbrains.kotlin:kotlin-dom-api-compat:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-js:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib:1.9.24 org.jetbrains.kotlin:kotlinx-atomicfu-runtime:1.8.20 org.jetbrains.kotlinx:atomicfu-js:0.21.0 org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -org.jetbrains:annotations:13.0 diff --git a/workflow-runtime/dependencies/jvmRuntimeClasspath.txt b/workflow-runtime/dependencies/jvmRuntimeClasspath.txt index 3ced6a669..9bb8a4666 100644 --- a/workflow-runtime/dependencies/jvmRuntimeClasspath.txt +++ b/workflow-runtime/dependencies/jvmRuntimeClasspath.txt @@ -1,9 +1,9 @@ com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlin:kotlin-bom:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib:1.9.24 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 diff --git a/workflow-rx2/dependencies/runtimeClasspath.txt b/workflow-rx2/dependencies/runtimeClasspath.txt index 1b8022059..00f28d4a7 100644 --- a/workflow-rx2/dependencies/runtimeClasspath.txt +++ b/workflow-rx2/dependencies/runtimeClasspath.txt @@ -1,11 +1,11 @@ com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 io.reactivex.rxjava2:rxjava:2.2.21 -org.jetbrains.kotlin:kotlin-bom:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlin:kotlin-bom:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib:1.9.24 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 diff --git a/workflow-testing/dependencies/runtimeClasspath.txt b/workflow-testing/dependencies/runtimeClasspath.txt index 1de4577fd..85ebe34a6 100644 --- a/workflow-testing/dependencies/runtimeClasspath.txt +++ b/workflow-testing/dependencies/runtimeClasspath.txt @@ -2,12 +2,12 @@ app.cash.turbine:turbine-jvm:1.0.0 app.cash.turbine:turbine:1.0.0 com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.9.10 -org.jetbrains.kotlin:kotlin-reflect:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlin:kotlin-bom:1.9.24 +org.jetbrains.kotlin:kotlin-reflect:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib:1.9.24 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 diff --git a/workflow-tracing/dependencies/runtimeClasspath.txt b/workflow-tracing/dependencies/runtimeClasspath.txt index 1562a659c..44200e3e2 100644 --- a/workflow-tracing/dependencies/runtimeClasspath.txt +++ b/workflow-tracing/dependencies/runtimeClasspath.txt @@ -2,11 +2,11 @@ com.squareup.moshi:moshi-adapters:1.15.0 com.squareup.moshi:moshi:1.15.0 com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlin:kotlin-bom:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib:1.9.24 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 diff --git a/workflow-ui/compose-tooling/build.gradle.kts b/workflow-ui/compose-tooling/build.gradle.kts index fddcb9a74..1ff5a300b 100644 --- a/workflow-ui/compose-tooling/build.gradle.kts +++ b/workflow-ui/compose-tooling/build.gradle.kts @@ -46,5 +46,5 @@ dependencies { implementation(project(":workflow-ui:compose")) implementation(project(":workflow-ui:core-android")) - implementation(project(":workflow-ui:core-common")) + implementation(project(":workflow-ui:core")) } diff --git a/workflow-ui/compose-tooling/dependencies/releaseRuntimeClasspath.txt b/workflow-ui/compose-tooling/dependencies/releaseRuntimeClasspath.txt index 6f7c4c2c0..fdfa30543 100644 --- a/workflow-ui/compose-tooling/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-ui/compose-tooling/dependencies/releaseRuntimeClasspath.txt @@ -1,41 +1,63 @@ -androidx.activity:activity-compose:1.6.1 -androidx.activity:activity-ktx:1.6.1 -androidx.activity:activity:1.6.1 +androidx.activity:activity-compose:1.7.0 +androidx.activity:activity-ktx:1.7.0 +androidx.activity:activity:1.7.0 androidx.annotation:annotation-experimental:1.3.0 -androidx.annotation:annotation-jvm:1.6.0 -androidx.annotation:annotation:1.6.0 +androidx.annotation:annotation-jvm:1.8.0 +androidx.annotation:annotation:1.8.0 androidx.arch.core:core-common:2.2.0 androidx.arch.core:core-runtime:2.2.0 androidx.autofill:autofill:1.0.0 -androidx.collection:collection:1.1.0 -androidx.compose.animation:animation-core:1.3.3 -androidx.compose.animation:animation:1.3.3 -androidx.compose.foundation:foundation-layout:1.3.1 -androidx.compose.foundation:foundation:1.3.1 -androidx.compose.runtime:runtime-saveable:1.3.3 -androidx.compose.runtime:runtime:1.3.3 -androidx.compose.ui:ui-geometry:1.3.3 -androidx.compose.ui:ui-graphics:1.3.3 -androidx.compose.ui:ui-text:1.3.3 -androidx.compose.ui:ui-tooling-preview:1.3.3 -androidx.compose.ui:ui-unit:1.3.3 -androidx.compose.ui:ui-util:1.3.3 -androidx.compose.ui:ui:1.3.3 -androidx.compose:compose-bom:2023.01.00 +androidx.collection:collection-jvm:1.4.0 +androidx.collection:collection-ktx:1.4.0 +androidx.collection:collection:1.4.0 +androidx.compose.animation:animation-android:1.6.7 +androidx.compose.animation:animation-core-android:1.6.7 +androidx.compose.animation:animation-core:1.6.7 +androidx.compose.animation:animation:1.6.7 +androidx.compose.foundation:foundation-android:1.6.7 +androidx.compose.foundation:foundation-layout-android:1.6.7 +androidx.compose.foundation:foundation-layout:1.6.7 +androidx.compose.foundation:foundation:1.6.7 +androidx.compose.runtime:runtime-android:1.6.7 +androidx.compose.runtime:runtime-saveable-android:1.6.7 +androidx.compose.runtime:runtime-saveable:1.6.7 +androidx.compose.runtime:runtime:1.6.7 +androidx.compose.ui:ui-android:1.6.7 +androidx.compose.ui:ui-geometry-android:1.6.7 +androidx.compose.ui:ui-geometry:1.6.7 +androidx.compose.ui:ui-graphics-android:1.6.7 +androidx.compose.ui:ui-graphics:1.6.7 +androidx.compose.ui:ui-text-android:1.6.7 +androidx.compose.ui:ui-text:1.6.7 +androidx.compose.ui:ui-tooling-preview-android:1.6.7 +androidx.compose.ui:ui-tooling-preview:1.6.7 +androidx.compose.ui:ui-unit-android:1.6.7 +androidx.compose.ui:ui-unit:1.6.7 +androidx.compose.ui:ui-util-android:1.6.7 +androidx.compose.ui:ui-util:1.6.7 +androidx.compose.ui:ui:1.6.7 +androidx.compose:compose-bom:2024.05.00 androidx.concurrent:concurrent-futures:1.1.0 androidx.core:core-ktx:1.12.0 androidx.core:core:1.12.0 androidx.customview:customview-poolingcontainer:1.0.0 +androidx.emoji2:emoji2:1.3.0 androidx.interpolator:interpolator:1.0.0 -androidx.lifecycle:lifecycle-common-java8:2.6.1 -androidx.lifecycle:lifecycle-common:2.6.1 -androidx.lifecycle:lifecycle-livedata-core:2.6.1 -androidx.lifecycle:lifecycle-runtime-ktx:2.6.1 -androidx.lifecycle:lifecycle-runtime:2.6.1 -androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1 -androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1 -androidx.lifecycle:lifecycle-viewmodel:2.6.1 -androidx.profileinstaller:profileinstaller:1.3.0 +androidx.lifecycle:lifecycle-common-jvm:2.8.0 +androidx.lifecycle:lifecycle-common:2.8.0 +androidx.lifecycle:lifecycle-livedata-core:2.8.0 +androidx.lifecycle:lifecycle-process:2.8.0 +androidx.lifecycle:lifecycle-runtime-android:2.8.0 +androidx.lifecycle:lifecycle-runtime-compose-android:2.8.0 +androidx.lifecycle:lifecycle-runtime-compose:2.8.0 +androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.0 +androidx.lifecycle:lifecycle-runtime-ktx:2.8.0 +androidx.lifecycle:lifecycle-runtime:2.8.0 +androidx.lifecycle:lifecycle-viewmodel-android:2.8.0 +androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0 +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.0 +androidx.lifecycle:lifecycle-viewmodel:2.8.0 +androidx.profileinstaller:profileinstaller:1.3.1 androidx.savedstate:savedstate-ktx:1.2.1 androidx.savedstate:savedstate:1.2.1 androidx.startup:startup-runtime:1.1.1 @@ -45,11 +67,17 @@ androidx.versionedparcelable:versionedparcelable:1.1.1 com.google.guava:listenablefuture:1.0 com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.8.0 +org.jetbrains.compose.components:components-ui-tooling-preview-android:1.6.11 +org.jetbrains.compose.components:components-ui-tooling-preview:1.6.11 +org.jetbrains.compose.foundation:foundation:1.6.11 +org.jetbrains.compose.runtime:runtime:1.6.11 +org.jetbrains.compose.ui:ui:1.6.11 +org.jetbrains.kotlin:kotlin-bom:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib:1.9.24 org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 diff --git a/workflow-ui/compose/api/android/compose.api b/workflow-ui/compose/api/android/compose.api new file mode 100644 index 000000000..e1d73482a --- /dev/null +++ b/workflow-ui/compose/api/android/compose.api @@ -0,0 +1,78 @@ +public final class com/squareup/workflow1/ui/compose/ComposableSingletons$ScreenComposableFactoryFinderKt { + public static final field INSTANCE Lcom/squareup/workflow1/ui/compose/ComposableSingletons$ScreenComposableFactoryFinderKt; + public static field lambda-1 Lkotlin/jvm/functions/Function4; + public static field lambda-2 Lkotlin/jvm/functions/Function4; + public static field lambda-3 Lkotlin/jvm/functions/Function4; + public fun ()V + public final fun getLambda-1$wf1_compose ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-2$wf1_compose ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-3$wf1_compose ()Lkotlin/jvm/functions/Function4; +} + +public abstract interface class com/squareup/workflow1/ui/compose/ComposeScreen : com/squareup/workflow1/ui/Screen { + public abstract fun Content (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroidx/compose/runtime/Composer;I)V +} + +public final class com/squareup/workflow1/ui/compose/ComposeScreenKt { + public static final fun ComposeScreen (Lkotlin/jvm/functions/Function3;)Lcom/squareup/workflow1/ui/compose/ComposeScreen; +} + +public final class com/squareup/workflow1/ui/compose/CompositionRootKt { + public static final fun withCompositionRoot (Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function3;)Lcom/squareup/workflow1/ui/ViewEnvironment; + public static final fun withCompositionRoot (Lcom/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder;Lkotlin/jvm/functions/Function3;)Lcom/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder; +} + +public final class com/squareup/workflow1/ui/compose/RenderAsStateKt { + public static final fun renderAsState (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/util/List;Lkotlinx/coroutines/CoroutineScope;Ljava/util/Set;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/State; +} + +public abstract interface class com/squareup/workflow1/ui/compose/ScreenComposableFactory : com/squareup/workflow1/ui/ViewRegistry$Entry { + public abstract fun Content (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroidx/compose/runtime/Composer;I)V + public abstract fun getKey ()Lcom/squareup/workflow1/ui/ViewRegistry$Key; + public abstract fun getType ()Lkotlin/reflect/KClass; +} + +public final class com/squareup/workflow1/ui/compose/ScreenComposableFactory$DefaultImpls { + public static fun getKey (Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory;)Lcom/squareup/workflow1/ui/ViewRegistry$Key; +} + +public final class com/squareup/workflow1/ui/compose/ScreenComposableFactoryAndroidKt { + public static final fun asComposableFactory (Lcom/squareup/workflow1/ui/ScreenViewFactory;)Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory; + public static final fun asViewFactory (Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory;)Lcom/squareup/workflow1/ui/ScreenViewFactory; +} + +public abstract interface class com/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder { + public static final field Companion Lcom/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder$Companion; + public abstract fun getComposableFactoryForRendering (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/Screen;)Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory; +} + +public final class com/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder$Companion : com/squareup/workflow1/ui/ViewEnvironmentKey { + public fun getDefault ()Lcom/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder; + public synthetic fun getDefault ()Ljava/lang/Object; +} + +public final class com/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder$DefaultImpls { + public static fun getComposableFactoryForRendering (Lcom/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder;Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/Screen;)Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory; +} + +public final class com/squareup/workflow1/ui/compose/ScreenComposableFactoryFinderKt { + public static final fun requireComposableFactoryForRendering (Lcom/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder;Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/Screen;)Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory; +} + +public final class com/squareup/workflow1/ui/compose/ScreenComposableFactoryKt { + public static final fun ScreenComposableFactory (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function4;)Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory; + public static final fun toComposableFactory (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory; +} + +public final class com/squareup/workflow1/ui/compose/TextControllerAsMutableStateKt { + public static final fun asMutableState (Lcom/squareup/workflow1/ui/TextController;Landroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/MutableState; +} + +public final class com/squareup/workflow1/ui/compose/ViewEnvironmentWithComposeSupportKt { + public static final fun withComposeInteropSupport (Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/ViewEnvironment; +} + +public final class com/squareup/workflow1/ui/compose/WorkflowRenderingKt { + public static final fun WorkflowRendering (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V +} + diff --git a/workflow-ui/compose/api/jvm/compose.api b/workflow-ui/compose/api/jvm/compose.api new file mode 100644 index 000000000..ebe74e6df --- /dev/null +++ b/workflow-ui/compose/api/jvm/compose.api @@ -0,0 +1,69 @@ +public final class com/squareup/workflow1/ui/compose/ComposableSingletons$ScreenComposableFactoryFinderKt { + public static final field INSTANCE Lcom/squareup/workflow1/ui/compose/ComposableSingletons$ScreenComposableFactoryFinderKt; + public static field lambda-1 Lkotlin/jvm/functions/Function4; + public static field lambda-2 Lkotlin/jvm/functions/Function4; + public static field lambda-3 Lkotlin/jvm/functions/Function4; + public fun ()V + public final fun getLambda-1$wf1_compose ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-2$wf1_compose ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-3$wf1_compose ()Lkotlin/jvm/functions/Function4; +} + +public abstract interface class com/squareup/workflow1/ui/compose/ComposeScreen : com/squareup/workflow1/ui/Screen { + public abstract fun Content (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroidx/compose/runtime/Composer;I)V +} + +public final class com/squareup/workflow1/ui/compose/ComposeScreenKt { + public static final fun ComposeScreen (Lkotlin/jvm/functions/Function3;)Lcom/squareup/workflow1/ui/compose/ComposeScreen; +} + +public final class com/squareup/workflow1/ui/compose/CompositionRootKt { + public static final fun withCompositionRoot (Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function3;)Lcom/squareup/workflow1/ui/ViewEnvironment; + public static final fun withCompositionRoot (Lcom/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder;Lkotlin/jvm/functions/Function3;)Lcom/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder; +} + +public final class com/squareup/workflow1/ui/compose/RenderAsStateKt { + public static final fun renderAsState (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/util/List;Lkotlinx/coroutines/CoroutineScope;Ljava/util/Set;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/State; +} + +public abstract interface class com/squareup/workflow1/ui/compose/ScreenComposableFactory : com/squareup/workflow1/ui/ViewRegistry$Entry { + public abstract fun Content (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroidx/compose/runtime/Composer;I)V + public abstract fun getKey ()Lcom/squareup/workflow1/ui/ViewRegistry$Key; + public abstract fun getType ()Lkotlin/reflect/KClass; +} + +public final class com/squareup/workflow1/ui/compose/ScreenComposableFactory$DefaultImpls { + public static fun getKey (Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory;)Lcom/squareup/workflow1/ui/ViewRegistry$Key; +} + +public abstract interface class com/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder { + public static final field Companion Lcom/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder$Companion; + public abstract fun getComposableFactoryForRendering (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/Screen;)Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory; +} + +public final class com/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder$Companion : com/squareup/workflow1/ui/ViewEnvironmentKey { + public fun getDefault ()Lcom/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder; + public synthetic fun getDefault ()Ljava/lang/Object; +} + +public final class com/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder$DefaultImpls { + public static fun getComposableFactoryForRendering (Lcom/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder;Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/Screen;)Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory; +} + +public final class com/squareup/workflow1/ui/compose/ScreenComposableFactoryFinderKt { + public static final fun requireComposableFactoryForRendering (Lcom/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder;Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/Screen;)Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory; +} + +public final class com/squareup/workflow1/ui/compose/ScreenComposableFactoryKt { + public static final fun ScreenComposableFactory (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function4;)Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory; + public static final fun toComposableFactory (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory; +} + +public final class com/squareup/workflow1/ui/compose/TextControllerAsMutableStateKt { + public static final fun asMutableState (Lcom/squareup/workflow1/ui/TextController;Landroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/MutableState; +} + +public final class com/squareup/workflow1/ui/compose/WorkflowRenderingKt { + public static final fun WorkflowRendering (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V +} + diff --git a/workflow-ui/compose/build.gradle.kts b/workflow-ui/compose/build.gradle.kts index ab5ef8990..c9a67a550 100644 --- a/workflow-ui/compose/build.gradle.kts +++ b/workflow-ui/compose/build.gradle.kts @@ -1,60 +1,92 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import com.squareup.workflow1.buildsrc.iosTargets +import org.jetbrains.compose.ExperimentalComposeLibrary +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.dsl.KotlinTargetContainerWithPresetFunctions +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree plugins { + alias(libs.plugins.jetbrains.compose.plugin) + id("kotlin-multiplatform") id("com.android.library") - id("kotlin-android") id("android-defaults") id("android-ui-tests") - id("published") + // id("published") } -android { - buildFeatures.compose = true - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() +fun KotlinTargetContainerWithPresetFunctions.androidTargetWithTesting() { + androidTarget { + @OptIn(ExperimentalKotlinGradlePluginApi::class) + instrumentedTestVariant { + sourceSetTree.set(KotlinSourceSetTree.test) + + dependencies { + debugImplementation(libs.androidx.compose.ui.test.manifest) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + androidTestImplementation(libs.squareup.leakcanary.instrumentation) + androidTestImplementation(project(":workflow-ui:internal-testing-android")) + } + } } - namespace = "com.squareup.workflow1.ui.compose" - testNamespace = "$namespace.test" } -tasks.withType { - kotlinOptions { - @Suppress("SuspiciousCollectionReassignment") - freeCompilerArgs += listOf( - "-opt-in=kotlin.RequiresOptIn" - ) +kotlin { + targets.all { + compilations.all { + kotlinOptions { + freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" + } + } + } + + val targets = project.findProperty("workflow.targets") ?: "kmp" + + listOf( + "ios" to { iosTargets() }, + "jvm" to { jvm() }, + "js" to { js(IR).browser() }, + "android" to { androidTargetWithTesting() }, + ).forEach { (target, action) -> + if (targets == "kmp" || targets == target) { + action() + } + } + + sourceSets { + commonMain.dependencies { + api(project(":workflow-ui:core")) + + implementation(compose.foundation) + implementation(compose.components.uiToolingPreview) + implementation(compose.runtime) + implementation(compose.ui) + implementation(libs.jetbrains.lifecycle.runtime.compose) + + implementation(project(":workflow-core")) + implementation(project(":workflow-runtime")) + } + + commonTest.dependencies { + implementation(libs.kotlin.test.jdk) + + @OptIn(ExperimentalComposeLibrary::class) + implementation(compose.uiTest) + } + + androidMain.dependencies { + api(project(":workflow-ui:core-android")) + implementation(libs.androidx.activity.compose) + } } } -dependencies { - val composeBom = platform(libs.androidx.compose.bom) - - androidTestImplementation(libs.androidx.activity.core) - androidTestImplementation(composeBom) - androidTestImplementation(libs.androidx.compose.foundation) - androidTestImplementation(libs.androidx.compose.ui.test.junit4) - androidTestImplementation(libs.androidx.test.core) - androidTestImplementation(libs.androidx.test.truth) - androidTestImplementation(libs.kotlin.test.jdk) - - androidTestImplementation(project(":workflow-ui:internal-testing-compose")) - - api(libs.androidx.compose.runtime) - api(libs.kotlin.common) - - api(project(":workflow-ui:core-android")) - api(project(":workflow-ui:core-common")) - - implementation(composeBom) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.compose.foundation.layout) - implementation(libs.androidx.compose.runtime.saveable) - implementation(libs.androidx.compose.ui) - implementation(libs.androidx.lifecycle.common) - implementation(libs.androidx.lifecycle.core) - implementation(libs.kotlinx.coroutines.core) - implementation(libs.squareup.okio) - - implementation(project(":workflow-core")) - implementation(project(":workflow-runtime")) +android { + buildFeatures.compose = true + composeOptions.kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() + namespace = "com.squareup.workflow1.ui.compose" + testNamespace = "$namespace.test" + + dependencies { + debugImplementation(compose.uiTooling) + } } diff --git a/workflow-ui/compose/dependencies/releaseRuntimeClasspath.txt b/workflow-ui/compose/dependencies/releaseRuntimeClasspath.txt index 20dbb06a8..5c87d8040 100644 --- a/workflow-ui/compose/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-ui/compose/dependencies/releaseRuntimeClasspath.txt @@ -42,11 +42,11 @@ androidx.versionedparcelable:versionedparcelable:1.1.1 com.google.guava:listenablefuture:1.0 com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlin:kotlin-bom:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib:1.9.24 org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 diff --git a/workflow-ui/compose/src/androidTest/AndroidManifest.xml b/workflow-ui/compose/src/androidInstrumentedTest/AndroidManifest.xml similarity index 100% rename from workflow-ui/compose/src/androidTest/AndroidManifest.xml rename to workflow-ui/compose/src/androidInstrumentedTest/AndroidManifest.xml diff --git a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt b/workflow-ui/compose/src/androidInstrumentedTest/kotlin/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt similarity index 100% rename from workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt rename to workflow-ui/compose/src/androidInstrumentedTest/kotlin/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt diff --git a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/CompositionRootTest.kt b/workflow-ui/compose/src/androidInstrumentedTest/kotlin/com/squareup/workflow1/ui/compose/CompositionRootTest.kt similarity index 100% rename from workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/CompositionRootTest.kt rename to workflow-ui/compose/src/androidInstrumentedTest/kotlin/com/squareup/workflow1/ui/compose/CompositionRootTest.kt diff --git a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/NoTransitionBackStackContainer.kt b/workflow-ui/compose/src/androidInstrumentedTest/kotlin/com/squareup/workflow1/ui/compose/NoTransitionBackStackContainer.kt similarity index 100% rename from workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/NoTransitionBackStackContainer.kt rename to workflow-ui/compose/src/androidInstrumentedTest/kotlin/com/squareup/workflow1/ui/compose/NoTransitionBackStackContainer.kt diff --git a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/RenderAsStateTest.kt b/workflow-ui/compose/src/androidInstrumentedTest/kotlin/com/squareup/workflow1/ui/compose/RenderAsStateTest.kt similarity index 100% rename from workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/RenderAsStateTest.kt rename to workflow-ui/compose/src/androidInstrumentedTest/kotlin/com/squareup/workflow1/ui/compose/RenderAsStateTest.kt diff --git a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ScreenComposableFactoryTest.kt b/workflow-ui/compose/src/androidInstrumentedTest/kotlin/com/squareup/workflow1/ui/compose/ScreenComposableFactoryTest.kt similarity index 100% rename from workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ScreenComposableFactoryTest.kt rename to workflow-ui/compose/src/androidInstrumentedTest/kotlin/com/squareup/workflow1/ui/compose/ScreenComposableFactoryTest.kt diff --git a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/WorkflowRenderingTest.kt b/workflow-ui/compose/src/androidInstrumentedTest/kotlin/com/squareup/workflow1/ui/compose/WorkflowRenderingTest.kt similarity index 100% rename from workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/WorkflowRenderingTest.kt rename to workflow-ui/compose/src/androidInstrumentedTest/kotlin/com/squareup/workflow1/ui/compose/WorkflowRenderingTest.kt diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ScreenComposableFactory.kt b/workflow-ui/compose/src/androidMain/kotlin/com/squareup/workflow1/ui/compose/ScreenComposableFactoryAndroid.kt similarity index 66% rename from workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ScreenComposableFactory.kt rename to workflow-ui/compose/src/androidMain/kotlin/com/squareup/workflow1/ui/compose/ScreenComposableFactoryAndroid.kt index b8a6e6b20..84b487a45 100644 --- a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ScreenComposableFactory.kt +++ b/workflow-ui/compose/src/androidMain/kotlin/com/squareup/workflow1/ui/compose/ScreenComposableFactoryAndroid.kt @@ -14,104 +14,12 @@ import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.ScreenViewHolder import com.squareup.workflow1.ui.ViewEnvironment -import com.squareup.workflow1.ui.ViewRegistry -import com.squareup.workflow1.ui.ViewRegistry.Key import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.androidx.OnBackPressedDispatcherOwnerKey import com.squareup.workflow1.ui.show import com.squareup.workflow1.ui.startShowing import kotlin.reflect.KClass -@WorkflowUiExperimentalApi -public inline fun ScreenComposableFactory( - noinline content: @Composable ( - rendering: ScreenT, - environment: ViewEnvironment - ) -> Unit -): ScreenComposableFactory = ScreenComposableFactory(ScreenT::class, content) - -@PublishedApi -@WorkflowUiExperimentalApi -internal fun ScreenComposableFactory( - type: KClass, - content: @Composable ( - rendering: ScreenT, - environment: ViewEnvironment - ) -> Unit -): ScreenComposableFactory = object : ScreenComposableFactory { - override val type: KClass = type - - @Composable override fun Content( - rendering: ScreenT, - environment: ViewEnvironment - ) { - content(rendering, environment) - } -} - -/** - * A [ViewRegistry.Entry] that uses a [Composable] function to display [ScreenT]. - * This is the fundamental unit of Compose tooling in Workflow UI, the Compose analogue of - * [ScreenViewFactory][com.squareup.workflow1.ui.ScreenViewFactory]. - * - * [ScreenComposableFactory] is also a bit cumbersome to use directly, - * so [ComposeScreen] is provided as a convenience. Most developers will - * have no reason to work with [ScreenComposableFactory] directly, or even - * be aware of it. - * - * - See [ComposeScreen] for a more complete description of using Compose to - * build a Workflow-based UI. - * - * - See [WorkflowRendering] to display a nested [Screen] from [ComposeScreen.Content] - * or from [ScreenComposableFactory.Content] - * - * Use [ScreenComposableFactory] directly if you need to prevent your - * [Screen] rendering classes from depending on Compose at compile time. - * - * Example: - * - * val fooComposableFactory = ScreenComposableFactory { screen, _ -> - * Text(screen.message) - * } - * - * val viewRegistry = ViewRegistry(fooComposableFactory, …) - * val viewEnvironment = ViewEnvironment.EMPTY + viewRegistry - * - * renderWorkflowIn( - * workflow = MyWorkflow.mapRendering { it.withEnvironment(viewEnvironment) } - * ) - */ -@WorkflowUiExperimentalApi -public interface ScreenComposableFactory : ViewRegistry.Entry { - public val type: KClass - - override val key: Key> - get() = Key(type, ScreenComposableFactory::class) - - /** - * The composable content of this [ScreenComposableFactory]. This method will be called - * any time [rendering] or [environment] change. It is the Compose-based analogue of - * [ScreenViewRunner.showRendering][com.squareup.workflow1.ui.ScreenViewRunner.showRendering]. - */ - @Composable public fun Content( - rendering: ScreenT, - environment: ViewEnvironment - ) -} - -/** - * It is rare to call this method directly. Instead the most common path is to pass [Screen] - * instances to [WorkflowRendering], which will apply the [ScreenComposableFactory] - * and [ScreenComposableFactoryFinder] machinery for you. - */ -@WorkflowUiExperimentalApi -public fun ScreenT.toComposableFactory( - environment: ViewEnvironment -): ScreenComposableFactory { - return environment[ScreenComposableFactoryFinder] - .requireComposableFactoryForRendering(environment, this) -} - /** * Convert a [ScreenComposableFactory] into a [ScreenViewFactory] * by using a [ComposeView] to host [ScreenComposableFactory.Content]. diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ViewEnvironmentWithComposeSupport.kt b/workflow-ui/compose/src/androidMain/kotlin/com/squareup/workflow1/ui/compose/ViewEnvironmentWithComposeSupport.kt similarity index 100% rename from workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ViewEnvironmentWithComposeSupport.kt rename to workflow-ui/compose/src/androidMain/kotlin/com/squareup/workflow1/ui/compose/ViewEnvironmentWithComposeSupport.kt diff --git a/workflow-ui/compose/src/main/res/values/ids.xml b/workflow-ui/compose/src/androidMain/res/values/ids.xml similarity index 100% rename from workflow-ui/compose/src/main/res/values/ids.xml rename to workflow-ui/compose/src/androidMain/res/values/ids.xml diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeScreen.kt b/workflow-ui/compose/src/commonMain/kotlin/com/squareup/workflow1/ui/compose/ComposeScreen.kt similarity index 100% rename from workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeScreen.kt rename to workflow-ui/compose/src/commonMain/kotlin/com/squareup/workflow1/ui/compose/ComposeScreen.kt diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/CompositionRoot.kt b/workflow-ui/compose/src/commonMain/kotlin/com/squareup/workflow1/ui/compose/CompositionRoot.kt similarity index 98% rename from workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/CompositionRoot.kt rename to workflow-ui/compose/src/commonMain/kotlin/com/squareup/workflow1/ui/compose/CompositionRoot.kt index 1902c7f9f..6db3be614 100644 --- a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/CompositionRoot.kt +++ b/workflow-ui/compose/src/commonMain/kotlin/com/squareup/workflow1/ui/compose/CompositionRoot.kt @@ -8,7 +8,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.staticCompositionLocalOf import com.squareup.workflow1.ui.Screen -import com.squareup.workflow1.ui.ScreenViewFactoryFinder import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/RenderAsState.kt b/workflow-ui/compose/src/commonMain/kotlin/com/squareup/workflow1/ui/compose/RenderAsState.kt similarity index 100% rename from workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/RenderAsState.kt rename to workflow-ui/compose/src/commonMain/kotlin/com/squareup/workflow1/ui/compose/RenderAsState.kt diff --git a/workflow-ui/compose/src/commonMain/kotlin/com/squareup/workflow1/ui/compose/ScreenComposableFactory.kt b/workflow-ui/compose/src/commonMain/kotlin/com/squareup/workflow1/ui/compose/ScreenComposableFactory.kt new file mode 100644 index 000000000..f5e6b55d3 --- /dev/null +++ b/workflow-ui/compose/src/commonMain/kotlin/com/squareup/workflow1/ui/compose/ScreenComposableFactory.kt @@ -0,0 +1,99 @@ +package com.squareup.workflow1.ui.compose + +import androidx.compose.runtime.Composable +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.ViewRegistry +import com.squareup.workflow1.ui.ViewRegistry.Key +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import kotlin.reflect.KClass + +@WorkflowUiExperimentalApi +public inline fun ScreenComposableFactory( + noinline content: @Composable ( + rendering: ScreenT, + environment: ViewEnvironment + ) -> Unit +): ScreenComposableFactory = ScreenComposableFactory(ScreenT::class, content) + +@PublishedApi +@WorkflowUiExperimentalApi +internal fun ScreenComposableFactory( + type: KClass, + content: @Composable ( + rendering: ScreenT, + environment: ViewEnvironment + ) -> Unit +): ScreenComposableFactory = object : ScreenComposableFactory { + override val type: KClass = type + + @Composable override fun Content( + rendering: ScreenT, + environment: ViewEnvironment + ) { + content(rendering, environment) + } +} + +/** + * A [ViewRegistry.Entry] that uses a [Composable] function to display [ScreenT]. + * This is the fundamental unit of Compose tooling in Workflow UI, the Compose analogue of + * [ScreenViewFactory][com.squareup.workflow1.ui.ScreenViewFactory]. + * + * [ScreenComposableFactory] is also a bit cumbersome to use directly, + * so [ComposeScreen] is provided as a convenience. Most developers will + * have no reason to work with [ScreenComposableFactory] directly, or even + * be aware of it. + * + * - See [ComposeScreen] for a more complete description of using Compose to + * build a Workflow-based UI. + * + * - See [WorkflowRendering] to display a nested [Screen] from [ComposeScreen.Content] + * or from [ScreenComposableFactory.Content] + * + * Use [ScreenComposableFactory] directly if you need to prevent your + * [Screen] rendering classes from depending on Compose at compile time. + * + * Example: + * + * val fooComposableFactory = ScreenComposableFactory { screen, _ -> + * Text(screen.message) + * } + * + * val viewRegistry = ViewRegistry(fooComposableFactory, …) + * val viewEnvironment = ViewEnvironment.EMPTY + viewRegistry + * + * renderWorkflowIn( + * workflow = MyWorkflow.mapRendering { it.withEnvironment(viewEnvironment) } + * ) + */ +@WorkflowUiExperimentalApi +public interface ScreenComposableFactory : ViewRegistry.Entry { + public val type: KClass + + override val key: Key> + get() = Key(type, ScreenComposableFactory::class) + + /** + * The composable content of this [ScreenComposableFactory]. This method will be called + * any time [rendering] or [environment] change. It is the Compose-based analogue of + * [ScreenViewRunner.showRendering][com.squareup.workflow1.ui.ScreenViewRunner.showRendering]. + */ + @Composable public fun Content( + rendering: ScreenT, + environment: ViewEnvironment + ) +} + +/** + * It is rare to call this method directly. Instead the most common path is to pass [Screen] + * instances to [WorkflowRendering], which will apply the [ScreenComposableFactory] + * and [ScreenComposableFactoryFinder] machinery for you. + */ +@WorkflowUiExperimentalApi +public fun ScreenT.toComposableFactory( + environment: ViewEnvironment +): ScreenComposableFactory { + return environment[ScreenComposableFactoryFinder] + .requireComposableFactoryForRendering(environment, this) +} diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder.kt b/workflow-ui/compose/src/commonMain/kotlin/com/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder.kt similarity index 100% rename from workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder.kt rename to workflow-ui/compose/src/commonMain/kotlin/com/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder.kt diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/TextControllerAsMutableState.kt b/workflow-ui/compose/src/commonMain/kotlin/com/squareup/workflow1/ui/compose/TextControllerAsMutableState.kt similarity index 100% rename from workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/TextControllerAsMutableState.kt rename to workflow-ui/compose/src/commonMain/kotlin/com/squareup/workflow1/ui/compose/TextControllerAsMutableState.kt diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt b/workflow-ui/compose/src/commonMain/kotlin/com/squareup/workflow1/ui/compose/WorkflowRendering.kt similarity index 96% rename from workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt rename to workflow-ui/compose/src/commonMain/kotlin/com/squareup/workflow1/ui/compose/WorkflowRendering.kt index e8a4f9b24..e2d9c1ec0 100644 --- a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt +++ b/workflow-ui/compose/src/commonMain/kotlin/com/squareup/workflow1/ui/compose/WorkflowRendering.kt @@ -16,12 +16,8 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import com.squareup.workflow1.ui.Compatible import com.squareup.workflow1.ui.Screen -import com.squareup.workflow1.ui.ScreenViewFactoryFinder -import com.squareup.workflow1.ui.ScreenViewHolder import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.WorkflowViewStub -import com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner /** * Renders [rendering] into the composition using this [ViewEnvironment]'s diff --git a/workflow-ui/compose/src/iosTest/kotlin/com/squareup/workflow1/ui/compose/CompositionRootTestIos.kt b/workflow-ui/compose/src/iosTest/kotlin/com/squareup/workflow1/ui/compose/CompositionRootTestIos.kt new file mode 100644 index 000000000..41ab074f7 --- /dev/null +++ b/workflow-ui/compose/src/iosTest/kotlin/com/squareup/workflow1/ui/compose/CompositionRootTestIos.kt @@ -0,0 +1,107 @@ +@file:OptIn(ExperimentalTestApi::class, ExperimentalTestApi::class) + +package com.squareup.workflow1.ui.compose + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.runComposeUiTest +import kotlin.test.Test + +internal class CompositionRootTestIos { + + @Test fun wrappedWithRootIfNecessary_wrapsWhenNecessary() = runComposeUiTest { + val root: CompositionRoot = { content -> + Column { + BasicText("one") + content() + } + } + + setContent { + WrappedWithRootIfNecessary(root) { + BasicText("two") + } + } + + // These semantics used to merge, but as of dev15, they don't, which seems to be a bug. + // https://issuetracker.google.com/issues/161979921 + onNodeWithText("one").assertIsDisplayed() + onNodeWithText("two").assertIsDisplayed() + } + + @Test fun wrappedWithRootIfNecessary_onlyWrapsOnce() = runComposeUiTest { + val root: CompositionRoot = { content -> + Column { + BasicText("one") + content() + } + } + + setContentWithLifecycle { + WrappedWithRootIfNecessary(root) { + BasicText("two") + WrappedWithRootIfNecessary(root) { + BasicText("three") + } + } + } + + onNodeWithText("one").assertIsDisplayed() + onNodeWithText("two").assertIsDisplayed() + onNodeWithText("three").assertIsDisplayed() + } + + @Test fun wrappedWithRootIfNecessary_seesUpdatesFromRootWrapper() = runComposeUiTest { + val wrapperText = mutableStateOf("one") + val root: CompositionRoot = { content -> + Column { + BasicText(wrapperText.value) + content() + } + } + + setContentWithLifecycle { + WrappedWithRootIfNecessary(root) { + BasicText("two") + } + } + + onNodeWithText("one").assertIsDisplayed() + onNodeWithText("two").assertIsDisplayed() + wrapperText.value = "ENO" + onNodeWithText("ENO").assertIsDisplayed() + onNodeWithText("two").assertIsDisplayed() + } + + @Test fun wrappedWithRootIfNecessary_rewrapsWhenDifferentRoot() = runComposeUiTest { + val root1: CompositionRoot = { content -> + Column { + BasicText("one") + content() + } + } + val root2: CompositionRoot = { content -> + Column { + BasicText("ENO") + content() + } + } + val viewEnvironment = mutableStateOf(root1) + + setContentWithLifecycle { + WrappedWithRootIfNecessary(viewEnvironment.value) { + BasicText("two") + } + } + + onNodeWithText("one").assertIsDisplayed() + onNodeWithText("two").assertIsDisplayed() + viewEnvironment.value = root2 + onNodeWithText("ENO").assertIsDisplayed() + onNodeWithText("two").assertIsDisplayed() + } +} diff --git a/workflow-ui/compose/src/iosTest/kotlin/com/squareup/workflow1/ui/compose/IosTestUtils.kt b/workflow-ui/compose/src/iosTest/kotlin/com/squareup/workflow1/ui/compose/IosTestUtils.kt new file mode 100644 index 000000000..460ddb58f --- /dev/null +++ b/workflow-ui/compose/src/iosTest/kotlin/com/squareup/workflow1/ui/compose/IosTestUtils.kt @@ -0,0 +1,46 @@ +package com.squareup.workflow1.ui.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.saveable.LocalSaveableStateRegistry +import androidx.compose.runtime.saveable.SaveableStateRegistry +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.test.ComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.Lifecycle.State.DESTROYED +import androidx.lifecycle.Lifecycle.State.RESUMED +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry + +@OptIn(ExperimentalTestApi::class) +fun ComposeUiTest.setContentWithLifecycle( + lifecycleOwner: LifecycleOwner = IosLifecycleOwner(), + content: @Composable () -> Unit +) { + setContent { + (lifecycleOwner as? IosLifecycleOwner)?.let { + DisposableEffect(Unit) { + with(lifecycleOwner.registry) { + currentState = RESUMED + + onDispose { + currentState = DESTROYED + } + } + } + } + + CompositionLocalProvider(LocalLifecycleOwner provides lifecycleOwner) { + content() + } + } +} + + +class IosLifecycleOwner : LifecycleOwner { + val registry = LifecycleRegistry(this) + override val lifecycle: Lifecycle + get() = registry +} diff --git a/workflow-ui/compose/src/iosTest/kotlin/com/squareup/workflow1/ui/compose/RenderAsStateTestIos.kt b/workflow-ui/compose/src/iosTest/kotlin/com/squareup/workflow1/ui/compose/RenderAsStateTestIos.kt new file mode 100644 index 000000000..f964bdbbd --- /dev/null +++ b/workflow-ui/compose/src/iosTest/kotlin/com/squareup/workflow1/ui/compose/RenderAsStateTestIos.kt @@ -0,0 +1,384 @@ +@file:OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class) + +package com.squareup.workflow1.ui.compose + +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.State +import androidx.compose.runtime.currentComposer +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.LocalSaveableStateRegistry +import androidx.compose.runtime.saveable.SaveableStateRegistry +import androidx.compose.runtime.setValue +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.StateRestorationTester +import androidx.compose.ui.test.runComposeUiTest +import com.squareup.workflow1.Snapshot +import com.squareup.workflow1.StatefulWorkflow +import com.squareup.workflow1.Workflow +import com.squareup.workflow1.action +import com.squareup.workflow1.parse +import com.squareup.workflow1.readUtf8WithLength +import com.squareup.workflow1.rendering +import com.squareup.workflow1.stateless +import com.squareup.workflow1.ui.compose.RenderAsStateTestIos.SnapshottingWorkflow.SnapshottedRendering +import com.squareup.workflow1.writeUtf8WithLength +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.isActive +import kotlinx.coroutines.job +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import okio.ByteString +import okio.ByteString.Companion.decodeBase64 +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +internal class RenderAsStateTestIos { + + @Test fun passesPropsThrough() = runComposeUiTest { + val workflow = Workflow.stateless { it } + lateinit var initialRendering: String + + setContentWithLifecycle { + initialRendering = workflow.renderAsState(props = "foo", onOutput = {}).value + } + + runOnIdle { + assertEquals("foo", initialRendering) + } + } + + @Test fun seesPropsAndRenderingUpdates() = runComposeUiTest { + val workflow = Workflow.stateless { it } + val props = mutableStateOf("foo") + lateinit var rendering: String + + setContentWithLifecycle { + rendering = workflow.renderAsState(props.value, onOutput = {}).value + } + + runOnIdle { + assertEquals("foo", rendering) + props.value = "bar" + } + runOnIdle { + assertEquals("bar", rendering) + } + } + + @Test fun invokesOutputCallback() = runComposeUiTest { + val workflow = Workflow.stateless Unit> { + { string -> + actionSink.send(action { setOutput(string) }) + } + } + val receivedOutputs = mutableListOf() + lateinit var rendering: (String) -> Unit + + setContentWithLifecycle { + rendering = workflow.renderAsState(props = Unit, onOutput = { receivedOutputs += it }).value + } + + runOnIdle { + assertTrue { receivedOutputs.isEmpty() } + rendering("one") + } + + runOnIdle { + assertEquals(listOf("one"), receivedOutputs) + rendering("two") + } + + runOnIdle { + assertEquals(listOf("one", "two"), receivedOutputs) + } + } + + @Test fun savesSnapshot() = runComposeUiTest { + val workflow = SnapshottingWorkflow() + val savedStateRegistry = SaveableStateRegistry(emptyMap()) { true } + lateinit var rendering: SnapshottedRendering + val scope = TestScope() + + setContentWithLifecycle { + CompositionLocalProvider(LocalSaveableStateRegistry provides savedStateRegistry) { + rendering = renderAsState( + workflow = workflow, + scope = scope, + props = Unit, + interceptors = emptyList(), + onOutput = {}, + snapshotKey = SNAPSHOT_KEY + ).value + } + } + + runOnIdle { + assertTrue { rendering.string.isEmpty() } + rendering.updateString("foo") + } + + // Move along the Workflow. + scope.advanceUntilIdle() + + runOnIdle { + val savedValues = savedStateRegistry.performSave() + println("saved keys: ${savedValues.keys}") + // Relying on the int key across all runtimes is brittle, so use an explicit key. + @Suppress("UNCHECKED_CAST") + val snapshot = + ByteString.of(*((savedValues.getValue(SNAPSHOT_KEY).single() as State).value)) + println("snapshot: ${snapshot.base64()}") + assertEquals(EXPECTED_SNAPSHOT, snapshot) + } + } + + @Test fun restoresSnapshot() = runComposeUiTest { + val workflow = SnapshottingWorkflow() + val restoreValues = + mapOf(SNAPSHOT_KEY to listOf(mutableStateOf(EXPECTED_SNAPSHOT.toByteArray()))) + val savedStateRegistry = SaveableStateRegistry(restoreValues) { true } + lateinit var rendering: SnapshottedRendering + + setContentWithLifecycle { + CompositionLocalProvider(LocalSaveableStateRegistry provides savedStateRegistry) { + rendering = renderAsState( + workflow = workflow, + scope = rememberCoroutineScope(), + props = Unit, + interceptors = emptyList(), + onOutput = {}, + snapshotKey = "workflow-snapshot" + ).value + } + } + + runOnIdle { + assertEquals("foo", rendering.string) + } + } + + // This test can't run because we can't provide a LocalSaveableStateRegistry in the test due to + // how StateRestorationTester is setup + @Ignore + @Test fun savesAndRestoresSnapshotOnConfigChange() = runComposeUiTest { + val stateRestorationTester = StateRestorationTester(this) + val workflow = SnapshottingWorkflow() + lateinit var rendering: SnapshottedRendering + val scope = TestScope() + + stateRestorationTester.setContent { + rendering = workflow.renderAsState( + scope = scope, + props = Unit, + interceptors = emptyList(), + onOutput = {}, + ).value + + runOnIdle { + assertTrue { rendering.string.isEmpty() } + rendering.updateString("foo") + } + + // Move along workflow before saving state! + scope.advanceUntilIdle() + + stateRestorationTester.emulateSaveAndRestore() + + runOnIdle { + assertEquals("foo", rendering.string) + } + } + } + + @Test fun restoresFromSnapshotWhenWorkflowChanged() = runComposeUiTest { + val workflow1 = SnapshottingWorkflow() + val workflow2 = SnapshottingWorkflow() + val currentWorkflow = mutableStateOf(workflow1) + lateinit var rendering: SnapshottedRendering + // Since we have frame timeouts we need to control the scope of the Workflow Runtime as + // well as the scope of the Recomposer. + val scope = TestScope() + + var compositionCount = 0 + var lastCompositionCount = 0 + fun assertWasRecomposed() { + assertTrue { compositionCount > lastCompositionCount } + lastCompositionCount = compositionCount + } + + setContentWithLifecycle { + compositionCount++ + rendering = + currentWorkflow.value.renderAsState(props = Unit, onOutput = {}, scope = scope).value + } + + // Initialize the first workflow. + runOnIdle { + assertTrue { rendering.string.isEmpty() } + assertWasRecomposed() + rendering.updateString("one") + } + + // Move along the workflow. + scope.advanceUntilIdle() + + runOnIdle { + assertWasRecomposed() + assertEquals("one", rendering.string) + } + + // Change the workflow instance being rendered. This should restart the runtime, but restore + // it from the snapshot. + currentWorkflow.value = workflow2 + + scope.advanceUntilIdle() + + runOnIdle { + assertWasRecomposed() + assertEquals("one", rendering.string) + } + } + + @Test fun renderingIsAvailableImmediatelyWhenWorkflowScopeUsesDifferentDispatcher() = + runComposeUiTest { + val workflow = Workflow.rendering("hello") + val scope = TestScope() + + setContentWithLifecycle { + val initialRendering = workflow.renderAsState( + props = Unit, + onOutput = {}, + scope = scope + ) + assertTrue { initialRendering.value.isNotEmpty() } + } + } + + @Test fun runtimeIsCancelledWhenCompositionFails() = runComposeUiTest { + var innerJob: Job? = null + val workflow = Workflow.stateless { + runningSideEffect("test") { + innerJob = coroutineContext.job + awaitCancellation() + } + } + val scope = TestScope(UnconfinedTestDispatcher()) + + class CancelCompositionException : RuntimeException() + + scope.runTest { + assertFailsWith { + setContentWithLifecycle { + workflow.renderAsState(props = Unit, onOutput = {}, scope = scope) + throw CancelCompositionException() + } + } + + runOnIdle { + assertNotNull(innerJob) + assertTrue { innerJob!!.isCancelled } + } + } + } + + @Test fun workflowScopeIsNotCancelledWhenRemovedFromComposition() = runComposeUiTest { + val workflow = Workflow.stateless {} + val scope = TestScope() + var shouldRunWorkflow by mutableStateOf(true) + + scope.runTest { + setContentWithLifecycle { + if (shouldRunWorkflow) { + workflow.renderAsState(props = Unit, onOutput = {}, scope = scope) + } + } + + runOnIdle { + assertTrue { scope.isActive } + } + + shouldRunWorkflow = false + + runOnIdle { + scope.advanceUntilIdle() + assertTrue { scope.isActive } + } + } + } + + @Test fun runtimeIsCancelledWhenRemovedFromComposition() = runComposeUiTest { + var innerJob: Job? = null + val workflow = Workflow.stateless { + runningSideEffect("test") { + innerJob = coroutineContext.job + awaitCancellation() + } + } + var shouldRunWorkflow by mutableStateOf(true) + + setContentWithLifecycle { + if (shouldRunWorkflow) { + workflow.renderAsState(props = Unit, onOutput = {}) + } + } + + runOnIdle { + assertNotNull(innerJob) + assertTrue { innerJob!!.isActive } + } + + shouldRunWorkflow = false + + runOnIdle { + assertTrue { innerJob!!.isCancelled } + } + } + + private companion object { + const val SNAPSHOT_KEY = "workflow-snapshot" + + /** Golden value from [savesSnapshot]. */ + val EXPECTED_SNAPSHOT = "AAAABwAAAANmb28AAAAA".decodeBase64()!! + } + + // Seems to be a problem accessing Workflow.stateful. + private class SnapshottingWorkflow : + StatefulWorkflow() { + + data class SnapshottedRendering( + val string: String, + val updateString: (String) -> Unit + ) + + override fun initialState( + props: Unit, + snapshot: Snapshot? + ): String = snapshot?.bytes?.parse { it.readUtf8WithLength() } ?: "" + + override fun render( + renderProps: Unit, + renderState: String, + context: RenderContext + ) = SnapshottedRendering( + string = renderState, + updateString = { newString -> context.actionSink.send(updateString(newString)) } + ) + + override fun snapshotState(state: String): Snapshot = + Snapshot.write { it.writeUtf8WithLength(state) } + + private fun updateString(newString: String) = action { + state = newString + } + } +} diff --git a/workflow-ui/compose/src/iosTest/kotlin/com/squareup/workflow1/ui/compose/WorkflowRenderingTestIos.kt b/workflow-ui/compose/src/iosTest/kotlin/com/squareup/workflow1/ui/compose/WorkflowRenderingTestIos.kt new file mode 100644 index 000000000..1a2a3477d --- /dev/null +++ b/workflow-ui/compose/src/iosTest/kotlin/com/squareup/workflow1/ui/compose/WorkflowRenderingTestIos.kt @@ -0,0 +1,412 @@ +@file:Suppress("TestFunctionName") +@file:OptIn(ExperimentalTestApi::class) + +package com.squareup.workflow1.ui.compose + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertHeightIsEqualTo +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.assertWidthIsEqualTo +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.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.Lifecycle.Event +import androidx.lifecycle.Lifecycle.Event.ON_CREATE +import androidx.lifecycle.Lifecycle.Event.ON_DESTROY +import androidx.lifecycle.Lifecycle.Event.ON_PAUSE +import androidx.lifecycle.Lifecycle.Event.ON_RESUME +import androidx.lifecycle.Lifecycle.Event.ON_START +import androidx.lifecycle.Lifecycle.Event.ON_STOP +import androidx.lifecycle.Lifecycle.State +import androidx.lifecycle.Lifecycle.State.CREATED +import androidx.lifecycle.Lifecycle.State.DESTROYED +import androidx.lifecycle.Lifecycle.State.INITIALIZED +import androidx.lifecycle.Lifecycle.State.RESUMED +import androidx.lifecycle.Lifecycle.State.STARTED +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import com.squareup.workflow1.ui.Compatible +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.ViewRegistry +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.plus +import kotlin.reflect.KClass +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(WorkflowUiExperimentalApi::class) +internal class WorkflowRenderingTestIos { + + @Test fun doesNotRecompose_whenFactoryChanged() = runComposeUiTest { + data class TestRendering( + val text: String + ) : Screen + + val registry1 = ViewRegistry( + ScreenComposableFactory { rendering, _ -> + BasicText(rendering.text) + } + ) + val registry2 = ViewRegistry( + ScreenComposableFactory { rendering, _ -> + BasicText(rendering.text.reversed()) + } + ) + val registry = mutableStateOf(registry1) + + setContentWithLifecycle { + WorkflowRendering(TestRendering("hello"), ViewEnvironment.EMPTY + registry.value) + } + + onNodeWithText("hello").assertIsDisplayed() + registry.value = registry2 + onNodeWithText("hello").assertIsDisplayed() + onNodeWithText("olleh").assertDoesNotExist() + } + + @Test fun wrapsFactoryWithRoot_whenAlreadyInComposition() = runComposeUiTest { + data class TestRendering(val text: String) : Screen + + val testFactory = ScreenComposableFactory { rendering, _ -> + BasicText(rendering.text) + } + val viewEnvironment = (ViewEnvironment.EMPTY + ViewRegistry(testFactory)) + .withCompositionRoot { content -> + Column { + BasicText("one") + content() + } + } + + setContentWithLifecycle { + WorkflowRendering(TestRendering("two"), viewEnvironment) + } + + onNodeWithText("one").assertIsDisplayed() + onNodeWithText("two").assertIsDisplayed() + } + + @Test fun destroysChildLifecycle_fromCompose_whenIncompatibleRendering() = runComposeUiTest { + val lifecycleEvents = mutableListOf() + + class LifecycleRecorder : ComposableRendering { + @Composable override fun Content(viewEnvironment: ViewEnvironment) { + val lifecycle = LocalLifecycleOwner.current.lifecycle + DisposableEffect(lifecycle) { + lifecycle.addObserver( + LifecycleEventObserver { _, event -> + lifecycleEvents += event + } + ) + onDispose { + // Yes, we're leaking the observer. That's intentional: we need to make sure we see any + // lifecycle events that happen even after the composable is destroyed. + } + } + } + } + + class EmptyRendering : ComposableRendering { + @Composable override fun Content(viewEnvironment: ViewEnvironment) {} + } + + var rendering: Screen by mutableStateOf(LifecycleRecorder()) + setContentWithLifecycle { + WorkflowRendering(rendering, env) + } + + runOnIdle { + assertEquals(listOf(ON_CREATE, ON_START, ON_RESUME), lifecycleEvents) + lifecycleEvents.clear() + } + + rendering = EmptyRendering() + + runOnIdle { + assertEquals(listOf(ON_PAUSE, ON_STOP, ON_DESTROY), lifecycleEvents) + } + } + + @Test fun followsParentLifecycle() = runComposeUiTest { + val states = mutableListOf() + val parentOwner = object : LifecycleOwner { + val registry = LifecycleRegistry(this) + override val lifecycle: Lifecycle + get() = registry + } + + setContentWithLifecycle(parentOwner) { + WorkflowRendering(LifecycleRecorder(states), env) + } + + runOnIdle { + assertEquals(listOf(INITIALIZED), states) + states.clear() + parentOwner.registry.currentState = STARTED + } + + runOnIdle { + assertEquals(listOf(CREATED, STARTED), states) + states.clear() + parentOwner.registry.currentState = CREATED + } + + runOnIdle { + assertEquals(listOf(CREATED), states) + states.clear() + parentOwner.registry.currentState = RESUMED + } + + runOnIdle { + assertEquals(listOf(STARTED, RESUMED), states) + states.clear() + parentOwner.registry.currentState = DESTROYED + } + + runOnIdle { + assertEquals(listOf(STARTED, CREATED, DESTROYED), states) + } + } + + @Test fun handlesParentInitiallyDestroyed() = runComposeUiTest { + val states = mutableListOf() + val parentOwner = object : LifecycleOwner { + val registry = LifecycleRegistry(this) + override val lifecycle: Lifecycle + get() = registry + } + runOnIdle { + // Cannot go directly to DESTROYED + parentOwner.registry.currentState = CREATED + parentOwner.registry.currentState = DESTROYED + } + + setContentWithLifecycle(parentOwner) { + WorkflowRendering(LifecycleRecorder(states), env) + } + + runOnIdle { + assertEquals(listOf(INITIALIZED), states) + } + } + + @Test fun appliesModifierToComposableContent() = runComposeUiTest { + class Rendering : ComposableRendering { + @Composable override fun Content(viewEnvironment: ViewEnvironment) { + Box( + Modifier + .testTag("box") + .fillMaxSize() + ) + } + } + + setContentWithLifecycle { + WorkflowRendering( + Rendering(), + env, + Modifier.size(width = 42.dp, height = 43.dp) + ) + } + + onNodeWithTag("box") + .assertWidthIsEqualTo(42.dp) + .assertHeightIsEqualTo(43.dp) + } + + @Test fun propagatesMinConstraints() = runComposeUiTest { + class Rendering : ComposableRendering { + @Composable override fun Content(viewEnvironment: ViewEnvironment) { + Box(Modifier.testTag("box")) + } + } + + setContentWithLifecycle { + WorkflowRendering( + Rendering(), + env, + Modifier.sizeIn(minWidth = 42.dp, minHeight = 43.dp) + ) + } + + onNodeWithTag("box") + .assertWidthIsEqualTo(42.dp) + .assertHeightIsEqualTo(43.dp) + } + + @Test fun skipsPreviousContentWhenIncompatible() = runComposeUiTest { + var disposeCount = 0 + + class Rendering( + override val compatibilityKey: String + ) : ComposableRendering, Compatible { + @Composable override fun Content(viewEnvironment: ViewEnvironment) { + var counter by rememberSaveable { mutableStateOf(0) } + Column { + BasicText( + "$compatibilityKey: $counter", + Modifier + .testTag("tag") + .clickable { counter++ } + ) + DisposableEffect(Unit) { + onDispose { + disposeCount++ + } + } + } + } + } + + var key by mutableStateOf("one") + setContentWithLifecycle { + WorkflowRendering(Rendering(key), env) + } + + onNodeWithTag("tag") + .assertTextEquals("one: 0") + .performClick() + .assertTextEquals("one: 1") + + key = "two" + + onNodeWithTag("tag") + .assertTextEquals("two: 0") + runOnIdle { + assertEquals(1, disposeCount) + } + + key = "one" + + // State should not be restored. + onNodeWithTag("tag") + .assertTextEquals("one: 0") + runOnIdle { + assertEquals(2, disposeCount) + } + } + + @Test fun doesNotSkipPreviousContentWhenCompatible() = runComposeUiTest { + var disposeCount = 0 + + class Rendering(val text: String) : ComposableRendering { + @Composable override fun Content(viewEnvironment: ViewEnvironment) { + var counter by rememberSaveable { mutableStateOf(0) } + Column { + BasicText( + "$text: $counter", + Modifier + .testTag("tag") + .clickable { counter++ } + ) + DisposableEffect(Unit) { + onDispose { + disposeCount++ + } + } + } + } + } + + var text by mutableStateOf("one") + setContentWithLifecycle { + WorkflowRendering(Rendering(text), env) + } + + onNodeWithTag("tag") + .assertTextEquals("one: 0") + .performClick() + .assertTextEquals("one: 1") + + text = "two" + + // Counter state should be preserved. + onNodeWithTag("tag") + .assertTextEquals("two: 1") + runOnIdle { + assertEquals(0, disposeCount) + } + } + + private class LifecycleRecorder( + // For some reason, if we just capture the states val, it is null in the composable. + private val states: MutableList + ) : ComposableRendering { + @Composable override fun Content(viewEnvironment: ViewEnvironment) { + val lifecycle = LocalLifecycleOwner.current.lifecycle + DisposableEffect(lifecycle) { + this@LifecycleRecorder.states += lifecycle.currentState + lifecycle.addObserver( + LifecycleEventObserver { _, _ -> + this@LifecycleRecorder.states += lifecycle.currentState + } + ) + onDispose { + // Yes, we're leaking the observer. That's intentional: we need to make sure we see any + // lifecycle events that happen even after the composable is destroyed. + } + } + } + } + + /** + * It is significant that this returns a new instance on every call, since we can't rely on real + * implementations in the wild to reuse the same factory instance across rendering instances. + */ + private object InefficientComposableFinder : ScreenComposableFactoryFinder { + override fun getComposableFactoryForRendering( + environment: ViewEnvironment, + rendering: ScreenT + ): ScreenComposableFactory? { + return if (rendering is ComposableRendering) { + object : ScreenComposableFactory { + override val type: KClass get() = error("whatever") + + @Composable override fun Content( + rendering: ScreenT, + environment: ViewEnvironment + ) { + (rendering as ComposableRendering).Content(environment) + } + } + } else { + super.getComposableFactoryForRendering( + environment, + rendering + ) + } + } + } + + + + private val env = + (ViewEnvironment.EMPTY + (ScreenComposableFactoryFinder to InefficientComposableFinder)) + + private interface ComposableRendering : Screen { + @Composable fun Content(viewEnvironment: ViewEnvironment) + } + +} diff --git a/workflow-ui/core-android/build.gradle.kts b/workflow-ui/core-android/build.gradle.kts index 9f634d9f6..1bcc3d53d 100644 --- a/workflow-ui/core-android/build.gradle.kts +++ b/workflow-ui/core-android/build.gradle.kts @@ -20,7 +20,7 @@ dependencies { // Needs to be API for the WorkflowInterceptor argument to WorkflowRunner.Config. api(project(":workflow-runtime")) - api(project(":workflow-ui:core-common")) + api(project(":workflow-ui:core")) compileOnly(libs.androidx.viewbinding) diff --git a/workflow-ui/core-android/dependencies/releaseRuntimeClasspath.txt b/workflow-ui/core-android/dependencies/releaseRuntimeClasspath.txt index 21c26739e..a63664772 100644 --- a/workflow-ui/core-android/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-ui/core-android/dependencies/releaseRuntimeClasspath.txt @@ -1,7 +1,7 @@ androidx.activity:activity:1.6.1 androidx.annotation:annotation-experimental:1.3.0 -androidx.annotation:annotation-jvm:1.6.0 -androidx.annotation:annotation:1.6.0 +androidx.annotation:annotation-jvm:1.8.0 +androidx.annotation:annotation:1.8.0 androidx.arch.core:core-common:2.2.0 androidx.arch.core:core-runtime:2.2.0 androidx.collection:collection:1.1.0 @@ -9,13 +9,17 @@ androidx.concurrent:concurrent-futures:1.1.0 androidx.core:core-ktx:1.12.0 androidx.core:core:1.12.0 androidx.interpolator:interpolator:1.0.0 -androidx.lifecycle:lifecycle-common:2.6.1 -androidx.lifecycle:lifecycle-livedata-core:2.6.1 -androidx.lifecycle:lifecycle-runtime-ktx:2.6.1 -androidx.lifecycle:lifecycle-runtime:2.6.1 -androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1 -androidx.lifecycle:lifecycle-viewmodel:2.6.1 -androidx.profileinstaller:profileinstaller:1.3.0 +androidx.lifecycle:lifecycle-common-jvm:2.8.0 +androidx.lifecycle:lifecycle-common:2.8.0 +androidx.lifecycle:lifecycle-livedata-core:2.8.0 +androidx.lifecycle:lifecycle-runtime-android:2.8.0 +androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.0 +androidx.lifecycle:lifecycle-runtime-ktx:2.8.0 +androidx.lifecycle:lifecycle-runtime:2.8.0 +androidx.lifecycle:lifecycle-viewmodel-android:2.8.0 +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.0 +androidx.lifecycle:lifecycle-viewmodel:2.8.0 +androidx.profileinstaller:profileinstaller:1.3.1 androidx.savedstate:savedstate:1.2.1 androidx.startup:startup-runtime:1.1.1 androidx.tracing:tracing:1.0.0 @@ -24,11 +28,11 @@ androidx.versionedparcelable:versionedparcelable:1.1.1 com.google.guava:listenablefuture:1.0 com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlin:kotlin-bom:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib:1.9.24 org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 diff --git a/workflow-ui/core-common/dependencies/runtimeClasspath.txt b/workflow-ui/core-common/dependencies/runtimeClasspath.txt index d8cb7353d..3d72a2ba1 100644 --- a/workflow-ui/core-common/dependencies/runtimeClasspath.txt +++ b/workflow-ui/core-common/dependencies/runtimeClasspath.txt @@ -1,10 +1,10 @@ com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlin:kotlin-bom:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib:1.9.24 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 diff --git a/workflow-ui/core/api/core.api b/workflow-ui/core/api/core.api new file mode 100644 index 000000000..28781a928 --- /dev/null +++ b/workflow-ui/core/api/core.api @@ -0,0 +1,277 @@ +public abstract interface class com/squareup/workflow1/ui/Compatible { + public static final field Companion Lcom/squareup/workflow1/ui/Compatible$Companion; + public abstract fun getCompatibilityKey ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/ui/Compatible$Companion { + public final fun keyFor (Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/String; + public static synthetic fun keyFor$default (Lcom/squareup/workflow1/ui/Compatible$Companion;Ljava/lang/Object;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/String; +} + +public final class com/squareup/workflow1/ui/CompatibleKt { + public static final fun compatible (Ljava/lang/Object;Ljava/lang/Object;)Z +} + +public abstract interface class com/squareup/workflow1/ui/Container { + public abstract fun asSequence ()Lkotlin/sequences/Sequence; + public abstract fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container; +} + +public final class com/squareup/workflow1/ui/EnvironmentScreen : com/squareup/workflow1/ui/Screen, com/squareup/workflow1/ui/Wrapper { + public fun (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V + public synthetic fun (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun asSequence ()Lkotlin/sequences/Sequence; + public fun getCompatibilityKey ()Ljava/lang/String; + public fun getContent ()Lcom/squareup/workflow1/ui/Screen; + public synthetic fun getContent ()Ljava/lang/Object; + public final fun getEnvironment ()Lcom/squareup/workflow1/ui/ViewEnvironment; + public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container; + public fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/EnvironmentScreen; + public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Wrapper; +} + +public final class com/squareup/workflow1/ui/EnvironmentScreenKt { + public static final fun withEnvironment (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/EnvironmentScreen; + public static final fun withEnvironment (Lcom/squareup/workflow1/ui/Screen;Lkotlin/Pair;)Lcom/squareup/workflow1/ui/EnvironmentScreen; + public static synthetic fun withEnvironment$default (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/EnvironmentScreen; + public static final fun withRegistry (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewRegistry;)Lcom/squareup/workflow1/ui/EnvironmentScreen; +} + +public final class com/squareup/workflow1/ui/NamedScreen : com/squareup/workflow1/ui/Screen, com/squareup/workflow1/ui/Wrapper { + public fun (Lcom/squareup/workflow1/ui/Screen;Ljava/lang/String;)V + public fun asSequence ()Lkotlin/sequences/Sequence; + public final fun component1 ()Lcom/squareup/workflow1/ui/Screen; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Lcom/squareup/workflow1/ui/Screen;Ljava/lang/String;)Lcom/squareup/workflow1/ui/NamedScreen; + public static synthetic fun copy$default (Lcom/squareup/workflow1/ui/NamedScreen;Lcom/squareup/workflow1/ui/Screen;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/NamedScreen; + public fun equals (Ljava/lang/Object;)Z + public fun getCompatibilityKey ()Ljava/lang/String; + public fun getContent ()Lcom/squareup/workflow1/ui/Screen; + public synthetic fun getContent ()Ljava/lang/Object; + public final fun getName ()Ljava/lang/String; + public fun hashCode ()I + public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container; + public fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/NamedScreen; + public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Wrapper; + public fun toString ()Ljava/lang/String; +} + +public abstract interface class com/squareup/workflow1/ui/Screen { +} + +public abstract interface class com/squareup/workflow1/ui/TextController { + public abstract fun getOnTextChanged ()Lkotlinx/coroutines/flow/Flow; + public abstract fun getTextValue ()Ljava/lang/String; + public abstract fun setTextValue (Ljava/lang/String;)V +} + +public final class com/squareup/workflow1/ui/TextControllerKt { + public static final fun TextController (Ljava/lang/String;)Lcom/squareup/workflow1/ui/TextController; + public static synthetic fun TextController$default (Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/TextController; +} + +public final class com/squareup/workflow1/ui/ViewEnvironment { + public static final field Companion Lcom/squareup/workflow1/ui/ViewEnvironment$Companion; + public fun equals (Ljava/lang/Object;)Z + public final fun get (Lcom/squareup/workflow1/ui/ViewEnvironmentKey;)Ljava/lang/Object; + public final fun getMap ()Ljava/util/Map; + public fun hashCode ()I + public final fun plus (Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/ViewEnvironment; + public final fun plus (Lkotlin/Pair;)Lcom/squareup/workflow1/ui/ViewEnvironment; + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/ui/ViewEnvironment$Companion { + public final fun getEMPTY ()Lcom/squareup/workflow1/ui/ViewEnvironment; +} + +public abstract class com/squareup/workflow1/ui/ViewEnvironmentKey { + public fun ()V + public fun combine (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public final fun equals (Ljava/lang/Object;)Z + public abstract fun getDefault ()Ljava/lang/Object; + public final fun hashCode ()I +} + +public abstract interface class com/squareup/workflow1/ui/ViewRegistry { + public static final field Companion Lcom/squareup/workflow1/ui/ViewRegistry$Companion; + public abstract fun getEntryFor (Lcom/squareup/workflow1/ui/ViewRegistry$Key;)Lcom/squareup/workflow1/ui/ViewRegistry$Entry; + public abstract fun getKeys ()Ljava/util/Set; +} + +public final class com/squareup/workflow1/ui/ViewRegistry$Companion : com/squareup/workflow1/ui/ViewEnvironmentKey { + public fun combine (Lcom/squareup/workflow1/ui/ViewRegistry;Lcom/squareup/workflow1/ui/ViewRegistry;)Lcom/squareup/workflow1/ui/ViewRegistry; + public synthetic fun combine (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public fun getDefault ()Lcom/squareup/workflow1/ui/ViewRegistry; + public synthetic fun getDefault ()Ljava/lang/Object; +} + +public abstract interface class com/squareup/workflow1/ui/ViewRegistry$Entry { + public abstract fun getKey ()Lcom/squareup/workflow1/ui/ViewRegistry$Key; +} + +public final class com/squareup/workflow1/ui/ViewRegistry$Key { + public fun (Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getFactoryType ()Lkotlin/reflect/KClass; + public final fun getRenderingType ()Lkotlin/reflect/KClass; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/ui/ViewRegistryKt { + public static final fun ViewRegistry ()Lcom/squareup/workflow1/ui/ViewRegistry; + public static final fun ViewRegistry ([Lcom/squareup/workflow1/ui/ViewRegistry$Entry;)Lcom/squareup/workflow1/ui/ViewRegistry; + public static final fun merge (Lcom/squareup/workflow1/ui/ViewRegistry;Lcom/squareup/workflow1/ui/ViewRegistry;)Lcom/squareup/workflow1/ui/ViewRegistry; + public static final fun plus (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/ViewRegistry;)Lcom/squareup/workflow1/ui/ViewEnvironment; + public static final fun plus (Lcom/squareup/workflow1/ui/ViewRegistry;Lcom/squareup/workflow1/ui/ViewRegistry$Entry;)Lcom/squareup/workflow1/ui/ViewRegistry; + public static final fun plus (Lcom/squareup/workflow1/ui/ViewRegistry;Lcom/squareup/workflow1/ui/ViewRegistry;)Lcom/squareup/workflow1/ui/ViewRegistry; +} + +public abstract interface annotation class com/squareup/workflow1/ui/WorkflowUiExperimentalApi : java/lang/annotation/Annotation { +} + +public abstract interface class com/squareup/workflow1/ui/Wrapper : com/squareup/workflow1/ui/Compatible, com/squareup/workflow1/ui/Container { + public abstract fun asSequence ()Lkotlin/sequences/Sequence; + public abstract fun getCompatibilityKey ()Ljava/lang/String; + public abstract fun getContent ()Ljava/lang/Object; + public abstract fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Wrapper; +} + +public final class com/squareup/workflow1/ui/Wrapper$DefaultImpls { + public static fun asSequence (Lcom/squareup/workflow1/ui/Wrapper;)Lkotlin/sequences/Sequence; + public static fun getCompatibilityKey (Lcom/squareup/workflow1/ui/Wrapper;)Ljava/lang/String; +} + +public final class com/squareup/workflow1/ui/navigation/AlertOverlay : com/squareup/workflow1/ui/navigation/ModalOverlay { + public fun (Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)V + public synthetic fun (Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/Map; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Z + public final fun component5 ()Lkotlin/jvm/functions/Function1; + public final fun copy (Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/navigation/AlertOverlay; + public static synthetic fun copy$default (Lcom/squareup/workflow1/ui/navigation/AlertOverlay;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/navigation/AlertOverlay; + public fun equals (Ljava/lang/Object;)Z + public final fun getButtons ()Ljava/util/Map; + public final fun getCancelable ()Z + public final fun getMessage ()Ljava/lang/String; + public final fun getOnEvent ()Lkotlin/jvm/functions/Function1; + public final fun getTitle ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/ui/navigation/AlertOverlay$Button : java/lang/Enum { + public static final field NEGATIVE Lcom/squareup/workflow1/ui/navigation/AlertOverlay$Button; + public static final field NEUTRAL Lcom/squareup/workflow1/ui/navigation/AlertOverlay$Button; + public static final field POSITIVE Lcom/squareup/workflow1/ui/navigation/AlertOverlay$Button; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/squareup/workflow1/ui/navigation/AlertOverlay$Button; + public static fun values ()[Lcom/squareup/workflow1/ui/navigation/AlertOverlay$Button; +} + +public abstract class com/squareup/workflow1/ui/navigation/AlertOverlay$Event { +} + +public final class com/squareup/workflow1/ui/navigation/AlertOverlay$Event$ButtonClicked : com/squareup/workflow1/ui/navigation/AlertOverlay$Event { + public fun (Lcom/squareup/workflow1/ui/navigation/AlertOverlay$Button;)V + public final fun component1 ()Lcom/squareup/workflow1/ui/navigation/AlertOverlay$Button; + public final fun copy (Lcom/squareup/workflow1/ui/navigation/AlertOverlay$Button;)Lcom/squareup/workflow1/ui/navigation/AlertOverlay$Event$ButtonClicked; + public static synthetic fun copy$default (Lcom/squareup/workflow1/ui/navigation/AlertOverlay$Event$ButtonClicked;Lcom/squareup/workflow1/ui/navigation/AlertOverlay$Button;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/navigation/AlertOverlay$Event$ButtonClicked; + public fun equals (Ljava/lang/Object;)Z + public final fun getButton ()Lcom/squareup/workflow1/ui/navigation/AlertOverlay$Button; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/ui/navigation/AlertOverlay$Event$Canceled : com/squareup/workflow1/ui/navigation/AlertOverlay$Event { + public static final field INSTANCE Lcom/squareup/workflow1/ui/navigation/AlertOverlay$Event$Canceled; +} + +public final class com/squareup/workflow1/ui/navigation/BackStackConfig : java/lang/Enum { + public static final field Companion Lcom/squareup/workflow1/ui/navigation/BackStackConfig$Companion; + public static final field First Lcom/squareup/workflow1/ui/navigation/BackStackConfig; + public static final field None Lcom/squareup/workflow1/ui/navigation/BackStackConfig; + public static final field Other Lcom/squareup/workflow1/ui/navigation/BackStackConfig; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/squareup/workflow1/ui/navigation/BackStackConfig; + public static fun values ()[Lcom/squareup/workflow1/ui/navigation/BackStackConfig; +} + +public final class com/squareup/workflow1/ui/navigation/BackStackConfig$Companion : com/squareup/workflow1/ui/ViewEnvironmentKey { + public fun getDefault ()Lcom/squareup/workflow1/ui/navigation/BackStackConfig; + public synthetic fun getDefault ()Ljava/lang/Object; +} + +public final class com/squareup/workflow1/ui/navigation/BackStackConfigKt { + public static final fun plus (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/navigation/BackStackConfig;)Lcom/squareup/workflow1/ui/ViewEnvironment; +} + +public final class com/squareup/workflow1/ui/navigation/BackStackScreen : com/squareup/workflow1/ui/Container, com/squareup/workflow1/ui/Screen { + public static final field Companion Lcom/squareup/workflow1/ui/navigation/BackStackScreen$Companion; + public fun (Lcom/squareup/workflow1/ui/Screen;[Lcom/squareup/workflow1/ui/Screen;)V + public fun asSequence ()Lkotlin/sequences/Sequence; + public fun equals (Ljava/lang/Object;)Z + public final fun get (I)Lcom/squareup/workflow1/ui/Screen; + public final fun getBackStack ()Ljava/util/List; + public final fun getFrames ()Ljava/util/List; + public final fun getTop ()Lcom/squareup/workflow1/ui/Screen; + public fun hashCode ()I + public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container; + public fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/navigation/BackStackScreen; + public final fun mapIndexed (Lkotlin/jvm/functions/Function2;)Lcom/squareup/workflow1/ui/navigation/BackStackScreen; + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/ui/navigation/BackStackScreen$Companion { + public final fun fromList (Ljava/util/List;)Lcom/squareup/workflow1/ui/navigation/BackStackScreen; + public final fun fromListOrNull (Ljava/util/List;)Lcom/squareup/workflow1/ui/navigation/BackStackScreen; +} + +public final class com/squareup/workflow1/ui/navigation/BackStackScreenKt { + public static final fun plus (Lcom/squareup/workflow1/ui/navigation/BackStackScreen;Lcom/squareup/workflow1/ui/navigation/BackStackScreen;)Lcom/squareup/workflow1/ui/navigation/BackStackScreen; + public static final fun toBackStackScreen (Ljava/util/List;)Lcom/squareup/workflow1/ui/navigation/BackStackScreen; + public static final fun toBackStackScreenOrNull (Ljava/util/List;)Lcom/squareup/workflow1/ui/navigation/BackStackScreen; +} + +public final class com/squareup/workflow1/ui/navigation/BodyAndOverlaysScreen : com/squareup/workflow1/ui/Compatible, com/squareup/workflow1/ui/Screen { + public fun (Lcom/squareup/workflow1/ui/Screen;Ljava/util/List;Ljava/lang/String;)V + public synthetic fun (Lcom/squareup/workflow1/ui/Screen;Ljava/util/List;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getBody ()Lcom/squareup/workflow1/ui/Screen; + public fun getCompatibilityKey ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public final fun getOverlays ()Ljava/util/List; + public final fun mapBody (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/navigation/BodyAndOverlaysScreen; + public final fun mapOverlays (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/navigation/BodyAndOverlaysScreen; +} + +public final class com/squareup/workflow1/ui/navigation/FullScreenModal : com/squareup/workflow1/ui/navigation/ModalOverlay, com/squareup/workflow1/ui/navigation/ScreenOverlay { + public fun (Lcom/squareup/workflow1/ui/Screen;)V + public fun asSequence ()Lkotlin/sequences/Sequence; + public fun getCompatibilityKey ()Ljava/lang/String; + public fun getContent ()Lcom/squareup/workflow1/ui/Screen; + public synthetic fun getContent ()Ljava/lang/Object; + public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container; + public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Wrapper; + public fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/navigation/FullScreenModal; + public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/navigation/ScreenOverlay; +} + +public abstract interface class com/squareup/workflow1/ui/navigation/ModalOverlay : com/squareup/workflow1/ui/navigation/Overlay { +} + +public abstract interface class com/squareup/workflow1/ui/navigation/Overlay { +} + +public abstract interface class com/squareup/workflow1/ui/navigation/ScreenOverlay : com/squareup/workflow1/ui/Wrapper, com/squareup/workflow1/ui/navigation/Overlay { + public abstract fun getContent ()Lcom/squareup/workflow1/ui/Screen; + public abstract fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/navigation/ScreenOverlay; +} + +public final class com/squareup/workflow1/ui/navigation/ScreenOverlay$DefaultImpls { + public static fun asSequence (Lcom/squareup/workflow1/ui/navigation/ScreenOverlay;)Lkotlin/sequences/Sequence; + public static fun getCompatibilityKey (Lcom/squareup/workflow1/ui/navigation/ScreenOverlay;)Ljava/lang/String; +} + diff --git a/workflow-ui/core/build.gradle.kts b/workflow-ui/core/build.gradle.kts new file mode 100644 index 000000000..ffe9069a1 --- /dev/null +++ b/workflow-ui/core/build.gradle.kts @@ -0,0 +1,27 @@ +import com.squareup.workflow1.buildsrc.iosTargets + +plugins { + id("kotlin-multiplatform") + id("published") +} + +kotlin { + val targets = project.findProperty("workflow.targets") ?: "kmp" + if (targets == "kmp" || targets == "ios") { + iosTargets() + } + if (targets == "kmp" || targets == "jvm") { + jvm { withJava() } + } + if (targets == "kmp" || targets == "js") { + js(IR) { browser() } + } +} + +dependencies { + commonMainApi(libs.kotlin.jdk6) + commonMainApi(libs.kotlinx.coroutines.core) + + commonTestImplementation(libs.kotlinx.coroutines.test.common) + commonTestImplementation(libs.kotlin.test.jdk) +} diff --git a/workflow-ui/core/dependencies/jsRuntimeClasspath.txt b/workflow-ui/core/dependencies/jsRuntimeClasspath.txt new file mode 100644 index 000000000..029bf6660 --- /dev/null +++ b/workflow-ui/core/dependencies/jsRuntimeClasspath.txt @@ -0,0 +1,8 @@ +org.jetbrains.kotlin:kotlin-bom:1.9.24 +org.jetbrains.kotlin:kotlin-dom-api-compat:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-js:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib:1.9.24 +org.jetbrains.kotlin:kotlinx-atomicfu-runtime:1.8.20 +org.jetbrains.kotlinx:atomicfu-js:0.21.0 +org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 diff --git a/workflow-ui/core/dependencies/jvmRuntimeClasspath.txt b/workflow-ui/core/dependencies/jvmRuntimeClasspath.txt new file mode 100644 index 000000000..9d47f54c9 --- /dev/null +++ b/workflow-ui/core/dependencies/jvmRuntimeClasspath.txt @@ -0,0 +1,8 @@ +org.jetbrains.kotlin:kotlin-bom:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib:1.9.24 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 +org.jetbrains:annotations:23.0.0 diff --git a/workflow-ui/core/dependencies/runtimeClasspath.txt b/workflow-ui/core/dependencies/runtimeClasspath.txt new file mode 100644 index 000000000..b70b1727c --- /dev/null +++ b/workflow-ui/core/dependencies/runtimeClasspath.txt @@ -0,0 +1,9 @@ +org.jetbrains.kotlin:kotlin-bom:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib:1.9.24 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 +org.jetbrains:annotations:23.0.0 diff --git a/workflow-ui/core/gradle.properties b/workflow-ui/core/gradle.properties new file mode 100644 index 000000000..d15563e9a --- /dev/null +++ b/workflow-ui/core/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=workflow-ui-core +POM_NAME=Workflow UI Core +POM_PACKAGING=jar diff --git a/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/Compatible.kt b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/Compatible.kt new file mode 100644 index 000000000..7f15aeb50 --- /dev/null +++ b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/Compatible.kt @@ -0,0 +1,57 @@ +package com.squareup.workflow1.ui + +/** + * Normally returns true if [me] and [you] are instances of the same class. + * If that common class implements [Compatible], both instances must also + * have the same [Compatible.compatibilityKey]. + * + * A convenient way to take control over the matching behavior of objects that + * don't implement [Compatible] is to wrap them with [NamedScreen]. + */ +@WorkflowUiExperimentalApi +public fun compatible( + me: Any, + you: Any +): Boolean { + return when { + me::class != you::class -> false + me !is Compatible -> true + else -> me.compatibilityKey == (you as Compatible).compatibilityKey + } +} + +/** + * Implemented by objects whose [compatibility][compatible] requires more nuance + * than just being of the same type. + * + * Renderings that don't implement this interface directly can be distinguished + * by wrapping them with [NamedScreen]. + */ +@WorkflowUiExperimentalApi +public interface Compatible { + /** + * Instances of the same type are [compatible] iff they have the same [compatibilityKey]. + */ + public val compatibilityKey: String + + public companion object { + /** + * Calculates a suitable [Compatible.compatibilityKey] for a given [value], incorporating + * [name] if that is not blank. Includes the [compatibilityKey] for [value] if it + * implements [Compatible], to support recursion from wrapping. + * + * Style note: [name] is given more prominence than the key generate + */ + public fun keyFor( + value: Any, + name: String = "" + ): String { + var key = (value as? Compatible)?.compatibilityKey + if (key == null) { + key = value::class.toString() + } + + return name.takeIf { it.isNotEmpty() }?.let { "$name($key)" } ?: key + } + } +} diff --git a/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/CompositeViewRegistry.kt b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/CompositeViewRegistry.kt new file mode 100644 index 000000000..44501fe2f --- /dev/null +++ b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/CompositeViewRegistry.kt @@ -0,0 +1,61 @@ +package com.squareup.workflow1.ui + +import com.squareup.workflow1.ui.ViewRegistry.Entry +import com.squareup.workflow1.ui.ViewRegistry.Key + +/** + * A [ViewRegistry] that contains only other registries and delegates to their [getEntryFor] + * methods. + * + * Whenever any registries are combined using the [ViewRegistry] factory functions or `plus` + * operators, an instance of this class is returned. All registries' keys are checked at + * construction to ensure that no duplicate keys exist. + * + * The implementation of [getEntryFor] consists of a single layer of indirection – the responsible + * [ViewRegistry] is looked up in a map by key, and then that registry's [getEntryFor] is called. + * + * When multiple [CompositeViewRegistry]s are combined, they are flattened, so that there is never + * more than one layer of indirection. In other words, a [CompositeViewRegistry] will never contain + * a reference to another [CompositeViewRegistry]. + */ +@WorkflowUiExperimentalApi +internal class CompositeViewRegistry private constructor( + private val registriesByKey: Map, ViewRegistry> +) : ViewRegistry { + + constructor (vararg registries: ViewRegistry) : this(mergeRegistries(*registries)) + + override val keys: Set> get() = registriesByKey.keys + + override fun getEntryFor( + key: Key + ): Entry? = registriesByKey[key]?.getEntryFor(key) + + override fun toString(): String { + return "CompositeViewRegistry(${registriesByKey.values.toSet().map { it.toString() }})" + } + + companion object { + private fun mergeRegistries(vararg registries: ViewRegistry): Map, ViewRegistry> { + val registriesByKey = mutableMapOf, ViewRegistry>() + + fun putAllUnique(other: Map, ViewRegistry>) { + val duplicateKeys = registriesByKey.keys.intersect(other.keys) + require(duplicateKeys.isEmpty()) { + "Must not have duplicate entries: $duplicateKeys. Use merge to replace existing entries." + } + registriesByKey.putAll(other) + } + + registries.forEach { registry -> + if (registry is CompositeViewRegistry) { + // Try to keep the composite registry as flat as possible. + putAllUnique(registry.registriesByKey) + } else { + putAllUnique(registry.keys.associateWith { registry }) + } + } + return registriesByKey.toMap() + } + } +} diff --git a/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/Container.kt b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/Container.kt new file mode 100644 index 000000000..277d7b382 --- /dev/null +++ b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/Container.kt @@ -0,0 +1,74 @@ +package com.squareup.workflow1.ui + +import com.squareup.workflow1.ui.Compatible.Companion.keyFor + +/** + * A rendering type comprised of a set of other renderings. + * + * Why two parameter types? The separate [BaseT] type allows implementations + * and sub-interfaces to constrain the types that [map] is allowed to + * transform [C] to. E.g., it allows `FooWrapper` to declare + * that [map] is only able to transform `S` to other types of `Screen`. + * + * @param BaseT the invariant base type of the contents of such a container, + * usually [Screen] or [Overlay][com.squareup.workflow1.ui.navigation.Overlay]. + * It is common for the [Container] itself to implement [BaseT], but that is + * not a requirement. E.g., [ScreenOverlay][com.squareup.workflow1.ui.navigation.ScreenOverlay] + * is an [Overlay][com.squareup.workflow1.ui.navigation.Overlay], but it + * wraps a [Screen]. + * + * @param C the specific subtype of [BaseT] collected by this [Container]. + */ +@WorkflowUiExperimentalApi +public interface Container { + public fun asSequence(): Sequence + + /** + * Returns a [Container] with the [transform]ed contents of the receiver. + * It is expected that an implementation will take advantage of covariance + * to declare its own type as the return type, rather than plain old [Container]. + * This requirement is not enforced because recursive generics are a fussy nuisance. + * + * For example, suppose we want to create `LoggingScreen`, one that wraps any + * other screen to add some logging calls. Its implementation of this method + * would be expected to have a return type of `LoggingScreen` rather than `Container`: + * + * override fun map(transform: (C) -> D): LoggingScreen = + * LoggingScreen(transform(content)) + * + * By requiring all [Container] types to implement [map], we ensure that their + * contents can be repackaged in interesting ways, e.g.: + * + * val childBackStackScreen = renderChild(childWorkflow) { ... } + * val loggingBackStackScreen = childBackStackScreen.map { LoggingScreen(it) } + */ + public fun map(transform: (C) -> D): Container +} + +/** + * A [Container] rendering that wraps exactly one other rendering, its [content]. These are + * typically used to "add value" to the [content], e.g. an + * [EnvironmentScreen][com.squareup.workflow1.ui.EnvironmentScreen] that allows + * changes to be made to the [ViewEnvironment]. + * + * Usually a [Wrapper] is [Compatible] only with others of the same type with + * [Compatible] [content]. In aid of that, this interface extends [Compatible] and + * provides a convenient default implementation of [compatibilityKey]. + */ +@WorkflowUiExperimentalApi +public interface Wrapper : Container, Compatible { + public val content: C + + /** + * Default implementation makes this [Wrapper] compatible with others of the same type, + * and which wrap compatible [content]. + */ + public override val compatibilityKey: String + get() = keyFor(content, this::class.simpleName ?: "Wrapper") + + public override fun asSequence(): Sequence = sequenceOf(content) + + public override fun map( + transform: (C) -> D + ): Wrapper +} diff --git a/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/EnvironmentScreen.kt b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/EnvironmentScreen.kt new file mode 100644 index 000000000..cfbbe8ea2 --- /dev/null +++ b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/EnvironmentScreen.kt @@ -0,0 +1,64 @@ +package com.squareup.workflow1.ui + +/** + * Pairs a [content] rendering with a [environment] to support its display. + * Typically the rendering type (`RenderingT`) of the root of a UI workflow, + * but can be used at any point to modify the [ViewEnvironment] received from + * a parent view. + * + * UI kits are expected to provide handling for this class by default. + */ +@WorkflowUiExperimentalApi +public class EnvironmentScreen( + public override val content: C, + public val environment: ViewEnvironment = ViewEnvironment.EMPTY +) : Wrapper, Screen { + override fun map(transform: (C) -> D): EnvironmentScreen = + EnvironmentScreen(transform(content), environment) +} + +/** + * Returns an [EnvironmentScreen] derived from the receiver, whose + * [EnvironmentScreen.environment] includes [viewRegistry]. + * + * If the receiver is an [EnvironmentScreen], uses + * [ViewRegistry.merge][com.squareup.workflow1.ui.merge] to preserve the [ViewRegistry] + * entries of both. + */ +@WorkflowUiExperimentalApi +public fun Screen.withRegistry(viewRegistry: ViewRegistry): EnvironmentScreen<*> { + return withEnvironment(ViewEnvironment.EMPTY + viewRegistry) +} + +/** + * Returns an [EnvironmentScreen] derived from the receiver, + * whose [EnvironmentScreen.environment] includes the values in the given [environment]. + * + * If the receiver is an [EnvironmentScreen], uses + * [ViewRegistry.merge][com.squareup.workflow1.ui.merge] to preserve the [ViewRegistry] + * entries of both. + */ +@WorkflowUiExperimentalApi +public fun Screen.withEnvironment( + environment: ViewEnvironment = ViewEnvironment.EMPTY +): EnvironmentScreen<*> { + return when (this) { + is EnvironmentScreen<*> -> { + if (environment.map.isEmpty()) { + this + } else { + EnvironmentScreen(content, this.environment + environment) + } + } + else -> EnvironmentScreen(this, environment) + } +} + +/** + * Returns an [EnvironmentScreen] derived from the receiver, + * whose [EnvironmentScreen.environment] includes the given entry. + */ +@WorkflowUiExperimentalApi +public fun Screen.withEnvironment( + entry: Pair, T> +): EnvironmentScreen<*> = withEnvironment(ViewEnvironment.EMPTY + entry) diff --git a/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/NamedScreen.kt b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/NamedScreen.kt new file mode 100644 index 000000000..ad3e64715 --- /dev/null +++ b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/NamedScreen.kt @@ -0,0 +1,29 @@ +package com.squareup.workflow1.ui + +import com.squareup.workflow1.ui.Compatible.Companion + +/** + * Allows [Screen] renderings that do not implement [Compatible] themselves to be distinguished + * by more than just their type. Instances are [compatible] if they have the same name + * and have [compatible] [content] fields. + * + * UI kits are expected to provide handling for this class by default. + */ +@WorkflowUiExperimentalApi +public data class NamedScreen( + override val content: C, + val name: String +) : Screen, Wrapper { + init { + require(name.isNotBlank()) { "name must not be blank." } + } + + override val compatibilityKey: String = Companion.keyFor(content, "NamedScreen:$name") + + override fun map(transform: (C) -> D): NamedScreen = + NamedScreen(transform(content), name) + + override fun toString(): String { + return "${super.toString()}: $compatibilityKey" + } +} diff --git a/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/Screen.kt b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/Screen.kt new file mode 100644 index 000000000..37b586e8a --- /dev/null +++ b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/Screen.kt @@ -0,0 +1,7 @@ +package com.squareup.workflow1.ui + +/** + * Marker interface implemented by renderings that map to a UI system's 2d view class. + */ +@WorkflowUiExperimentalApi +public interface Screen diff --git a/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/TextController.kt b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/TextController.kt new file mode 100644 index 000000000..bef82ccff --- /dev/null +++ b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/TextController.kt @@ -0,0 +1,90 @@ +package com.squareup.workflow1.ui + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.drop + +/** + * Helper class for keeping a workflow in sync with editable text in a UI, + * without interfering with the user's typing. + * + * ## Usage + * + * 1. For every editable string in your state, create a property of type [TextController]. + * ``` + * data class State(val text: TextController = TextController()) + * ``` + * 2. Create a matching property in your rendering type. + * ``` + * data class Rendering(val text: TextController) + * ``` + * 3. In your `render` method, copy each [TextController] from your state to your rendering: + * ``` + * return Rendering(state.text) + * ``` + * 4. In your view code's `showRendering` method, call the appropriate extension + * function for your UI platform, e.g.: + * + * - `control()` for an Android EditText view + * - `asMutableState()` from an Android `@Composable` function + * + * If your workflow needs to access or change the current text value, get the value from [textValue]. + * If your workflow needs to react to changes, it can observe [onTextChanged] by converting it to a + * worker. + */ +@WorkflowUiExperimentalApi +public interface TextController { + + /** + * A [Flow] that emits the text value whenever it changes -- and only when it changes, the current value + * is not provided at subscription time. Workflows can safely observe changes by + * converting this value to a worker. (When using multiple instances, remember to provide unique + * key values to each `asWorker` call.) + * + * If you can do processing that doesn't require running a `WorkflowAction` or triggering a render + * pass, it can be done in regular Flow operators before converting to a worker. + */ + public val onTextChanged: Flow + + /** + * The current text value. + */ + public var textValue: String +} + +/** + * Create instance for default implementation of [TextController]. + */ +@WorkflowUiExperimentalApi +public fun TextController(initialValue: String = ""): TextController { + return TextControllerImpl(initialValue) +} + +/** + * Default implementation of [TextController]. + */ +@WorkflowUiExperimentalApi +private class TextControllerImpl(initialValue: String) : TextController { + + /** + * This flow is not exposed as a StateFlow intentionally. Doing so would encourage observing it from + * workflows, which is not desirable since StateFlows emit immediately upon subscription, which means + * that for a workflow runtime running N workflows that each observe M [TextController]s, the first + * render pass would trigger NxM useless render passes. + * + * Instead, only text _change_ events are exposed, as [onTextChanged], which is suitable for use as a + * worker. The current value is exposed as a separate var, [textValue]. + * + * Subscriptions from the view layer that need the initial value can call [textValue] + * to prime the pump manually. + */ + private val _textValue: MutableStateFlow = MutableStateFlow(initialValue) + + override val onTextChanged: Flow = _textValue.drop(1) + + override var textValue: String + get() = _textValue.value + set(value) { + _textValue.value = value + } +} diff --git a/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/TypedViewRegistry.kt b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/TypedViewRegistry.kt new file mode 100644 index 000000000..161a5ad4b --- /dev/null +++ b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/TypedViewRegistry.kt @@ -0,0 +1,43 @@ +package com.squareup.workflow1.ui + +import com.squareup.workflow1.ui.ViewRegistry.Entry +import com.squareup.workflow1.ui.ViewRegistry.Key +import kotlin.reflect.KClass + +/** + * A [ViewRegistry] that contains a set of [Entry]s, keyed by the [KClass]es of the + * rendering types. + */ +@WorkflowUiExperimentalApi +internal class TypedViewRegistry private constructor( + private val bindings: Map, Entry<*>> +) : ViewRegistry { + + constructor(vararg bindings: Entry<*>) : this( + bindings.associateBy { + require(it.key.factoryType.isInstance(it)) { + "Factory $it must be of the type declared in its key, ${it.key.factoryType}" + } + it.key + } + .apply { + check(keys.size == bindings.size) { + "${bindings.map { it.key }} must not have duplicate entries." + } + } as Map, Entry<*>> + ) + + override val keys: Set> get() = bindings.keys + + override fun getEntryFor( + key: Key + ): Entry? { + @Suppress("UNCHECKED_CAST") + return bindings[key] as? Entry + } + + override fun toString(): String { + val map = bindings.map { "${it.key}=${it.value::class}" } + return "TypedViewRegistry(bindings=$map)" + } +} diff --git a/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/ViewEnvironment.kt b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/ViewEnvironment.kt new file mode 100644 index 000000000..e7f891713 --- /dev/null +++ b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/ViewEnvironment.kt @@ -0,0 +1,86 @@ +package com.squareup.workflow1.ui + +/** + * Immutable map of values that a parent view can pass down to + * its children. Allows containers to give descendants information about + * the context in which they're drawing. + * + * Calling [Screen.withEnvironment][com.squareup.workflow1.ui.withEnvironment] + * on a [Screen] is the easiest way to customize its environment before rendering it. + */ +@WorkflowUiExperimentalApi +public class ViewEnvironment +private constructor( + public val map: Map, Any> = emptyMap() +) { + public operator fun get(key: ViewEnvironmentKey): T = getOrNull(key) ?: key.default + + public operator fun plus(pair: Pair, T>): ViewEnvironment { + val (newKey, newValue) = pair + val newPair = getOrNull(newKey) + ?.let { oldValue -> newKey to newKey.combine(oldValue, newValue) } + ?: pair + return ViewEnvironment(map + newPair) + } + + public operator fun plus(other: ViewEnvironment): ViewEnvironment { + if (this == other) return this + if (other.map.isEmpty()) return this + if (map.isEmpty()) return other + val newMap = map.toMutableMap() + other.map.entries.forEach { (key, value) -> + @Suppress("UNCHECKED_CAST") + newMap[key] = getOrNull(key as ViewEnvironmentKey) + ?.let { oldValue -> key.combine(oldValue, value) } + ?: value + } + return ViewEnvironment(newMap) + } + + override fun toString(): String = "ViewEnvironment($map)" + + override fun equals(other: Any?): Boolean = + (other as? ViewEnvironment)?.let { it.map == map } ?: false + + override fun hashCode(): Int = map.hashCode() + + @Suppress("UNCHECKED_CAST") + private fun getOrNull(key: ViewEnvironmentKey): T? = map[key] as? T + + public companion object { + public val EMPTY: ViewEnvironment = ViewEnvironment() + } +} + +/** + * Defines a value type [T] that can be provided by a [ViewEnvironment] map, + * and specifies its [default] value. + * + * It is hard to imagine a useful implementation of this that is not a Kotlin `object`. + * Preferred use is to have the `companion object` of [T] extend this class. See + * [BackStackConfig.Companion][com.squareup.workflow1.ui.navigation.BackStackConfig.Companion] + * for an example. + */ +@WorkflowUiExperimentalApi +public abstract class ViewEnvironmentKey { + /** + * Defines the default value for this key. It is a grievous error for this value to be + * dynamic in any way. + */ + public abstract val default: T + + /** + * Applied from [ViewEnvironment.plus] when the receiving environment already contains + * a value for this key. The default implementation replaces [left] with [right]. + */ + public open fun combine( + left: T, + right: T + ): T = right + + final override fun equals(other: Any?): Boolean { + return this === other || (other != null && this::class == other::class) + } + + final override fun hashCode(): Int = this::class.hashCode() +} diff --git a/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/ViewRegistry.kt b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/ViewRegistry.kt new file mode 100644 index 000000000..0c641971b --- /dev/null +++ b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/ViewRegistry.kt @@ -0,0 +1,214 @@ +package com.squareup.workflow1.ui + +import com.squareup.workflow1.ui.ViewRegistry.Entry +import com.squareup.workflow1.ui.ViewRegistry.Key +import kotlin.js.JsName +import kotlin.reflect.KClass +import kotlin.reflect.safeCast + +/** + * The [ViewEnvironment] service that can be used to display the stream of renderings + * from a workflow tree as [View] instances. This is the engine behind [AndroidViewRendering], + * [WorkflowViewStub] and [ViewFactory]. Most apps can ignore [ViewRegistry] as an implementation + * detail, by using [AndroidViewRendering] to tie their rendering classes to view code. + * + * To avoid that coupling between workflow code and the Android runtime, registries can + * be loaded with [ViewFactory] instances at runtime, and provided as an optional parameter to + * [WorkflowLayout.start]. + * + * For example: + * + * val AuthViewFactories = ViewRegistry( + * AuthorizingLayoutRunner, LoginLayoutRunner, SecondFactorLayoutRunner + * ) + * + * val TicTacToeViewFactories = ViewRegistry( + * NewGameLayoutRunner, GamePlayLayoutRunner, GameOverLayoutRunner + * ) + * + * val ApplicationViewFactories = ViewRegistry(ApplicationLayoutRunner) + + * AuthViewFactories + TicTacToeViewFactories + * + * override fun onCreate(savedInstanceState: Bundle?) { + * super.onCreate(savedInstanceState) + * + * val model: MyViewModel by viewModels() + * setContentView( + * WorkflowLayout(this).apply { start(model.renderings, ApplicationViewFactories) } + * ) + * } + * + * /** As always, use an androidx ViewModel for state that survives config change. */ + * class MyViewModel(savedState: SavedStateHandle) : ViewModel() { + * val renderings: StateFlow by lazy { + * renderWorkflowIn( + * workflow = rootWorkflow, + * scope = viewModelScope, + * savedStateHandle = savedState + * ) + * } + * } + * + * In the above example, it is assumed that the `companion object`s of the various + * decoupled [LayoutRunner] classes honor a convention of implementing [ViewFactory], in + * aid of this kind of assembly. + * + * class GamePlayLayoutRunner(view: View) : LayoutRunner { + * + * // ... + * + * companion object : ViewFactory by LayoutRunner.bind( + * R.layout.game_layout, ::GameLayoutRunner + * ) + * } + */ +@WorkflowUiExperimentalApi +public interface ViewRegistry { + /** + * Identifies a UI factory [Entry] in a [ViewRegistry]. + * + * @param renderingType the type of view model for which [factoryType] instances can build UI + * @param factoryType the type of the UI factory that can build UI for [renderingType] + */ + public class Key( + public val renderingType: KClass, + public val factoryType: KClass + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null) return false + if (this::class != other::class) return false + + other as Key<*, *> + + if (renderingType != other.renderingType) return false + return factoryType == other.factoryType + } + + override fun hashCode(): Int { + var result = renderingType.hashCode() + result = 31 * result + factoryType.hashCode() + return result + } + + override fun toString(): String { + return "Key(renderingType=$renderingType, factoryType=$factoryType)" + } + } + + /** + * Implemented by a factory that can build some kind of UI for view models + * of type [RenderingT], and which can be listed in a [ViewRegistry]. The + * [Key.factoryType] field of [key] must be the type of this [Entry]. + */ + public interface Entry { + public val key: Key + } + + /** + * The set of unique keys which this registry can derive from the renderings passed to + * [getEntryFor] and for which it knows how to create UI. + * + * Used to ensure that duplicate bindings are never registered. + */ + public val keys: Set> + + /** + * Returns the [Entry] that was registered for the given [key], or null + * if none was found. + */ + public fun getEntryFor( + key: Key + ): Entry? + + public companion object : ViewEnvironmentKey() { + override val default: ViewRegistry get() = ViewRegistry() + override fun combine( + left: ViewRegistry, + right: ViewRegistry + ): ViewRegistry = left.merge(right) + } +} + +@WorkflowUiExperimentalApi +public inline fun ViewRegistry.getFactoryFor( + rendering: RenderingT +): FactoryT? { + return FactoryT::class.safeCast(getEntryFor(Key(rendering::class, FactoryT::class))) +} + +@WorkflowUiExperimentalApi +public inline fun < + reified RenderingT : Any, + reified FactoryT : Any + > ViewRegistry.getFactoryFor(): FactoryT? { + return FactoryT::class.safeCast(getEntryFor(Key(RenderingT::class, FactoryT::class))) +} + +@WorkflowUiExperimentalApi +public inline operator fun ViewRegistry.get( + key: Key +): FactoryT? = FactoryT::class.safeCast(getEntryFor(key)) + +@WorkflowUiExperimentalApi +public fun ViewRegistry(vararg bindings: Entry<*>): ViewRegistry = + TypedViewRegistry(*bindings) + +/** + * Returns a [ViewRegistry] that contains no bindings. + * + * Exists as a separate overload from the other two functions to disambiguate between them. + */ +@WorkflowUiExperimentalApi +@JsName("CreateViewRegistry") +public fun ViewRegistry(): ViewRegistry = TypedViewRegistry() + +/** + * Transforms the receiver to add [entry], throwing [IllegalArgumentException] if the receiver + * already has a matching [entry]. Use [merge] to replace an existing entry with a new one. + */ +@WorkflowUiExperimentalApi +public operator fun ViewRegistry.plus(entry: Entry<*>): ViewRegistry = + this + ViewRegistry(entry) + +/** + * Transforms the receiver to add all entries from [other]. + * + * @throws [IllegalArgumentException] if the receiver already has an matching [Entry]. + * Use [merge] to replace existing entries instead. + */ +@WorkflowUiExperimentalApi +public operator fun ViewRegistry.plus(other: ViewRegistry): ViewRegistry { + if (other.keys.isEmpty()) return this + if (this.keys.isEmpty()) return other + return CompositeViewRegistry(this, other) +} + +/** + * Returns a new [ViewEnvironment] that adds [registry] to the receiver. + * If the receiver already has a [ViewRegistry], [ViewEnvironmentKey.combine] + * is applied as usual to [merge] its entries. + */ +@WorkflowUiExperimentalApi +public operator fun ViewEnvironment.plus(registry: ViewRegistry): ViewEnvironment { + if (this[ViewRegistry] === registry) return this + if (registry.keys.isEmpty()) return this + return this + (ViewRegistry to registry) +} + +/** + * Combines the receiver with [other]. If there are conflicting entries, + * those in [other] are preferred. + */ +@WorkflowUiExperimentalApi +public infix fun ViewRegistry.merge(other: ViewRegistry): ViewRegistry { + if (this === other) return this + if (other.keys.isEmpty()) return this + if (this.keys.isEmpty()) return other + + return (keys + other.keys).asSequence() + .map { other.getEntryFor(it) ?: getEntryFor(it)!! } + .toList() + .toTypedArray() + .let { ViewRegistry(*it) } +} diff --git a/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/WorkflowUiExperimentalApi.kt b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/WorkflowUiExperimentalApi.kt new file mode 100644 index 000000000..0f8f0fab7 --- /dev/null +++ b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/WorkflowUiExperimentalApi.kt @@ -0,0 +1,25 @@ +@file:JvmMultifileClass +@file:JvmName("Workflows") + +package com.squareup.workflow1.ui + +import kotlin.RequiresOptIn.Level.ERROR +import kotlin.annotation.AnnotationRetention.BINARY +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName + +/** + * Marks Workflow user interface APIs which are still in flux. Annotated code SHOULD NOT be used + * in library code or app code that you are not prepared to update when changing even minor + * workflow versions. Proceed with caution, and be ready to have the rug pulled out from under you. + */ +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.PROPERTY, + AnnotationTarget.FUNCTION, + AnnotationTarget.TYPEALIAS +) +@MustBeDocumented +@Retention(value = BINARY) +@RequiresOptIn(level = ERROR) +public annotation class WorkflowUiExperimentalApi diff --git a/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/AlertOverlay.kt b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/AlertOverlay.kt new file mode 100644 index 000000000..3486f3bf0 --- /dev/null +++ b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/AlertOverlay.kt @@ -0,0 +1,50 @@ +package com.squareup.workflow1.ui.navigation + +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +/** + * Models a typical "You sure about that?" alert box. + * + * UI kits are expected to provide handling for this class by default. + */ +@WorkflowUiExperimentalApi +public data class AlertOverlay( + val buttons: Map = emptyMap(), + val message: String = "", + val title: String = "", + val cancelable: Boolean = true, + val onEvent: (Event) -> Unit +) : ModalOverlay { + public enum class Button { + POSITIVE, + NEGATIVE, + NEUTRAL + } + + public sealed class Event { + public data class ButtonClicked(val button: Button) : Event() + + public object Canceled : Event() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null) return false + if (this::class != other::class) return false + + other as AlertOverlay + + return buttons == other.buttons && + message == other.message && + title == other.title && + cancelable == other.cancelable + } + + override fun hashCode(): Int { + var result = buttons.hashCode() + result = 31 * result + message.hashCode() + result = 31 * result + title.hashCode() + result = 31 * result + cancelable.hashCode() + return result + } +} diff --git a/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/BackStackConfig.kt b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/BackStackConfig.kt new file mode 100644 index 000000000..28702b918 --- /dev/null +++ b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/BackStackConfig.kt @@ -0,0 +1,39 @@ +package com.squareup.workflow1.ui.navigation + +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.ViewEnvironmentKey +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.navigation.BackStackConfig.First +import com.squareup.workflow1.ui.navigation.BackStackConfig.Other + +/** + * Informs views whether they're children of a [BackStackScreen], + * and if so whether they're the [first frame][First] or [not][Other]. + */ +@WorkflowUiExperimentalApi +public enum class BackStackConfig { + /** + * There is no [BackStackScreen] above here. + */ + None, + + /** + * This rendering is the first frame in a [BackStackScreen]. + * Useful as a hint to disable "go back" behavior, or replace it with "go up" behavior. + */ + First, + + /** + * This rendering is in a [BackStackScreen] but is not the first frame. + * Useful as a hint to enable "go back" behavior. + */ + Other; + + public companion object : ViewEnvironmentKey() { + override val default: BackStackConfig = None + } +} + +@WorkflowUiExperimentalApi +public operator fun ViewEnvironment.plus(config: BackStackConfig): ViewEnvironment = + this + (BackStackConfig to config) diff --git a/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/BackStackScreen.kt b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/BackStackScreen.kt new file mode 100644 index 000000000..ccddf6381 --- /dev/null +++ b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/BackStackScreen.kt @@ -0,0 +1,115 @@ +package com.squareup.workflow1.ui.navigation + +import com.squareup.workflow1.ui.Container +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.navigation.BackStackScreen.Companion +import com.squareup.workflow1.ui.navigation.BackStackScreen.Companion.fromList +import com.squareup.workflow1.ui.navigation.BackStackScreen.Companion.fromListOrNull + +/** + * Represents an active screen ([top]), and a set of previously visited screens to which we may + * return ([backStack]). By rendering the entire history we allow the UI to do things like maintain + * cached view state, implement drag-back gestures without waiting for the workflow, etc. + * + * Effectively a list that can never be empty. + * + * UI kits are expected to provide handling for this class by default. + * + * @see fromList + * @see fromListOrNull + */ +@WorkflowUiExperimentalApi +public class BackStackScreen internal constructor( + public val frames: List +) : Screen, Container { + /** + * Creates a screen with elements listed from the [bottom] to the top. + */ + public constructor( + bottom: StackedT, + vararg rest: StackedT + ) : this(listOf(bottom) + rest) + + override fun asSequence(): Sequence = frames.asSequence() + + /** + * The active screen. + */ + public val top: StackedT = frames.last() + + /** + * Screens to which we may return. + */ + public val backStack: List = frames.subList(0, frames.size - 1) + + public operator fun get(index: Int): StackedT = frames[index] + + public override fun map( + transform: (StackedT) -> StackedU + ): BackStackScreen { + return frames.map(transform).toBackStackScreen() + } + + public fun mapIndexed(transform: (index: Int, StackedT) -> R): BackStackScreen { + return frames.mapIndexed(transform) + .toBackStackScreen() + } + + override fun equals(other: Any?): Boolean { + return (other as? BackStackScreen<*>)?.frames == frames + } + + override fun hashCode(): Int { + return frames.hashCode() + } + + override fun toString(): String { + return "${this::class.simpleName}($frames)" + } + + public companion object { + /** + * Builds a [BackStackScreen] from a non-empty list of [frames]. + * + * @throws IllegalArgumentException is [frames] is empty + */ + public fun fromList(frames: List): BackStackScreen { + require(frames.isNotEmpty()) { + "A BackStackScreen must have at least one frame." + } + return BackStackScreen(frames) + } + + /** + * Builds a [BackStackScreen] from a list of [frames], or returns `null` + * if [frames] is empty. + */ + public fun fromListOrNull(frames: List): BackStackScreen? { + return when { + frames.isEmpty() -> null + else -> BackStackScreen(frames) + } + } + } +} + +/** + * Returns a new [BackStackScreen] with the [BackStackScreen.frames] of [other] added + * to those of the receiver. [other] is nullable for convenience when using with + * [toBackStackScreenOrNull]. + */ +@WorkflowUiExperimentalApi +public operator fun BackStackScreen.plus( + other: BackStackScreen? +): BackStackScreen { + return other?.let { BackStackScreen(frames + it.frames) } ?: this +} + +@WorkflowUiExperimentalApi +public fun List.toBackStackScreenOrNull(): BackStackScreen? = + fromListOrNull(this) + +@WorkflowUiExperimentalApi +public fun List.toBackStackScreen(): BackStackScreen = + Companion.fromList(this) diff --git a/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/BodyAndOverlaysScreen.kt b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/BodyAndOverlaysScreen.kt new file mode 100644 index 000000000..db22e4af3 --- /dev/null +++ b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/BodyAndOverlaysScreen.kt @@ -0,0 +1,90 @@ +package com.squareup.workflow1.ui.navigation + +import com.squareup.workflow1.ui.Compatible +import com.squareup.workflow1.ui.Compatible.Companion.keyFor +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +/** + * A screen that may stack a number of [Overlay]s over a body. + * If any members of [overlays] are [ModalOverlay], the body and + * lower-indexed members of that list are expected to ignore input + * events -- touch, keyboard, etc. + * + * UI kits are expected to provide handling for this class by default. + * + * Any [overlays] shown are expected to have their bounds restricted + * to the area above the [body]. For example, consider a layout where + * we want the option to show a tutorial bar below the main UI: + * + * +-------------------------+ + * | MyMainScreen | + * | | + * | | + * +-------------------------+ + * | MyTutorialScreen | + * +-------------------------+ + * + * And we want to ensure that any modal windows do not obscure the tutorial, if + * it's showing: + * + * +----+=============+------+ + * | My| | | + * | | MyEditModal | | + * | | | | + * +----+=============+------+ + * | MyTutorialScreen | + * +-------------------------+ + * + * We could model that this way: + * + * MyBodyAndBottomBarScreen( + * body = BodyAndOverlaysScreen( + * body = mainScreen, + * overlays = listOfNotNull(editModalOrNull) + * ), + * bar = tutorialScreenOrNull, + * ) + * + * It is also possible to nest [BodyAndOverlaysScreen] instances. For example, + * to show a higher priority modal that covers both `MyMainScreen` and `MyTutorialScreen`, + * we could render this: + * + * BodyAndOverlaysScreen( + * overlays = listOfNotNull(fullScreenModalOrNull), + * body = MyBodyAndBottomBarScreen( + * body = BodyAndOverlaysScreen( + * body = mainScreen, + * overlays = listOfNotNull(editModalOrNull) + * ), + * bar = tutorialScreenOrNull, + * ) + * ) + * + * Whatever structure you settle on for your root rendering, it is important + * to render the same structure every time. If your app will ever want to show + * an [Overlay], it should always render [BodyAndOverlaysScreen], even when + * there is no [Overlay] to show. Otherwise your entire view tree will be rebuilt, + * since the view built for a `MyBodyAndBottomBarScreen` cannot be updated to show + * a [BodyAndOverlaysScreen] rendering. + * + * @param name included in the [compatibilityKey] of this screen, for ease + * of nesting -- on Android, view state persistence support requires each + * BodyAndOverlaysScreen in a hierarchy to have a unique key + */ +@WorkflowUiExperimentalApi +public class BodyAndOverlaysScreen( + public val body: B, + public val overlays: List = emptyList(), + public val name: String = "" +) : Screen, Compatible { + override val compatibilityKey: String = keyFor(this, name) + + public fun mapBody(transform: (B) -> S): BodyAndOverlaysScreen { + return BodyAndOverlaysScreen(transform(body), overlays, name) + } + + public fun mapOverlays(transform: (O) -> N): BodyAndOverlaysScreen { + return BodyAndOverlaysScreen(body, overlays.map(transform), name) + } +} diff --git a/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/FullScreenModal.kt b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/FullScreenModal.kt new file mode 100644 index 000000000..1fe9e25b3 --- /dev/null +++ b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/FullScreenModal.kt @@ -0,0 +1,17 @@ +package com.squareup.workflow1.ui.navigation + +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +/** + * A basic [ScreenOverlay] that covers its container with the wrapped [content] [Screen]. + * + * UI kits are expected to provide handling for this class by default. + */ +@WorkflowUiExperimentalApi +public class FullScreenModal( + public override val content: C +) : ScreenOverlay, ModalOverlay { + override fun map(transform: (C) -> D): FullScreenModal = + FullScreenModal(transform(content)) +} diff --git a/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/ModalOverlay.kt b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/ModalOverlay.kt new file mode 100644 index 000000000..4d4c9f4b0 --- /dev/null +++ b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/ModalOverlay.kt @@ -0,0 +1,10 @@ +package com.squareup.workflow1.ui.navigation + +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +/** + * Marker interface identifying [Overlay] renderings whose presence + * indicates that events are blocked from lower layers. + */ +@WorkflowUiExperimentalApi +public interface ModalOverlay : Overlay diff --git a/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/Overlay.kt b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/Overlay.kt new file mode 100644 index 000000000..f98b593de --- /dev/null +++ b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/Overlay.kt @@ -0,0 +1,22 @@ +package com.squareup.workflow1.ui.navigation + +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +/** + * Marker interface implemented by window-like renderings that map to a layer above + * a base [Screen][com.squareup.workflow1.ui.Screen] by being placed in a + * [BodyAndOverlaysScreen.overlays] list. See [BodyAndOverlaysScreen] for more details. + * + * An [Overlay] can be any window-like part of the UI that visually floats in a layer + * above the main UI, or above other Overlays. Possible examples include alerts, drawers, + * and tooltips. + * + * Note in particular that an [Overlay] is not necessarily a modal window -- that is, + * one that prevents covered views and windows from processing UI events. + * Rendering types can opt into modality by extending [ModalOverlay]. + * + * See [ScreenOverlay] to define an [Overlay] whose content is provided by a wrapped + * [Screen][com.squareup.workflow1.ui.Screen]. + */ +@WorkflowUiExperimentalApi +public interface Overlay diff --git a/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/ScreenOverlay.kt b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/ScreenOverlay.kt new file mode 100644 index 000000000..b42122f82 --- /dev/null +++ b/workflow-ui/core/src/commonMain/kotlin/com/squareup/workflow1/ui/navigation/ScreenOverlay.kt @@ -0,0 +1,15 @@ +package com.squareup.workflow1.ui.navigation + +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.Wrapper + +/** + * An [Overlay] built around a root [content] [Screen]. + */ +@WorkflowUiExperimentalApi +public interface ScreenOverlay : Overlay, Wrapper { + public override val content: ContentT + + override fun map(transform: (ContentT) -> ContentU): ScreenOverlay +} diff --git a/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/CompatibleTest.kt b/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/CompatibleTest.kt new file mode 100644 index 000000000..b62fb0d1e --- /dev/null +++ b/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/CompatibleTest.kt @@ -0,0 +1,35 @@ +package com.squareup.workflow1.ui + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@OptIn(WorkflowUiExperimentalApi::class) +class CompatibleTest { + @Test fun different_types_do_not_match() { + val able = object : Any() {} + val baker = object : Any() {} + + assertFalse { compatible(able, baker) } + } + + @Test fun same_type_matches() { + assertTrue { compatible("Able", "Baker") } + } + + @Test fun isCompatibleWith_is_honored() { + data class K(override val compatibilityKey: String) : Compatible + + assertTrue { compatible(K("hey"), K("hey")) } + assertFalse { compatible(K("hey"), K("ho")) } + } + + @Test fun different_Compatible_types_do_not_match() { + abstract class A : Compatible + + class Able(override val compatibilityKey: String) : A() + class Alpha(override val compatibilityKey: String) : A() + + assertFalse { compatible(Able("Hey"), Alpha("Hey")) } + } +} diff --git a/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/CompositeViewRegistryTest.kt b/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/CompositeViewRegistryTest.kt new file mode 100644 index 000000000..8368515d3 --- /dev/null +++ b/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/CompositeViewRegistryTest.kt @@ -0,0 +1,90 @@ +package com.squareup.workflow1.ui + +import com.squareup.workflow1.ui.ViewRegistry.Entry +import com.squareup.workflow1.ui.ViewRegistry.Key +import kotlin.reflect.KClass +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNull +import kotlin.test.assertSame +import kotlin.test.assertTrue + +@OptIn(WorkflowUiExperimentalApi::class) +internal class CompositeViewRegistryTest { + + @Test fun constructor_throws_on_duplicates() { + val fooBarRegistry = TestRegistry(setOf(FooRendering::class, BarRendering::class)) + val barBazRegistry = TestRegistry(setOf(BarRendering::class, BazRendering::class)) + + val error = assertFailsWith { + fooBarRegistry + barBazRegistry + } + assertTrue { error.message!!.startsWith("Must not have duplicate entries: ") } + assertTrue { error.message!!.contains(BarRendering::class.toString()) } + } + + @Test fun getFactoryFor_delegates_to_composite_registries() { + val fooFactory = TestEntry(FooRendering::class) + val barFactory = TestEntry(BarRendering::class) + val bazFactory = TestEntry(BazRendering::class) + val fooBarRegistry = TestRegistry( + mapOf( + fooFactory.key to fooFactory, + barFactory.key to barFactory + ) + ) + val bazRegistry = TestRegistry(factories = mapOf(bazFactory.key to bazFactory)) + val registry = fooBarRegistry + bazRegistry + + assertSame(fooFactory, registry.getEntryFor(Key(FooRendering::class, TestEntry::class))) + assertSame(barFactory, registry.getEntryFor(Key(BarRendering::class, TestEntry::class))) + assertSame(bazFactory, registry.getEntryFor(Key(BazRendering::class, TestEntry::class))) + } + + @Test fun getFactoryFor_returns_null_on_missing_registry() { + val fooRegistry = TestRegistry(setOf(FooRendering::class)) + val registry = CompositeViewRegistry(ViewRegistry(), fooRegistry) + + assertNull(registry.getEntryFor(Key(BarRendering::class, TestEntry::class))) + } + + @Test fun keys_includes_all_composite_registries_keys() { + val fooBarRegistry = TestRegistry(setOf(FooRendering::class, BarRendering::class)) + val bazRegistry = TestRegistry(setOf(BazRendering::class)) + val registry = CompositeViewRegistry(fooBarRegistry, bazRegistry) + + assertEquals( + setOf( + Key(FooRendering::class, TestEntry::class), + Key(BarRendering::class, TestEntry::class), + Key(BazRendering::class, TestEntry::class) + ), + registry.keys + ) + } + + private class TestEntry(type: KClass) : Entry { + override val key = Key(type, TestEntry::class) + } + + private object FooRendering + private object BarRendering + private object BazRendering + + private class TestRegistry(private val factories: Map, Entry<*>>) : ViewRegistry { + constructor(keys: Set>) : this( + keys.associate { + val entry = TestEntry(it) + entry.key to entry + } + ) + + override val keys: Set> get() = factories.keys + + @Suppress("UNCHECKED_CAST") + override fun getEntryFor( + key: Key + ): Entry = factories.getValue(key) as Entry + } +} diff --git a/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/EnvironmentScreenTest.kt b/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/EnvironmentScreenTest.kt new file mode 100644 index 000000000..4632d9fb9 --- /dev/null +++ b/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/EnvironmentScreenTest.kt @@ -0,0 +1,115 @@ +package com.squareup.workflow1.ui + +import com.squareup.workflow1.ui.ViewEnvironment.Companion.EMPTY +import com.squareup.workflow1.ui.ViewRegistry.Key +import kotlin.reflect.KClass +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertSame + +@OptIn(WorkflowUiExperimentalApi::class) +internal class EnvironmentScreenTest { + private class TestFactory( + type: KClass + ) : ViewRegistry.Entry { + override val key = Key(type, TestFactory::class) + } + + private data class TestValue(val value: String) { + companion object : ViewEnvironmentKey() { + override val default: TestValue get() = error("Set a default") + } + } + + private operator fun ViewEnvironment.plus(other: TestValue): ViewEnvironment { + return this + (TestValue to other) + } + + private object FooScreen : Screen + private object BarScreen : Screen + + @Test fun screen_withRegistry_works() { + val fooFactory = TestFactory(FooScreen::class) + val viewRegistry = ViewRegistry(fooFactory) + val envScreen = FooScreen.withRegistry(viewRegistry) + + assertSame( + fooFactory, + envScreen.environment[ViewRegistry].getFactoryFor>(FooScreen) + + ) + + assertNull( + envScreen.environment[ViewRegistry].getFactoryFor>(BarScreen) + ) + } + + @Test fun screen_withEnvironment_works() { + val fooFactory = TestFactory(FooScreen::class) + val viewRegistry = ViewRegistry(fooFactory) + val envScreen = FooScreen.withEnvironment( + EMPTY + viewRegistry + TestValue("foo") + ) + + assertSame( + fooFactory, + envScreen.environment[ViewRegistry].getFactoryFor>(FooScreen) + ) + assertNull( + envScreen.environment[ViewRegistry].getFactoryFor>(BarScreen) + ) + assertEquals( + TestValue("foo"), + envScreen.environment[TestValue] + ) + } + + @Test fun environmentScreen_withRegistry_merges() { + val fooFactory1 = TestFactory(FooScreen::class) + val fooFactory2 = TestFactory(FooScreen::class) + val barFactory = TestFactory(BarScreen::class) + + val left = FooScreen.withRegistry(ViewRegistry(fooFactory1, barFactory)) + val union = left.withRegistry(ViewRegistry(fooFactory2)) + + assertSame( + fooFactory2, + union.environment[ViewRegistry].getFactoryFor>(FooScreen) + ) + + assertSame( + barFactory, + union.environment[ViewRegistry].getFactoryFor>(BarScreen) + ) + } + + @Test fun environmentScreen_withEnvironment_merges() { + val fooFactory1 = TestFactory(FooScreen::class) + val fooFactory2 = TestFactory(FooScreen::class) + val barFactory = TestFactory(BarScreen::class) + + val left = FooScreen.withEnvironment( + EMPTY + ViewRegistry(fooFactory1, barFactory) + TestValue("left") + ) + + val union = left.withEnvironment( + EMPTY + ViewRegistry(fooFactory2) + TestValue("right") + ) + + assertSame( + fooFactory2, + union.environment[ViewRegistry].getFactoryFor>(FooScreen) + ) + assertSame( + barFactory, + union.environment[ViewRegistry].getFactoryFor>(BarScreen), + ) + assertEquals(TestValue("right"), union.environment[TestValue]) + } + + @Test fun keep_existing_instance_on_vacuous_merge() { + val left = FooScreen.withEnvironment(EMPTY + TestValue("whatever")) + assertSame(left, left.withEnvironment()) + } +} diff --git a/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/NamedScreenTest.kt b/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/NamedScreenTest.kt new file mode 100644 index 000000000..cf816834c --- /dev/null +++ b/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/NamedScreenTest.kt @@ -0,0 +1,105 @@ +package com.squareup.workflow1.ui + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +@OptIn(WorkflowUiExperimentalApi::class) +internal class NamedScreenTest { + object Whut : Screen + object Hey : Screen + + @Test fun same_type_same_name_matches() { + assertTrue { + compatible(NamedScreen(Hey, "eh"), NamedScreen(Hey, "eh")) + } + } + + @Test fun same_type_diff_name_matches() { + assertFalse { + compatible(NamedScreen(Hey, "blam"), NamedScreen(Hey, "bloom")) + } + } + + @Test fun diff_type_same_name_no_match() { + assertFalse { + compatible(NamedScreen(Hey, "a"), NamedScreen(Whut, "a")) + } + } + + @Test fun recursion() { + assertTrue { + compatible( + NamedScreen(NamedScreen(Hey, "one"), "ho"), + NamedScreen(NamedScreen(Hey, "one"), "ho") + ) + } + + assertFalse { + compatible( + NamedScreen(NamedScreen(Hey, "one"), "ho"), + NamedScreen(NamedScreen(Hey, "two"), "ho") + ) + } + + assertFalse { + compatible( + NamedScreen(NamedScreen(Hey, "a"), "ho"), + NamedScreen(NamedScreen(Whut, "a"), "ho") + ) + } + } + + @Test fun key_recursion() { + assertEquals( + NamedScreen(NamedScreen(Hey, "one"), "ho").compatibilityKey, + NamedScreen(NamedScreen(Hey, "one"), "ho").compatibilityKey + ) + + assertNotEquals( + NamedScreen(NamedScreen(Hey, "two"), "ho").compatibilityKey, + NamedScreen(NamedScreen(Hey, "one"), "ho").compatibilityKey + ) + + assertEquals( + NamedScreen(NamedScreen(Whut, "a"), "ho").compatibilityKey, + NamedScreen(NamedScreen(Whut, "a"), "ho").compatibilityKey + ) + } + + @Test fun recursive_keys_are_legible() { + assertEquals( + "NamedScreen:ho(NamedScreen:one(${Hey::class}))", + NamedScreen(NamedScreen(Hey, "one"), "ho").compatibilityKey + ) + } + + private class Foo(override val compatibilityKey: String) : Compatible, Screen + + @Test fun the_test_Compatible_class_actually_works() { + assertTrue { compatible(Foo("bar"), Foo("bar")) } + assertFalse { compatible(Foo("bar"), Foo("baz")) } + } + + @Test fun wrapping_custom_Compatible_compatibility_works() { + assertTrue { + compatible(NamedScreen(Foo("bar"), "name"), NamedScreen(Foo("bar"), "name")) + } + assertFalse { + compatible(NamedScreen(Foo("bar"), "name"), NamedScreen(Foo("baz"), "name")) + } + } + + @Test fun wrapping_custom_Compatible_keys_work() { + assertEquals( + NamedScreen(Foo("bar"), "name").compatibilityKey, + NamedScreen(Foo("bar"), "name").compatibilityKey + ) + assertNotEquals( + NamedScreen(Foo("baz"), "name").compatibilityKey, + NamedScreen(Foo("bar"), "name").compatibilityKey + ) + } +} diff --git a/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/ViewEnvironmentTest.kt b/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/ViewEnvironmentTest.kt new file mode 100644 index 000000000..ed157dac6 --- /dev/null +++ b/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/ViewEnvironmentTest.kt @@ -0,0 +1,122 @@ +package com.squareup.workflow1.ui + +import com.squareup.workflow1.ui.ViewEnvironment.Companion.EMPTY +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertSame + +@OptIn(WorkflowUiExperimentalApi::class) +internal class ViewEnvironmentTest { + private object StringHint : ViewEnvironmentKey() { + override val default = "" + } + + private object OtherStringHint : ViewEnvironmentKey() { + override val default = "" + } + + private data class DataHint( + val int: Int = -1, + val string: String = "" + ) { + companion object : ViewEnvironmentKey() { + override val default = DataHint() + } + } + + @Test fun defaults() { + assertEquals(DataHint(), EMPTY[DataHint]) + } + + @Test fun put() { + val environment = EMPTY + + (StringHint to "fnord") + + (DataHint to DataHint(42, "foo")) + + assertEquals("fnord", environment[StringHint]) + assertEquals(DataHint(42, "foo"), environment[DataHint]) + } + + @Test fun map_equality() { + val env1 = EMPTY + + (StringHint to "fnord") + + (DataHint to DataHint(42, "foo")) + + val env2 = EMPTY + + (StringHint to "fnord") + + (DataHint to DataHint(42, "foo")) + + assertEquals(env2, env1) + } + + @Test fun map_inequality() { + val env1 = EMPTY + + (StringHint to "fnord") + + (DataHint to DataHint(42, "foo")) + + val env2 = EMPTY + + (StringHint to "fnord") + + (DataHint to DataHint(43, "foo")) + + assertNotEquals(env2, env1) + } + + @Test fun key_equality() { + assertEquals(StringHint, StringHint) + } + + @Test fun key_inequality() { + assertNotEquals>(OtherStringHint, StringHint) + } + + @Test fun override() { + val environment = EMPTY + + (StringHint to "able") + + (StringHint to "baker") + + assertEquals("baker", environment[StringHint]) + } + + @Test fun keys_of_the_same_type() { + val environment = EMPTY + + (StringHint to "able") + + (OtherStringHint to "baker") + + assertEquals("able", environment[StringHint]) + assertEquals("baker", environment[OtherStringHint]) + } + + @Test fun preserve_this_when_merging_empty() { + val environment = EMPTY + (StringHint to "able") + assertSame(environment, environment + EMPTY) + } + + @Test fun preserve_other_when_merging_to_empty() { + val environment = EMPTY + (StringHint to "able") + assertSame(environment, EMPTY + environment) + } + + @Test fun self_plus_self_is_self() { + val environment = EMPTY + (StringHint to "able") + assertSame(environment, environment + environment) + } + + @Test fun honors_combine() { + val combiningHint = object : ViewEnvironmentKey() { + override val default: String + get() = error("") + + override fun combine( + left: String, + right: String + ): String { + return "$left-$right" + } + } + + val left = EMPTY + (combiningHint to "able") + val right = EMPTY + (combiningHint to "baker") + assertEquals("able-baker", (left + right)[combiningHint]) + } +} diff --git a/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/ViewRegistryTest.kt b/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/ViewRegistryTest.kt new file mode 100644 index 000000000..aa6e8e872 --- /dev/null +++ b/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/ViewRegistryTest.kt @@ -0,0 +1,139 @@ +package com.squareup.workflow1.ui + +import com.squareup.workflow1.ui.ViewEnvironment.Companion.EMPTY +import com.squareup.workflow1.ui.ViewRegistry.Entry +import com.squareup.workflow1.ui.ViewRegistry.Key +import kotlin.reflect.KClass +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertSame +import kotlin.test.assertTrue + +@OptIn(WorkflowUiExperimentalApi::class) +internal class ViewRegistryTest { + + @Test fun keys_from_bindings() { + val factory1 = TestEntry(FooRendering::class) + val factory2 = TestEntry(BarRendering::class) + val registry = ViewRegistry(factory1, factory2) + + assertEquals(setOf(factory1.key, factory2.key), registry.keys) + } + + @Test fun constructor_throws_on_duplicates() { + val factory1 = TestEntry(FooRendering::class) + val factory2 = TestEntry(FooRendering::class) + + val error = assertFailsWith { + ViewRegistry(factory1, factory2) + } + assertTrue { error.message!!.endsWith("must not have duplicate entries.") } + assertTrue { error.message!!.contains(FooRendering::class.toString()) } + } + + @Test fun getFactoryFor_works() { + val fooFactory = TestEntry(FooRendering::class) + val registry = ViewRegistry(fooFactory) + + val factory = registry[Key(FooRendering::class, TestEntry::class)] + assertSame(fooFactory, factory) + } + + @Test fun getFactoryFor_returns_null_on_missing_binding() { + val fooFactory = TestEntry(FooRendering::class) + val registry = ViewRegistry(fooFactory) + + assertNull(registry[Key(BarRendering::class, TestEntry::class)]) + } + + @Test fun viewRegistry_with_no_arguments_infers_type() { + val registry = ViewRegistry() + assertTrue(registry.keys.isEmpty()) + } + + @Test fun merge_prefers_right_side() { + val factory1 = TestEntry(FooRendering::class) + val factory2 = TestEntry(FooRendering::class) + val merged = ViewRegistry(factory1) merge ViewRegistry(factory2) + + assertSame(factory2, merged[Key(FooRendering::class, TestEntry::class)]) + } + + @Test fun viewEnvironment_plus_ViewRegistry_prefers_new_registry_values() { + val leftBar = TestEntry(BarRendering::class) + val rightBar = TestEntry(BarRendering::class) + + val env = EMPTY + ViewRegistry(leftBar) + val merged = env + ViewRegistry(rightBar, TestEntry(FooRendering::class)) + + assertSame(rightBar, merged[ViewRegistry][Key(BarRendering::class, TestEntry::class)]) + assertNotNull(merged[ViewRegistry][Key(FooRendering::class, TestEntry::class)]) + } + + @Test fun viewEnvironment_plus_ViewEnvironment_prefers_right_ViewRegistry() { + val leftBar = TestEntry(BarRendering::class) + val rightBar = TestEntry(BarRendering::class) + + val leftEnv = EMPTY + ViewRegistry(leftBar) + val rightEnv = EMPTY + ViewRegistry(rightBar, TestEntry(FooRendering::class)) + val merged = leftEnv + rightEnv + + assertSame(rightBar, merged[ViewRegistry][Key(BarRendering::class, TestEntry::class)]) + assertNotNull(merged[ViewRegistry][Key(FooRendering::class, TestEntry::class)]) + } + + @Test fun plus_of_empty_returns_this() { + val reg = ViewRegistry(TestEntry(FooRendering::class)) + assertSame(reg, reg + ViewRegistry()) + } + + @Test fun plus_to_empty_returns_other() { + val reg = ViewRegistry(TestEntry(FooRendering::class)) + assertSame(reg, ViewRegistry() + reg) + } + + @Test fun merge_of_empty_reg_returns_this() { + val reg = ViewRegistry(TestEntry(FooRendering::class)) + assertSame(reg, reg merge ViewRegistry()) + } + + @Test fun merge_to_empty_reg_returns_other() { + val reg = ViewRegistry(TestEntry(FooRendering::class)) + assertSame(reg, ViewRegistry() merge reg) + } + + @Test fun env_plus_empty_reg_returns_env() { + val env = EMPTY + ViewRegistry(TestEntry(FooRendering::class)) + assertSame(env, env + ViewRegistry()) + } + + @Test fun env_plus_same_reg_returns_self() { + val reg = ViewRegistry(TestEntry(FooRendering::class)) + val env = EMPTY + reg + assertSame(env, env + reg) + } + + @Test fun reg_plus_self_throws_dup_entries() { + val reg = ViewRegistry(TestEntry(FooRendering::class)) + assertFailsWith { + reg + reg + } + } + + @Test fun registry_merge_self_returns_self() { + val reg = ViewRegistry(TestEntry(FooRendering::class)) + assertSame(reg, reg merge reg) + } + + private class TestEntry( + type: KClass + ) : Entry { + override val key = Key(type, TestEntry::class) + } + + private object FooRendering + private object BarRendering +} diff --git a/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/navigation/BackStackScreenTest.kt b/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/navigation/BackStackScreenTest.kt new file mode 100644 index 000000000..b70191552 --- /dev/null +++ b/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/navigation/BackStackScreenTest.kt @@ -0,0 +1,141 @@ +package com.squareup.workflow1.ui.navigation + +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotEquals +import kotlin.test.assertNull + +@OptIn(WorkflowUiExperimentalApi::class) +internal class BackStackScreenTest { + data class FooScreen(val value: T) : Screen + data class BarScreen(val value: T) : Screen + + @Test fun top_is_last() { + assertEquals( + FooScreen(4), + BackStackScreen(FooScreen(1), FooScreen(2), FooScreen(3), FooScreen(4)).top + ) + } + + @Test fun backstack_is_all_but_top() { + assertEquals( + listOf(FooScreen(1), FooScreen(2), FooScreen(3)), + BackStackScreen(FooScreen(1), FooScreen(2), FooScreen(3), FooScreen(4)).backStack + ) + } + + @Test fun get_works() { + assertEquals( + FooScreen("baker"), + BackStackScreen(FooScreen("able"), FooScreen("baker"), FooScreen("charlie"))[1] + ) + } + + @Test fun plus_another_stack() { + assertEquals( + BackStackScreen( + FooScreen(1), + FooScreen(2), + FooScreen(3), + FooScreen(8), + FooScreen(9), + FooScreen(0) + ), + BackStackScreen(FooScreen(1), FooScreen(2), FooScreen(3)) + BackStackScreen( + FooScreen(8), + FooScreen(9), + FooScreen(0) + ) + ) + } + + @Test fun unequal_by_order() { + assertNotEquals( + BackStackScreen(FooScreen(3), FooScreen(2), FooScreen(1)), + BackStackScreen(FooScreen(1), FooScreen(2), FooScreen(3)) + ) + } + + @Test fun equal_have_matching_hash() { + assertEquals( + BackStackScreen(FooScreen(1), FooScreen(2), FooScreen(3)).hashCode(), + BackStackScreen(FooScreen(1), FooScreen(2), FooScreen(3)).hashCode() + ) + } + + @Test fun unequal_have_mismatching_hash() { + assertNotEquals( + BackStackScreen(FooScreen(1), FooScreen(2), FooScreen(3)).hashCode(), + BackStackScreen(FooScreen(1), FooScreen(2)).hashCode() + ) + } + + @Test fun bottom_and_rest() { + assertEquals( + BackStackScreen(FooScreen(1), FooScreen(2), FooScreen(3), FooScreen(4)), + BackStackScreen.fromList( + listOf(element = FooScreen(1)) + listOf(FooScreen(2), FooScreen(3), FooScreen(4)) + ) + ) + } + + @Test fun singleton() { + val stack = BackStackScreen(FooScreen("hi")) + assertEquals(FooScreen("hi"), stack.top) + assertEquals(listOf(FooScreen("hi")), stack.frames) + assertEquals(BackStackScreen(FooScreen("hi")), stack) + } + + @Test fun map() { + assertEquals( + BackStackScreen(FooScreen(2), FooScreen(4), FooScreen(6)), + BackStackScreen(FooScreen(1), FooScreen(2), FooScreen(3)).map { + FooScreen(it.value * 2) + } + ) + } + + @Test fun mapIndexed() { + val source = BackStackScreen(FooScreen("able"), FooScreen("baker"), FooScreen("charlie")) + assertEquals( + BackStackScreen(FooScreen("0: able"), FooScreen("1: baker"), FooScreen("2: charlie")), + source.mapIndexed { index, frame -> FooScreen("$index: ${frame.value}") } + ) + } + + @Test fun nullFromEmptyList() { + assertNull(emptyList>().toBackStackScreenOrNull()) + } + + @Test fun throwFromEmptyList() { + assertFailsWith { emptyList>().toBackStackScreen() } + } + + @Test fun fromList() { + assertEquals( + BackStackScreen(FooScreen(1), FooScreen(2), FooScreen(3)), + listOf(FooScreen(1), FooScreen(2), FooScreen(3)).toBackStackScreen() + ) + } + + @Test fun fromListOrNull() { + assertEquals( + BackStackScreen(FooScreen(1), FooScreen(2), FooScreen(3)), + listOf(FooScreen(1), FooScreen(2), FooScreen(3)).toBackStackScreenOrNull() + ) + } + + /** + * To reminds us why we want the `out` in `BackStackScreen`. + * Without this, using `BackStackScreen<*>` as `RenderingT` is not practical. + */ + @Test fun heterogenousPlusIsTolerable() { + val foo = BackStackScreen(FooScreen(1)) + val bar = BackStackScreen(BarScreen(1)) + val both = foo + bar + assertEquals(foo + bar, both) + } +} diff --git a/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/navigation/BodyAndOverlaysScreenTest.kt b/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/navigation/BodyAndOverlaysScreenTest.kt new file mode 100644 index 000000000..ec247d092 --- /dev/null +++ b/workflow-ui/core/src/commonTest/kotlin/com/squareup.workflow1.ui/navigation/BodyAndOverlaysScreenTest.kt @@ -0,0 +1,55 @@ +package com.squareup.workflow1.ui.navigation + +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.compatible +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertSame +import kotlin.test.assertTrue + +@OptIn(WorkflowUiExperimentalApi::class) +internal class BodyAndOverlaysScreenTest { + data class S(val value: T) : Screen + data class O(val value: T) : Overlay + + @Test fun mapBody() { + val before = BodyAndOverlaysScreen(S("s-before"), listOf(O("o-before")), name = "fnord") + val after = before.mapBody { + assertEquals("s-before", it.value) + S(25) + } + + assertEquals(25, after.body.value) + assertEquals(1, after.overlays.size) + assertSame(before.overlays[0], after.overlays.first()) + assertEquals("fnord", after.name) + assertTrue { compatible(before, after) } + } + + @Test fun mapOverlays() { + val before = BodyAndOverlaysScreen(S("s-before"), listOf(O("o-before")), name = "bagel") + val after = before.mapOverlays { + assertEquals("o-before", it.value) + O(25) + } + + assertSame(before.body, after.body) + assertEquals(1, after.overlays.size) + assertEquals(25, after.overlays.first().value) + assertEquals("bagel", after.name) + assertTrue { compatible(before, after) } + } + + @Test fun nameAffectsCompatibility() { + val unnamed = BodyAndOverlaysScreen(S(1)) + val alsoUnnamed = BodyAndOverlaysScreen(S("string")) + val named = BodyAndOverlaysScreen(S(1), name = "name1") + val alsoNamed = BodyAndOverlaysScreen(S("string"), name = "name2") + + assertTrue { compatible(unnamed, alsoUnnamed) } + assertFalse { compatible(unnamed, named) } + assertFalse { compatible(named, alsoNamed) } + } +} diff --git a/workflow-ui/internal-testing-android/build.gradle.kts b/workflow-ui/internal-testing-android/build.gradle.kts index c61f8dfe7..d036f9788 100644 --- a/workflow-ui/internal-testing-android/build.gradle.kts +++ b/workflow-ui/internal-testing-android/build.gradle.kts @@ -27,7 +27,7 @@ dependencies { api(libs.truth) api(project(":workflow-ui:core-android")) - api(project(":workflow-ui:core-common")) + api(project(":workflow-ui:core")) implementation(libs.androidx.lifecycle.common) implementation(libs.squareup.leakcanary.instrumentation) diff --git a/workflow-ui/radiography/build.gradle.kts b/workflow-ui/radiography/build.gradle.kts index 57ae06267..84982144a 100644 --- a/workflow-ui/radiography/build.gradle.kts +++ b/workflow-ui/radiography/build.gradle.kts @@ -17,5 +17,5 @@ dependencies { implementation(libs.squareup.radiography) implementation(project(":workflow-ui:core-android")) - implementation(project(":workflow-ui:core-common")) + implementation(project(":workflow-ui:core")) } diff --git a/workflow-ui/radiography/dependencies/releaseRuntimeClasspath.txt b/workflow-ui/radiography/dependencies/releaseRuntimeClasspath.txt index 0c08955f8..30d07fe27 100644 --- a/workflow-ui/radiography/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-ui/radiography/dependencies/releaseRuntimeClasspath.txt @@ -1,7 +1,7 @@ androidx.activity:activity:1.6.1 androidx.annotation:annotation-experimental:1.3.0 -androidx.annotation:annotation-jvm:1.6.0 -androidx.annotation:annotation:1.6.0 +androidx.annotation:annotation-jvm:1.8.0 +androidx.annotation:annotation:1.8.0 androidx.arch.core:core-common:2.2.0 androidx.arch.core:core-runtime:2.2.0 androidx.collection:collection:1.1.0 @@ -9,13 +9,17 @@ androidx.concurrent:concurrent-futures:1.1.0 androidx.core:core-ktx:1.12.0 androidx.core:core:1.12.0 androidx.interpolator:interpolator:1.0.0 -androidx.lifecycle:lifecycle-common:2.6.1 -androidx.lifecycle:lifecycle-livedata-core:2.6.1 -androidx.lifecycle:lifecycle-runtime-ktx:2.6.1 -androidx.lifecycle:lifecycle-runtime:2.6.1 -androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1 -androidx.lifecycle:lifecycle-viewmodel:2.6.1 -androidx.profileinstaller:profileinstaller:1.3.0 +androidx.lifecycle:lifecycle-common-jvm:2.8.0 +androidx.lifecycle:lifecycle-common:2.8.0 +androidx.lifecycle:lifecycle-livedata-core:2.8.0 +androidx.lifecycle:lifecycle-runtime-android:2.8.0 +androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.0 +androidx.lifecycle:lifecycle-runtime-ktx:2.8.0 +androidx.lifecycle:lifecycle-runtime:2.8.0 +androidx.lifecycle:lifecycle-viewmodel-android:2.8.0 +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.0 +androidx.lifecycle:lifecycle-viewmodel:2.8.0 +androidx.profileinstaller:profileinstaller:1.3.1 androidx.savedstate:savedstate:1.2.1 androidx.startup:startup-runtime:1.1.1 androidx.tracing:tracing:1.0.0 @@ -26,11 +30,11 @@ com.squareup.curtains:curtains:1.2.2 com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 com.squareup.radiography:radiography:2.4.1 -org.jetbrains.kotlin:kotlin-bom:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlin:kotlin-bom:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.24 +org.jetbrains.kotlin:kotlin-stdlib:1.9.24 org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3