diff --git a/artifacts.json b/artifacts.json index 0eaadafa8..c48ee05c3 100644 --- a/artifacts.json +++ b/artifacts.json @@ -62,6 +62,42 @@ "javaVersion": "1.8", "publicationName": "kotlinMultiplatform" }, + { + "gradlePath": ":workflow-core-compose", + "group": "com.squareup.workflow1", + "artifactId": "workflow-core-compose-iosarm64", + "description": "Workflow Core Compose", + "packaging": "klib", + "javaVersion": "1.8", + "publicationName": "iosArm64" + }, + { + "gradlePath": ":workflow-core-compose", + "group": "com.squareup.workflow1", + "artifactId": "workflow-core-compose-iosx64", + "description": "Workflow Core Compose", + "packaging": "klib", + "javaVersion": "1.8", + "publicationName": "iosX64" + }, + { + "gradlePath": ":workflow-core-compose", + "group": "com.squareup.workflow1", + "artifactId": "workflow-core-compose-jvm", + "description": "Workflow Core Compose", + "packaging": "jar", + "javaVersion": "1.8", + "publicationName": "jvm" + }, + { + "gradlePath": ":workflow-core-compose", + "group": "com.squareup.workflow1", + "artifactId": "workflow-core-compose", + "description": "Workflow Core Compose", + "packaging": "jar", + "javaVersion": "1.8", + "publicationName": "kotlinMultiplatform" + }, { "gradlePath": ":workflow-runtime", "group": "com.squareup.workflow1", @@ -215,4 +251,4 @@ "javaVersion": "1.8", "publicationName": "maven" } -] \ No newline at end of file +] diff --git a/benchmarks/performance-poetry/complex-poetry/build.gradle.kts b/benchmarks/performance-poetry/complex-poetry/build.gradle.kts index 62f9d8e00..e4aa2f013 100644 --- a/benchmarks/performance-poetry/complex-poetry/build.gradle.kts +++ b/benchmarks/performance-poetry/complex-poetry/build.gradle.kts @@ -2,6 +2,7 @@ plugins { id("com.android.application") `kotlin-android` id("kotlin-parcelize") + id("app.cash.molecule") } android { compileSdk = 32 @@ -56,6 +57,7 @@ dependencies { api(project(":samples:containers:android")) api(project(":samples:containers:common")) api(project(":samples:containers:poetry")) + api(project(":workflow-core-compose")) api(project(":workflow-core")) api(project(":workflow-runtime")) api(project(":workflow-ui:core-android")) diff --git a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/MaybeLoadingGatekeeperWorkflow.kt b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/MaybeLoadingGatekeeperWorkflow.kt index d6c64532e..f3f43ba13 100644 --- a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/MaybeLoadingGatekeeperWorkflow.kt +++ b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/MaybeLoadingGatekeeperWorkflow.kt @@ -1,5 +1,6 @@ package com.squareup.benchmarks.performance.complex.poetry +import androidx.compose.runtime.Composable import com.squareup.benchmarks.performance.complex.poetry.instrumentation.ActionHandlingTracingInterceptor import com.squareup.benchmarks.performance.complex.poetry.instrumentation.asTraceableWorker import com.squareup.benchmarks.performance.complex.poetry.views.LoaderSpinner @@ -8,19 +9,21 @@ import com.squareup.sample.container.overviewdetail.OverviewDetailScreen import com.squareup.workflow1.Snapshot import com.squareup.workflow1.StatefulWorkflow import com.squareup.workflow1.Workflow +import com.squareup.workflow1.WorkflowExperimentalRuntime import com.squareup.workflow1.action +import com.squareup.workflow1.compose.StatefulComposeWorkflow import com.squareup.workflow1.runningWorker import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import kotlinx.coroutines.flow.Flow typealias IsLoading = Boolean -@OptIn(WorkflowUiExperimentalApi::class) +@OptIn(WorkflowUiExperimentalApi::class, WorkflowExperimentalRuntime::class) class MaybeLoadingGatekeeperWorkflow( private val childWithLoading: Workflow, private val childProps: T, private val isLoading: Flow -) : StatefulWorkflow() { +) : StatefulComposeWorkflow() { override fun initialState( props: Unit, snapshot: Snapshot? @@ -29,7 +32,7 @@ class MaybeLoadingGatekeeperWorkflow( override fun render( renderProps: Unit, renderState: IsLoading, - context: RenderContext + context: StatefulWorkflow.RenderContext ): MayBeLoadingScreen { context.runningWorker(isLoading.asTraceableWorker("GatekeeperLoading")) { action { @@ -49,4 +52,29 @@ class MaybeLoadingGatekeeperWorkflow( } override fun snapshotState(state: IsLoading): Snapshot? = null + @Composable + override fun Rendering( + renderProps: Unit, + renderState: IsLoading, + context: RenderContext + ): MayBeLoadingScreen { + context.runningWorker(isLoading.asTraceableWorker("GatekeeperLoading")) { + action { + state = it + } + } + val maybeLoadingChild = context.ChildRendering( + childWithLoading, childProps, "", + ) { + action(ActionHandlingTracingInterceptor.keyForTrace("GatekeeperChildFinished")) { + setOutput( + Unit + ) + } + } + return MayBeLoadingScreen( + baseScreen = maybeLoadingChild, + loaders = if (renderState) listOf(LoaderSpinner) else emptyList() + ) + } } diff --git a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoemWorkflow.kt b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoemWorkflow.kt index 9cfc4e592..2be58c867 100644 --- a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoemWorkflow.kt +++ b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoemWorkflow.kt @@ -1,5 +1,6 @@ package com.squareup.benchmarks.performance.complex.poetry +import androidx.compose.runtime.Composable import com.squareup.benchmarks.performance.complex.poetry.PerformancePoemWorkflow.Action.ClearSelection import com.squareup.benchmarks.performance.complex.poetry.PerformancePoemWorkflow.Action.HandleStanzaListOutput import com.squareup.benchmarks.performance.complex.poetry.PerformancePoemWorkflow.Action.SelectNext @@ -30,7 +31,9 @@ import com.squareup.workflow1.StatefulWorkflow import com.squareup.workflow1.Worker import com.squareup.workflow1.WorkflowAction import com.squareup.workflow1.WorkflowAction.Companion.noAction +import com.squareup.workflow1.WorkflowExperimentalRuntime import com.squareup.workflow1.action +import com.squareup.workflow1.compose.StatefulComposeWorkflow import com.squareup.workflow1.runningWorker import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi @@ -57,10 +60,11 @@ import kotlinx.coroutines.flow.flow * break ties/conflicts with a token in the start/stop requests. We leave that complexity out * here. ** */ +@OptIn(WorkflowExperimentalRuntime::class) class PerformancePoemWorkflow( private val simulatedPerfConfig: SimulatedPerfConfig = SimulatedPerfConfig.NO_SIMULATED_PERF, private val isLoading: MutableStateFlow, -) : PoemWorkflow, StatefulWorkflow() { +) : PoemWorkflow, StatefulComposeWorkflow() { sealed class State { val isLoading: Boolean = false @@ -94,7 +98,7 @@ class PerformancePoemWorkflow( override fun render( renderProps: Poem, renderState: State, - context: RenderContext + context: StatefulWorkflow.RenderContext ): OverviewDetailScreen { if (simulatedPerfConfig.simultaneousActions > 0) { repeat(simulatedPerfConfig.simultaneousActions) { index -> @@ -315,4 +319,126 @@ class PerformancePoemWorkflow( } } } + + @OptIn(WorkflowUiExperimentalApi::class) + @Composable + override fun Rendering( + renderProps: Poem, + renderState: State, + context: RenderContext + ): OverviewDetailScreen { + when (renderState) { + Initializing -> { + // Again, the entire `Initializing` state is a smell, which is most obvious from the + // use of `Worker.from { Unit }`. A Worker doing no work and only shuttling the state + // along is usually the sign you have an extraneous state that can be collapsed! + // Don't try this at home. + context.runningWorker( + Worker.from { + isLoading.value = true + }, + "initializing" + ) { + action { + isLoading.value = false + state = Selected(NO_SELECTED_STANZA) + } + } + return OverviewDetailScreen(overviewRendering = BackStackScreen(BlankScreen)) + } + else -> { + val (stanzaIndex, currentStateIsLoading, repeat) = when (renderState) { + is ComplexCall -> Triple(renderState.payload, true, renderState.repeater) + is Selected -> Triple(renderState.stanzaIndex, false, 0) + Initializing -> throw IllegalStateException("No longer initializing.") + } + + if (currentStateIsLoading) { + if (repeat > 0) { + // Running a flow that emits 'repeat' number of times + context.runningWorker( + flow { + while (true) { + // As long as this Worker is running we want to be emitting values. + delay(2) + emit(repeat) + } + }.asTraceableWorker("EventRepetition") + ) { + action { + (state as? ComplexCall)?.let { currentState -> + // Still repeating the complex call + state = ComplexCall( + payload = currentState.payload, + repeater = (currentState.repeater - 1).coerceAtLeast(0) + ) + } + } + } + } else { + context.runningWorker( + worker = TraceableWorker.from("PoemLoading") { + isLoading.value = true + delay(simulatedPerfConfig.complexityDelay) + // No Output for Worker is necessary because the selected index + // is already in the state. + } + ) { + action { + isLoading.value = false + (state as? ComplexCall)?.let { currentState -> + state = Selected(currentState.payload) + } + } + } + } + } + + val stanzaListOverview = context.ChildRendering( + StanzaListWorkflow, + StanzaListWorkflow.Props( + poem = renderProps, + eventHandlerTag = ActionHandlingTracingInterceptor::keyForTrace + ), + key = "", + ) { selected -> + HandleStanzaListOutput(simulatedPerfConfig, selected) + } + .copy(selection = stanzaIndex) + + if (stanzaIndex != NO_SELECTED_STANZA) { + val stackedStanzas = renderProps.stanzas.subList(0, stanzaIndex + 1) + .mapIndexed { index, _ -> + context.ChildRendering( + StanzaWorkflow, + Props( + poem = renderProps, + index = index, + eventHandlerTag = ActionHandlingTracingInterceptor::keyForTrace + ), + key = "$index", + ) { + when (it) { + CloseStanzas -> ClearSelection(simulatedPerfConfig) + ShowPreviousStanza -> SelectPrevious(simulatedPerfConfig) + ShowNextStanza -> SelectNext(simulatedPerfConfig) + } + } + }.toBackStackScreen() + + return OverviewDetailScreen( + overviewRendering = BackStackScreen(stanzaListOverview), + detailRendering = stackedStanzas + ) + } + + return OverviewDetailScreen( + overviewRendering = BackStackScreen(stanzaListOverview), + selectDefault = { + context.actionSink.send(HandleStanzaListOutput(simulatedPerfConfig, 0)) + } + ) + } + } + } } diff --git a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoemsBrowserWorkflow.kt b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoemsBrowserWorkflow.kt index 9c5c1619e..22f242631 100644 --- a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoemsBrowserWorkflow.kt +++ b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoemsBrowserWorkflow.kt @@ -1,5 +1,6 @@ package com.squareup.benchmarks.performance.complex.poetry +import androidx.compose.runtime.Composable import com.squareup.benchmarks.performance.complex.poetry.PerformancePoemsBrowserWorkflow.State import com.squareup.benchmarks.performance.complex.poetry.PerformancePoemsBrowserWorkflow.State.ComplexCall import com.squareup.benchmarks.performance.complex.poetry.PerformancePoemsBrowserWorkflow.State.Initializing @@ -20,7 +21,9 @@ import com.squareup.sample.poetry.model.Poem import com.squareup.workflow1.Snapshot import com.squareup.workflow1.StatefulWorkflow import com.squareup.workflow1.WorkflowAction.Companion.noAction +import com.squareup.workflow1.WorkflowExperimentalRuntime import com.squareup.workflow1.action +import com.squareup.workflow1.compose.StatefulComposeWorkflow import com.squareup.workflow1.runningWorker import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.container.BackStackScreen @@ -44,13 +47,14 @@ import kotlinx.coroutines.flow.MutableStateFlow * break ties/conflicts with a token in the start/stop requests. We leave that complexity out * here. ** */ +@OptIn(WorkflowExperimentalRuntime::class) class PerformancePoemsBrowserWorkflow( private val simulatedPerfConfig: SimulatedPerfConfig, private val poemWorkflow: PoemWorkflow, private val isLoading: MutableStateFlow, ) : PoemsBrowserWorkflow, - StatefulWorkflow, State, Unit, OverviewDetailScreen>() { + StatefulComposeWorkflow, State, Unit, OverviewDetailScreen>() { sealed class State { // N.B. This state is a smell. We include it to be able to mimic smells @@ -76,7 +80,7 @@ class PerformancePoemsBrowserWorkflow( override fun render( renderProps: List, renderState: State, - context: RenderContext + context: StatefulWorkflow, State, Unit, OverviewDetailScreen>.RenderContext ): OverviewDetailScreen { if (simulatedPerfConfig.simultaneousActions > 0) { repeat(simulatedPerfConfig.simultaneousActions) { index -> @@ -183,4 +187,101 @@ class PerformancePoemsBrowserWorkflow( } private val clearSelection = choosePoem(NO_POEM_SELECTED) + + @OptIn(WorkflowUiExperimentalApi::class) + @Composable + override fun Rendering( + renderProps: List, + renderState: State, + context: RenderContext + ): OverviewDetailScreen { + + // Again, then entire `Initializing` state is a smell, which is most obvious from the + // use of `Worker.from { Unit }`. A Worker doing no work and only shuttling the state + // along is usually the sign you have an extraneous state that can be collapsed! + // Don't try this at home. + if (renderState is Initializing) { + context.runningWorker(TraceableWorker.from("BrowserInitializing") { Unit }, "init") { + isLoading.value = true + action { + isLoading.value = false + state = NoSelection + } + } + return OverviewDetailScreen(overviewRendering = BackStackScreen(BlankScreen)) + } + + val poemListProps = Props( + poems = renderProps, + eventHandlerTag = ActionHandlingTracingInterceptor::keyForTrace + ) + + val poemListRendering = context.ChildRendering( + child = PoemListWorkflow, + props = poemListProps, + key = "", + ) { selected -> + choosePoem(selected) + } + when (renderState) { + is NoSelection -> { + return OverviewDetailScreen( + overviewRendering = BackStackScreen( + poemListRendering.copy(selection = NO_POEM_SELECTED) + ) + ) + } + is ComplexCall -> { + context.runningWorker( + TraceableWorker.from("ComplexCallBrowser(${renderState.payload})") { + isLoading.value = true + delay(simulatedPerfConfig.complexityDelay) + // No Output for Worker is necessary because the selected index + // is already in the state. + } + ) { + action { + isLoading.value = false + (state as? ComplexCall)?.let { currentState -> + state = if (currentState.payload != NO_POEM_SELECTED) { + Selected(currentState.payload) + } else { + NoSelection + } + } + } + } + val poemOverview = OverviewDetailScreen( + overviewRendering = BackStackScreen( + poemListRendering.copy(selection = renderState.payload) + ) + ) + val poems = if (renderState.payload != NO_POEM_SELECTED) { + poemOverview + context.ChildRendering( + poemWorkflow, + renderProps[renderState.payload], + key = "", + ) { clearSelection } + } else { + poemOverview + } + return poems + } + is Selected -> { + val poemOverview = OverviewDetailScreen( + overviewRendering = BackStackScreen( + poemListRendering.copy(selection = renderState.poemIndex) + ) + ) + return poemOverview + context.ChildRendering( + poemWorkflow, + renderProps[renderState.poemIndex], + key = "", + ) { clearSelection } + } + else -> { + throw IllegalStateException("$renderState state is impossible.") + } + } + } } diff --git a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoetryActivity.kt b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoetryActivity.kt index 392ecd6b7..06ba4212e 100644 --- a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoetryActivity.kt +++ b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoetryActivity.kt @@ -24,6 +24,7 @@ import com.squareup.workflow1.RuntimeConfig import com.squareup.workflow1.RuntimeConfig.RenderPerAction import com.squareup.workflow1.WorkflowExperimentalRuntime import com.squareup.workflow1.WorkflowInterceptor +import com.squareup.workflow1.compose.ComposeRuntimePlugin import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ViewEnvironment.Companion.EMPTY import com.squareup.workflow1.ui.ViewRegistry @@ -261,13 +262,15 @@ class PoetryModel( interceptor: WorkflowInterceptor?, runtimeConfig: RuntimeConfig ) : ViewModel() { - @OptIn(WorkflowUiExperimentalApi::class) val renderings: StateFlow by lazy { + @OptIn(WorkflowUiExperimentalApi::class, WorkflowExperimentalRuntime::class) + val renderings: StateFlow by lazy { renderWorkflowIn( workflow = workflow, scope = viewModelScope, savedStateHandle = savedState, interceptors = interceptor?.let { listOf(it) } ?: emptyList(), - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowRuntimePlugin = ComposeRuntimePlugin ) } diff --git a/build.gradle.kts b/build.gradle.kts index 5ab4bf20c..14b095698 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,6 +5,7 @@ import org.jlleitschuh.gradle.ktlint.reporter.ReporterType buildscript { dependencies { classpath(libs.android.gradle.plugin) + classpath(libs.molecule.gradle.plugin) classpath(libs.kotlinx.benchmark.gradle.plugin) classpath(libs.dokka.gradle.plugin) classpath(libs.kotlin.serialization.gradle.plugin) @@ -52,6 +53,23 @@ subprojects { } } +subprojects { + tasks.withType().configureEach { + kotlinOptions { + if (project.findProperty("enableComposeCompilerReports") == "true") { + freeCompilerArgs = freeCompilerArgs + listOf( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + + project.buildDir.absolutePath + "/compose_metrics", + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + + project.buildDir.absolutePath + "/compose_metrics" + ) + } + } + } +} + apply(from = rootProject.file(".buildscript/binary-validation.gradle")) // This plugin needs to be applied to the root projects for the dokkaGfmCollector task we use to diff --git a/buildSrc/src/main/java/android-defaults.gradle.kts b/buildSrc/src/main/java/android-defaults.gradle.kts index 33cc37a08..a1bc2df0f 100644 --- a/buildSrc/src/main/java/android-defaults.gradle.kts +++ b/buildSrc/src/main/java/android-defaults.gradle.kts @@ -1,7 +1,7 @@ import com.android.build.gradle.TestedExtension configure { - compileSdkVersion(31) + compileSdkVersion(33) compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 diff --git a/gradle.properties b/gradle.properties index 4db29f21d..3ef30a432 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ android.useAndroidX=true systemProp.org.gradle.internal.publish.checksums.insecure=true GROUP=com.squareup.workflow1 -VERSION_NAME=1.8.0-beta11-SNAPSHOT +VERSION_NAME=1.8.0-beta11-local-SNAPSHOT POM_DESCRIPTION=Square Workflow @@ -25,3 +25,5 @@ POM_DEVELOPER_ID=square POM_DEVELOPER_NAME=Square, Inc. POM_DEVELOPER_URL=https://github.com/square/ SONATYPE_STAGING_PROFILE=com.squareup + +kotlin.mpp.stability.nowarn=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9dee05445..de5f45e3f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ androidTools = "7.3.0" -compileSdk = "31" +compileSdk = "33" minSdkVersion = "21" targetSdk = "30" @@ -10,8 +10,8 @@ androidx-activity = "1.3.0" androidx-appcompat = "1.3.1" androidx-benchmark = "1.1.0-rc03" androidx-cardview = "1.0.0" -androidx-compose = "1.1.0-rc01" -androidx-compose-compiler = "1.2.0-rc02" +androidx-compose = "1.2.1" +#androidx-compose-compiler = "1.2.0-rc02" androidx-constraintlayout = "2.1.2" androidx-core = "1.6.0" androidx-fragment = "1.3.6" @@ -33,13 +33,16 @@ androidx-transition = "1.4.1" androidx-viewbinding = "4.2.1" androidx-work = "2.6.0" +compose = "1.2.0" +compose-compiler = "1.3.0" + detekt = "1.19.0" dokka = "1.5.31" dependencyGuard = "0.1.0" google-accompanist = "0.18.0" google-dagger = "2.40.5" -google-ksp = "1.6.21-1.0.6" +google-ksp = "1.7.10-1.0.6" google-material = "1.4.0" groovy = "3.0.9" @@ -47,7 +50,7 @@ jUnit = "4.13.2" javaParser = "3.24.0" jmh = "1.34" kotest = "5.1.0" -kotlin = "1.6.21" +kotlin = "1.7.10" kotlinx-binary-compatibility = "0.11.1" kotlinx-coroutines = "1.6.4" @@ -63,6 +66,9 @@ mockito-core = "3.3.3" mockito-kotlin = "3.2.0" mockk = "1.11.0" + +molecule = "0.5.0" + robolectric = "4.6.1" rxjava2-android = "2.1.1" @@ -99,8 +105,12 @@ kotlinx-benchmark = { id = "org.jetbrains.kotlinx.benchmark", version.ref = "kot ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktech-publish" } +molecule = { id = "app.cash.molecule", version.ref = "molecule"} + [libraries] +molecule-gradle-plugin = { module = "app.cash.molecule:molecule-gradle-plugin", version.ref = "molecule"} + android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "androidTools" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } @@ -126,6 +136,9 @@ androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4 androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "androidx-compose" } androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "androidx-compose" } +compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "compose"} +compose-compiler = { module = "org.jetbrains.compose.compiler:compiler", version.ref = "compose"} + androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" } androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } @@ -214,6 +227,9 @@ mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = " mockk = { module = "io.mockk:mockk", version.ref = "mockk" } +molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule"} +molecule-testing = { module = "app.cash.molecule:molecule-testing", version.ref = "molecule"} + reactivestreams = "org.reactivestreams:reactive-streams:1.0.3" robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } diff --git a/samples/compose-samples/build.gradle.kts b/samples/compose-samples/build.gradle.kts index f7b69d236..e23fcbbbc 100644 --- a/samples/compose-samples/build.gradle.kts +++ b/samples/compose-samples/build.gradle.kts @@ -14,7 +14,7 @@ android { compose = true } composeOptions { - kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() } namespace = "com.squareup.sample.compose" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 662672fed..70f23fbae 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -63,6 +63,7 @@ include( ":workflow-config:config-android", ":workflow-config:config-jvm", ":workflow-core", + ":workflow-core-compose", ":workflow-runtime", ":workflow-rx2", ":workflow-testing", diff --git a/workflow-core-compose/README.md b/workflow-core-compose/README.md new file mode 100644 index 000000000..81ef869ad --- /dev/null +++ b/workflow-core-compose/README.md @@ -0,0 +1,9 @@ +# Workflow Runtime with Compose Optimizations. + +This module contains extensions on the Workflow Core and Workflow Runtime classes that allow +for the Compose runtime to optimize which workflows are rendered in a render pass. + +This is entirely experimental and has no dedicated tests yet, so please do not use unless you +are experimenting. + +To use it you can pass the [ComposeRuntimePlugin] to [renderWorkflowIn]. diff --git a/workflow-core-compose/api/workflow-core-compose.api b/workflow-core-compose/api/workflow-core-compose.api new file mode 100644 index 000000000..277fa9e98 --- /dev/null +++ b/workflow-core-compose/api/workflow-core-compose.api @@ -0,0 +1,210 @@ +public abstract interface class com/squareup/workflow1/compose/BaseComposeRenderContext : com/squareup/workflow1/BaseRenderContext { + public abstract fun ChildRendering (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; + public abstract fun RunningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V +} + +public final class com/squareup/workflow1/compose/BaseComposeRenderContext$DefaultImpls { + public static fun eventHandler (Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function10;)Lkotlin/jvm/functions/Function9; + public static fun eventHandler (Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function11;)Lkotlin/jvm/functions/Function10; + public static fun eventHandler (Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function0; + public static fun eventHandler (Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)Lkotlin/jvm/functions/Function1; + public static fun eventHandler (Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;)Lkotlin/jvm/functions/Function2; + public static fun eventHandler (Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function4;)Lkotlin/jvm/functions/Function3; + public static fun eventHandler (Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function5;)Lkotlin/jvm/functions/Function4; + public static fun eventHandler (Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function6;)Lkotlin/jvm/functions/Function5; + public static fun eventHandler (Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function7;)Lkotlin/jvm/functions/Function6; + public static fun eventHandler (Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function8;)Lkotlin/jvm/functions/Function7; + public static fun eventHandler (Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function9;)Lkotlin/jvm/functions/Function8; +} + +public final class com/squareup/workflow1/compose/BaseComposeRenderContextKt { + public static final fun ChildRendering (Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Landroidx/compose/runtime/Composer;II)Ljava/lang/Object; + public static final fun ChildRendering (Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lcom/squareup/workflow1/Workflow;Ljava/lang/String;Landroidx/compose/runtime/Composer;II)Ljava/lang/Object; + public static final fun ChildRendering (Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lcom/squareup/workflow1/Workflow;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Ljava/lang/Object; + public static final fun RunningWorker (Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lcom/squareup/workflow1/Worker;Lkotlin/reflect/KType;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V +} + +public final class com/squareup/workflow1/compose/ChainedComposeWorkflowInterceptor : com/squareup/workflow1/ChainedWorkflowInterceptor, com/squareup/workflow1/compose/ComposeWorkflowInterceptor { + public static final field $stable I + public fun (Ljava/util/List;)V + public fun Rendering (Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; + public final fun wrap (Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor$ComposeRenderContextInterceptor;Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor$ComposeRenderContextInterceptor;)Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor$ComposeRenderContextInterceptor; +} + +public final class com/squareup/workflow1/compose/ComposeRuntimePlugin : com/squareup/workflow1/WorkflowRuntimePlugin { + public static final field $stable I + public static final field INSTANCE Lcom/squareup/workflow1/compose/ComposeRuntimePlugin; + public fun chainedInterceptors (Ljava/util/List;)Lcom/squareup/workflow1/WorkflowInterceptor; + public fun createWorkflowRunner (Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/flow/StateFlow;Lcom/squareup/workflow1/TreeSnapshot;Lcom/squareup/workflow1/WorkflowInterceptor;Lcom/squareup/workflow1/RuntimeConfig;)Lcom/squareup/workflow1/WorkflowRunner; + public fun initializeRenderingStream (Lcom/squareup/workflow1/WorkflowRunner;Lkotlinx/coroutines/CoroutineScope;)Lkotlinx/coroutines/flow/StateFlow; + public fun nextRendering (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class com/squareup/workflow1/compose/ComposeSubtreeManager : com/squareup/workflow1/SubtreeManager, com/squareup/workflow1/compose/RealComposeRenderContext$ComposeRenderer { + public static final field $stable I + public fun (Ljava/util/Map;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor;Lcom/squareup/workflow1/IdCounter;)V + public synthetic fun (Ljava/util/Map;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor;Lcom/squareup/workflow1/IdCounter;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun Rendering (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; + public synthetic fun createChildNode (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/WorkflowChildNode; + public synthetic fun getInterceptor ()Lcom/squareup/workflow1/WorkflowInterceptor; +} + +public abstract interface class com/squareup/workflow1/compose/ComposeWorkflowInterceptor : com/squareup/workflow1/WorkflowInterceptor { + public abstract fun Rendering (Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; +} + +public abstract interface class com/squareup/workflow1/compose/ComposeWorkflowInterceptor$ComposeRenderContextInterceptor : com/squareup/workflow1/WorkflowInterceptor$RenderContextInterceptor { + public abstract fun ChildRendering (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function6;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; + public abstract fun RunningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;I)V +} + +public final class com/squareup/workflow1/compose/ComposeWorkflowInterceptor$ComposeRenderContextInterceptor$DefaultImpls { + public static fun ChildRendering (Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor$ComposeRenderContextInterceptor;Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function6;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; + public static fun RunningSideEffect (Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor$ComposeRenderContextInterceptor;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;I)V + public static fun onActionSent (Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor$ComposeRenderContextInterceptor;Lcom/squareup/workflow1/WorkflowAction;Lkotlin/jvm/functions/Function1;)V + public static fun onRenderChild (Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor$ComposeRenderContextInterceptor;Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;)Ljava/lang/Object; + public static fun onRunningSideEffect (Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor$ComposeRenderContextInterceptor;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V +} + +public final class com/squareup/workflow1/compose/ComposeWorkflowInterceptor$DefaultImpls { + public static fun Rendering (Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor;Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; + public static fun onInitialState (Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor;Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlin/jvm/functions/Function2;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; + public static fun onPropsChanged (Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; + public static fun onRender (Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor;Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/BaseRenderContext;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; + public static fun onSessionStarted (Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor;Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V + public static fun onSnapshotState (Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/Snapshot; +} + +public final class com/squareup/workflow1/compose/ComposeWorkflowInterceptorKt { + public static final fun asComposeWorkflowInterceptor (Lcom/squareup/workflow1/WorkflowInterceptor;)Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor; + public static final fun intercept (Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor;Lcom/squareup/workflow1/compose/StatefulComposeWorkflow;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/compose/StatefulComposeWorkflow; +} + +public class com/squareup/workflow1/compose/InterceptedComposeRenderContext : com/squareup/workflow1/InterceptedRenderContext, com/squareup/workflow1/Sink, com/squareup/workflow1/compose/BaseComposeRenderContext { + public static final field $stable I + public fun (Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor$ComposeRenderContextInterceptor;)V + public fun ChildRendering (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; + public fun RunningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V +} + +public final class com/squareup/workflow1/compose/NoopComposeWorkflowInterceptor : com/squareup/workflow1/compose/ComposeWorkflowInterceptor { + public static final field $stable I + public static final field INSTANCE Lcom/squareup/workflow1/compose/NoopComposeWorkflowInterceptor; + public fun Rendering (Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; + public fun onInitialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlin/jvm/functions/Function2;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; + public fun onPropsChanged (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; + public fun onRender (Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/BaseRenderContext;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; + public fun onSessionStarted (Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V + public fun onSnapshotState (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/Snapshot; +} + +public final class com/squareup/workflow1/compose/RealComposeRenderContext : com/squareup/workflow1/RealRenderContext, com/squareup/workflow1/compose/BaseComposeRenderContext { + public static final field $stable I + public fun (Lcom/squareup/workflow1/compose/RealComposeRenderContext$ComposeRenderer;Lcom/squareup/workflow1/RealRenderContext$SideEffectRunner;Lkotlinx/coroutines/channels/SendChannel;)V + public fun ChildRendering (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; + public fun RunningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V + public synthetic fun getRenderer ()Lcom/squareup/workflow1/RealRenderContext$Renderer; +} + +public abstract interface class com/squareup/workflow1/compose/RealComposeRenderContext$ComposeRenderer : com/squareup/workflow1/RealRenderContext$Renderer { + public abstract fun Rendering (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; +} + +public abstract class com/squareup/workflow1/compose/StatefulComposeWorkflow : com/squareup/workflow1/StatefulWorkflow { + public static final field $stable I + public fun ()V + public abstract fun Rendering (Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/compose/StatefulComposeWorkflow$RenderContext;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; +} + +public final class com/squareup/workflow1/compose/StatefulComposeWorkflow$RenderContext : com/squareup/workflow1/StatefulWorkflow$RenderContext, com/squareup/workflow1/compose/BaseComposeRenderContext { + public fun ChildRendering (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; + public fun RunningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function10;)Lkotlin/jvm/functions/Function9; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function11;)Lkotlin/jvm/functions/Function10; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function0; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)Lkotlin/jvm/functions/Function1; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;)Lkotlin/jvm/functions/Function2; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function4;)Lkotlin/jvm/functions/Function3; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function5;)Lkotlin/jvm/functions/Function4; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function6;)Lkotlin/jvm/functions/Function5; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function7;)Lkotlin/jvm/functions/Function6; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function8;)Lkotlin/jvm/functions/Function7; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function9;)Lkotlin/jvm/functions/Function8; + public fun getActionSink ()Lcom/squareup/workflow1/Sink; + public fun renderChild (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public fun runningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V +} + +public final class com/squareup/workflow1/compose/StatefulComposeWorkflowKt { + public static final fun ComposeRenderContext (Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lcom/squareup/workflow1/compose/StatefulComposeWorkflow;)Lcom/squareup/workflow1/compose/StatefulComposeWorkflow$RenderContext; + public static final fun asComposeWorkflow (Lcom/squareup/workflow1/StatefulWorkflow;Lkotlin/jvm/functions/Function6;)Lcom/squareup/workflow1/compose/StatefulComposeWorkflow; + public static synthetic fun asComposeWorkflow$default (Lcom/squareup/workflow1/StatefulWorkflow;Lkotlin/jvm/functions/Function6;ILjava/lang/Object;)Lcom/squareup/workflow1/compose/StatefulComposeWorkflow; + public static final fun composedStateful (Lcom/squareup/workflow1/Workflow$Companion;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function4;)Lcom/squareup/workflow1/StatefulWorkflow; + public static final fun composedStateful (Lcom/squareup/workflow1/Workflow$Companion;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/StatefulWorkflow; + public static final fun composedStateful (Lcom/squareup/workflow1/Workflow$Companion;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function3;)Lcom/squareup/workflow1/StatefulWorkflow; + public static final fun composedStateful (Lcom/squareup/workflow1/Workflow$Companion;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;)Lcom/squareup/workflow1/StatefulWorkflow; + public static synthetic fun composedStateful$default (Lcom/squareup/workflow1/Workflow$Companion;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lcom/squareup/workflow1/StatefulWorkflow; + public static synthetic fun composedStateful$default (Lcom/squareup/workflow1/Workflow$Companion;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lcom/squareup/workflow1/StatefulWorkflow; +} + +public abstract class com/squareup/workflow1/compose/StatelessComposeWorkflow : com/squareup/workflow1/StatelessWorkflow { + public static final field $stable I + public fun ()V + public abstract fun Rendering (Ljava/lang/Object;Lcom/squareup/workflow1/compose/StatelessComposeWorkflow$RenderContext;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; + protected fun getStatefulWorkflow ()Lcom/squareup/workflow1/StatefulWorkflow; +} + +public final class com/squareup/workflow1/compose/StatelessComposeWorkflow$RenderContext : com/squareup/workflow1/StatelessWorkflow$RenderContext, com/squareup/workflow1/compose/BaseComposeRenderContext { + public fun (Lcom/squareup/workflow1/compose/StatelessComposeWorkflow;Lcom/squareup/workflow1/compose/BaseComposeRenderContext;)V + public fun ChildRendering (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; + public fun RunningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function10;)Lkotlin/jvm/functions/Function9; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function11;)Lkotlin/jvm/functions/Function10; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function0; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)Lkotlin/jvm/functions/Function1; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;)Lkotlin/jvm/functions/Function2; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function4;)Lkotlin/jvm/functions/Function3; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function5;)Lkotlin/jvm/functions/Function4; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function6;)Lkotlin/jvm/functions/Function5; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function7;)Lkotlin/jvm/functions/Function6; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function8;)Lkotlin/jvm/functions/Function7; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function9;)Lkotlin/jvm/functions/Function8; + public fun getActionSink ()Lcom/squareup/workflow1/Sink; + public fun renderChild (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public fun runningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V +} + +public final class com/squareup/workflow1/compose/StatelessComposeWorkflowKt { + public static final fun ComposeRenderContext (Lcom/squareup/workflow1/compose/BaseComposeRenderContext;Lcom/squareup/workflow1/compose/StatelessComposeWorkflow;)Lcom/squareup/workflow1/compose/StatelessComposeWorkflow$RenderContext; + public static final fun asComposeWorkflow (Lcom/squareup/workflow1/StatelessWorkflow;Lkotlin/jvm/functions/Function5;)Lcom/squareup/workflow1/compose/StatelessComposeWorkflow; + public static synthetic fun asComposeWorkflow$default (Lcom/squareup/workflow1/StatelessWorkflow;Lkotlin/jvm/functions/Function5;ILjava/lang/Object;)Lcom/squareup/workflow1/compose/StatelessComposeWorkflow; + public static final fun composedStateless (Lcom/squareup/workflow1/Workflow$Companion;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function2;)Lcom/squareup/workflow1/Workflow; +} + +public final class com/squareup/workflow1/compose/WorkflowComposeChildNode : com/squareup/workflow1/WorkflowChildNode { + public static final field $stable I + public fun (Lcom/squareup/workflow1/Workflow;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowNode;)V + public final fun Rendering (Lcom/squareup/workflow1/compose/StatefulComposeWorkflow;Ljava/lang/Object;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; +} + +public final class com/squareup/workflow1/compose/WorkflowComposeNode : com/squareup/workflow1/WorkflowNode { + public static final field $stable I + public fun (Lcom/squareup/workflow1/WorkflowNodeId;Lcom/squareup/workflow1/compose/StatefulComposeWorkflow;Ljava/lang/Object;Lcom/squareup/workflow1/TreeSnapshot;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor;Lcom/squareup/workflow1/IdCounter;)V + public synthetic fun (Lcom/squareup/workflow1/WorkflowNodeId;Lcom/squareup/workflow1/compose/StatefulComposeWorkflow;Ljava/lang/Object;Lcom/squareup/workflow1/TreeSnapshot;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor;Lcom/squareup/workflow1/IdCounter;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun Rendering (Lcom/squareup/workflow1/compose/StatefulComposeWorkflow;Ljava/lang/Object;Landroidx/compose/runtime/MutableState;Landroidx/compose/runtime/Composer;I)V + public synthetic fun getSubtreeManager ()Lcom/squareup/workflow1/SubtreeManager; + public synthetic fun getWorkflow ()Lcom/squareup/workflow1/StatefulWorkflow; + public fun startSession ()V +} + +public final class com/squareup/workflow1/compose/WorkflowComposeRunner : com/squareup/workflow1/WorkflowRunner { + public static final field $stable I + public fun (Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/flow/StateFlow;Lcom/squareup/workflow1/TreeSnapshot;Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor;Lcom/squareup/workflow1/RuntimeConfig;)V + public synthetic fun getRootNode ()Lcom/squareup/workflow1/WorkflowNode; + public final fun nextComposedRendering (Landroidx/compose/runtime/Composer;I)Lcom/squareup/workflow1/RenderingAndSnapshot; +} + +public final class com/squareup/workflow1/compose/WorkflowsKt { + public static final fun mapRenderingComposable (Lcom/squareup/workflow1/Workflow;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/Workflow; +} + diff --git a/workflow-core-compose/build.gradle.kts b/workflow-core-compose/build.gradle.kts new file mode 100644 index 000000000..3eb779be1 --- /dev/null +++ b/workflow-core-compose/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + `kotlin-multiplatform` + published + id("app.cash.molecule") +} + +kotlin { + jvm { withJava() } + ios() + + sourceSets { + all { + languageSettings.apply { + optIn("kotlin.RequiresOptIn") + } + } + val commonMain by getting { + dependencies { + api(project(":workflow-core")) + api(project(":workflow-runtime")) + api(libs.kotlin.jdk6) + api(libs.compose.runtime) + api(libs.kotlinx.coroutines.core) + implementation(libs.molecule.runtime) + } + } + val commonTest by getting { + dependencies { + implementation(libs.kotlinx.atomicfu) + implementation(libs.kotlinx.coroutines.test.common) + implementation(libs.kotlin.test.jdk) + } + } + } +} diff --git a/workflow-core-compose/gradle.properties b/workflow-core-compose/gradle.properties new file mode 100644 index 000000000..e79c8dfe3 --- /dev/null +++ b/workflow-core-compose/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=workflow-core-compose +POM_NAME=Workflow Core Compose +POM_PACKAGING=jar diff --git a/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/BaseComposeRenderContext.kt b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/BaseComposeRenderContext.kt new file mode 100644 index 000000000..8011bedc2 --- /dev/null +++ b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/BaseComposeRenderContext.kt @@ -0,0 +1,137 @@ +package com.squareup.workflow1.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.squareup.workflow1.BaseRenderContext +import com.squareup.workflow1.Worker +import com.squareup.workflow1.Workflow +import com.squareup.workflow1.WorkflowAction +import com.squareup.workflow1.WorkflowAction.Companion.noAction +import kotlinx.coroutines.CoroutineScope +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +/** + * @see [BaseRenderContext]. This is the version which adds support for the Compose optimized + * runtime. + */ +public interface BaseComposeRenderContext : + BaseRenderContext { + + /** + * @see [BaseRenderContext.renderChild] as this is equivalent, except as a Composable. + */ + @Composable + public fun ChildRendering( + child: Workflow, + props: ChildPropsT, + key: String, + handler: (ChildOutputT) -> WorkflowAction + ): ChildRenderingT + + /** + * @see [BaseRenderContext.runningSideEffect] as this is equivalent, except as a Composable. + */ + @Composable + public fun RunningSideEffect( + key: String, + sideEffect: suspend CoroutineScope.() -> Unit + ) +} + +/** + * Convenience alias of [BaseComposeRenderContext.ChildRendering] for workflows that don't take props. + */ +@Composable +public fun +BaseComposeRenderContext.ChildRendering( + child: Workflow, + key: String = "", + handler: (ChildOutputT) -> WorkflowAction +): ChildRenderingT = ChildRendering(child, Unit, key, handler) +/** + * Convenience alias of [BaseComposeRenderContext.ChildRendering] for workflows that don't emit output. + */ +@Composable +public fun +BaseComposeRenderContext.ChildRendering( + child: Workflow, + props: ChildPropsT, + key: String = "", +): ChildRenderingT = ChildRendering(child, props, key) { noAction() } +/** + * Convenience alias of [BaseComposeRenderContext.ChildRendering] for children that don't take props or emit + * output. + */ +@Composable +public fun +BaseComposeRenderContext.ChildRendering( + child: Workflow, + key: String = "", +): ChildRenderingT = ChildRendering(child, Unit, key) { noAction() } + +/** + * Ensures a [Worker] that never emits anything is running. Since [worker] can't emit anything, + * it can't trigger any [WorkflowAction]s. + * + * If your [Worker] does not output anything, then simply use [runningSideEffect]. + * + * @param key An optional string key that is used to distinguish between identical [Worker]s. + */ +@Composable +public inline fun , PropsT, StateT, OutputT> +BaseComposeRenderContext.RunningWorker( + worker: W, + key: String = "" +) { + RunningWorker(worker, key) { + // The compiler thinks this code is unreachable, and it is correct. But we have to pass a lambda + // here so we might as well check at runtime as well. + @Suppress("UNREACHABLE_CODE", "ThrowableNotThrown") + throw AssertionError("Worker emitted $it") + } +} + +/** + * Ensures [worker] is running. When the [Worker] emits an output, [handler] is called + * to determine the [WorkflowAction] to take. When the worker finishes, nothing happens (although + * another render pass may be triggered). + * + * Like workflows, workers are kept alive across multiple render passes if they're the same type, + * and different workers of distinct types can be run concurrently. However, unlike workflows, + * workers are compared by their _declared_ type, not their actual type. This means that if you + * pass a worker stored in a variable to this function, the type that will be used to compare the + * worker will be the type of the variable, not the type of the object the variable refers to. + * + * @param key An optional string key that is used to distinguish between identical [Worker]s. + */ +@Composable +public inline fun , PropsT, StateT, OutputT> +BaseComposeRenderContext.RunningWorker( + worker: W, + key: String = "", + noinline handler: (T) -> WorkflowAction +) { + RunningWorker(worker, typeOf(), key, handler) +} + +/** + * Ensures [worker] is running. When the [Worker] emits an output, [handler] is called + * to determine the [WorkflowAction] to take. When the worker finishes, nothing happens (although + * another render pass may be triggered). + * + * @param workerType `typeOf()` + * @param key An optional string key that is used to distinguish between identical [Worker]s. + */ +@PublishedApi +@Composable +internal fun +BaseComposeRenderContext.RunningWorker( + worker: Worker, + workerType: KType, + key: String = "", + handler: (T) -> WorkflowAction +) { + val workerWorkflow = remember(workerType, key) { ComposeWorkerWorkflow(workerType, key) } + ChildRendering(workerWorkflow, props = worker, key = key, handler = handler) +} diff --git a/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/ChainedComposeWorkflowInterceptor.kt b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/ChainedComposeWorkflowInterceptor.kt new file mode 100644 index 000000000..a09ba3b91 --- /dev/null +++ b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/ChainedComposeWorkflowInterceptor.kt @@ -0,0 +1,138 @@ +package com.squareup.workflow1.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.squareup.workflow1.ChainedWorkflowInterceptor +import com.squareup.workflow1.Workflow +import com.squareup.workflow1.WorkflowAction +import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor +import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession +import com.squareup.workflow1.compose.ComposeWorkflowInterceptor.ComposeRenderContextInterceptor + +internal fun List.chained(): ComposeWorkflowInterceptor = + when { + isEmpty() -> NoopComposeWorkflowInterceptor + size == 1 -> single() + else -> ChainedComposeWorkflowInterceptor(this) + } + +public class ChainedComposeWorkflowInterceptor( + override val interceptors: List +) : ChainedWorkflowInterceptor(interceptors), ComposeWorkflowInterceptor { + + @Composable + public override fun Rendering( + renderProps: P, + renderState: S, + context: BaseComposeRenderContext, + session: WorkflowSession, + proceed: @Composable (P, S, ComposeRenderContextInterceptor?) -> R + ): R { + val chainedProceed = remember(session) { + interceptors.foldRight(proceed) { workflowInterceptor, proceedAcc -> + { props, state, outerContextInterceptor -> + // Holding compiler's hand for function type. + val proceedInternal = + remember<@Composable (P, S, ComposeRenderContextInterceptor?) -> R>( + outerContextInterceptor + ) { + @Composable { p: P, + s: S, + innerContextInterceptor: ComposeRenderContextInterceptor? -> + val contextInterceptor = remember(innerContextInterceptor) { + outerContextInterceptor.wrap(innerContextInterceptor) + } + proceedAcc(p, s, contextInterceptor) + } + } + workflowInterceptor.Rendering( + props, + state, + context, + proceed = proceedInternal, + session = session, + ) + } + } + } + return chainedProceed(renderProps, renderState, null) + } + + public fun ComposeRenderContextInterceptor?.wrap( + inner: ComposeRenderContextInterceptor? + ): ComposeRenderContextInterceptor? = when { + this == null && inner == null -> null + this == null -> inner + inner == null -> this + else -> { + // Share the base implementation. + val regularRenderContextInterceptor = (this as RenderContextInterceptor).wrap(inner) + object : ComposeRenderContextInterceptor { + // If we don't use !!, the compiler complains about the non-elvis dot accesses below. + @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") + val outer = this@wrap!! + + override fun onActionSent( + action: WorkflowAction, + proceed: (WorkflowAction) -> Unit + ) = regularRenderContextInterceptor!!.onActionSent(action, proceed) + + override fun onRenderChild( + child: Workflow, + childProps: CP, + key: String, + handler: (CO) -> WorkflowAction, + proceed: ( + child: Workflow, + props: CP, + key: String, + handler: (CO) -> WorkflowAction + ) -> CR + ): CR = + regularRenderContextInterceptor!!.onRenderChild(child, childProps, key, handler, proceed) + + @Composable + override fun ChildRendering( + child: Workflow, + childProps: CP, + key: String, + handler: (CO) -> WorkflowAction, + proceed: @Composable ( + child: Workflow, + childProps: CP, + key: String, + handler: (CO) -> WorkflowAction + ) -> CR + ): CR = + outer.ChildRendering( + child, + childProps, + key, + handler + ) @Composable { c, p, k, h -> + inner.ChildRendering(c, p, k, h, proceed) + } + + @Composable + override fun RunningSideEffect( + key: String, + sideEffect: suspend () -> Unit, + proceed: @Composable (key: String, sideEffect: suspend () -> Unit) -> Unit + ) { + outer.RunningSideEffect( + key, + sideEffect + ) @Composable { k, s -> + inner.RunningSideEffect(k, s, proceed) + } + } + + override fun onRunningSideEffect( + key: String, + sideEffect: suspend () -> Unit, + proceed: (key: String, sideEffect: suspend () -> Unit) -> Unit + ) = regularRenderContextInterceptor!!.onRunningSideEffect(key, sideEffect, proceed) + } + } + } +} diff --git a/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/ComposeRuntimePlugin.kt b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/ComposeRuntimePlugin.kt new file mode 100644 index 000000000..fe70a51f3 --- /dev/null +++ b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/ComposeRuntimePlugin.kt @@ -0,0 +1,79 @@ +package com.squareup.workflow1.compose + +import androidx.compose.runtime.BroadcastFrameClock +import androidx.compose.runtime.remember +import app.cash.molecule.RecompositionClock.ContextClock +import app.cash.molecule.launchMolecule +import com.squareup.workflow1.RenderingAndSnapshot +import com.squareup.workflow1.RuntimeConfig +import com.squareup.workflow1.TreeSnapshot +import com.squareup.workflow1.Workflow +import com.squareup.workflow1.WorkflowExperimentalRuntime +import com.squareup.workflow1.WorkflowInterceptor +import com.squareup.workflow1.WorkflowRunner +import com.squareup.workflow1.WorkflowRuntimePlugin +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.CONFLATED +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.plus + +/** + * [WorkflowRuntimePlugin] implementation that adds in a Compose optimized runtime. This will + * attempt to prevent any unnecessary renderings when the state (tracked using Compose) has + * not changed. + * + * Use [StatefulComposeWorkflow] and [StatelessComposeWorkflow] to take advantage of these + * runtime optimizations if your [Workflow] is not a leaf in the tree. Leaf workflows will be + * converted and handled automatically. + */ +@WorkflowExperimentalRuntime +public object ComposeRuntimePlugin : WorkflowRuntimePlugin { + + private val nextComposeFrameGate = Channel(CONFLATED).apply { + // Bootstrap with the gate open since first compose is synchronous. + trySend(Unit) + } + private val composeRuntimeClock: BroadcastFrameClock = BroadcastFrameClock { + // When we have the Recomposer waiting, then open the gate. + nextComposeFrameGate.trySend(Unit) + } + + override fun createWorkflowRunner( + scope: CoroutineScope, + protoWorkflow: Workflow, + props: StateFlow, + snapshot: TreeSnapshot?, + interceptor: WorkflowInterceptor, + runtimeConfig: RuntimeConfig + ): WorkflowRunner = WorkflowComposeRunner( + scope, + protoWorkflow, + props, + snapshot, + interceptor.asComposeWorkflowInterceptor(), + runtimeConfig, + ) + + override fun initializeRenderingStream( + workflowRunner: WorkflowRunner, + runtimeScope: CoroutineScope + ): StateFlow> { + val clockedScope = runtimeScope + composeRuntimeClock + + return clockedScope.launchMolecule(clock = ContextClock) { + val runner = remember(workflowRunner) { (workflowRunner as WorkflowComposeRunner) } + runner.nextComposedRendering() + } + } + + override suspend fun nextRendering() { + nextComposeFrameGate.receive() + // Gate is open, so send the frame to the Recomposer and wait to receive the signal that the + // rendering has been composed. + composeRuntimeClock.sendFrame(0L) + } + + override fun chainedInterceptors(interceptors: List): WorkflowInterceptor = + interceptors.map { it.asComposeWorkflowInterceptor() }.chained() +} diff --git a/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/ComposeSubtreeManager.kt b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/ComposeSubtreeManager.kt new file mode 100644 index 000000000..fd37be41b --- /dev/null +++ b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/ComposeSubtreeManager.kt @@ -0,0 +1,124 @@ +package com.squareup.workflow1.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import com.squareup.workflow1.IdCounter +import com.squareup.workflow1.SubtreeManager +import com.squareup.workflow1.TreeSnapshot +import com.squareup.workflow1.Workflow +import com.squareup.workflow1.WorkflowAction +import com.squareup.workflow1.WorkflowExperimentalRuntime +import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession +import com.squareup.workflow1.WorkflowNodeId +import com.squareup.workflow1.id +import com.squareup.workflow1.identifier +import kotlin.coroutines.CoroutineContext + +/** + * @see [SubtreeManager]. This is the version which adds support for the Compose optimized runtime. + */ +@WorkflowExperimentalRuntime +public class ComposeSubtreeManager( + snapshotCache: Map?, + contextForChildren: CoroutineContext, + emitActionToParent: (WorkflowAction) -> Any?, + workflowSession: WorkflowSession? = null, + override val interceptor: ComposeWorkflowInterceptor = NoopComposeWorkflowInterceptor, + idCounter: IdCounter? = null +) : SubtreeManager( + snapshotCache, + contextForChildren, + emitActionToParent, + workflowSession, + interceptor, + idCounter +), + RealComposeRenderContext.ComposeRenderer { + + @Composable + override fun Rendering( + child: Workflow, + props: ChildPropsT, + key: String, + handler: (ChildOutputT) -> WorkflowAction + ): ChildRenderingT { + val stagedChild = + StagedChild( + child, + props, + key, + handler + ) + val statefulChild = remember(child) { child.asStatefulWorkflow().asComposeWorkflow() } + return stagedChild.Rendering(statefulChild, props) + } + + /** + * Prepare the staged child while only modifying [children] in a SideEffect. This will ensure + * that we do not inappropriately modify non-snapshot state. + */ + @Composable + private fun StagedChild( + child: Workflow, + props: ChildPropsT, + key: String, + handler: (ChildOutputT) -> WorkflowAction + ): WorkflowComposeChildNode<*, *, *, *, *> { + val childState = remember(child, key, props, handler) { + children.forEachStaging { + require(!(it.matches(child, key))) { + "Expected keys to be unique for ${child.identifier}: key=\"$key\"" + } + } + mutableStateOf( + children.firstActiveOrNull { + it.matches(child, key) + } ?: createChildNode(child, props, key, handler) + ) + } + + SideEffect { + // Modify the [children] lists in a side-effect when composition is committed. + children.removeAndStage( + predicate = { it.matches(child, key) }, + child = childState.value + ) + } + return childState.value as WorkflowComposeChildNode<*, *, *, *, *> + } + + override fun createChildNode( + child: Workflow, + initialProps: ChildPropsT, + key: String, + handler: (ChildOutputT) -> WorkflowAction + ): WorkflowComposeChildNode { + val id = child.id(key) + lateinit var node: WorkflowComposeChildNode + + fun acceptChildOutput(output: ChildOutputT): Any? { + val action = node.acceptChildOutput(output) + return emitActionToParent(action) + } + + val childTreeSnapshots = snapshotCache?.get(id) + + val workflowNode = WorkflowComposeNode( + id = id, + child.asStatefulWorkflow().asComposeWorkflow(), + initialProps, + childTreeSnapshots, + contextForChildren, + ::acceptChildOutput, + workflowSession, + interceptor, + idCounter = idCounter + ).apply { + startSession() + } + return WorkflowComposeChildNode(child, handler, workflowNode) + .also { node = it } + } +} diff --git a/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/ComposeWorkerWorkflow.kt b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/ComposeWorkerWorkflow.kt new file mode 100644 index 000000000..40c036018 --- /dev/null +++ b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/ComposeWorkerWorkflow.kt @@ -0,0 +1,140 @@ +package com.squareup.workflow1.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.remember +import com.squareup.workflow1.ImpostorWorkflow +import com.squareup.workflow1.Sink +import com.squareup.workflow1.Snapshot +import com.squareup.workflow1.StatefulWorkflow +import com.squareup.workflow1.Worker +import com.squareup.workflow1.WorkflowAction +import com.squareup.workflow1.WorkflowExperimentalRuntime +import com.squareup.workflow1.WorkflowIdentifier +import com.squareup.workflow1.collectToSink +import com.squareup.workflow1.unsnapshottableIdentifier +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext +import kotlin.reflect.KType + +/** + * The [Workflow] that implements the logic for actually running [Worker]s. + * + * This workflow is an [ImpostorWorkflow] and uses the entire [KType] of the [Worker] as its + * [realIdentifier], so that the runtime can ensure that distinct worker types are allowed to run + * concurrently. Implements [Worker.doesSameWorkAs] by taking the actual worker instance as its + * props, and checking [Worker.doesSameWorkAs] in [onPropsChanged]. When this returns false, it + * means a new worker session needs to be started, and that is achieved by storing a monotonically- + * increasing integer as the state, and incrementing it whenever the worker needs to be restarted. + * + * Note that since this workflow uses an [unsnapshottableIdentifier] as its [realIdentifier], it is + * not snapshottable, but that's fine because the only state this workflow maintains is only used + * to determine whether to restart workers during the lifetime of a single runtime instance. + * + * @param workerType The [KType] representing the particular type of `Worker`. + * @param key The key used to render this workflow, as passed to + * [BaseRenderContext.runningWorker]. Used for naming the worker's coroutine. + */ +@OptIn(WorkflowExperimentalRuntime::class) +internal class ComposeWorkerWorkflow( + val workerType: KType, + private val key: String +) : StatefulComposeWorkflow, Int, OutputT, Unit>(), + ImpostorWorkflow { + + override val realIdentifier: WorkflowIdentifier = unsnapshottableIdentifier(workerType) + override fun describeRealIdentifier(): String = "worker $workerType" + + override fun initialState( + props: Worker, + snapshot: Snapshot? + ): Int = 0 + + override fun onPropsChanged( + old: Worker, + new: Worker, + state: Int + ): Int = if (!old.doesSameWorkAs(new)) state + 1 else state + + override fun render( + renderProps: Worker, + renderState: Int, + context: StatefulWorkflow, Int, OutputT, Unit>.RenderContext + ) { + // Scope the side effect coroutine to the state value, so the worker will be re-started when + // it changes (such that doesSameWorkAs returns false above). + context.runningSideEffect(renderState.toString()) { + runWorker(renderProps, key, context.actionSink) + } + } + + override fun snapshotState(state: Int): Snapshot? = null + + @Composable + override fun Rendering( + renderProps: Worker, + renderState: Int, + context: RenderContext + ) { + val stateKey = remember(renderState) { renderState.toString() } + SideEffect { + context.runningSideEffect(stateKey) { + runWorker(renderProps, key, context.actionSink) + } + } + } +} + +/** + * Does the actual running of a worker passed to [BaseRenderContext.runningWorker] by setting up the + * coroutine environment for the worker, performing some validation, etc., and finally actually + * collecting the worker's [Flow]. + * + * Visible for testing. + */ +internal suspend fun runWorker( + worker: Worker, + renderKey: String, + actionSink: Sink, Int, OutputT>> +) { + withContext(CoroutineName(worker.debugName(renderKey))) { + worker.runWithNullCheck() + .collectToSink(actionSink) { output -> + EmitWorkerOutputAction(worker, renderKey, output) + } + } +} + +private class EmitWorkerOutputAction( + private val worker: Worker<*>, + private val renderKey: String, + private val output: O +) : WorkflowAction() { + override fun toString(): String = + "${EmitWorkerOutputAction::class.qualifiedName}(worker=$worker, key=\"$renderKey\")" + + override fun Updater.apply() { + setOutput(output) + } +} + +/** + * In unit tests, if you use a mocking library to create a Worker, the run method will return null + * even though the return type is non-nullable in Kotlin. Kotlin helps out with this by throwing an + * NPE before before any kotlin code gets the null, but the NPE that it throws includes an almost + * completely useless stacktrace and no other details. + * + * This method does an explicit null check and throws an exception with a more helpful message. + * + * See [#842](https://github.com/square/workflow/issues/842). + */ +@Suppress("USELESS_ELVIS") +private fun Worker.runWithNullCheck(): Flow = + run() ?: throw NullPointerException( + "Worker $this returned a null Flow. " + + "If this is a test mock, make sure you mock the run() method!" + ) + +private fun Worker<*>.debugName(key: String) = + toString().let { if (key.isBlank()) it else "$it:$key" } diff --git a/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/ComposeWorkflowInterceptor.kt b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/ComposeWorkflowInterceptor.kt new file mode 100644 index 000000000..73cc65bb3 --- /dev/null +++ b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/ComposeWorkflowInterceptor.kt @@ -0,0 +1,196 @@ +package com.squareup.workflow1.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.squareup.workflow1.BaseRenderContext +import com.squareup.workflow1.InterceptedRenderContext +import com.squareup.workflow1.Sink +import com.squareup.workflow1.Snapshot +import com.squareup.workflow1.Workflow +import com.squareup.workflow1.WorkflowAction +import com.squareup.workflow1.WorkflowExperimentalRuntime +import com.squareup.workflow1.WorkflowInterceptor +import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor +import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession +import com.squareup.workflow1.compose.ComposeWorkflowInterceptor.ComposeRenderContextInterceptor +import com.squareup.workflow1.intercept +import kotlinx.coroutines.CoroutineScope + +/** + * Provides hooks into the workflow runtime when it is using the Compose optimizations. + * It can be used to instrument or modify the behavior of workflows. + * + * @see [WorkflowInterceptor] for full documentation. + */ +public interface ComposeWorkflowInterceptor : WorkflowInterceptor { + + @Composable + public fun Rendering( + renderProps: P, + renderState: S, + context: BaseComposeRenderContext, + session: WorkflowSession, + proceed: @Composable (P, S, ComposeRenderContextInterceptor?) -> R + ): R = proceed(renderProps, renderState, null) + + /** + * Intercepts calls to [BaseComposeRenderContext.ChildRendering], allowing the + * interceptor to wrap or replace the [child] Workflow, its [childProps], + * [key], and the [handler] function to be applied to the child's output. + * + * @see [RenderContextInterceptor] + */ + public interface ComposeRenderContextInterceptor : RenderContextInterceptor { + @Composable + public fun ChildRendering( + child: Workflow, + childProps: CP, + key: String, + handler: (CO) -> WorkflowAction, + proceed: @Composable ( + child: Workflow, + childProps: CP, + key: String, + handler: (CO) -> WorkflowAction + ) -> CR + ): CR = proceed(child, childProps, key, handler) + + @Composable + public fun RunningSideEffect( + key: String, + sideEffect: suspend () -> Unit, + proceed: @Composable (key: String, sideEffect: suspend () -> Unit) -> Unit + ) { + proceed(key, sideEffect) + } + } +} + +public fun WorkflowInterceptor.asComposeWorkflowInterceptor(): ComposeWorkflowInterceptor { + val originalInterceptor = this + if (originalInterceptor is ComposeWorkflowInterceptor) { + return originalInterceptor + } + return object : ComposeWorkflowInterceptor { + + override fun onSessionStarted( + workflowScope: CoroutineScope, + session: WorkflowSession + ) = originalInterceptor.onSessionStarted(workflowScope, session) + + override fun onInitialState( + props: P, + snapshot: Snapshot?, + proceed: (P, Snapshot?) -> S, + session: WorkflowSession + ): S = originalInterceptor.onInitialState(props, snapshot, proceed, session) + + override fun onPropsChanged( + old: P, + new: P, + state: S, + proceed: (P, P, S) -> S, + session: WorkflowSession + ): S = originalInterceptor.onPropsChanged(old, new, state, proceed, session) + + override fun onRender( + renderProps: P, + renderState: S, + context: BaseRenderContext, + proceed: (P, S, RenderContextInterceptor?) -> R, + session: WorkflowSession + ): R = originalInterceptor.onRender(renderProps, renderState, context, proceed, session) + + override fun onSnapshotState( + state: S, + proceed: (S) -> Snapshot?, + session: WorkflowSession + ): Snapshot? = originalInterceptor.onSnapshotState(state, proceed, session) + } +} + +/** A [ComposeWorkflowInterceptor] that does not intercept anything. */ +public object NoopComposeWorkflowInterceptor : ComposeWorkflowInterceptor + +/** + * Returns a [StatefulComposeWorkflow] that will intercept all calls to [workflow] via this + * [ComposeWorkflowInterceptor]. + */ +@WorkflowExperimentalRuntime +public fun ComposeWorkflowInterceptor.intercept( + workflow: StatefulComposeWorkflow, + workflowSession: WorkflowSession +): StatefulComposeWorkflow = if (this === NoopComposeWorkflowInterceptor) { + workflow +} else { + (this as WorkflowInterceptor).intercept(workflow, workflowSession).asComposeWorkflow( + RenderingImpl = { renderProps, renderState, context -> + // Cannot annotate anonymous functions with @Composable and cannot infer type of + // this when a lambda. So need this variable to make it explicit. + val reifiedProceed: @Composable (P, S, ComposeRenderContextInterceptor?) -> R = + @Composable { props: P, + state: S, + interceptor: ComposeRenderContextInterceptor? -> + val interceptedContext = interceptor?.let { InterceptedComposeRenderContext(context, it) } + ?: context + val renderContext = ComposeRenderContext(interceptedContext, this) + workflow.Rendering( + props, + state, + renderContext + ) + } + Rendering( + renderProps = renderProps, + renderState = renderState, + context = context, + session = workflowSession, + proceed = reifiedProceed + ) + } + ) +} + +public open class InterceptedComposeRenderContext( + private val baseRenderContext: BaseComposeRenderContext, + private val interceptor: ComposeRenderContextInterceptor +) : BaseComposeRenderContext, Sink>, + InterceptedRenderContext( + baseRenderContext, + interceptor + ) { + + @Composable + override fun ChildRendering( + child: Workflow, + props: ChildPropsT, + key: String, + handler: (ChildOutputT) -> WorkflowAction + ): ChildRenderingT = + interceptor.ChildRendering( + child, + props, + key, + handler + ) @Composable { iChild, iProps, iKey, iHandler -> + baseRenderContext.ChildRendering(iChild, iProps, iKey, iHandler) + } + + @Composable + override fun RunningSideEffect( + key: String, + sideEffect: suspend CoroutineScope.() -> Unit + ) { + val withScopeReceiver = remember(sideEffect) { + suspend { + CoroutineScope(activeCoroutineContext()).sideEffect() + } + } + + interceptor.RunningSideEffect(key, withScopeReceiver) { iKey, iSideEffect -> + baseRenderContext.RunningSideEffect(iKey) { + iSideEffect() + } + } + } +} diff --git a/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/RealComposeRenderContext.kt b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/RealComposeRenderContext.kt new file mode 100644 index 000000000..b855e1dea --- /dev/null +++ b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/RealComposeRenderContext.kt @@ -0,0 +1,58 @@ +package com.squareup.workflow1.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.squareup.workflow1.RealRenderContext +import com.squareup.workflow1.Workflow +import com.squareup.workflow1.WorkflowAction +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.SendChannel + +/** + * @see [RealRenderContext]. This is the version that supports Compose runtime optimizations. + */ +public class RealComposeRenderContext( + override val renderer: ComposeRenderer, + sideEffectRunner: SideEffectRunner, + eventActionsChannel: SendChannel> +) : RealRenderContext( + renderer, + sideEffectRunner, + eventActionsChannel, +), + BaseComposeRenderContext { + + public interface ComposeRenderer : Renderer { + @Composable + public fun Rendering( + child: Workflow, + props: ChildPropsT, + key: String, + handler: (ChildOutputT) -> WorkflowAction + ): ChildRenderingT + } + + @Composable + override fun ChildRendering( + child: Workflow, + props: ChildPropsT, + key: String, + handler: (ChildOutputT) -> WorkflowAction + ): ChildRenderingT { + remember(this) { + checkNotFrozen() + } + return renderer.Rendering(child, props, key, handler) + } + + @Composable + override fun RunningSideEffect( + key: String, + sideEffect: suspend CoroutineScope.() -> Unit + ) { + remember(this) { + checkNotFrozen() + } + sideEffectRunner.runningSideEffect(key, sideEffect) + } +} diff --git a/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/StatefulComposeWorkflow.kt b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/StatefulComposeWorkflow.kt new file mode 100644 index 000000000..88a519142 --- /dev/null +++ b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/StatefulComposeWorkflow.kt @@ -0,0 +1,228 @@ +package com.squareup.workflow1.compose + +import androidx.compose.runtime.Composable +import com.squareup.workflow1.BaseRenderContext +import com.squareup.workflow1.Snapshot +import com.squareup.workflow1.StatefulWorkflow +import com.squareup.workflow1.Workflow +import com.squareup.workflow1.WorkflowExperimentalRuntime + +/** + * @see [StatefulWorkflow]. This is the extension of that which supports the Compose runtime + * optimizations for the children of this Workflow - i.e. Rendering() will not be called if the + * state of children has not changed. + * + * N.B. This is easily confused with + * [com.squareup.sample.compose.hellocomposeworkflow.ComposeWorkflow] which is a sample showing a + * much more radical modification of the Workflow API to support using Compose directly for more + * than just render() optimizations. + */ +@WorkflowExperimentalRuntime +public abstract class StatefulComposeWorkflow : + StatefulWorkflow() { + + @Suppress("DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE") + public inner class RenderContext internal constructor( + baseContext: BaseComposeRenderContext + ) : StatefulWorkflow.RenderContext(baseContext), + BaseComposeRenderContext<@UnsafeVariance PropsT, StateT, @UnsafeVariance OutputT> by baseContext + + @Composable + public abstract fun Rendering( + renderProps: PropsT, + renderState: StateT, + context: RenderContext, + ): RenderingT +} + +/** + * Turn this [StatefulWorkflow] into a [StatefulComposeWorkflow] with the [RenderingImpl] function. + * + * If none is provided, it will default to calling [StatefulWorkflow.render]. + */ +@WorkflowExperimentalRuntime +public fun +StatefulWorkflow.asComposeWorkflow( + RenderingImpl: @Composable StatefulComposeWorkflow.( + PropsT, + StateT, + StatefulComposeWorkflow.RenderContext + ) -> RenderingT = { p, s, rc -> + render(p, s, rc) + } +): + StatefulComposeWorkflow { + val originalWorkflow = this + if (originalWorkflow is StatefulComposeWorkflow) { + return originalWorkflow + } + return object : StatefulComposeWorkflow() { + + @Composable + override fun Rendering( + renderProps: PropsT, + renderState: StateT, + context: RenderContext + ): RenderingT = RenderingImpl(renderProps, renderState, context) + + override fun initialState( + props: PropsT, + snapshot: Snapshot? + ): StateT = originalWorkflow.initialState(props, snapshot) + + override fun snapshotState(state: StateT): Snapshot? = originalWorkflow.snapshotState(state) + + override fun render( + renderProps: PropsT, + renderState: StateT, + context: StatefulWorkflow.RenderContext + ): RenderingT = originalWorkflow.render(renderProps, renderState, context) + } +} + +/** + * Creates a [StatefulComposeWorkflow.RenderContext] from a [BaseComposeRenderContext] for the given + * [StatefulComposeWorkflow]. + */ +@WorkflowExperimentalRuntime +@Suppress("UNCHECKED_CAST") +public fun ComposeRenderContext( + baseContext: BaseComposeRenderContext, + workflow: StatefulComposeWorkflow +): StatefulComposeWorkflow.RenderContext = + (baseContext as? StatefulComposeWorkflow.RenderContext) + ?: workflow.RenderContext(baseContext) + +/** + * Returns a Composed stateful [Workflow], defined by the given functions. + */ +@WorkflowExperimentalRuntime +public inline fun Workflow.Companion.composedStateful( + crossinline initialState: (PropsT, Snapshot?) -> StateT, + crossinline render: BaseRenderContext.( + props: PropsT, + state: StateT + ) -> RenderingT, + noinline Rendering: @Composable BaseComposeRenderContext.( + props: PropsT, + state: StateT + ) -> RenderingT = { props, state -> + render(props, state) + }, + crossinline snapshot: (StateT) -> Snapshot?, + crossinline onPropsChanged: ( + old: PropsT, + new: PropsT, + state: StateT + ) -> StateT = { _, _, state -> state } +): StatefulWorkflow = + object : StatefulComposeWorkflow() { + override fun initialState( + props: PropsT, + snapshot: Snapshot? + ): StateT = initialState(props, snapshot) + + override fun onPropsChanged( + old: PropsT, + new: PropsT, + state: StateT + ): StateT = onPropsChanged(old, new, state) + + override fun render( + renderProps: PropsT, + renderState: StateT, + context: StatefulWorkflow.RenderContext + ): RenderingT = render(context, renderProps, renderState) + + override fun snapshotState(state: StateT) = snapshot(state) + + @Composable + override fun Rendering( + renderProps: PropsT, + renderState: StateT, + context: RenderContext, + ): RenderingT = Rendering(context, renderProps, renderState) + } + +/** + * Returns a Composed stateful [Workflow], with no props, implemented via the given functions. + */ +@WorkflowExperimentalRuntime +public fun Workflow.Companion.composedStateful( + initialState: (Snapshot?) -> StateT, + render: BaseRenderContext.(state: StateT) -> RenderingT, + Rendering: @Composable BaseComposeRenderContext.( + state: StateT + ) -> RenderingT, + snapshot: (StateT) -> Snapshot? +): StatefulWorkflow { + @Suppress("LocalVariableName") + val RenderingWithProps: @Composable BaseComposeRenderContext.( + props: Unit, + state: StateT + ) -> RenderingT = @Composable { _: Unit, state: StateT -> + Rendering(state) + } + return composedStateful( + initialState = { _: Unit, initialSnapshot: Snapshot? -> initialState(initialSnapshot) }, + render = { _: Unit, state: StateT -> render(state) }, + Rendering = RenderingWithProps, + snapshot = snapshot + ) +} + +/** + * Returns a Composed stateful [Workflow] implemented via the given functions. + * + * This overload does not support snapshotting, but there are other overloads that do. + */ +@WorkflowExperimentalRuntime +public inline fun Workflow.Companion.composedStateful( + crossinline initialState: (PropsT) -> StateT, + crossinline render: BaseRenderContext.( + props: PropsT, + state: StateT + ) -> RenderingT, + noinline Rendering: @Composable BaseComposeRenderContext.( + props: PropsT, + state: StateT + ) -> RenderingT, + crossinline onPropsChanged: ( + old: PropsT, + new: PropsT, + state: StateT + ) -> StateT = { _, _, state -> state } +): StatefulWorkflow = composedStateful( + initialState = { props: PropsT, _ -> initialState(props) }, + render = render, + Rendering = Rendering, + snapshot = { null }, + onPropsChanged = onPropsChanged +) + +/** + * Returns a Composed stateful [Workflow], with no props, implemented via the given function. + * + * This overload does not support snapshots, but there are others that do. + */ +@WorkflowExperimentalRuntime +public fun Workflow.Companion.composedStateful( + initialState: StateT, + render: BaseRenderContext.(state: StateT) -> RenderingT, + Rendering: @Composable BaseComposeRenderContext.( + state: StateT + ) -> RenderingT, +): StatefulWorkflow { + @Suppress("LocalVariableName") + val RenderWithProps: @Composable BaseComposeRenderContext.( + props: Unit, + state: StateT + ) -> RenderingT = @Composable { _: Unit, state: StateT -> + Rendering(state) + } + return composedStateful( + initialState = { initialState }, + render = { _, state -> render(state) }, + Rendering = RenderWithProps, + ) +} diff --git a/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/StatelessComposeWorkflow.kt b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/StatelessComposeWorkflow.kt new file mode 100644 index 000000000..a61bd9a2e --- /dev/null +++ b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/StatelessComposeWorkflow.kt @@ -0,0 +1,138 @@ +package com.squareup.workflow1.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.squareup.workflow1.BaseRenderContext +import com.squareup.workflow1.RenderContext +import com.squareup.workflow1.StatefulWorkflow +import com.squareup.workflow1.StatelessWorkflow +import com.squareup.workflow1.Workflow +import com.squareup.workflow1.WorkflowExperimentalRuntime + +/** + * @see [StatelessWorkflow]. This is the extension of that which supports the Compose runtime + * optimizations for the children of this Workflow - i.e. Rendering() will not be called if the + * state of children has not changed. + * + * * N.B. This is easily confused with + * [com.squareup.sample.compose.hellocomposeworkflow.ComposeWorkflow] which is a sample showing a + * much more radical modification of the Workflow API to support using Compose directly for more + * than just render() optimizations. + */ +@WorkflowExperimentalRuntime +public abstract class StatelessComposeWorkflow : + StatelessWorkflow() { + + @Suppress("UNCHECKED_CAST", "DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE") + public inner class RenderContext constructor( + baseContext: BaseComposeRenderContext + ) : StatelessWorkflow.RenderContext(baseContext), + BaseComposeRenderContext<@UnsafeVariance PropsT, Nothing, @UnsafeVariance OutputT> by + baseContext as BaseComposeRenderContext + + override val statefulWorkflow: StatefulWorkflow + get() { + @Suppress("LocalVariableName") + val Rendering: @Composable BaseComposeRenderContext + .(props: PropsT, _: Unit) -> RenderingT = + @Composable { props, _ -> + val context = remember(this, this@StatelessComposeWorkflow) { + ComposeRenderContext(this, this@StatelessComposeWorkflow) + } + (this@StatelessComposeWorkflow).Rendering(props, context) + } + return Workflow.composedStateful( + initialState = { Unit }, + render = { props, _ -> + render( + props, + RenderContext( + this, + this@StatelessComposeWorkflow as StatelessWorkflow + ) + ) + }, + Rendering = Rendering + ) + } + + @Composable + public abstract fun Rendering( + renderProps: PropsT, + context: RenderContext, + ): RenderingT +} + +/** + * Turn this [StatelessWorkflow] into a [StatelessComposeWorkflow] with the [Rendering] function. + * + * If none is provided, it will default to calling [StatelessWorkflow.render]. + */ +@WorkflowExperimentalRuntime +public fun +StatelessWorkflow.asComposeWorkflow( + RenderingImpl: @Composable StatelessComposeWorkflow.( + PropsT, + StatelessWorkflow.RenderContext + ) -> RenderingT = { p, rc -> + render(p, rc) + } +): StatelessComposeWorkflow { + val originalWorkflow = this + if (originalWorkflow is StatelessComposeWorkflow) { + return originalWorkflow + } + return object : StatelessComposeWorkflow() { + + @Composable + override fun Rendering( + renderProps: PropsT, + context: RenderContext + ): RenderingT = RenderingImpl(renderProps, context) + + override fun render( + renderProps: PropsT, + context: StatelessWorkflow.RenderContext + ): RenderingT = originalWorkflow.render(renderProps, context) + } +} + +/** + * Creates a [StatelessComposeWorkflow.RenderContext] from a [BaseComposeRenderContext] + * for the given [StatelessComposeWorkflow]. + */ +@WorkflowExperimentalRuntime +@Suppress("UNCHECKED_CAST") +public fun ComposeRenderContext( + baseContext: BaseComposeRenderContext, + workflow: StatelessComposeWorkflow +): StatelessComposeWorkflow.RenderContext = + (baseContext as? StatelessComposeWorkflow.RenderContext) + ?: workflow.RenderContext(baseContext) + +/** + * Returns a stateless, composed [Workflow] via the given [render] function. + * + * Note that while the returned workflow doesn't have any _internal_ state of its own, it may use + * [props][PropsT] received from its parent, and it may render child workflows that do have + * their own internal state. + */ +@WorkflowExperimentalRuntime +public inline fun Workflow.Companion.composedStateless( + noinline Rendering: @Composable BaseComposeRenderContext.( + props: PropsT + ) -> RenderingT, + crossinline render: BaseRenderContext.(props: PropsT) -> RenderingT, +): Workflow = + object : StatelessComposeWorkflow() { + override fun render( + renderProps: PropsT, + context: StatelessWorkflow.RenderContext + ): RenderingT = render(context, renderProps) + + @Composable + override fun Rendering( + renderProps: PropsT, + context: RenderContext + ): RenderingT = Rendering(context, renderProps) + } diff --git a/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/WorkflowComposeChildNode.kt b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/WorkflowComposeChildNode.kt new file mode 100644 index 000000000..0fb2433d9 --- /dev/null +++ b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/WorkflowComposeChildNode.kt @@ -0,0 +1,62 @@ +package com.squareup.workflow1.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import com.squareup.workflow1.Workflow +import com.squareup.workflow1.WorkflowAction +import com.squareup.workflow1.WorkflowChildNode +import com.squareup.workflow1.WorkflowExperimentalRuntime +import com.squareup.workflow1.WorkflowNode + +/** + * @see [WorkflowChildNode]. This version supports piping Composable [Rendering]. + */ +@WorkflowExperimentalRuntime +public class WorkflowComposeChildNode< + ChildPropsT, + ChildOutputT, + ParentPropsT, + ParentStateT, + ParentOutputT + >( + workflow: Workflow<*, ChildOutputT, *>, + handler: (ChildOutputT) -> WorkflowAction, + workflowNode: WorkflowNode +) : WorkflowChildNode< + ChildPropsT, + ChildOutputT, + ParentPropsT, + ParentStateT, + ParentOutputT + >( + workflow, handler, workflowNode +) { + + @Composable + public fun Rendering( + workflow: StatefulComposeWorkflow<*, *, *, *>, + props: Any? + ): R { + val rendering = remember { mutableStateOf(null) } + val node = remember { + @Suppress("UNCHECKED_CAST") + (workflowNode as WorkflowComposeNode) + } + val reifiedWorkflow = remember(workflow) { + @Suppress("UNCHECKED_CAST") + workflow as StatefulComposeWorkflow + } + val reifiedProps = remember(props) { + @Suppress("UNCHECKED_CAST") + props as ChildPropsT + } + node.Rendering( + reifiedWorkflow, + reifiedProps, + rendering, + ) + @Suppress("UNCHECKED_CAST") // R can be nullable. + return rendering.value as R + } +} diff --git a/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/WorkflowComposeNode.kt b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/WorkflowComposeNode.kt new file mode 100644 index 000000000..635091a56 --- /dev/null +++ b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/WorkflowComposeNode.kt @@ -0,0 +1,153 @@ +package com.squareup.workflow1.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import com.squareup.workflow1.IdCounter +import com.squareup.workflow1.TreeSnapshot +import com.squareup.workflow1.WorkflowExperimentalRuntime +import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession +import com.squareup.workflow1.WorkflowNode +import com.squareup.workflow1.WorkflowNodeId +import com.squareup.workflow1.WorkflowOutput +import kotlin.coroutines.CoroutineContext + +/** + * @see [WorkflowNode]. This version extends that to support Compose runtime optimizations. + */ +@WorkflowExperimentalRuntime +public class WorkflowComposeNode( + id: WorkflowNodeId, + override val workflow: StatefulComposeWorkflow, + initialProps: PropsT, + snapshot: TreeSnapshot?, + baseContext: CoroutineContext, + emitOutputToParent: (OutputT) -> Any? = { WorkflowOutput(it) }, + parent: WorkflowSession? = null, + interceptor: ComposeWorkflowInterceptor = NoopComposeWorkflowInterceptor, + idCounter: IdCounter? = null +) : WorkflowNode( + id, + workflow, + initialProps, + snapshot, + baseContext, + emitOutputToParent, + parent, + interceptor, + idCounter, +) { + + /** + * Back [state] with a [MutableState] so the Compose runtime can track changes. + */ + private lateinit var backingMutableState: MutableState + + override var state: StateT + get() { + return backingMutableState.value + } + set(value) { + if (!this::backingMutableState.isInitialized) { + backingMutableState = mutableStateOf(value) + } + backingMutableState.value = value + } + + override val subtreeManager: ComposeSubtreeManager = + ComposeSubtreeManager( + snapshotCache = snapshot?.childTreeSnapshots, + contextForChildren = coroutineContext, + emitActionToParent = ::applyAction, + workflowSession = this, + interceptor = interceptor, + idCounter = idCounter + ) + + override fun startSession() { + context = RealComposeRenderContext( + renderer = subtreeManager, + sideEffectRunner = this, + eventActionsChannel = eventActionsChannel + ) + interceptor.onSessionStarted(workflowScope = this, session = this) + state = interceptor + .asComposeWorkflowInterceptor() + .intercept(workflow = workflow, workflowSession = this) + .initialState(initialProps, initialSnapshot?.workflowSnapshot) + } + + /** + * This returns Unit so that the Recomposer will consider this a separate Recompose scope that + * can be independently recomposed. + * + * We pass in the MutableState directly rather than setRendering() to save Compose + * having to memoize the lambda for such a frequent call. + */ + @Suppress("UNCHECKED_CAST") + @Composable + public fun Rendering( + workflow: StatefulComposeWorkflow, + input: PropsT, + rendering: MutableState + ) { + RenderingWithStateType( + workflow as StatefulComposeWorkflow, + input, + rendering + ) + } + + @Composable + private fun RenderingWithStateType( + workflow: StatefulComposeWorkflow, + props: PropsT, + rendering: MutableState + ) { + + val composableInterceptor = remember { + interceptor.asComposeWorkflowInterceptor() + } + + val interceptedComposableWorkflow = remember(workflow) { + composableInterceptor.intercept(workflow, this) + } + + UpdatePropsAndState(interceptedComposableWorkflow, props) + + val renderContext = remember( + state, + props, + workflow, + rendering.value + ) { + context.unfreeze() + ComposeRenderContext(workflow = workflow, baseContext = context as RealComposeRenderContext) + } + + rendering.value = interceptedComposableWorkflow.Rendering(props, state, renderContext) + + SideEffect { + // Gets called on each recomposition - may already be frozen. + context.unsafeFreeze() + commitAndUpdateScopes() + } + } + + @Composable + private fun UpdatePropsAndState( + workflow: StatefulComposeWorkflow, + newProps: PropsT + ) { + remember(newProps) { + if (newProps != lastProps) { + state = workflow.onPropsChanged(lastProps, newProps, state) + } + } + SideEffect { + lastProps = newProps + } + } +} diff --git a/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/WorkflowComposeRunner.kt b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/WorkflowComposeRunner.kt new file mode 100644 index 000000000..9a326a8dc --- /dev/null +++ b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/WorkflowComposeRunner.kt @@ -0,0 +1,66 @@ +package com.squareup.workflow1.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.squareup.workflow1.RenderingAndSnapshot +import com.squareup.workflow1.RuntimeConfig +import com.squareup.workflow1.TreeSnapshot +import com.squareup.workflow1.Workflow +import com.squareup.workflow1.WorkflowExperimentalRuntime +import com.squareup.workflow1.WorkflowRunner +import com.squareup.workflow1.id +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow + +/** + * @see [WorkflowRunner]. This version supports running with the Compose runtime optimizations. + */ +@WorkflowExperimentalRuntime +public class WorkflowComposeRunner( + scope: CoroutineScope, + protoWorkflow: Workflow, + props: StateFlow, + snapshot: TreeSnapshot?, + interceptor: ComposeWorkflowInterceptor, + runtimeConfig: RuntimeConfig +) : WorkflowRunner( + scope, + protoWorkflow, + props, + snapshot, + interceptor, + runtimeConfig, +) { + + override var currentProps: PropsT by mutableStateOf(props.value) + + override val rootNode: WorkflowComposeNode = + WorkflowComposeNode( + id = workflow.id(), + workflow = workflow.asComposeWorkflow(), + initialProps = currentProps, + snapshot = snapshot, + baseContext = scope.coroutineContext, + interceptor = interceptor, + idCounter = idCounter + ).apply { + startSession() + } + + @Composable + public fun nextComposedRendering(): RenderingAndSnapshot { + val rendering = remember { mutableStateOf(null) } + + rootNode.Rendering(workflow as StatefulComposeWorkflow, currentProps, rendering) + + val snapshot = remember(workflow) { + // need to key this on state inside WorkflowNode. Likely have a Compose version. + rootNode.snapshot(workflow) + } + + return RenderingAndSnapshot(rendering.value!!, snapshot) + } +} diff --git a/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/Workflows.kt b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/Workflows.kt new file mode 100644 index 000000000..496c2f7d1 --- /dev/null +++ b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/Workflows.kt @@ -0,0 +1,54 @@ +package com.squareup.workflow1.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.squareup.workflow1.ImpostorWorkflow +import com.squareup.workflow1.StatelessWorkflow +import com.squareup.workflow1.Workflow +import com.squareup.workflow1.WorkflowExperimentalRuntime +import com.squareup.workflow1.WorkflowIdentifier +import com.squareup.workflow1.action +import com.squareup.workflow1.identifier + +/** + * Uses the given [function][transform] to transform a [Workflow] that + * renders [FromRenderingT] to one renders [ToRenderingT], + */ +@OptIn(WorkflowExperimentalRuntime::class) +public fun +Workflow.mapRenderingComposable( + transform: (FromRenderingT) -> ToRenderingT +): Workflow = + object : StatelessComposeWorkflow(), ImpostorWorkflow { + override val realIdentifier: WorkflowIdentifier get() = this@mapRenderingComposable.identifier + + override fun render( + renderProps: PropsT, + context: StatelessWorkflow.RenderContext + ): ToRenderingT { + val rendering = context.renderChild(this@mapRenderingComposable, renderProps) { output -> + action({ "mapRendering" }) { setOutput(output) } + } + return transform(rendering) + } + + @Composable + override fun Rendering( + renderProps: PropsT, + context: RenderContext + ): ToRenderingT { + val rendering = + context.ChildRendering(this@mapRenderingComposable, renderProps, "") { output -> + action({ "mapRendering" }) { setOutput(output) } + } + val transformed = remember(rendering) { + transform(rendering) + } + return transformed + } + + override fun describeRealIdentifier(): String = + "${this@mapRenderingComposable.identifier}.mapRendering()" + + override fun toString(): String = "${this@mapRenderingComposable}.mapRendering()" + } diff --git a/workflow-core/api/workflow-core.api b/workflow-core/api/workflow-core.api index 5a896de3e..b8e0753a7 100644 --- a/workflow-core/api/workflow-core.api +++ b/workflow-core/api/workflow-core.api @@ -124,7 +124,8 @@ public abstract class com/squareup/workflow1/StatefulWorkflow : com/squareup/wor public abstract fun snapshotState (Ljava/lang/Object;)Lcom/squareup/workflow1/Snapshot; } -public final class com/squareup/workflow1/StatefulWorkflow$RenderContext : com/squareup/workflow1/BaseRenderContext { +public class com/squareup/workflow1/StatefulWorkflow$RenderContext : com/squareup/workflow1/BaseRenderContext { + public fun (Lcom/squareup/workflow1/StatefulWorkflow;Lcom/squareup/workflow1/BaseRenderContext;)V public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function10;)Lkotlin/jvm/functions/Function9; public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function11;)Lkotlin/jvm/functions/Function10; public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function0; @@ -144,10 +145,12 @@ public final class com/squareup/workflow1/StatefulWorkflow$RenderContext : com/s public abstract class com/squareup/workflow1/StatelessWorkflow : com/squareup/workflow1/Workflow { public fun ()V public final fun asStatefulWorkflow ()Lcom/squareup/workflow1/StatefulWorkflow; + protected fun getStatefulWorkflow ()Lcom/squareup/workflow1/StatefulWorkflow; public abstract fun render (Ljava/lang/Object;Lcom/squareup/workflow1/StatelessWorkflow$RenderContext;)Ljava/lang/Object; } -public final class com/squareup/workflow1/StatelessWorkflow$RenderContext : com/squareup/workflow1/BaseRenderContext { +public class com/squareup/workflow1/StatelessWorkflow$RenderContext : com/squareup/workflow1/BaseRenderContext { + public fun (Lcom/squareup/workflow1/StatelessWorkflow;Lcom/squareup/workflow1/BaseRenderContext;)V public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function10;)Lkotlin/jvm/functions/Function9; public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function11;)Lkotlin/jvm/functions/Function10; public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function0; @@ -283,6 +286,7 @@ public final class com/squareup/workflow1/Workflows { public static synthetic fun action$default (Lcom/squareup/workflow1/StatelessWorkflow;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/WorkflowAction; public static synthetic fun action$default (Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/WorkflowAction; public static final fun applyTo (Lcom/squareup/workflow1/WorkflowAction;Ljava/lang/Object;Ljava/lang/Object;)Lkotlin/Pair; + public static final fun collectToSink (Lkotlinx/coroutines/flow/Flow;Lcom/squareup/workflow1/Sink;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun contraMap (Lcom/squareup/workflow1/Sink;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/Sink; public static final fun getIdentifier (Lcom/squareup/workflow1/Workflow;)Lcom/squareup/workflow1/WorkflowIdentifier; public static final fun mapRendering (Lcom/squareup/workflow1/Workflow;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/Workflow; diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/BaseRenderContext.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/BaseRenderContext.kt index c727556de..53717117a 100644 --- a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/BaseRenderContext.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/BaseRenderContext.kt @@ -42,7 +42,7 @@ import kotlin.reflect.typeOf * * See [renderChild]. */ -public interface BaseRenderContext { +public interface BaseRenderContext { /** * Accepts a single [WorkflowAction], invokes that action by calling [WorkflowAction.apply] diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Sink.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Sink.kt index 9ad52680c..fb9bcdbab 100644 --- a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Sink.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Sink.kt @@ -56,7 +56,7 @@ public fun Sink.contraMap(transform: (T2) -> T1): Sink = Sink { * } * ``` */ -internal suspend fun Flow.collectToSink( +public suspend fun Flow.collectToSink( actionSink: Sink>, handler: (T) -> WorkflowAction ) { diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/StatefulWorkflow.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/StatefulWorkflow.kt index 031235ff4..320ad532f 100644 --- a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/StatefulWorkflow.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/StatefulWorkflow.kt @@ -71,7 +71,7 @@ public abstract class StatefulWorkflow< out RenderingT > : Workflow { - public inner class RenderContext internal constructor( + public open inner class RenderContext constructor( baseContext: BaseRenderContext ) : BaseRenderContext<@UnsafeVariance PropsT, StateT, @UnsafeVariance OutputT> by baseContext diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/StatelessWorkflow.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/StatelessWorkflow.kt index f6545524e..65ffea134 100644 --- a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/StatelessWorkflow.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/StatelessWorkflow.kt @@ -27,16 +27,17 @@ public abstract class StatelessWorkflow Workflow { @Suppress("UNCHECKED_CAST") - public inner class RenderContext internal constructor( + public open inner class RenderContext constructor( baseContext: BaseRenderContext ) : BaseRenderContext<@UnsafeVariance PropsT, Nothing, @UnsafeVariance OutputT> by baseContext as BaseRenderContext @Suppress("UNCHECKED_CAST") - private val statefulWorkflow = Workflow.stateful( - initialState = { Unit }, - render = { props, _ -> render(props, RenderContext(this, this@StatelessWorkflow)) } - ) + protected open val statefulWorkflow: StatefulWorkflow = + Workflow.stateful( + initialState = { Unit }, + render = { props, _ -> render(props, RenderContext(this, this@StatelessWorkflow)) } + ) /** * Called at least once any time one of the following things happens: diff --git a/workflow-runtime/api/workflow-runtime.api b/workflow-runtime/api/workflow-runtime.api index 36c734eba..f4513f4e5 100644 --- a/workflow-runtime/api/workflow-runtime.api +++ b/workflow-runtime/api/workflow-runtime.api @@ -1,3 +1,70 @@ +public final class com/squareup/workflow1/ActiveStagingList { + public fun ()V + public final fun commitStaging (Lkotlin/jvm/functions/Function1;)V + public final fun firstActiveOrNull (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/InlineLinkedList$InlineListNode; + public final fun forEachStaging (Lkotlin/jvm/functions/Function1;)V + public final fun getActive ()Lcom/squareup/workflow1/InlineLinkedList; + public final fun getStaging ()Lcom/squareup/workflow1/InlineLinkedList; + public final fun removeAndStage (Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/InlineLinkedList$InlineListNode;)V + public final fun setActive (Lcom/squareup/workflow1/InlineLinkedList;)V + public final fun setStaging (Lcom/squareup/workflow1/InlineLinkedList;)V +} + +public class com/squareup/workflow1/ChainedWorkflowInterceptor : com/squareup/workflow1/WorkflowInterceptor { + public fun (Ljava/util/List;)V + protected fun getInterceptors ()Ljava/util/List; + public fun onInitialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlin/jvm/functions/Function2;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; + public fun onPropsChanged (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; + public fun onRender (Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/BaseRenderContext;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; + public fun onSessionStarted (Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V + public fun onSnapshotState (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/Snapshot; + protected final fun wrap (Lcom/squareup/workflow1/WorkflowInterceptor$RenderContextInterceptor;Lcom/squareup/workflow1/WorkflowInterceptor$RenderContextInterceptor;)Lcom/squareup/workflow1/WorkflowInterceptor$RenderContextInterceptor; +} + +public final class com/squareup/workflow1/IdCounter { + public fun ()V + public final fun createId ()J +} + +public final class com/squareup/workflow1/InlineLinkedList { + public fun ()V + public final fun clear ()V + public final fun firstOrNull (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/InlineLinkedList$InlineListNode; + public final fun forEach (Lkotlin/jvm/functions/Function1;)V + public final fun getHead ()Lcom/squareup/workflow1/InlineLinkedList$InlineListNode; + public final fun getTail ()Lcom/squareup/workflow1/InlineLinkedList$InlineListNode; + public final fun plusAssign (Lcom/squareup/workflow1/InlineLinkedList$InlineListNode;)V + public final fun removeFirst (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/InlineLinkedList$InlineListNode; + public final fun setHead (Lcom/squareup/workflow1/InlineLinkedList$InlineListNode;)V + public final fun setTail (Lcom/squareup/workflow1/InlineLinkedList$InlineListNode;)V +} + +public abstract interface class com/squareup/workflow1/InlineLinkedList$InlineListNode { + public abstract fun getNextListNode ()Lcom/squareup/workflow1/InlineLinkedList$InlineListNode; + public abstract fun setNextListNode (Lcom/squareup/workflow1/InlineLinkedList$InlineListNode;)V +} + +public class com/squareup/workflow1/InterceptedRenderContext : com/squareup/workflow1/BaseRenderContext, com/squareup/workflow1/Sink { + public fun (Lcom/squareup/workflow1/BaseRenderContext;Lcom/squareup/workflow1/WorkflowInterceptor$RenderContextInterceptor;)V + protected final fun activeCoroutineContext (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function10;)Lkotlin/jvm/functions/Function9; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function11;)Lkotlin/jvm/functions/Function10; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function0; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)Lkotlin/jvm/functions/Function1; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;)Lkotlin/jvm/functions/Function2; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function4;)Lkotlin/jvm/functions/Function3; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function5;)Lkotlin/jvm/functions/Function4; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function6;)Lkotlin/jvm/functions/Function5; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function7;)Lkotlin/jvm/functions/Function6; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function8;)Lkotlin/jvm/functions/Function7; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function9;)Lkotlin/jvm/functions/Function8; + public fun getActionSink ()Lcom/squareup/workflow1/Sink; + public fun renderChild (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public fun runningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V + public fun send (Lcom/squareup/workflow1/WorkflowAction;)V + public synthetic fun send (Ljava/lang/Object;)V +} + public final class com/squareup/workflow1/NoopWorkflowInterceptor : com/squareup/workflow1/WorkflowInterceptor { public static final field INSTANCE Lcom/squareup/workflow1/NoopWorkflowInterceptor; public fun onInitialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlin/jvm/functions/Function2;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; @@ -7,9 +74,43 @@ public final class com/squareup/workflow1/NoopWorkflowInterceptor : com/squareup public fun onSnapshotState (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/Snapshot; } +public class com/squareup/workflow1/RealRenderContext : com/squareup/workflow1/BaseRenderContext, com/squareup/workflow1/Sink { + public fun (Lcom/squareup/workflow1/RealRenderContext$Renderer;Lcom/squareup/workflow1/RealRenderContext$SideEffectRunner;Lkotlinx/coroutines/channels/SendChannel;)V + protected final fun checkNotFrozen ()V + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function10;)Lkotlin/jvm/functions/Function9; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function11;)Lkotlin/jvm/functions/Function10; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function0; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)Lkotlin/jvm/functions/Function1; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;)Lkotlin/jvm/functions/Function2; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function4;)Lkotlin/jvm/functions/Function3; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function5;)Lkotlin/jvm/functions/Function4; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function6;)Lkotlin/jvm/functions/Function5; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function7;)Lkotlin/jvm/functions/Function6; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function8;)Lkotlin/jvm/functions/Function7; + public fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function9;)Lkotlin/jvm/functions/Function8; + public final fun freeze ()V + public fun getActionSink ()Lcom/squareup/workflow1/Sink; + protected fun getRenderer ()Lcom/squareup/workflow1/RealRenderContext$Renderer; + protected final fun getSideEffectRunner ()Lcom/squareup/workflow1/RealRenderContext$SideEffectRunner; + public fun renderChild (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public fun runningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V + public fun send (Lcom/squareup/workflow1/WorkflowAction;)V + public synthetic fun send (Ljava/lang/Object;)V + public final fun unfreeze ()V + public final fun unsafeFreeze ()V +} + +public abstract interface class com/squareup/workflow1/RealRenderContext$Renderer { + public abstract fun render (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; +} + +public abstract interface class com/squareup/workflow1/RealRenderContext$SideEffectRunner { + public abstract fun runningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V +} + public final class com/squareup/workflow1/RenderWorkflowKt { - public static final fun renderWorkflowIn (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Lcom/squareup/workflow1/TreeSnapshot;Ljava/util/List;Lcom/squareup/workflow1/RuntimeConfig;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow; - public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Lcom/squareup/workflow1/TreeSnapshot;Ljava/util/List;Lcom/squareup/workflow1/RuntimeConfig;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; + public static final fun renderWorkflowIn (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Lcom/squareup/workflow1/TreeSnapshot;Ljava/util/List;Lcom/squareup/workflow1/RuntimeConfig;Lcom/squareup/workflow1/WorkflowRuntimePlugin;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow; + public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Lcom/squareup/workflow1/TreeSnapshot;Ljava/util/List;Lcom/squareup/workflow1/RuntimeConfig;Lcom/squareup/workflow1/WorkflowRuntimePlugin;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; } public final class com/squareup/workflow1/RenderingAndSnapshot { @@ -20,8 +121,10 @@ public final class com/squareup/workflow1/RenderingAndSnapshot { public final fun getSnapshot ()Lcom/squareup/workflow1/TreeSnapshot; } -public abstract interface class com/squareup/workflow1/RuntimeConfig { +public abstract class com/squareup/workflow1/RuntimeConfig { public static final field Companion Lcom/squareup/workflow1/RuntimeConfig$Companion; + public final fun getUseComposeInRuntime ()Z + public final fun setUseComposeInRuntime (Z)V } public final class com/squareup/workflow1/RuntimeConfig$Companion { @@ -49,9 +152,29 @@ public class com/squareup/workflow1/SimpleLoggingWorkflowInterceptor : com/squar public fun onSnapshotState (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/Snapshot; } +public class com/squareup/workflow1/SubtreeManager : com/squareup/workflow1/RealRenderContext$Renderer { + public fun (Ljava/util/Map;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;Lcom/squareup/workflow1/WorkflowInterceptor;Lcom/squareup/workflow1/IdCounter;)V + public synthetic fun (Ljava/util/Map;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;Lcom/squareup/workflow1/WorkflowInterceptor;Lcom/squareup/workflow1/IdCounter;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun commitRenderedChildren ()V + protected fun createChildNode (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/WorkflowChildNode; + public final fun createChildSnapshots ()Ljava/util/Map; + protected final fun getChildren ()Lcom/squareup/workflow1/ActiveStagingList; + protected final fun getContextForChildren ()Lkotlin/coroutines/CoroutineContext; + protected final fun getEmitActionToParent ()Lkotlin/jvm/functions/Function1; + protected final fun getIdCounter ()Lcom/squareup/workflow1/IdCounter; + protected fun getInterceptor ()Lcom/squareup/workflow1/WorkflowInterceptor; + protected final fun getSnapshotCache ()Ljava/util/Map; + protected final fun getWorkflowSession ()Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession; + public fun render (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + protected final fun setChildren (Lcom/squareup/workflow1/ActiveStagingList;)V + protected final fun setSnapshotCache (Ljava/util/Map;)V +} + public final class com/squareup/workflow1/TreeSnapshot { public static final field Companion Lcom/squareup/workflow1/TreeSnapshot$Companion; public fun equals (Ljava/lang/Object;)Z + public final fun getChildTreeSnapshots ()Ljava/util/Map; + public final fun getWorkflowSnapshot ()Lcom/squareup/workflow1/Snapshot; public fun hashCode ()I public final fun toByteString ()Lokio/ByteString; } @@ -61,6 +184,20 @@ public final class com/squareup/workflow1/TreeSnapshot$Companion { public final fun parse (Lokio/ByteString;)Lcom/squareup/workflow1/TreeSnapshot; } +public class com/squareup/workflow1/WorkflowChildNode : com/squareup/workflow1/InlineLinkedList$InlineListNode { + public fun (Lcom/squareup/workflow1/Workflow;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowNode;)V + public final fun acceptChildOutput (Ljava/lang/Object;)Lcom/squareup/workflow1/WorkflowAction; + public final fun getId ()Lcom/squareup/workflow1/WorkflowNodeId; + public synthetic fun getNextListNode ()Lcom/squareup/workflow1/InlineLinkedList$InlineListNode; + public fun getNextListNode ()Lcom/squareup/workflow1/WorkflowChildNode; + public final fun getWorkflow ()Lcom/squareup/workflow1/Workflow; + public final fun getWorkflowNode ()Lcom/squareup/workflow1/WorkflowNode; + public final fun matches (Lcom/squareup/workflow1/Workflow;Ljava/lang/String;)Z + public final fun render (Lcom/squareup/workflow1/StatefulWorkflow;Ljava/lang/Object;)Ljava/lang/Object; + public synthetic fun setNextListNode (Lcom/squareup/workflow1/InlineLinkedList$InlineListNode;)V + public fun setNextListNode (Lcom/squareup/workflow1/WorkflowChildNode;)V +} + public abstract interface annotation class com/squareup/workflow1/WorkflowExperimentalRuntime : java/lang/annotation/Annotation { } @@ -99,3 +236,74 @@ public abstract interface class com/squareup/workflow1/WorkflowInterceptor$Workf public abstract fun getSessionId ()J } +public final class com/squareup/workflow1/WorkflowInterceptorKt { + public static final fun intercept (Lcom/squareup/workflow1/WorkflowInterceptor;Lcom/squareup/workflow1/StatefulWorkflow;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/StatefulWorkflow; +} + +public class com/squareup/workflow1/WorkflowNode : com/squareup/workflow1/RealRenderContext$SideEffectRunner, com/squareup/workflow1/WorkflowInterceptor$WorkflowSession, kotlinx/coroutines/CoroutineScope { + protected field context Lcom/squareup/workflow1/RealRenderContext; + public fun (Lcom/squareup/workflow1/WorkflowNodeId;Lcom/squareup/workflow1/StatefulWorkflow;Ljava/lang/Object;Lcom/squareup/workflow1/TreeSnapshot;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;Lcom/squareup/workflow1/WorkflowInterceptor;Lcom/squareup/workflow1/IdCounter;)V + public synthetic fun (Lcom/squareup/workflow1/WorkflowNodeId;Lcom/squareup/workflow1/StatefulWorkflow;Ljava/lang/Object;Lcom/squareup/workflow1/TreeSnapshot;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;Lcom/squareup/workflow1/WorkflowInterceptor;Lcom/squareup/workflow1/IdCounter;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + protected final fun applyAction (Lcom/squareup/workflow1/WorkflowAction;)Ljava/lang/Object; + protected final fun commitAndUpdateScopes ()V + protected final fun getContext ()Lcom/squareup/workflow1/RealRenderContext; + public fun getCoroutineContext ()Lkotlin/coroutines/CoroutineContext; + protected final fun getEmitOutputToParent ()Lkotlin/jvm/functions/Function1; + protected final fun getEventActionsChannel ()Lkotlinx/coroutines/channels/Channel; + public final fun getId ()Lcom/squareup/workflow1/WorkflowNodeId; + public fun getIdentifier ()Lcom/squareup/workflow1/WorkflowIdentifier; + protected final fun getInitialProps ()Ljava/lang/Object; + protected final fun getInitialSnapshot ()Lcom/squareup/workflow1/TreeSnapshot; + protected final fun getInterceptor ()Lcom/squareup/workflow1/WorkflowInterceptor; + protected final fun getLastProps ()Ljava/lang/Object; + public fun getParent ()Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession; + public fun getRenderKey ()Ljava/lang/String; + public fun getSessionId ()J + protected fun getState ()Ljava/lang/Object; + protected fun getSubtreeManager ()Lcom/squareup/workflow1/SubtreeManager; + protected fun getWorkflow ()Lcom/squareup/workflow1/StatefulWorkflow; + public final fun render (Lcom/squareup/workflow1/StatefulWorkflow;Ljava/lang/Object;)Ljava/lang/Object; + public fun runningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V + protected final fun setContext (Lcom/squareup/workflow1/RealRenderContext;)V + protected final fun setLastProps (Ljava/lang/Object;)V + protected fun setState (Ljava/lang/Object;)V + public final fun snapshot (Lcom/squareup/workflow1/StatefulWorkflow;)Lcom/squareup/workflow1/TreeSnapshot; + public fun startSession ()V + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/WorkflowNodeId { + public fun (Lcom/squareup/workflow1/Workflow;Ljava/lang/String;)V + public synthetic fun (Lcom/squareup/workflow1/Workflow;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lcom/squareup/workflow1/WorkflowIdentifier;Ljava/lang/String;)V + public synthetic fun (Lcom/squareup/workflow1/WorkflowIdentifier;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun copy (Lcom/squareup/workflow1/WorkflowIdentifier;Ljava/lang/String;)Lcom/squareup/workflow1/WorkflowNodeId; + public static synthetic fun copy$default (Lcom/squareup/workflow1/WorkflowNodeId;Lcom/squareup/workflow1/WorkflowIdentifier;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/workflow1/WorkflowNodeId; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/WorkflowNodeIdKt { + public static final fun id (Lcom/squareup/workflow1/Workflow;Ljava/lang/String;)Lcom/squareup/workflow1/WorkflowNodeId; + public static synthetic fun id$default (Lcom/squareup/workflow1/Workflow;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/workflow1/WorkflowNodeId; +} + +public class com/squareup/workflow1/WorkflowRunner { + public fun (Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/flow/StateFlow;Lcom/squareup/workflow1/TreeSnapshot;Lcom/squareup/workflow1/WorkflowInterceptor;Lcom/squareup/workflow1/RuntimeConfig;)V + protected fun getCurrentProps ()Ljava/lang/Object; + protected final fun getIdCounter ()Lcom/squareup/workflow1/IdCounter; + protected final fun getPropsChannel ()Lkotlinx/coroutines/channels/ReceiveChannel; + protected fun getRootNode ()Lcom/squareup/workflow1/WorkflowNode; + protected final fun getRuntimeConfig ()Lcom/squareup/workflow1/RuntimeConfig; + protected final fun getWorkflow ()Lcom/squareup/workflow1/StatefulWorkflow; + protected fun setCurrentProps (Ljava/lang/Object;)V +} + +public abstract interface class com/squareup/workflow1/WorkflowRuntimePlugin { + public abstract fun chainedInterceptors (Ljava/util/List;)Lcom/squareup/workflow1/WorkflowInterceptor; + public abstract fun createWorkflowRunner (Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/flow/StateFlow;Lcom/squareup/workflow1/TreeSnapshot;Lcom/squareup/workflow1/WorkflowInterceptor;Lcom/squareup/workflow1/RuntimeConfig;)Lcom/squareup/workflow1/WorkflowRunner; + public abstract fun initializeRenderingStream (Lcom/squareup/workflow1/WorkflowRunner;Lkotlinx/coroutines/CoroutineScope;)Lkotlinx/coroutines/flow/StateFlow; + public abstract fun nextRendering (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/ActiveStagingList.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/ActiveStagingList.kt similarity index 67% rename from workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/ActiveStagingList.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/ActiveStagingList.kt index 2bc1b97ba..a6e6ee1ed 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/ActiveStagingList.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/ActiveStagingList.kt @@ -1,6 +1,6 @@ -package com.squareup.workflow1.internal +package com.squareup.workflow1 -import com.squareup.workflow1.internal.InlineLinkedList.InlineListNode +import com.squareup.workflow1.InlineLinkedList.InlineListNode /** * Switches between two lists and provides certain lookup and swapping operations. @@ -14,7 +14,7 @@ import com.squareup.workflow1.internal.InlineLinkedList.InlineListNode * to swap the lists and clear the old active list. On commit, all items in the old active list will * be passed to the lambda passed to [commitStaging]. */ -internal class ActiveStagingList> { +public class ActiveStagingList> { /** * When not in the middle of a render pass, this list represents the active child workflows. @@ -24,7 +24,7 @@ internal class ActiveStagingList> { * During rendering, when a child is rendered, if it exists in this list it is removed from here * and added to [staging]. */ - private var active = InlineLinkedList() + public var active: InlineLinkedList = InlineLinkedList() /** * When not in the middle of a render pass, this list is empty. @@ -33,13 +33,13 @@ internal class ActiveStagingList> { * When [commitStaging] is called, this list is swapped with [active] and the old active list is * cleared. */ - private var staging = InlineLinkedList() + public var staging: InlineLinkedList = InlineLinkedList() /** * Looks for the first item matching [predicate] in the active list and moves it to the staging * list if found, else creates and appends a new item. */ - inline fun retainOrCreate( + internal inline fun retainOrCreate( predicate: (T) -> Boolean, create: () -> T ): T { @@ -48,11 +48,33 @@ internal class ActiveStagingList> { return staged } + /** + * Looks for the first item matching [predicate] in the active list and removes it from the active + * list. Then puts [child] into the staging list. + */ + public inline fun removeAndStage( + predicate: (T) -> Boolean, + child: T? + ) { + active.removeFirst(predicate) + child?.let { + staging += it + } + } + + /** + * Returns a reference to the first item matching [predicate] in the active list, or null if + * not found. + */ + public inline fun firstActiveOrNull( + predicate: (T) -> Boolean + ): T? = active.firstOrNull(predicate) + /** * Swaps the active and staging list and clears the old active list, passing items in the * old active list to [onRemove]. */ - inline fun commitStaging(onRemove: (T) -> Unit) { + public inline fun commitStaging(onRemove: (T) -> Unit) { // Any children left in the previous active list after the render finishes were not re-rendered // and must be torn down. active.forEach(onRemove) @@ -67,10 +89,10 @@ internal class ActiveStagingList> { /** * Iterates over the active list. */ - inline fun forEachActive(block: (T) -> Unit) = active.forEach(block) + internal inline fun forEachActive(block: (T) -> Unit) = active.forEach(block) /** * Iterates over the staging list. */ - inline fun forEachStaging(block: (T) -> Unit) = staging.forEach(block) + public inline fun forEachStaging(block: (T) -> Unit): Unit = staging.forEach(block) } diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptor.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/ChainedWorkflowInterceptor.kt similarity index 89% rename from workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptor.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/ChainedWorkflowInterceptor.kt index 82493b0c8..b89ac00e1 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptor.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/ChainedWorkflowInterceptor.kt @@ -1,11 +1,5 @@ -package com.squareup.workflow1.internal +package com.squareup.workflow1 -import com.squareup.workflow1.BaseRenderContext -import com.squareup.workflow1.NoopWorkflowInterceptor -import com.squareup.workflow1.Snapshot -import com.squareup.workflow1.Workflow -import com.squareup.workflow1.WorkflowAction -import com.squareup.workflow1.WorkflowInterceptor import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession import kotlinx.coroutines.CoroutineScope @@ -17,8 +11,8 @@ internal fun List.chained(): WorkflowInterceptor = else -> ChainedWorkflowInterceptor(this) } -internal class ChainedWorkflowInterceptor( - private val interceptors: List +public open class ChainedWorkflowInterceptor( + protected open val interceptors: List ) : WorkflowInterceptor { override fun onSessionStarted( @@ -94,9 +88,9 @@ internal class ChainedWorkflowInterceptor( return chainedProceed(state) } - private fun RenderContextInterceptor?.wrap( + protected fun RenderContextInterceptor?.wrap( inner: RenderContextInterceptor? - ) = when { + ): RenderContextInterceptor? = when { this == null && inner == null -> null this == null -> inner inner == null -> this diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/IdCounter.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/IdCounter.kt similarity index 74% rename from workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/IdCounter.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/IdCounter.kt index f3eb10be7..2e45b2de3 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/IdCounter.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/IdCounter.kt @@ -1,12 +1,12 @@ -package com.squareup.workflow1.internal +package com.squareup.workflow1 /** * Monotonically-increasing counter that produces longs, used to assign * [com.squareup.workflow1.WorkflowInterceptor.WorkflowSession.sessionId]. */ -internal class IdCounter { +public class IdCounter { private var nextId = 0L - fun createId(): Long = nextId++ + public fun createId(): Long = nextId++ } @Suppress("NOTHING_TO_INLINE") diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/InlineLinkedList.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/InlineLinkedList.kt similarity index 73% rename from workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/InlineLinkedList.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/InlineLinkedList.kt index 500b4d377..907594550 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/InlineLinkedList.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/InlineLinkedList.kt @@ -1,6 +1,6 @@ -package com.squareup.workflow1.internal +package com.squareup.workflow1 -import com.squareup.workflow1.internal.InlineLinkedList.InlineListNode +import com.squareup.workflow1.InlineLinkedList.InlineListNode /** * A simple singly-linked list that uses the list elements themselves to store the links. @@ -15,7 +15,7 @@ import com.squareup.workflow1.internal.InlineLinkedList.InlineListNode * - [plusAssign] * - [clear] */ -internal class InlineLinkedList> { +public class InlineLinkedList> { /** * Interface to be implemented by something that can be stored in an [InlineLinkedList]. @@ -23,19 +23,19 @@ internal class InlineLinkedList> { * @property nextListNode For use by [InlineLinkedList] only – implementors should never mutate * this property. It's default value should be null. */ - interface InlineListNode> { - var nextListNode: T? + public interface InlineListNode> { + public var nextListNode: T? } - var head: T? = null - var tail: T? = null + public var head: T? = null + public var tail: T? = null /** * Append an element to the end of the list. * * @throws IllegalArgumentException If node is already linked in another list. */ - operator fun plusAssign(node: T) { + public operator fun plusAssign(node: T) { require(node.nextListNode == null) { "Expected node to not be linked." } tail?.let { oldTail -> @@ -57,7 +57,7 @@ internal class InlineLinkedList> { * * @return The matching element, or null if not found. */ - inline fun removeFirst(predicate: (T) -> Boolean): T? { + public inline fun removeFirst(predicate: (T) -> Boolean): T? { var currentNode: T? = head var previousNode: T? = null @@ -87,7 +87,7 @@ internal class InlineLinkedList> { /** * Iterates over the list. */ - inline fun forEach(block: (T) -> Unit) { + public inline fun forEach(block: (T) -> Unit) { var currentNode = head while (currentNode != null) { block(currentNode) @@ -95,10 +95,24 @@ internal class InlineLinkedList> { } } + /** + * Returns the first item matching [predicate] in the list, or null. + */ + public inline fun firstOrNull(predicate: (T) -> Boolean): T? { + var currentNode = head + while (currentNode != null) { + if (predicate(currentNode)) { + return currentNode + } + currentNode = currentNode.nextListNode + } + return null + } + /** * Removes all elements from the list. */ - fun clear() { + public fun clear() { head = null tail = null } diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/RealRenderContext.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RealRenderContext.kt similarity index 75% rename from workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/RealRenderContext.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RealRenderContext.kt index 5fd78266b..23fbe9ff5 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/RealRenderContext.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RealRenderContext.kt @@ -1,22 +1,18 @@ @file:Suppress("DEPRECATION") -package com.squareup.workflow1.internal +package com.squareup.workflow1 -import com.squareup.workflow1.BaseRenderContext -import com.squareup.workflow1.Sink -import com.squareup.workflow1.Workflow -import com.squareup.workflow1.WorkflowAction import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.SendChannel -internal class RealRenderContext( - private val renderer: Renderer, - private val sideEffectRunner: SideEffectRunner, +public open class RealRenderContext( + protected open val renderer: Renderer, + protected val sideEffectRunner: SideEffectRunner, private val eventActionsChannel: SendChannel> ) : BaseRenderContext, Sink> { - interface Renderer { - fun render( + public interface Renderer { + public fun render( child: Workflow, props: ChildPropsT, key: String, @@ -24,8 +20,8 @@ internal class RealRenderContext( ): ChildRenderingT } - interface SideEffectRunner { - fun runningSideEffect( + public interface SideEffectRunner { + public fun runningSideEffect( key: String, sideEffect: suspend CoroutineScope.() -> Unit ) @@ -57,7 +53,7 @@ internal class RealRenderContext( key: String, handler: (ChildOutputT) -> WorkflowAction ): ChildRenderingT { - checkNotFrozen() + // checkNotFrozen() return renderer.render(child, props, key, handler) } @@ -65,26 +61,30 @@ internal class RealRenderContext( key: String, sideEffect: suspend CoroutineScope.() -> Unit ) { - checkNotFrozen() + // checkNotFrozen() sideEffectRunner.runningSideEffect(key, sideEffect) } /** * Freezes this context so that any further calls to this context will throw. */ - fun freeze() { + public fun freeze() { checkNotFrozen() frozen = true } + public fun unsafeFreeze() { + frozen = true + } + /** * Unfreezes when the node is about to render() again. */ - fun unfreeze() { + public fun unfreeze() { frozen = false } - private fun checkNotFrozen() = check(!frozen) { + protected fun checkNotFrozen(): Unit = check(!frozen) { "RenderContext cannot be used after render method returns." } } diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt index ddd390f0c..7b1cc6168 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt @@ -1,8 +1,6 @@ package com.squareup.workflow1 import com.squareup.workflow1.RuntimeConfig.ConflateStaleRenderings -import com.squareup.workflow1.internal.WorkflowRunner -import com.squareup.workflow1.internal.chained import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -98,6 +96,9 @@ import kotlinx.coroutines.launch * @param runtimeConfig * Configuration parameters for the Workflow Runtime. * + * @param workflowRuntimePlugin + * This is used to plug in Runtime functionality that lives in other modules. + * * @return * A [StateFlow] of [RenderingAndSnapshot]s that will emit any time the root workflow creates a new * rendering. @@ -110,29 +111,62 @@ public fun renderWorkflowIn( initialSnapshot: TreeSnapshot? = null, interceptors: List = emptyList(), runtimeConfig: RuntimeConfig = RuntimeConfig.DEFAULT_CONFIG, + workflowRuntimePlugin: WorkflowRuntimePlugin? = null, onOutput: suspend (OutputT) -> Unit ): StateFlow> { - val chainedInterceptor = interceptors.chained() + val chainedInterceptor = workflowRuntimePlugin?.chainedInterceptors(interceptors) + ?: interceptors.chained() + + val runner = workflowRuntimePlugin?.createWorkflowRunner( + scope, workflow, props, initialSnapshot, chainedInterceptor, runtimeConfig + ) + ?: WorkflowRunner(scope, workflow, props, initialSnapshot, chainedInterceptor, runtimeConfig) - val runner = - WorkflowRunner(scope, workflow, props, initialSnapshot, chainedInterceptor, runtimeConfig) + fun firstRenderingFlow(): StateFlow> = + MutableStateFlow( + try { + runner.nextRendering() + } catch (e: Throwable) { + // If any part of the workflow runtime fails, the scope should be cancelled. We're not in a + // coroutine yet however, so if the first render pass fails it won't cancel the runtime, + // but this is an implementation detail so we must cancel the scope manually to keep the + // contract. + val cancellation = + (e as? CancellationException) ?: CancellationException("Workflow runtime failed", e) + runner.cancelRuntime(cancellation) + throw e + } + ) + val useComposeInRuntime = workflowRuntimePlugin != null && runtimeConfig.useComposeInRuntime // Rendering is synchronous, so we can run the first render pass before launching the runtime // coroutine to calculate the initial rendering. - val renderingsAndSnapshots = MutableStateFlow( + val renderingsAndSnapshots = if (useComposeInRuntime) { + require(workflowRuntimePlugin != null) { + "Cannot use compose without plugging in" + + " the workflow-compose-core module." + } try { - runner.nextRendering() - } catch (e: Throwable) { - // If any part of the workflow runtime fails, the scope should be cancelled. We're not in a - // coroutine yet however, so if the first render pass fails it won't cancel the runtime, - // but this is an implementation detail so we must cancel the scope manually to keep the - // contract. + workflowRuntimePlugin.initializeRenderingStream( + runner, + runtimeScope = scope + ) + } + // catch (npe: NullPointerException) { + // // See https://android-review.googlesource.com/c/platform/frameworks/support/+/2267995 where + // // canceled/completed scope crashes Compose + // useComposeInRuntime = false + // firstRenderingFlow() + // } + catch (e: Throwable) { val cancellation = (e as? CancellationException) ?: CancellationException("Workflow runtime failed", e) runner.cancelRuntime(cancellation) throw e } - ) + } else { + firstRenderingFlow() + } suspend fun sendOutput( actionResult: ActionProcessingResult?, @@ -151,7 +185,6 @@ public fun renderWorkflowIn( scope.launch { while (isActive) { - lateinit var nextRenderAndSnapshot: RenderingAndSnapshot // It might look weird to start by processing an action before getting the rendering below, // but remember the first render pass already occurred above, before this coroutine was even // launched. @@ -161,7 +194,11 @@ public fun renderWorkflowIn( // we don't surprise anyone with an unexpected rendering pass. Show's over, go home. if (!isActive) return@launch - nextRenderAndSnapshot = runner.nextRendering() + var nextRenderAndSnapshot: RenderingAndSnapshot? = if (!useComposeInRuntime) { + runner.nextRendering() + } else { + null + } if (runtimeConfig == ConflateStaleRenderings) { // Only null will allow us to continue processing actions and conflating stale renderings. @@ -180,8 +217,17 @@ public fun renderWorkflowIn( } } - // Pass on to the UI. - renderingsAndSnapshots.value = nextRenderAndSnapshot + if (useComposeInRuntime) { + // TODO (https://github.com/square/workflow-kotlin/issues/835): Figure out how to handle + // the case where the state changes on the first action as this is broken now. + // This will wait until the rendering is placed into the stateflow by molecule after it is + // composed. + workflowRuntimePlugin?.nextRendering() + } else { + // Pass on to the UI. + (renderingsAndSnapshots as MutableStateFlow).value = nextRenderAndSnapshot!! + } + // And emit the Output. sendOutput(actionResult, onOutput) } diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt index 6d662fb7e..67f50e77f 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt @@ -18,18 +18,21 @@ public annotation class WorkflowExperimentalRuntime /** * A specification of the Workflow Runtime. */ -public sealed interface RuntimeConfig { +public sealed class RuntimeConfig { + @WorkflowExperimentalRuntime + public var useComposeInRuntime: Boolean = true + /** * This is the baseline runtime which will process one action at a time, calling render() after * each one. */ - public object RenderPerAction : RuntimeConfig + public object RenderPerAction : RuntimeConfig() /** * If we have more actions to process, do so before passing the rendering to the UI layer. */ @WorkflowExperimentalRuntime - public object ConflateStaleRenderings : RuntimeConfig + public object ConflateStaleRenderings : RuntimeConfig() public companion object { public val DEFAULT_CONFIG: RuntimeConfig = RenderPerAction diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/SubtreeManager.kt similarity index 81% rename from workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/SubtreeManager.kt index 38504d33f..90a951e33 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/SubtreeManager.kt @@ -1,13 +1,7 @@ -package com.squareup.workflow1.internal +package com.squareup.workflow1 -import com.squareup.workflow1.ActionProcessingResult -import com.squareup.workflow1.NoopWorkflowInterceptor -import com.squareup.workflow1.TreeSnapshot -import com.squareup.workflow1.Workflow -import com.squareup.workflow1.WorkflowAction -import com.squareup.workflow1.WorkflowInterceptor +import com.squareup.workflow1.RealRenderContext.Renderer import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession -import com.squareup.workflow1.identifier import kotlinx.coroutines.selects.SelectBuilder import kotlin.coroutines.CoroutineContext @@ -81,15 +75,15 @@ import kotlin.coroutines.CoroutineContext * snapshots are extracted into this cache. Then, when those children are started for the * first time, they are also restored from their snapshots. */ -internal class SubtreeManager( - private var snapshotCache: Map?, - private val contextForChildren: CoroutineContext, - private val emitActionToParent: (WorkflowAction) -> Any?, - private val workflowSession: WorkflowSession? = null, - private val interceptor: WorkflowInterceptor = NoopWorkflowInterceptor, - private val idCounter: IdCounter? = null -) : RealRenderContext.Renderer { - private var children = ActiveStagingList>() +public open class SubtreeManager( + protected var snapshotCache: Map?, + protected val contextForChildren: CoroutineContext, + protected val emitActionToParent: (WorkflowAction) -> Any?, + protected val workflowSession: WorkflowSession? = null, + protected open val interceptor: WorkflowInterceptor = NoopWorkflowInterceptor, + protected val idCounter: IdCounter? = null +) : Renderer { + protected var children: ActiveStagingList> = ActiveStagingList() /** * Moves all the nodes that have been accumulated in the staging list to the active list, making @@ -97,7 +91,7 @@ internal class SubtreeManager( * * This should be called after this node's render method returns. */ - fun commitRenderedChildren() { + public fun commitRenderedChildren() { // Any children left in the previous active list after the render finishes were not re-rendered // and must be torn down. children.commitStaging { child -> @@ -114,6 +108,21 @@ internal class SubtreeManager( key: String, handler: (ChildOutputT) -> WorkflowAction ): ChildRenderingT { + val stagedChild = prepareStagedChild( + child, + props, + key, + handler + ) + return stagedChild.render(child.asStatefulWorkflow(), props) + } + + private fun prepareStagedChild( + child: Workflow, + props: ChildPropsT, + key: String, + handler: (ChildOutputT) -> WorkflowAction + ): WorkflowChildNode<*, *, *, *, *> { // Prevent duplicate workflows with the same key. children.forEachStaging { require(!(it.matches(child, key))) { @@ -127,7 +136,7 @@ internal class SubtreeManager( create = { createChildNode(child, props, key, handler) } ) stagedChild.setHandler(handler) - return stagedChild.render(child.asStatefulWorkflow(), props) + return stagedChild } /** @@ -136,7 +145,7 @@ internal class SubtreeManager( * * @return [Boolean] whether or not the children action queues are empty. */ - fun tickChildren(selector: SelectBuilder): Boolean { + internal fun tickChildren(selector: SelectBuilder): Boolean { var empty = true children.forEachActive { child -> // Do this separately so the compiler doesn't avoid it if empty is already false. @@ -146,7 +155,7 @@ internal class SubtreeManager( return empty } - fun createChildSnapshots(): Map { + public fun createChildSnapshots(): Map { val snapshots = mutableMapOf() children.forEachActive { child -> val childWorkflow = child.workflow.asStatefulWorkflow() @@ -155,7 +164,7 @@ internal class SubtreeManager( return snapshots } - private fun createChildNode( + protected open fun createChildNode( child: Workflow, initialProps: ChildPropsT, key: String, @@ -181,7 +190,9 @@ internal class SubtreeManager( workflowSession, interceptor, idCounter = idCounter - ) + ).apply { + startSession() + } return WorkflowChildNode(child, handler, workflowNode) .also { node = it } } diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/TreeSnapshot.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/TreeSnapshot.kt index 0ee986a1a..9700c2ed5 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/TreeSnapshot.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/TreeSnapshot.kt @@ -2,7 +2,6 @@ package com.squareup.workflow1 import com.squareup.workflow1.TreeSnapshot.Companion.forRootOnly import com.squareup.workflow1.TreeSnapshot.Companion.parse -import com.squareup.workflow1.internal.WorkflowNodeId import okio.Buffer import okio.ByteString import kotlin.LazyThreadSafetyMode.NONE @@ -27,7 +26,7 @@ public class TreeSnapshot internal constructor( * The [Snapshot] for the root workflow, or null if that snapshot was empty or unspecified. * Computed lazily to avoid serializing the snapshot until necessary. */ - internal val workflowSnapshot: Snapshot? by lazy(NONE) { + public val workflowSnapshot: Snapshot? by lazy(NONE) { workflowSnapshot?.takeUnless { it.bytes.size == 0 } } @@ -35,7 +34,7 @@ public class TreeSnapshot internal constructor( * The map of child snapshots by child [WorkflowNodeId]. Computed lazily so the entire snapshot * tree isn't parsed upfront. */ - internal val childTreeSnapshots: Map + public val childTreeSnapshots: Map by lazy(NONE, childTreeSnapshots) /** diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowChildNode.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowChildNode.kt similarity index 71% rename from workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowChildNode.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowChildNode.kt index b6a833a95..b093267a1 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowChildNode.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowChildNode.kt @@ -1,9 +1,6 @@ -package com.squareup.workflow1.internal +package com.squareup.workflow1 -import com.squareup.workflow1.StatefulWorkflow -import com.squareup.workflow1.Workflow -import com.squareup.workflow1.WorkflowAction -import com.squareup.workflow1.internal.InlineLinkedList.InlineListNode +import com.squareup.workflow1.InlineLinkedList.InlineListNode /** * Representation of a child workflow that has been rendered by another workflow. @@ -11,26 +8,26 @@ import com.squareup.workflow1.internal.InlineLinkedList.InlineListNode * Associates the child's [WorkflowNode] (which includes the key passed to `renderChild`) with the * output handler function that was passed to `renderChild`. */ -internal class WorkflowChildNode< +public open class WorkflowChildNode< ChildPropsT, ChildOutputT, ParentPropsT, ParentStateT, ParentOutputT >( - val workflow: Workflow<*, ChildOutputT, *>, + public val workflow: Workflow<*, ChildOutputT, *>, private var handler: (ChildOutputT) -> WorkflowAction, - val workflowNode: WorkflowNode + public val workflowNode: WorkflowNode ) : InlineListNode> { override var nextListNode: WorkflowChildNode<*, *, *, *, *>? = null /** The [WorkflowNode]'s [WorkflowNodeId]. */ - val id get() = workflowNode.id + public val id: WorkflowNodeId get() = workflowNode.id /** * Returns true if this child has the same type as [otherWorkflow] and key as [key]. */ - fun matches( + public fun matches( otherWorkflow: Workflow<*, *, *>, key: String ): Boolean = id.matches(otherWorkflow, key) @@ -38,7 +35,7 @@ internal class WorkflowChildNode< /** * Updates the handler function that will be invoked by [acceptChildOutput]. */ - fun setHandler(newHandler: (CO) -> WorkflowAction) { + internal fun setHandler(newHandler: (CO) -> WorkflowAction) { @Suppress("UNCHECKED_CAST") handler = newHandler as (ChildOutputT) -> WorkflowAction @@ -47,7 +44,7 @@ internal class WorkflowChildNode< /** * Wrapper around [WorkflowNode.render] that allows calling it with erased types. */ - fun render( + public fun render( workflow: StatefulWorkflow<*, *, *, *>, props: Any? ): R { @@ -62,6 +59,7 @@ internal class WorkflowChildNode< * Wrapper around [handler] that allows calling it with erased types. */ @Suppress("UNCHECKED_CAST") - fun acceptChildOutput(output: Any?): WorkflowAction = + public fun acceptChildOutput(output: Any?): + WorkflowAction = handler(output as ChildOutputT) } diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt index 6cc3c808e..6f405f8c0 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt @@ -236,7 +236,7 @@ public object NoopWorkflowInterceptor : WorkflowInterceptor * Returns a [StatefulWorkflow] that will intercept all calls to [workflow] via this * [WorkflowInterceptor]. */ -internal fun WorkflowInterceptor.intercept( +public fun WorkflowInterceptor.intercept( workflow: StatefulWorkflow, workflowSession: WorkflowSession ): StatefulWorkflow = if (this === NoopWorkflowInterceptor) { @@ -277,7 +277,7 @@ internal fun WorkflowInterceptor.intercept( } } -private class InterceptedRenderContext( +public open class InterceptedRenderContext( private val baseRenderContext: BaseRenderContext, private val interceptor: RenderContextInterceptor ) : BaseRenderContext, Sink> { @@ -322,7 +322,7 @@ private class InterceptedRenderContext( * to `CoroutineScope.coroutineContext` instead of `suspend val coroutineContext`. * Call this and always get the latter. */ - private suspend inline fun activeCoroutineContext(): CoroutineContext { + protected suspend inline fun activeCoroutineContext(): CoroutineContext { return coroutineContext } } diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowNode.kt similarity index 72% rename from workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowNode.kt index 525e1ccd6..60a155a1a 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowNode.kt @@ -1,19 +1,8 @@ -package com.squareup.workflow1.internal - -import com.squareup.workflow1.ActionProcessingResult -import com.squareup.workflow1.NoopWorkflowInterceptor -import com.squareup.workflow1.RenderContext -import com.squareup.workflow1.StatefulWorkflow -import com.squareup.workflow1.TreeSnapshot -import com.squareup.workflow1.Workflow -import com.squareup.workflow1.WorkflowAction -import com.squareup.workflow1.WorkflowIdentifier -import com.squareup.workflow1.WorkflowInterceptor +package com.squareup.workflow1 + +import com.squareup.workflow1.RealRenderContext.SideEffectRunner import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession -import com.squareup.workflow1.WorkflowOutput -import com.squareup.workflow1.applyTo -import com.squareup.workflow1.intercept -import com.squareup.workflow1.internal.RealRenderContext.SideEffectRunner +import com.squareup.workflow1.internal.SideEffectNode import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope @@ -38,15 +27,15 @@ import kotlin.coroutines.CoroutineContext * hard-coded values added to worker contexts. It must not contain a [Job] element (it would violate * structured concurrency). */ -internal class WorkflowNode( - val id: WorkflowNodeId, - workflow: StatefulWorkflow, - initialProps: PropsT, - snapshot: TreeSnapshot?, +public open class WorkflowNode( + public val id: WorkflowNodeId, + protected open val workflow: StatefulWorkflow, + protected val initialProps: PropsT, + protected val initialSnapshot: TreeSnapshot?, baseContext: CoroutineContext, - private val emitOutputToParent: (OutputT) -> Any? = { WorkflowOutput(it) }, + protected val emitOutputToParent: (OutputT) -> Any? = { WorkflowOutput(it) }, override val parent: WorkflowSession? = null, - private val interceptor: WorkflowInterceptor = NoopWorkflowInterceptor, + protected val interceptor: WorkflowInterceptor = NoopWorkflowInterceptor, idCounter: IdCounter? = null ) : CoroutineScope, SideEffectRunner, WorkflowSession { @@ -54,38 +43,57 @@ internal class WorkflowNode( * Context that has a job that will live as long as this node. * Also adds a debug name to this coroutine based on its ID. */ - override val coroutineContext = baseContext + Job(baseContext[Job]) + CoroutineName(id.toString()) + override val coroutineContext: CoroutineContext = + baseContext + Job(baseContext[Job]) + CoroutineName(id.toString()) // WorkflowInstance properties override val identifier: WorkflowIdentifier get() = id.identifier override val renderKey: String get() = id.name override val sessionId: Long = idCounter.createId() - private val subtreeManager = SubtreeManager( - snapshotCache = snapshot?.childTreeSnapshots, - contextForChildren = coroutineContext, - emitActionToParent = ::applyAction, - workflowSession = this, - interceptor = interceptor, - idCounter = idCounter - ) - private val sideEffects = ActiveStagingList() - private var lastProps: PropsT = initialProps - private val eventActionsChannel = - Channel>(capacity = UNLIMITED) - private var state: StateT - - private val context = RealRenderContext( - renderer = subtreeManager, - sideEffectRunner = this, - eventActionsChannel = eventActionsChannel - ) - - init { - interceptor.onSessionStarted(this, this) - - state = interceptor.intercept(workflow, this) - .initialState(initialProps, snapshot?.workflowSnapshot) + protected open val subtreeManager: SubtreeManager by lazy { + SubtreeManager( + snapshotCache = initialSnapshot?.childTreeSnapshots, + contextForChildren = coroutineContext, + emitActionToParent = ::applyAction, + workflowSession = this, + interceptor = interceptor, + idCounter = idCounter + ) + } + + private val sideEffects: ActiveStagingList = ActiveStagingList() + protected var lastProps: PropsT = initialProps + protected val eventActionsChannel: Channel> = + Channel(capacity = UNLIMITED) + + protected lateinit var context: RealRenderContext + + private var backingState: StateT? = null + + protected open var state: StateT + get() { + requireNotNull(backingState) + return backingState!! + } + set(value) { + backingState = value + } + + /** + * Initialize the session to handle polymorphic class creation. + * + * TODO: Handle this better as this is a very dangerous implicit API connection. + */ + public open fun startSession() { + context = RealRenderContext( + renderer = subtreeManager, + sideEffectRunner = this, + eventActionsChannel = eventActionsChannel + ) + interceptor.onSessionStarted(workflowScope = this, session = this) + state = interceptor.intercept(workflow = workflow, workflowSession = this) + .initialState(initialProps, initialSnapshot?.workflowSnapshot) } override fun toString(): String { @@ -104,7 +112,7 @@ internal class WorkflowNode( * render themselves and aggregate those child renderings. */ @Suppress("UNCHECKED_CAST") - fun render( + public fun render( workflow: StatefulWorkflow, input: PropsT ): RenderingT = @@ -114,7 +122,8 @@ internal class WorkflowNode( * Walk the tree of state machines again, this time gathering snapshots and aggregating them * automatically. */ - fun snapshot(workflow: StatefulWorkflow<*, *, *, *>): TreeSnapshot { + public fun snapshot(workflow: StatefulWorkflow<*, *, *, *>): TreeSnapshot { + // TODO: Figure out how to use `rememberSaveable` for Compose runtime here. @Suppress("UNCHECKED_CAST") val typedWorkflow = workflow as StatefulWorkflow val childSnapshots = subtreeManager.createChildSnapshots() @@ -156,7 +165,7 @@ internal class WorkflowNode( * time of suspending. */ @OptIn(ExperimentalCoroutinesApi::class) - fun tick(selector: SelectBuilder): Boolean { + internal fun tick(selector: SelectBuilder): Boolean { // Listen for any child workflow updates. var empty = subtreeManager.tickChildren(selector) @@ -177,7 +186,7 @@ internal class WorkflowNode( * This must be called when the caller will no longer call [tick]. It is an error to call [tick] * after calling this method. */ - fun cancel(cause: CancellationException? = null) { + internal fun cancel(cause: CancellationException? = null) { // No other cleanup work should be done in this function, since it will only be invoked when // this workflow is *directly* discarded by its parent (or the host). // If you need to do something whenever this workflow is torn down, add it to the @@ -200,14 +209,18 @@ internal class WorkflowNode( .render(props, state, RenderContext(context, workflow)) context.freeze() + commitAndUpdateScopes() + + return rendering + } + + protected fun commitAndUpdateScopes() { // Tear down workflows and workers that are obsolete. subtreeManager.commitRenderedChildren() // Side effect jobs are launched lazily, since they can send actions to the sink, and can only // be started after context is frozen. sideEffects.forEachStaging { it.job.start() } sideEffects.commitStaging { it.job.cancel() } - - return rendering } private fun updatePropsAndState( @@ -226,7 +239,7 @@ internal class WorkflowNode( * Applies [action] to this workflow's [state] and * [emits an output to its parent][emitOutputToParent] if necessary. */ - private fun applyAction(action: WorkflowAction): T? { + protected fun applyAction(action: WorkflowAction): T? { val (newState, tickResult) = action.applyTo(lastProps, state) state = newState @Suppress("UNCHECKED_CAST") diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNodeId.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowNodeId.kt similarity index 72% rename from workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNodeId.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowNodeId.kt index e8b765c2d..e538c3f28 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNodeId.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowNodeId.kt @@ -1,12 +1,5 @@ -package com.squareup.workflow1.internal +package com.squareup.workflow1 -import com.squareup.workflow1.Workflow -import com.squareup.workflow1.WorkflowIdentifier -import com.squareup.workflow1.identifier -import com.squareup.workflow1.readByteStringWithLength -import com.squareup.workflow1.readUtf8WithLength -import com.squareup.workflow1.writeByteStringWithLength -import com.squareup.workflow1.writeUtf8WithLength import okio.Buffer import okio.ByteString @@ -14,16 +7,16 @@ import okio.ByteString * Value type that can be used to distinguish between different workflows of different types or * the same type (in that case using a [name]). */ -internal data class WorkflowNodeId( +public data class WorkflowNodeId( internal val identifier: WorkflowIdentifier, internal val name: String = "" ) { - constructor( + public constructor( workflow: Workflow<*, *, *>, name: String = "" ) : this(workflow.identifier, name) - fun matches( + internal fun matches( otherWorkflow: Workflow<*, *, *>, otherName: String ): Boolean = identifier == otherWorkflow.identifier && name == otherName @@ -50,5 +43,5 @@ internal data class WorkflowNodeId( } } -internal fun , I, O, R> +public fun , I, O, R> W.id(key: String = ""): WorkflowNodeId = WorkflowNodeId(this, key) diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowRunner.kt similarity index 74% rename from workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowRunner.kt index f242662bf..b5af6f2b9 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowRunner.kt @@ -1,19 +1,11 @@ -package com.squareup.workflow1.internal +package com.squareup.workflow1 -import com.squareup.workflow1.ActionProcessingResult -import com.squareup.workflow1.ActionsExhausted -import com.squareup.workflow1.PropsUpdated -import com.squareup.workflow1.RenderingAndSnapshot -import com.squareup.workflow1.RuntimeConfig import com.squareup.workflow1.RuntimeConfig.ConflateStaleRenderings -import com.squareup.workflow1.TreeSnapshot -import com.squareup.workflow1.Workflow -import com.squareup.workflow1.WorkflowExperimentalRuntime -import com.squareup.workflow1.WorkflowInterceptor import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.dropWhile import kotlinx.coroutines.flow.produceIn @@ -21,17 +13,19 @@ import kotlinx.coroutines.selects.SelectBuilder import kotlinx.coroutines.selects.select @OptIn(ExperimentalCoroutinesApi::class) -internal class WorkflowRunner( +public open class WorkflowRunner( scope: CoroutineScope, protoWorkflow: Workflow, props: StateFlow, snapshot: TreeSnapshot?, interceptor: WorkflowInterceptor, - private val runtimeConfig: RuntimeConfig + protected val runtimeConfig: RuntimeConfig ) { - private val workflow = protoWorkflow.asStatefulWorkflow() - private val idCounter = IdCounter() - private var currentProps: PropsT = props.value + protected val workflow: StatefulWorkflow = + protoWorkflow.asStatefulWorkflow() + protected val idCounter: IdCounter = IdCounter() + private val firstPropsValue: PropsT = props.value + protected open var currentProps: PropsT = props.value // Props is a StateFlow, it will immediately produce an item. Without additional handling, the // first call to processActions will see that new props value and trigger another render pass, @@ -45,18 +39,25 @@ internal class WorkflowRunner( // which can't happen until the dropWhile predicate evaluates to false, after which the dropWhile // predicate will never be invoked again, so it's fine to read the mutable value here. @OptIn(FlowPreview::class) - private val propsChannel = props.dropWhile { it == currentProps } - .produceIn(scope) + protected val propsChannel: ReceiveChannel by lazy { + props.dropWhile { it == firstPropsValue } + .produceIn(scope) + } - private val rootNode = WorkflowNode( - id = workflow.id(), - workflow = workflow, - initialProps = currentProps, - snapshot = snapshot, - baseContext = scope.coroutineContext, - interceptor = interceptor, - idCounter = idCounter - ) + // Lazy because child class could override currentProps which is an input here. + protected open val rootNode: WorkflowNode by lazy { + WorkflowNode( + id = workflow.id(), + workflow = workflow, + initialProps = currentProps, + initialSnapshot = snapshot, + baseContext = scope.coroutineContext, + interceptor = interceptor, + idCounter = idCounter + ).apply { + startSession() + } + } /** * Perform a render pass and a snapshot pass and return the results. @@ -64,7 +65,7 @@ internal class WorkflowRunner( * This method must be called before the first call to [processAction], and must be called again * between every subsequent call to [processAction]. */ - fun nextRendering(): RenderingAndSnapshot { + internal fun nextRendering(): RenderingAndSnapshot { val rendering = rootNode.render(workflow, currentProps) val snapshot = rootNode.snapshot(workflow) return RenderingAndSnapshot(rendering, snapshot) @@ -78,7 +79,7 @@ internal class WorkflowRunner( * coroutine and no others. */ @OptIn(WorkflowExperimentalRuntime::class) - suspend fun processAction(waitForAnAction: Boolean = true): ActionProcessingResult? { + internal suspend fun processAction(waitForAnAction: Boolean = true): ActionProcessingResult? { // If waitForAction is true we block and wait until there is an action to process. return select { onPropsUpdated() @@ -111,7 +112,7 @@ internal class WorkflowRunner( } } - fun cancelRuntime(cause: CancellationException? = null) { + internal fun cancelRuntime(cause: CancellationException? = null) { rootNode.cancel(cause) } } diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowRuntimePlugin.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowRuntimePlugin.kt new file mode 100644 index 000000000..5a0886390 --- /dev/null +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowRuntimePlugin.kt @@ -0,0 +1,41 @@ +package com.squareup.workflow1 + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow + +/** + * A plugin mechanism to provide a way to define runtime optimization behaviour that requires + * Compiler optimizations and extensive dependencies (such as Compose) in a separate module. + */ +public interface WorkflowRuntimePlugin { + + /** + * Initialize the stream of [RenderingAndSnapshot] that the UI layer will receive. + */ + public fun initializeRenderingStream( + workflowRunner: WorkflowRunner, + runtimeScope: CoroutineScope + ): StateFlow> + + /** + * Create a [WorkflowRunner] to drive the root [WorkflowNode]. + */ + public fun createWorkflowRunner( + scope: CoroutineScope, + protoWorkflow: Workflow, + props: StateFlow, + snapshot: TreeSnapshot?, + interceptor: WorkflowInterceptor, + runtimeConfig: RuntimeConfig + ): WorkflowRunner + + /** + * Trigger the next rendering in the runtime. + */ + public suspend fun nextRendering() + + /** + * Create a chain of interceptors for all that are passed in to [renderWorkflowIn] + */ + public fun chainedInterceptors(interceptors: List): WorkflowInterceptor +} diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SideEffectNode.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SideEffectNode.kt index 0370ebb7f..8ea641400 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SideEffectNode.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SideEffectNode.kt @@ -1,6 +1,6 @@ package com.squareup.workflow1.internal -import com.squareup.workflow1.internal.InlineLinkedList.InlineListNode +import com.squareup.workflow1.InlineLinkedList.InlineListNode import kotlinx.coroutines.Job /** diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/ActiveStagingListTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/ActiveStagingListTest.kt similarity index 95% rename from workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/ActiveStagingListTest.kt rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/ActiveStagingListTest.kt index ddaebfbbb..4869a4dc9 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/ActiveStagingListTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/ActiveStagingListTest.kt @@ -1,6 +1,6 @@ -package com.squareup.workflow1.internal +package com.squareup.workflow1 -import com.squareup.workflow1.internal.InlineLinkedList.InlineListNode +import com.squareup.workflow1.InlineLinkedList.InlineListNode import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.fail diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptorTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/ChainedWorkflowInterceptorTest.kt similarity index 94% rename from workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptorTest.kt rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/ChainedWorkflowInterceptorTest.kt index a006c0bc2..b0d03e09e 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptorTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/ChainedWorkflowInterceptorTest.kt @@ -1,20 +1,7 @@ -@file:Suppress("UNCHECKED_CAST") - -package com.squareup.workflow1.internal - -import com.squareup.workflow1.BaseRenderContext -import com.squareup.workflow1.NoopWorkflowInterceptor -import com.squareup.workflow1.Sink -import com.squareup.workflow1.Snapshot -import com.squareup.workflow1.Workflow -import com.squareup.workflow1.WorkflowAction -import com.squareup.workflow1.WorkflowIdentifier -import com.squareup.workflow1.WorkflowInterceptor +package com.squareup.workflow1 + import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession -import com.squareup.workflow1.identifier -import com.squareup.workflow1.parse -import com.squareup.workflow1.rendering import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/InlineLinkedListTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/InlineLinkedListTest.kt similarity index 98% rename from workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/InlineLinkedListTest.kt rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/InlineLinkedListTest.kt index 14e6cf9d6..91ff5c3ad 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/InlineLinkedListTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/InlineLinkedListTest.kt @@ -1,10 +1,16 @@ -package com.squareup.workflow1.internal +package com.squareup.workflow1 -import com.squareup.workflow1.internal.InlineLinkedList.InlineListNode +import com.squareup.workflow1.InlineLinkedList.InlineListNode import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull +private class StringElement( + val value: String +) : InlineListNode { + override var nextListNode: StringElement? = null +} + internal class InlineLinkedListTest { @Test fun `forEach empty list`() { @@ -207,9 +213,3 @@ internal class InlineLinkedListTest { return items } } - -private class StringElement( - val value: String -) : InlineListNode { - override var nextListNode: StringElement? = null -} diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/RealRenderContextTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RealRenderContextTest.kt similarity index 93% rename from workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/RealRenderContextTest.kt rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RealRenderContextTest.kt index b2d39b5fc..2305cfb0a 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/RealRenderContextTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RealRenderContextTest.kt @@ -1,22 +1,11 @@ -@file:Suppress("EXPERIMENTAL_API_USAGE", "OverridingDeprecatedMember") - -package com.squareup.workflow1.internal - -import com.squareup.workflow1.Snapshot -import com.squareup.workflow1.StatefulWorkflow -import com.squareup.workflow1.Workflow -import com.squareup.workflow1.WorkflowAction -import com.squareup.workflow1.action -import com.squareup.workflow1.applyTo -import com.squareup.workflow1.internal.RealRenderContext.Renderer -import com.squareup.workflow1.internal.RealRenderContext.SideEffectRunner -import com.squareup.workflow1.internal.RealRenderContextTest.TestRenderer.Rendering -import com.squareup.workflow1.renderChild -import com.squareup.workflow1.stateless +package com.squareup.workflow1 + +import com.squareup.workflow1.RealRenderContext.Renderer +import com.squareup.workflow1.RealRenderContext.SideEffectRunner +import com.squareup.workflow1.RealRenderContextTest.TestRenderer.Rendering import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -95,7 +84,7 @@ internal class RealRenderContextTest { } private val eventActionsChannel = - Channel>(capacity = UNLIMITED) + Channel>(capacity = Channel.UNLIMITED) @OptIn(ExperimentalCoroutinesApi::class) @Test fun `send completes update`() { diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/SubtreeManagerTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/SubtreeManagerTest.kt similarity index 92% rename from workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/SubtreeManagerTest.kt rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/SubtreeManagerTest.kt index 8bf5d26ef..88c90306d 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/SubtreeManagerTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/SubtreeManagerTest.kt @@ -1,18 +1,7 @@ -@file:Suppress("EXPERIMENTAL_API_USAGE") - -package com.squareup.workflow1.internal - -import com.squareup.workflow1.ActionProcessingResult -import com.squareup.workflow1.Snapshot -import com.squareup.workflow1.StatefulWorkflow -import com.squareup.workflow1.TreeSnapshot -import com.squareup.workflow1.WorkflowAction -import com.squareup.workflow1.WorkflowOutput -import com.squareup.workflow1.action -import com.squareup.workflow1.applyTo -import com.squareup.workflow1.identifier -import com.squareup.workflow1.internal.SubtreeManagerTest.TestWorkflow.Rendering -import kotlinx.coroutines.Dispatchers.Unconfined +package com.squareup.workflow1 + +import com.squareup.workflow1.SubtreeManagerTest.TestWorkflow.Rendering +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.runBlocking import kotlinx.coroutines.selects.select @@ -88,7 +77,7 @@ internal class SubtreeManagerTest { } } - private val context = Unconfined + private val context = Dispatchers.Unconfined @Test fun `render starts new child`() { val manager = subtreeManagerForTest() diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/TreeSnapshotTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/TreeSnapshotTest.kt index 5f5625522..b5d4e25eb 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/TreeSnapshotTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/TreeSnapshotTest.kt @@ -1,7 +1,5 @@ package com.squareup.workflow1 -import com.squareup.workflow1.internal.WorkflowNodeId -import com.squareup.workflow1.internal.id import okio.ByteString import kotlin.reflect.typeOf import kotlin.test.Test diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/WorkflowNodeTest.kt similarity index 90% rename from workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/WorkflowNodeTest.kt index afa96a876..93b5d0a50 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/WorkflowNodeTest.kt @@ -1,36 +1,11 @@ -@file:Suppress("EXPERIMENTAL_API_USAGE", "DEPRECATION") -@file:OptIn(ExperimentalCoroutinesApi::class) - -package com.squareup.workflow1.internal - -import com.squareup.workflow1.ActionProcessingResult -import com.squareup.workflow1.BaseRenderContext -import com.squareup.workflow1.Sink -import com.squareup.workflow1.Snapshot -import com.squareup.workflow1.StatefulWorkflow -import com.squareup.workflow1.TreeSnapshot -import com.squareup.workflow1.Workflow -import com.squareup.workflow1.WorkflowAction -import com.squareup.workflow1.WorkflowIdentifier -import com.squareup.workflow1.WorkflowInterceptor +package com.squareup.workflow1 + import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession -import com.squareup.workflow1.WorkflowOutput -import com.squareup.workflow1.action -import com.squareup.workflow1.contraMap -import com.squareup.workflow1.identifier -import com.squareup.workflow1.parse -import com.squareup.workflow1.readUtf8WithLength -import com.squareup.workflow1.renderChild -import com.squareup.workflow1.rendering -import com.squareup.workflow1.stateful -import com.squareup.workflow1.stateless -import com.squareup.workflow1.writeUtf8WithLength import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers.Unconfined -import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow @@ -92,7 +67,7 @@ internal class WorkflowNodeTest { } } - private val context: CoroutineContext = Unconfined + Job() + private val context: CoroutineContext = Dispatchers.Unconfined + Job() @AfterTest fun tearDown() { context.cancel() @@ -105,6 +80,7 @@ internal class WorkflowNodeTest { return@PropsRenderingWorkflow state } val node = WorkflowNode(workflow.id(), workflow, "old", null, context) + .apply { startSession() } node.render(workflow, "new") @@ -118,6 +94,7 @@ internal class WorkflowNodeTest { return@PropsRenderingWorkflow state } val node = WorkflowNode(workflow.id(), workflow, "old", null, context) + .apply { startSession() } node.render(workflow, "old") @@ -129,6 +106,7 @@ internal class WorkflowNodeTest { "$old->$new" } val node = WorkflowNode(workflow.id(), workflow, "foo", null, context) + .apply { startSession() } val rendering = node.render(workflow, "foo2") @@ -172,7 +150,7 @@ internal class WorkflowNodeTest { val node = WorkflowNode( workflow.id(), workflow, "", null, context, emitOutputToParent = { WorkflowOutput("tick:$it") } - ) + ).apply { startSession() } node.render(workflow, "")("event") runTest { @@ -206,7 +184,7 @@ internal class WorkflowNodeTest { val node = WorkflowNode( workflow.id(), workflow, "", null, context, emitOutputToParent = { WorkflowOutput("tick:$it") } - ) + ).apply { startSession() } val sink = node.render(workflow, "") sink("event") @@ -245,6 +223,7 @@ internal class WorkflowNodeTest { } } val node = WorkflowNode(workflow.id(), workflow, "", null, context) + .apply { startSession() } node.render(workflow, "") sink.send(action { setOutput("event") }) @@ -263,8 +242,8 @@ internal class WorkflowNodeTest { } val node = WorkflowNode( workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, baseContext = context - ) + initialSnapshot = null, baseContext = context + ).apply { startSession() } runTest { node.render(workflow.asStatefulWorkflow(), Unit) @@ -281,8 +260,8 @@ internal class WorkflowNodeTest { } val node = WorkflowNode( workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, baseContext = context - ) + initialSnapshot = null, baseContext = context + ).apply { startSession() } node.render(workflow.asStatefulWorkflow(), Unit) assertEquals(WorkflowNodeId(workflow).toString(), node.coroutineContext[CoroutineName]!!.name) @@ -300,8 +279,8 @@ internal class WorkflowNodeTest { } val node = WorkflowNode( workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, baseContext = context - ) + initialSnapshot = null, baseContext = context + ).apply { startSession() } node.render(workflow.asStatefulWorkflow(), Unit) runTest { @@ -329,8 +308,8 @@ internal class WorkflowNodeTest { } val node = WorkflowNode( workflow.id(), workflow.asStatefulWorkflow(), initialProps = true, - snapshot = null, baseContext = context - ) + initialSnapshot = null, baseContext = context + ).apply { startSession() } runTest { node.render(workflow.asStatefulWorkflow(), true) @@ -355,8 +334,8 @@ internal class WorkflowNodeTest { } val node = WorkflowNode( workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, baseContext = context - ) + initialSnapshot = null, baseContext = context + ).apply { startSession() } runTest { node.render(workflow.asStatefulWorkflow(), Unit) @@ -381,8 +360,8 @@ internal class WorkflowNodeTest { } val node = WorkflowNode( workflow.id(), workflow.asStatefulWorkflow(), initialProps = 0, - snapshot = null, baseContext = context - ) + initialSnapshot = null, baseContext = context + ).apply { startSession() } runTest { node.render(workflow.asStatefulWorkflow(), 0) @@ -406,8 +385,8 @@ internal class WorkflowNodeTest { } val node = WorkflowNode( workflow.id(), workflow.asStatefulWorkflow(), initialProps = 0, - snapshot = null, baseContext = context - ) + initialSnapshot = null, baseContext = context + ).apply { startSession() } runTest { node.render(workflow.asStatefulWorkflow(), 0) @@ -427,8 +406,8 @@ internal class WorkflowNodeTest { } val node = WorkflowNode( workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, baseContext = context - ) + initialSnapshot = null, baseContext = context + ).apply { startSession() } val error = assertFailsWith { node.render(workflow.asStatefulWorkflow(), Unit) @@ -454,9 +433,9 @@ internal class WorkflowNodeTest { } .asStatefulWorkflow() val node = WorkflowNode( - workflow.id(), workflow, initialProps = 0, snapshot = null, + workflow.id(), workflow, initialProps = 0, initialSnapshot = null, baseContext = context - ) + ).apply { startSession() } node.render(workflow, 0) assertEquals(listOf("started"), events1) @@ -488,8 +467,8 @@ internal class WorkflowNodeTest { } val node = WorkflowNode( workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, baseContext = context - ) + initialSnapshot = null, baseContext = context + ).apply { startSession() } assertFalse(started1) assertFalse(started2) @@ -517,9 +496,9 @@ internal class WorkflowNodeTest { workflow.id(), workflow, initialProps = "initial props", - snapshot = null, - baseContext = Unconfined - ) + initialSnapshot = null, + baseContext = Dispatchers.Unconfined + ).apply { startSession() } assertEquals("initial props", originalNode.render(workflow, "foo")) val snapshot = originalNode.snapshot(workflow) @@ -530,9 +509,9 @@ internal class WorkflowNodeTest { workflow, // These props should be ignored, since snapshot is non-null. initialProps = "new props", - snapshot = snapshot, - baseContext = Unconfined - ) + initialSnapshot = snapshot, + baseContext = Dispatchers.Unconfined + ).apply { startSession() } assertEquals("initial props", restoredNode.render(workflow, "foo")) } @@ -546,9 +525,9 @@ internal class WorkflowNodeTest { workflow.id(), workflow, initialProps = "initial props", - snapshot = null, - baseContext = Unconfined - ) + initialSnapshot = null, + baseContext = Dispatchers.Unconfined + ).apply { startSession() } assertEquals("initial props", originalNode.render(workflow, "foo")) val snapshot = originalNode.snapshot(workflow) @@ -559,9 +538,9 @@ internal class WorkflowNodeTest { workflow, // These props should be ignored, since snapshot is non-null. initialProps = "new props", - snapshot = snapshot, - baseContext = Unconfined - ) + initialSnapshot = snapshot, + baseContext = Dispatchers.Unconfined + ).apply { startSession() } assertEquals("restored", restoredNode.render(workflow, "foo")) } @@ -603,9 +582,9 @@ internal class WorkflowNodeTest { parentWorkflow.id(), parentWorkflow, initialProps = "initial props", - snapshot = null, - baseContext = Unconfined - ) + initialSnapshot = null, + baseContext = Dispatchers.Unconfined + ).apply { startSession() } assertEquals("initial props|child props", originalNode.render(parentWorkflow, "foo")) val snapshot = originalNode.snapshot(parentWorkflow) @@ -616,9 +595,9 @@ internal class WorkflowNodeTest { parentWorkflow, // These props should be ignored, since snapshot is non-null. initialProps = "new props", - snapshot = snapshot, - baseContext = Unconfined - ) + initialSnapshot = snapshot, + baseContext = Dispatchers.Unconfined + ).apply { startSession() } assertEquals("initial props|child props", restoredNode.render(parentWorkflow, "foo")) assertEquals("child props", restoredChildState) assertEquals("initial props", restoredParentState) @@ -642,7 +621,8 @@ internal class WorkflowNodeTest { } } ) - val node = WorkflowNode(workflow.id(), workflow, Unit, null, Unconfined) + val node = WorkflowNode(workflow.id(), workflow, Unit, null, Dispatchers.Unconfined) + .apply { startSession() } assertEquals(0, snapshotCalls) assertEquals(0, snapshotWrites) @@ -660,7 +640,8 @@ internal class WorkflowNodeTest { assertEquals(1, snapshotWrites) assertEquals(0, restoreCalls) - WorkflowNode(workflow.id(), workflow, Unit, snapshot, Unconfined) + WorkflowNode(workflow.id(), workflow, Unit, snapshot, Dispatchers.Unconfined) + .apply { startSession() } assertEquals(1, snapshotCalls) assertEquals(1, snapshotWrites) @@ -683,9 +664,9 @@ internal class WorkflowNodeTest { workflow.id(), workflow, initialProps = "initial props", - snapshot = null, - baseContext = Unconfined - ) + initialSnapshot = null, + baseContext = Dispatchers.Unconfined + ).apply { startSession() } assertEquals("initial props", originalNode.render(workflow, "foo")) val snapshot = originalNode.snapshot(workflow) @@ -695,9 +676,9 @@ internal class WorkflowNodeTest { workflow.id(), workflow, initialProps = "new props", - snapshot = snapshot, - baseContext = Unconfined - ) + initialSnapshot = snapshot, + baseContext = Dispatchers.Unconfined + ).apply { startSession() } assertEquals("props:new props|state:initial props", restoredNode.render(workflow, "foo")) } @@ -707,10 +688,10 @@ internal class WorkflowNodeTest { id = workflow.id(key = "foo"), workflow = workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, - baseContext = Unconfined, + initialSnapshot = null, + baseContext = Dispatchers.Unconfined, parent = null - ) + ).apply { startSession() } assertEquals( "WorkflowInstance(identifier=${workflow.identifier}, renderKey=foo, " + @@ -725,10 +706,10 @@ internal class WorkflowNodeTest { id = workflow.id(key = "foo"), workflow = workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, - baseContext = Unconfined, + initialSnapshot = null, + baseContext = Dispatchers.Unconfined, parent = TestSession(42) - ) + ).apply { startSession() } assertEquals( "WorkflowInstance(identifier=${workflow.identifier}, renderKey=foo, " + @@ -758,11 +739,11 @@ internal class WorkflowNodeTest { id = workflow.id(key = "foo"), workflow = workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, + initialSnapshot = null, interceptor = interceptor, - baseContext = Unconfined, + baseContext = Dispatchers.Unconfined, parent = TestSession(42) - ) + ).apply { startSession() } assertSame(node.coroutineContext, interceptedScope.coroutineContext) assertEquals(workflow.identifier, interceptedSession.identifier) @@ -802,11 +783,11 @@ internal class WorkflowNodeTest { id = workflow.id(key = "foo"), workflow = workflow.asStatefulWorkflow(), initialProps = "props", - snapshot = TreeSnapshot.forRootOnly(Snapshot.of("snapshot")), + initialSnapshot = TreeSnapshot.forRootOnly(Snapshot.of("snapshot")), interceptor = interceptor, - baseContext = Unconfined, + baseContext = Dispatchers.Unconfined, parent = TestSession(42) - ) + ).apply { startSession() } assertEquals("props", interceptedProps) assertEquals(Snapshot.of("snapshot"), interceptedSnapshot) @@ -848,11 +829,11 @@ internal class WorkflowNodeTest { id = workflow.id(key = "foo"), workflow = workflow.asStatefulWorkflow(), initialProps = "old", - snapshot = null, + initialSnapshot = null, interceptor = interceptor, - baseContext = Unconfined, + baseContext = Dispatchers.Unconfined, parent = TestSession(42) - ) + ).apply { startSession() } val rendering = node.render(workflow, "new") assertEquals("old", interceptedOld) @@ -894,11 +875,11 @@ internal class WorkflowNodeTest { id = workflow.id(key = "foo"), workflow = workflow.asStatefulWorkflow(), initialProps = "props", - snapshot = null, + initialSnapshot = null, interceptor = interceptor, - baseContext = Unconfined, + baseContext = Dispatchers.Unconfined, parent = TestSession(42) - ) + ).apply { startSession() } val rendering = node.render(workflow, "props") assertEquals("props", interceptedProps) @@ -936,11 +917,11 @@ internal class WorkflowNodeTest { id = workflow.id(key = "foo"), workflow = workflow.asStatefulWorkflow(), initialProps = "old", - snapshot = null, + initialSnapshot = null, interceptor = interceptor, - baseContext = Unconfined, + baseContext = Dispatchers.Unconfined, parent = TestSession(42) - ) + ).apply { startSession() } val snapshot = node.snapshot(workflow) assertEquals("state", interceptedState) @@ -977,11 +958,11 @@ internal class WorkflowNodeTest { id = workflow.id(key = "foo"), workflow = workflow.asStatefulWorkflow(), initialProps = "old", - snapshot = null, + initialSnapshot = null, interceptor = interceptor, - baseContext = Unconfined, + baseContext = Dispatchers.Unconfined, parent = TestSession(42) - ) + ).apply { startSession() } val snapshot = node.snapshot(workflow) assertEquals("state", interceptedState) @@ -1018,12 +999,12 @@ internal class WorkflowNodeTest { id = rootWorkflow.id(key = "foo"), workflow = rootWorkflow.asStatefulWorkflow(), initialProps = "props", - snapshot = null, + initialSnapshot = null, interceptor = interceptor, - baseContext = Unconfined, + baseContext = Dispatchers.Unconfined, parent = TestSession(42), idCounter = IdCounter() - ) + ).apply { startSession() } val rendering = node.render(rootWorkflow.asStatefulWorkflow(), "props") assertEquals("[root([leaf([[props]], [[props]])])]", rendering) @@ -1038,9 +1019,9 @@ internal class WorkflowNodeTest { workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, - baseContext = Unconfined - ) + initialSnapshot = null, + baseContext = Dispatchers.Unconfined + ).apply { startSession() } val error = assertFailsWith { node.render(workflow.asStatefulWorkflow(), Unit) @@ -1066,9 +1047,9 @@ internal class WorkflowNodeTest { workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, - baseContext = Unconfined - ) + initialSnapshot = null, + baseContext = Dispatchers.Unconfined + ).apply { startSession() } val error = assertFailsWith { node.render(workflow.asStatefulWorkflow(), Unit) @@ -1093,9 +1074,9 @@ internal class WorkflowNodeTest { workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, - baseContext = Unconfined - ) + initialSnapshot = null, + baseContext = Dispatchers.Unconfined + ).apply { startSession() } val (_, sink) = node.render(workflow.asStatefulWorkflow(), Unit) sink.send("hello") @@ -1118,10 +1099,10 @@ internal class WorkflowNodeTest { workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, - baseContext = Unconfined, + initialSnapshot = null, + baseContext = Dispatchers.Unconfined, emitOutputToParent = { WorkflowOutput("output:$it") } - ) + ).apply { startSession() } val rendering = node.render(workflow.asStatefulWorkflow(), Unit) rendering.send("hello") @@ -1142,10 +1123,10 @@ internal class WorkflowNodeTest { workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, - baseContext = Unconfined, + initialSnapshot = null, + baseContext = Dispatchers.Unconfined, emitOutputToParent = { WorkflowOutput(it) } - ) + ).apply { startSession() } val rendering = node.render(workflow.asStatefulWorkflow(), Unit) rendering.send("hello") @@ -1172,9 +1153,9 @@ internal class WorkflowNodeTest { workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, - baseContext = Unconfined - ) + initialSnapshot = null, + baseContext = Dispatchers.Unconfined + ).apply { startSession() } node.render(workflow.asStatefulWorkflow(), Unit) runTest { @@ -1197,10 +1178,10 @@ internal class WorkflowNodeTest { workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, - baseContext = Unconfined, + initialSnapshot = null, + baseContext = Dispatchers.Unconfined, emitOutputToParent = { WorkflowOutput("output:$it") } - ) + ).apply { startSession() } node.render(workflow.asStatefulWorkflow(), Unit) runTest { @@ -1221,10 +1202,10 @@ internal class WorkflowNodeTest { workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, - baseContext = Unconfined, + initialSnapshot = null, + baseContext = Dispatchers.Unconfined, emitOutputToParent = { WorkflowOutput(it) } - ) + ).apply { startSession() } node.render(workflow.asStatefulWorkflow(), Unit) runTest { diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowRunnerTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/WorkflowRunnerTest.kt similarity index 94% rename from workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowRunnerTest.kt rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/WorkflowRunnerTest.kt index d999bbf94..6f5976bd1 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowRunnerTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/WorkflowRunnerTest.kt @@ -1,18 +1,8 @@ -package com.squareup.workflow1.internal +package com.squareup.workflow1 -import com.squareup.workflow1.NoopWorkflowInterceptor -import com.squareup.workflow1.RuntimeConfig -import com.squareup.workflow1.RuntimeConfig.Companion import com.squareup.workflow1.RuntimeConfig.ConflateStaleRenderings import com.squareup.workflow1.RuntimeConfig.RenderPerAction -import com.squareup.workflow1.Worker -import com.squareup.workflow1.Workflow -import com.squareup.workflow1.WorkflowExperimentalRuntime -import com.squareup.workflow1.WorkflowOutput -import com.squareup.workflow1.action -import com.squareup.workflow1.runningWorker -import com.squareup.workflow1.stateful -import com.squareup.workflow1.stateless +import com.squareup.workflow1.internal.ParameterizedTestRunner import kotlinx.coroutines.CancellationException import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async @@ -334,7 +324,7 @@ internal class WorkflowRunnerTest { private fun WorkflowRunner( workflow: Workflow, props: StateFlow

, - runtimeConfig: RuntimeConfig = Companion.DEFAULT_CONFIG + runtimeConfig: RuntimeConfig = RuntimeConfig.DEFAULT_CONFIG ): WorkflowRunner = WorkflowRunner( scope, workflow, diff --git a/workflow-runtime/src/jvmWorkflowNode/kotlin/com/squareup/workflow1/WorkflowNodeBenchmark.kt b/workflow-runtime/src/jvmWorkflowNode/kotlin/com/squareup/workflow1/WorkflowNodeBenchmark.kt index bcbdf0ef4..8e618bd8e 100644 --- a/workflow-runtime/src/jvmWorkflowNode/kotlin/com/squareup/workflow1/WorkflowNodeBenchmark.kt +++ b/workflow-runtime/src/jvmWorkflowNode/kotlin/com/squareup/workflow1/WorkflowNodeBenchmark.kt @@ -6,8 +6,6 @@ import com.squareup.workflow1.FractalWorkflow.Props.RENDER_LEAVES import com.squareup.workflow1.FractalWorkflow.Props.RUN_WORKERS import com.squareup.workflow1.FractalWorkflow.Props.SKIP_FIRST_LEAF import com.squareup.workflow1.WorkflowAction.Companion.noAction -import com.squareup.workflow1.internal.WorkflowNode -import com.squareup.workflow1.internal.id import kotlinx.coroutines.Dispatchers.Unconfined import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -108,9 +106,9 @@ internal open class WorkflowNodeBenchmark { id = this.id(), workflow = this, initialProps = RENDER_LEAVES, - snapshot = null, + initialSnapshot = null, baseContext = context - ) + ).apply { startSession() } } /** diff --git a/workflow-ui/compose-tooling/build.gradle.kts b/workflow-ui/compose-tooling/build.gradle.kts index bf7e67d62..7e9472945 100644 --- a/workflow-ui/compose-tooling/build.gradle.kts +++ b/workflow-ui/compose-tooling/build.gradle.kts @@ -11,7 +11,7 @@ plugins { android { buildFeatures.compose = true composeOptions { - kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() } namespace = "com.squareup.workflow1.ui.compose.tooling" } diff --git a/workflow-ui/compose/build.gradle.kts b/workflow-ui/compose/build.gradle.kts index 7d37d9de0..ec0c48e88 100644 --- a/workflow-ui/compose/build.gradle.kts +++ b/workflow-ui/compose/build.gradle.kts @@ -11,7 +11,7 @@ plugins { android { buildFeatures.compose = true composeOptions { - kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() } namespace = "com.squareup.workflow1.ui.compose" testNamespace = "$namespace.test" diff --git a/workflow-ui/core-android/api/core-android.api b/workflow-ui/core-android/api/core-android.api index 0632693c4..bb8f15394 100644 --- a/workflow-ui/core-android/api/core-android.api +++ b/workflow-ui/core-android/api/core-android.api @@ -1,10 +1,10 @@ public final class com/squareup/workflow1/ui/AndroidRenderWorkflowKt { - public static final fun renderWorkflowIn (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Lcom/squareup/workflow1/RuntimeConfig;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow; - public static final fun renderWorkflowIn (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Ljava/lang/Object;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Lcom/squareup/workflow1/RuntimeConfig;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow; - public static final fun renderWorkflowIn (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Lcom/squareup/workflow1/RuntimeConfig;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow; - public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Lcom/squareup/workflow1/RuntimeConfig;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; - public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Ljava/lang/Object;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Lcom/squareup/workflow1/RuntimeConfig;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; - public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Lcom/squareup/workflow1/RuntimeConfig;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; + public static final fun renderWorkflowIn (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Lcom/squareup/workflow1/RuntimeConfig;Lcom/squareup/workflow1/WorkflowRuntimePlugin;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow; + public static final fun renderWorkflowIn (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Ljava/lang/Object;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Lcom/squareup/workflow1/RuntimeConfig;Lcom/squareup/workflow1/WorkflowRuntimePlugin;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow; + public static final fun renderWorkflowIn (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Lcom/squareup/workflow1/RuntimeConfig;Lcom/squareup/workflow1/WorkflowRuntimePlugin;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow; + public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Lcom/squareup/workflow1/RuntimeConfig;Lcom/squareup/workflow1/WorkflowRuntimePlugin;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; + public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Ljava/lang/Object;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Lcom/squareup/workflow1/RuntimeConfig;Lcom/squareup/workflow1/WorkflowRuntimePlugin;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; + public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Lcom/squareup/workflow1/RuntimeConfig;Lcom/squareup/workflow1/WorkflowRuntimePlugin;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; } public abstract interface class com/squareup/workflow1/ui/AndroidScreen : com/squareup/workflow1/ui/Screen { diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidRenderWorkflow.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidRenderWorkflow.kt index ab32271c5..fef58c850 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidRenderWorkflow.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidRenderWorkflow.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import com.squareup.workflow1.RuntimeConfig import com.squareup.workflow1.Workflow import com.squareup.workflow1.WorkflowInterceptor +import com.squareup.workflow1.WorkflowRuntimePlugin import com.squareup.workflow1.renderWorkflowIn import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -81,6 +82,7 @@ public fun renderWorkflowIn( savedStateHandle: SavedStateHandle? = null, interceptors: List = emptyList(), runtimeConfig: RuntimeConfig = RuntimeConfig.DEFAULT_CONFIG, + workflowRuntimePlugin: WorkflowRuntimePlugin? = null, onOutput: suspend (OutputT) -> Unit = {} ): StateFlow { return renderWorkflowIn( @@ -90,6 +92,7 @@ public fun renderWorkflowIn( savedStateHandle = savedStateHandle, interceptors = interceptors, runtimeConfig = runtimeConfig, + workflowRuntimePlugin = workflowRuntimePlugin, onOutput = onOutput ) } @@ -155,6 +158,9 @@ public fun renderWorkflowIn( * @param runtimeConfig * Configuration for the Workflow Runtime. * + * @param workflowRuntimePlugin + * This is used to plug in Runtime functionality that lives in other modules. + * * @return * A [StateFlow] of [RenderingT]s that will emit any time the root workflow creates a new * rendering. @@ -168,9 +174,17 @@ public fun renderWorkflowIn( savedStateHandle: SavedStateHandle? = null, interceptors: List = emptyList(), runtimeConfig: RuntimeConfig = RuntimeConfig.DEFAULT_CONFIG, + workflowRuntimePlugin: WorkflowRuntimePlugin? = null, onOutput: suspend (OutputT) -> Unit = {} ): StateFlow = renderWorkflowIn( - workflow, scope, MutableStateFlow(prop), savedStateHandle, interceptors, runtimeConfig, onOutput + workflow = workflow, + scope = scope, + props = MutableStateFlow(prop), + savedStateHandle = savedStateHandle, + interceptors = interceptors, + runtimeConfig = runtimeConfig, + workflowRuntimePlugin = workflowRuntimePlugin, + onOutput = onOutput ) /** @@ -250,6 +264,9 @@ public fun renderWorkflowIn( * @param runtimeConfig * Configuration for the Workflow Runtime. * + * @param workflowRuntimePlugin + * This is used to plug in Runtime functionality that lives in other modules. + * * @return * A [StateFlow] of [RenderingT]s that will emit any time the root workflow creates a new * rendering. @@ -263,11 +280,19 @@ public fun renderWorkflowIn( savedStateHandle: SavedStateHandle? = null, interceptors: List = emptyList(), runtimeConfig: RuntimeConfig = RuntimeConfig.DEFAULT_CONFIG, + workflowRuntimePlugin: WorkflowRuntimePlugin? = null, onOutput: suspend (OutputT) -> Unit = {} ): StateFlow { val restoredSnap = savedStateHandle?.get(KEY)?.snapshot val renderingsAndSnapshots = renderWorkflowIn( - workflow, scope, props, restoredSnap, interceptors, runtimeConfig, onOutput + workflow, + scope, + props, + restoredSnap, + interceptors, + runtimeConfig, + workflowRuntimePlugin, + onOutput ) return renderingsAndSnapshots diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowLayout.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowLayout.kt index ab25c0338..966b3c9e7 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowLayout.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowLayout.kt @@ -271,12 +271,12 @@ public class WorkflowLayout( val scope = CoroutineScope(Dispatchers.Main.immediate) var job: Job? = null - override fun onViewAttachedToWindow(v: View?) { + override fun onViewAttachedToWindow(v: View) { job = source.onEach { screen -> update(screen) } .launchIn(scope) } - override fun onViewDetachedFromWindow(v: View?) { + override fun onViewDetachedFromWindow(v: View) { job?.cancel() job = null }