diff --git a/artifacts.json b/artifacts.json index 6cddeb3fa..13a589739 100644 --- a/artifacts.json +++ b/artifacts.json @@ -23,6 +23,14 @@ "packaging": "jar", "javaVersion": "1.8" }, + { + "gradlePath": ":workflow-core-compose", + "group": "com.squareup.workflow1", + "artifactId": "workflow-core-compose", + "description": "Workflow Core Compose", + "packaging": "jar", + "javaVersion": "1.8" + }, { "gradlePath": ":workflow-runtime", "group": "com.squareup.workflow1", diff --git a/benchmarks/performance-poetry/complex-poetry/build.gradle.kts b/benchmarks/performance-poetry/complex-poetry/build.gradle.kts index 1b141a7fb..3358153d1 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 @@ -51,6 +52,7 @@ dependencies { // API on an app module so these are transitive dependencies for the benchmarks. api(project(":samples:containers:android")) api(project(":samples:containers:poetry")) + api(project(":workflow-core-compose")) api(project(":workflow-ui:core-android")) implementation(libs.androidx.activity.ktx) diff --git a/benchmarks/performance-poetry/complex-poetry/src/androidTest/java/com/squareup/benchmarks/performance/complex/poetry/RenderPassTest.kt b/benchmarks/performance-poetry/complex-poetry/src/androidTest/java/com/squareup/benchmarks/performance/complex/poetry/RenderPassTest.kt index 611b9288d..6c9c8fa38 100644 --- a/benchmarks/performance-poetry/complex-poetry/src/androidTest/java/com/squareup/benchmarks/performance/complex/poetry/RenderPassTest.kt +++ b/benchmarks/performance-poetry/complex-poetry/src/androidTest/java/com/squareup/benchmarks/performance/complex/poetry/RenderPassTest.kt @@ -8,6 +8,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.squareup.benchmarks.performance.complex.poetry.PerformancePoetryActivity.Companion.EXTRA_PERF_CONFIG_INITIALIZING import com.squareup.benchmarks.performance.complex.poetry.PerformancePoetryActivity.Companion.EXTRA_PERF_CONFIG_REPEAT +import com.squareup.benchmarks.performance.complex.poetry.PerformancePoetryActivity.Companion.EXTRA_RUNTIME_COMPOSE_RUNTIME import com.squareup.benchmarks.performance.complex.poetry.PerformancePoetryActivity.Companion.EXTRA_RUNTIME_FRAME_TIMEOUT import com.squareup.benchmarks.performance.complex.poetry.cyborgs.landscapeOrientation import com.squareup.benchmarks.performance.complex.poetry.cyborgs.openRavenAndNavigate @@ -16,6 +17,7 @@ import com.squareup.benchmarks.performance.complex.poetry.cyborgs.waitForPoetry import com.squareup.benchmarks.performance.complex.poetry.instrumentation.RenderPassCountingInterceptor import org.junit.Assert.fail import org.junit.Before +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith @@ -30,7 +32,8 @@ class RenderPassTest { val useInitializingState: Boolean, val useHighFrequencyRange: Boolean, val baselineExpectation: RenderExpectation, - val frameTimeoutExpectation: RenderExpectation + val frameTimeoutExpectation: RenderExpectation, + val frameTimeoutComposeExpectation: RenderExpectation ) data class RenderExpectation( @@ -56,15 +59,15 @@ class RenderPassTest { } @Test fun renderPassCounterBaselineComplexWithInitializingState() { - runRenderPassCounter(COMPLEX_INITIALIZING, useFrameTimeout = false) + runRenderPassCounter(COMPLEX_INITIALIZING) } @Test fun renderPassCounterBaselineComplexNoInitializingState() { - runRenderPassCounter(COMPLEX_NO_INITIALIZING, useFrameTimeout = false) + runRenderPassCounter(COMPLEX_NO_INITIALIZING) } @Test fun renderPassCounterBaselineComplexNoInitializingStateHighFrequencyEvents() { - runRenderPassCounter(COMPLEX_NO_INITIALIZING_HIGH_FREQUENCY, useFrameTimeout = false) + runRenderPassCounter(COMPLEX_NO_INITIALIZING_HIGH_FREQUENCY) } @Test fun renderPassCounterFrameTimeoutComplexWithInitializingState() { @@ -79,10 +82,33 @@ class RenderPassTest { runRenderPassCounter(COMPLEX_NO_INITIALIZING_HIGH_FREQUENCY, useFrameTimeout = true) } + @Ignore( + "Not sure why but this gets stuck on initializing. Compose doesn't get the next" + + " frame when this is started by the test, but it does when running directly. See #835" + ) + @Test fun renderPassCounterFrameTimeoutComposeComplexWithInitializingState() { + runRenderPassCounter(COMPLEX_INITIALIZING, useFrameTimeout = true, useCompose = true) + } + + @Test fun renderPassCounterFrameTimeoutComposeComplexNoInitializingState() { + runRenderPassCounter(COMPLEX_NO_INITIALIZING, useFrameTimeout = true, useCompose = true) + } + + @Test fun renderPassCounterFrameTimeoutComposeComplexNoInitializingStateHighFrequencyEvents() { + runRenderPassCounter( + COMPLEX_NO_INITIALIZING_HIGH_FREQUENCY, + useFrameTimeout = true, + useCompose = true + ) + } + private fun runRenderPassCounter( scenario: Scenario, - useFrameTimeout: Boolean + useFrameTimeout: Boolean = false, + useCompose: Boolean = false ) { + if (useCompose) require(useFrameTimeout) { "Only use Compose Frame Timeout." } + val intent = Intent(context, PerformancePoetryActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) @@ -90,8 +116,9 @@ class RenderPassTest { EXTRA_PERF_CONFIG_INITIALIZING, scenario.useInitializingState ) + putExtra(EXTRA_RUNTIME_FRAME_TIMEOUT, useFrameTimeout) if (useFrameTimeout) { - putExtra(EXTRA_RUNTIME_FRAME_TIMEOUT, useFrameTimeout) + putExtra(EXTRA_RUNTIME_COMPOSE_RUNTIME, useCompose) } if (scenario.useHighFrequencyRange) { putExtra(EXTRA_PERF_CONFIG_REPEAT, PerformancePoetryActivity.HIGH_FREQUENCY_REPEAT_COUNT) @@ -111,11 +138,19 @@ class RenderPassTest { device.openRavenAndNavigate() val expectation = - if (useFrameTimeout) scenario.frameTimeoutExpectation else + if (useFrameTimeout) { + if (useCompose) { + scenario.frameTimeoutComposeExpectation + } else { + scenario.frameTimeoutExpectation + } + } else { scenario.baselineExpectation + } val title = if (useFrameTimeout) { - "Runtime: FrameTimeout; " + val usingCompose = if (useCompose) "(Using Compose Optimizations)" else "(No Compose)" + "Runtime: FrameTimeout $usingCompose;" } else { "Runtime: RenderPerAction; " } + scenario.title @@ -227,6 +262,11 @@ class RenderPassTest { totalPasses = 41..42, freshRenderedNodes = 85..85, staleRenderedNodes = 436..436 + ), + frameTimeoutComposeExpectation = RenderExpectation( + totalPasses = 41..42, + freshRenderedNodes = 85..85, + staleRenderedNodes = 436..436 ) ) @@ -243,6 +283,11 @@ class RenderPassTest { totalPasses = 40..41, freshRenderedNodes = 83..83, staleRenderedNodes = 431..431 + ), + frameTimeoutComposeExpectation = RenderExpectation( + totalPasses = 40..41, + freshRenderedNodes = 113..113, + staleRenderedNodes = 82..82 ) ) @@ -270,9 +315,14 @@ class RenderPassTest { staleRenderedNodes = 2350..2350 ), frameTimeoutExpectation = RenderExpectation( - totalPasses = 88..97, + totalPasses = 60..64, freshRenderedNodes = 106..108, staleRenderedNodes = 679..698 + ), + frameTimeoutComposeExpectation = RenderExpectation( + totalPasses = 59..64, + freshRenderedNodes = 259..259, + staleRenderedNodes = 207..207 ) ) 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..929c3857b 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 { @@ -48,5 +51,31 @@ class MaybeLoadingGatekeeperWorkflow( ) } + @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() + ) + } + override fun snapshotState(state: IsLoading): Snapshot? = null } 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 a24d2cf55..22ded5608 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(WorkflowUiExperimentalApi::class, WorkflowExperimentalRuntime::class) class PerformancePoemWorkflow( private val simulatedPerfConfig: SimulatedPerfConfig = SimulatedPerfConfig.NO_SIMULATED_PERF, private val isLoading: MutableStateFlow, -) : PoemWorkflow, StatefulWorkflow() { +) : PoemWorkflow, StatefulComposeWorkflow() { sealed class State { // N.B. This state is a smell. We include it to be able to mimic smells @@ -93,7 +97,7 @@ class PerformancePoemWorkflow( override fun render( renderProps: Poem, renderState: State, - context: RenderContext + context: StatefulWorkflow.RenderContext ): OverviewDetailScreen { return when (renderState) { Initializing -> { @@ -234,6 +238,127 @@ class PerformancePoemWorkflow( } } + @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)) + } + ) + } + } + } + override fun snapshotState(state: State): Snapshot? = null internal sealed class Action : WorkflowAction() { 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 bb12e905c..2d5c7d763 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 @@ -18,12 +19,15 @@ import com.squareup.sample.poetry.PoemsBrowserWorkflow import com.squareup.sample.poetry.model.Poem import com.squareup.workflow1.Snapshot import com.squareup.workflow1.StatefulWorkflow +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 import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow +import java.lang.IllegalStateException /** * Version of [PoemsBrowserWorkflow] that takes in a [SimulatedPerfConfig] to control the @@ -42,13 +46,14 @@ import kotlinx.coroutines.flow.MutableStateFlow * break ties/conflicts with a token in the start/stop requests. We leave that complexity out * here. ** */ +@OptIn(WorkflowUiExperimentalApi::class, 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 @@ -70,11 +75,10 @@ class PerformancePoemsBrowserWorkflow( return if (simulatedPerfConfig.useInitializingState) Initializing else NoSelection } - @OptIn(WorkflowUiExperimentalApi::class) override fun render( renderProps: List, renderState: State, - context: RenderContext + context: StatefulWorkflow, State, Unit, OverviewDetailScreen>.RenderContext ): OverviewDetailScreen { val poemListProps = Props( poems = renderProps, @@ -154,6 +158,102 @@ class PerformancePoemsBrowserWorkflow( } } + @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.") + } + } + } + override fun snapshotState(state: State): Snapshot? = null private fun choosePoem( 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 19d178423..7349110e0 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 @@ -1,3 +1,5 @@ +@file:OptIn(WorkflowExperimentalRuntime::class) + package com.squareup.benchmarks.performance.complex.poetry import android.content.pm.ApplicationInfo @@ -25,6 +27,7 @@ import com.squareup.workflow1.RuntimeConfig.FrameTimeout 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 @@ -84,7 +87,12 @@ class PerformancePoetryActivity : AppCompatActivity() { } val isFrameTimeout = intent.getBooleanExtra(EXTRA_RUNTIME_FRAME_TIMEOUT, false) - val runtimeConfig = if (isFrameTimeout) FrameTimeout() else RenderPerAction + val useCompose = intent.getBooleanExtra(EXTRA_RUNTIME_COMPOSE_RUNTIME, false) + val runtimeConfig = if (isFrameTimeout) { + FrameTimeout(useComposeInRuntime = useCompose) + } else { + RenderPerAction + } val component = PerformancePoetryComponent(installedInterceptor, simulatedPerfConfig, runtimeConfig) @@ -243,6 +251,8 @@ class PerformancePoetryActivity : AppCompatActivity() { const val EXTRA_PERF_CONFIG_DELAY = "complex.poetry.performance.config.delay.length" const val EXTRA_RUNTIME_FRAME_TIMEOUT = "complex.poetry.performance.config.runtime.frametimeout" + const val EXTRA_RUNTIME_COMPOSE_RUNTIME = + "complex.poetry.performance.config.runtime.compose" const val SELECT_ON_TIMEOUT_LOG_NAME = "kotlinx.coroutines.selects.SelectBuilderImpl\$onTimeout\$\$inlined\$Runnable" @@ -263,12 +273,14 @@ class PoetryModel( runtimeConfig: RuntimeConfig ) : ViewModel() { @OptIn(WorkflowUiExperimentalApi::class) val renderings: StateFlow by lazy { + val runtimePlugin = if (runtimeConfig.useComposeInRuntime) ComposeRuntimePlugin else null renderWorkflowIn( workflow = workflow, scope = viewModelScope, savedStateHandle = savedState, interceptors = interceptor?.let { listOf(it) } ?: emptyList(), - runtimeConfig = runtimeConfig + runtimeConfig = runtimeConfig, + workflowRuntimePlugin = runtimePlugin ) } diff --git a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/cyborgs/PoetryCyborgs.kt b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/cyborgs/PoetryCyborgs.kt index 50298a8ec..de3c38846 100644 --- a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/cyborgs/PoetryCyborgs.kt +++ b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/cyborgs/PoetryCyborgs.kt @@ -48,6 +48,7 @@ fun UiDevice.resetToRootPoetryList() { */ fun UiDevice.openRavenAndNavigate() { waitForIdle() + waitForIdle(5_000) waitForAndClick(RavenPoemSelector) waitForLoadingInterstitial() waitForAndClick(By.textStartsWith("Deep into that darkness peering")) diff --git a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/instrumentation/ActionHandlingTracingInterceptor.kt b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/instrumentation/ActionHandlingTracingInterceptor.kt index 9d851dbb1..d89fa8d7e 100644 --- a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/instrumentation/ActionHandlingTracingInterceptor.kt +++ b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/instrumentation/ActionHandlingTracingInterceptor.kt @@ -1,11 +1,16 @@ package com.squareup.benchmarks.performance.complex.poetry.instrumentation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.tracing.trace import com.squareup.workflow1.BaseRenderContext import com.squareup.workflow1.WorkflowAction import com.squareup.workflow1.WorkflowInterceptor import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession +import com.squareup.workflow1.compose.BaseComposeRenderContext +import com.squareup.workflow1.compose.ComposeWorkflowInterceptor +import com.squareup.workflow1.compose.ComposeWorkflowInterceptor.ComposeRenderContextInterceptor /** * We use this [WorkflowInterceptor] to add in tracing for the main thread messages that handle @@ -20,13 +25,13 @@ import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession * annotate the [Worker] using [TraceableWorker] which will set it up with a key such that when * the action for the result is sent to the sink the main thread message will be traced. */ -class ActionHandlingTracingInterceptor : WorkflowInterceptor, Resettable { +class ActionHandlingTracingInterceptor : ComposeWorkflowInterceptor, Resettable { private val actionCounts: MutableMap = mutableMapOf() class EventHandlingTracingRenderContextInterceptor( private val actionCounts: MutableMap - ) : RenderContextInterceptor { + ) : ComposeRenderContextInterceptor { override fun onActionSent( action: WorkflowAction, proceed: (WorkflowAction) -> Unit @@ -70,6 +75,24 @@ class ActionHandlingTracingInterceptor : WorkflowInterceptor, Resettable { ) } + @Composable + override fun Rendering( + renderProps: P, + renderState: S, + context: BaseComposeRenderContext, + session: WorkflowSession, + proceed: @Composable (P, S, ComposeRenderContextInterceptor?) -> R + ): R { + val rci = remember { + EventHandlingTracingRenderContextInterceptor(actionCounts) + } + return proceed( + renderProps, + renderState, + rci + ) + } + override fun reset() { actionCounts.clear() } diff --git a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/instrumentation/PerformanceTracingInterceptor.kt b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/instrumentation/PerformanceTracingInterceptor.kt index 4b0756915..1accb5327 100644 --- a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/instrumentation/PerformanceTracingInterceptor.kt +++ b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/instrumentation/PerformanceTracingInterceptor.kt @@ -1,12 +1,17 @@ package com.squareup.benchmarks.performance.complex.poetry.instrumentation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.remember import androidx.tracing.Trace import com.squareup.benchmarks.performance.complex.poetry.PerformancePoemWorkflow import com.squareup.benchmarks.performance.complex.poetry.PerformancePoemsBrowserWorkflow import com.squareup.workflow1.BaseRenderContext -import com.squareup.workflow1.WorkflowInterceptor import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession +import com.squareup.workflow1.compose.BaseComposeRenderContext +import com.squareup.workflow1.compose.ComposeWorkflowInterceptor +import com.squareup.workflow1.compose.ComposeWorkflowInterceptor.ComposeRenderContextInterceptor import com.squareup.workflow1.workflowIdentifier /** @@ -18,7 +23,7 @@ import com.squareup.workflow1.workflowIdentifier */ class PerformanceTracingInterceptor( private val sample: Boolean = false -) : WorkflowInterceptor, Resettable { +) : ComposeWorkflowInterceptor, Resettable { private var totalRenderPasses = 0 override fun onRender( @@ -28,8 +33,40 @@ class PerformanceTracingInterceptor( proceed: (P, S, RenderContextInterceptor?) -> R, session: WorkflowSession ): R { - val isRoot = session.parent == null val traceIdIndex = NODES_TO_TRACE.indexOfFirst { it.second == session.identifier } + val isRoot = before(traceIdIndex, session) + return proceed(renderProps, renderState, null).also { + after(traceIdIndex = traceIdIndex, isRoot = isRoot) + } + } + + @Composable + override fun Rendering( + renderProps: P, + renderState: S, + context: BaseComposeRenderContext, + session: WorkflowSession, + proceed: @Composable (P, S, ComposeRenderContextInterceptor?) -> R + ): R { + // TODO: Fix that these are illegal side effects in a Composable + val traceIdIndex = remember(session) { + NODES_TO_TRACE.indexOfFirst { it.second == session.identifier } + } + val isRoot = remember(session) { + before(traceIdIndex, session) + } + SideEffect { + after(traceIdIndex = traceIdIndex, isRoot = isRoot) + } + return proceed(renderProps, renderState, null) + } + + private fun before( + traceIdIndex: Int, + session: WorkflowSession + ): Boolean { + val isRoot = session.parent == null + val renderPassMarker = totalRenderPasses.toString() .padStart(RENDER_PASS_DIGITS, '0') @@ -44,17 +81,21 @@ class PerformanceTracingInterceptor( "${NODES_TO_TRACE[traceIdIndex].first}_" Trace.beginSection(sectionName) } + return isRoot + } - return proceed(renderProps, renderState, null).also { - if (traceIdIndex > -1 && !sample) { + private fun after( + traceIdIndex: Int, + isRoot: Boolean + ) { + if (traceIdIndex > -1 && !sample) { + Trace.endSection() + } + if (isRoot) { + if (!sample || totalRenderPasses.mod(2) == 0) { Trace.endSection() } - if (isRoot) { - if (!sample || totalRenderPasses.mod(2) == 0) { - Trace.endSection() - } - totalRenderPasses++ - } + totalRenderPasses++ } } diff --git a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/instrumentation/RenderPassCountingInterceptor.kt b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/instrumentation/RenderPassCountingInterceptor.kt index 748b69132..af4ed26e8 100644 --- a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/instrumentation/RenderPassCountingInterceptor.kt +++ b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/instrumentation/RenderPassCountingInterceptor.kt @@ -1,9 +1,14 @@ package com.squareup.benchmarks.performance.complex.poetry.instrumentation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.remember import com.squareup.workflow1.BaseRenderContext -import com.squareup.workflow1.WorkflowInterceptor import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession +import com.squareup.workflow1.compose.BaseComposeRenderContext +import com.squareup.workflow1.compose.ComposeWorkflowInterceptor +import com.squareup.workflow1.compose.ComposeWorkflowInterceptor.ComposeRenderContextInterceptor /** * Used to count the number of render passes for a Workflow tree as well as each time that a node @@ -13,9 +18,9 @@ import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession * This is convenient to use in integration tests that verify that the # of render passes and the * ratio of 'fresh' to 'stale' renderings for a scenario are constant. */ -class RenderPassCountingInterceptor : WorkflowInterceptor, Resettable { +class RenderPassCountingInterceptor : ComposeWorkflowInterceptor, Resettable { val renderEfficiencyTracking = RenderEfficiency() - lateinit var renderPassStats: RenderStats + private var renderPassStats: RenderStats = RenderStats() private val nodeStates: MutableMap = mutableMapOf() override fun onRender( @@ -25,11 +30,35 @@ class RenderPassCountingInterceptor : WorkflowInterceptor, Resettable { proceed: (P, S, RenderContextInterceptor?) -> R, session: WorkflowSession ): R { - val isRoot = session.parent == null + val isRoot = before(session, renderState) + return proceed(renderProps, renderState, null).also { + after(isRoot) + } + } - if (isRoot) { - renderPassStats = RenderStats() + @Composable + override fun Rendering( + renderProps: P, + renderState: S, + context: BaseComposeRenderContext, + session: WorkflowSession, + proceed: @Composable + (P, S, ComposeRenderContextInterceptor?) -> R + ): R { + val isRoot = remember(session, renderState) { + before(session, renderState) } + SideEffect { + after(isRoot) + } + return proceed(renderProps, renderState, null) + } + + private fun before( + session: WorkflowSession, + renderState: S + ): Boolean { + val isRoot = session.parent == null renderPassStats.apply { // Update stats for this render pass with this node. @@ -46,12 +75,14 @@ class RenderPassCountingInterceptor : WorkflowInterceptor, Resettable { } nodeStates[session.sessionId] = renderStateString } + return isRoot + } - return proceed(renderProps, renderState, null).also { - if (isRoot) { - renderEfficiencyTracking.totalRenderPasses += 1 - renderEfficiencyTracking.totalNodeStats += renderPassStats - } + private fun after(isRoot: Boolean) { + if (isRoot) { + renderEfficiencyTracking.totalRenderPasses += 1 + renderEfficiencyTracking.totalNodeStats += renderPassStats + renderPassStats = RenderStats() } } diff --git a/build.gradle.kts b/build.gradle.kts index 5ab4bf20c..463d690d8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,7 @@ buildscript { classpath(libs.kotlin.serialization.gradle.plugin) classpath(libs.kotlinx.binaryCompatibility.gradle.plugin) classpath(libs.kotlin.gradle.plugin) + classpath(libs.molecule.gradle.plugin) classpath(libs.google.ksp) classpath(libs.ktlint.gradle) classpath(libs.vanniktech.publish) @@ -19,6 +20,9 @@ buildscript { mavenCentral() gradlePluginPortal() google() + maven { + url = uri("https://oss.sonatype.org/content/repositories/snapshots/") + } // For binary compatibility validator. maven { url = uri("https://kotlin.bintray.com/kotlinx") } } diff --git a/dependencies/classpath.txt b/dependencies/classpath.txt index 82368bdac..563e003c9 100644 --- a/dependencies/classpath.txt +++ b/dependencies/classpath.txt @@ -1,5 +1,6 @@ androidx.databinding:databinding-common:7.1.3 androidx.databinding:databinding-compiler-common:7.1.3 +app.cash.molecule:molecule-gradle-plugin:0.3.0-SNAPSHOT com.android.databinding:baseLibrary:7.1.3 com.android.tools.analytics-library:crash:30.1.3 com.android.tools.analytics-library:protos:30.1.3 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e572fc5df..924ed0edb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,8 +9,6 @@ targetSdk = "30" androidx-activity = "1.3.0" androidx-appcompat = "1.3.1" androidx-benchmark = "1.1.0-rc03" -androidx-compose = "1.1.0-rc01" -androidx-compose-compiler = "1.1.0-rc02" androidx-constraintlayout = "2.1.2" androidx-core = "1.6.0" androidx-fragment = "1.3.6" @@ -32,6 +30,9 @@ androidx-transition = "1.4.1" androidx-viewbinding = "4.2.1" androidx-work = "2.6.0" +compose = "1.1.0" +compose-compiler = "1.1.0" + detekt = "1.19.0" dokka = "1.5.31" dependencyGuard = "0.1.0" @@ -49,8 +50,8 @@ kotest = "5.1.0" kotlin = "1.6.10" kotlinx-binary-compatibility = "0.6.0" -kotlinx-coroutines = "1.5.1" -# The 1.5.1 test artifact is jvm-only. The commonTest module should use 1.6.1. +kotlinx-coroutines = "1.5.2" +# The 1.5.2 test artifact is jvm-only. The commonTest module should use 1.6.1. kotlinx-coroutines-test-common = "1.6.1" kotlinx-serialization-json = "1.3.2" kotlinx-benchmark = "0.4.2" @@ -64,6 +65,9 @@ mockito-core = "3.3.3" mockito-kotlin = "3.2.0" mockk = "1.11.0" + +molecule = "0.3.0-SNAPSHOT" + robolectric = "4.6.1" rxjava2-android = "2.1.1" @@ -100,8 +104,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" } @@ -112,13 +120,16 @@ androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "a androidx-macro-benchmark = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "androidx-benchmark" } -androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "androidx-compose" } +androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } + +androidx-compose-material = { module = "androidx.compose.material:material", version.ref = "compose" } -androidx-compose-material = { module = "androidx.compose.material:material", version.ref = "androidx-compose" } +androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } +androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" } +androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } -androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "androidx-compose" } -androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-compose" } -androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", 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" } @@ -203,6 +214,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"} + robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } rxjava2-rxandroid = { module = "io.reactivex.rxjava2:rxandroid", version.ref = "rxjava2-android" } diff --git a/samples/compose-samples/build.gradle.kts b/samples/compose-samples/build.gradle.kts index c7ee68f31..91908dac2 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() } } diff --git a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListWorkflow.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListWorkflow.kt index 570cdee4e..a4a1bee38 100644 --- a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListWorkflow.kt +++ b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListWorkflow.kt @@ -1,5 +1,6 @@ package com.squareup.sample.poetry +import com.squareup.sample.poetry.StanzaListWorkflow.NO_SELECTED_STANZA import com.squareup.sample.poetry.StanzaListWorkflow.Props import com.squareup.sample.poetry.model.Poem import com.squareup.workflow1.StatelessWorkflow diff --git a/settings.gradle.kts b/settings.gradle.kts index 662672fed..19d7d39a9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,6 +9,8 @@ pluginManagement { google() // For binary compatibility validator. maven { url = uri("https://kotlin.bintray.com/kotlinx") } + // For molecule SNAPSHOT + maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots/")} } } @@ -29,6 +31,8 @@ dependencyResolutionManagement { repositories { mavenCentral() google() + // For molecule SNAPSHOT + maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots/")} // See androidx.dev (can use this for Snapshot builds of AndroidX) // maven { url = java.net.URI.create("https://androidx.dev/snapshots/builds/8224905/artifacts/repository") } } @@ -63,6 +67,7 @@ include( ":workflow-config:config-android", ":workflow-config:config-jvm", ":workflow-core", + ":workflow-core-compose", ":workflow-runtime", ":workflow-rx2", ":workflow-testing", diff --git a/trace-encoder/dependencies/runtimeClasspath.txt b/trace-encoder/dependencies/runtimeClasspath.txt index ec8fab80a..327123338 100644 --- a/trace-encoder/dependencies/runtimeClasspath.txt +++ b/trace-encoder/dependencies/runtimeClasspath.txt @@ -6,6 +6,6 @@ org.jetbrains.kotlin:kotlin-stdlib-common:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10 org.jetbrains.kotlin:kotlin-stdlib:1.6.10 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2 org.jetbrains:annotations:13.0 diff --git a/workflow-config/config-android/dependencies/releaseRuntimeClasspath.txt b/workflow-config/config-android/dependencies/releaseRuntimeClasspath.txt index 0e0e4fb6e..30bf3f85e 100644 --- a/workflow-config/config-android/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-config/config-android/dependencies/releaseRuntimeClasspath.txt @@ -7,6 +7,6 @@ org.jetbrains.kotlin:kotlin-stdlib-common:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10 org.jetbrains.kotlin:kotlin-stdlib:1.6.10 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2 org.jetbrains:annotations:13.0 diff --git a/workflow-config/config-jvm/dependencies/runtimeClasspath.txt b/workflow-config/config-jvm/dependencies/runtimeClasspath.txt index 0e0e4fb6e..30bf3f85e 100644 --- a/workflow-config/config-jvm/dependencies/runtimeClasspath.txt +++ b/workflow-config/config-jvm/dependencies/runtimeClasspath.txt @@ -7,6 +7,6 @@ org.jetbrains.kotlin:kotlin-stdlib-common:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10 org.jetbrains.kotlin:kotlin-stdlib:1.6.10 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2 org.jetbrains:annotations:13.0 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..89e3bfa7d --- /dev/null +++ b/workflow-core-compose/api/workflow-core-compose.api @@ -0,0 +1,201 @@ +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 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 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/ChainedComposeWorkflowInterceptorKt { + public static final fun chained (Ljava/util/List;)Lcom/squareup/workflow1/compose/ComposeWorkflowInterceptor; +} + +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 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 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 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 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 (Lcom/squareup/workflow1/compose/StatefulComposeWorkflow;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 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 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 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; +} + diff --git a/workflow-core-compose/build.gradle.kts b/workflow-core-compose/build.gradle.kts new file mode 100644 index 000000000..06c6c19e5 --- /dev/null +++ b/workflow-core-compose/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + `kotlin-multiplatform` + published + id("app.cash.molecule") +} + +kotlin { + jvm { withJava() } + // TODO: No native targets yet for Molecule until Compose 1.2.0 available in JB KMP runtime. + // 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/dependencies/jvmRuntimeClasspath.txt b/workflow-core-compose/dependencies/jvmRuntimeClasspath.txt new file mode 100644 index 000000000..82c4153cf --- /dev/null +++ b/workflow-core-compose/dependencies/jvmRuntimeClasspath.txt @@ -0,0 +1,15 @@ +:workflow-core +:workflow-runtime +app.cash.molecule:molecule-runtime-jvm:0.3.0-SNAPSHOT +app.cash.molecule:molecule-runtime:0.3.0-SNAPSHOT +com.squareup.okio:okio-jvm:3.0.0 +com.squareup.okio:okio:3.0.0 +org.jetbrains.compose.runtime:runtime-desktop:1.1.0 +org.jetbrains.compose.runtime:runtime:1.1.0 +org.jetbrains.kotlin:kotlin-bom:1.6.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10 +org.jetbrains.kotlin:kotlin-stdlib:1.6.10 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2 +org.jetbrains:annotations:13.0 diff --git a/workflow-core-compose/dependencies/runtimeClasspath.txt b/workflow-core-compose/dependencies/runtimeClasspath.txt new file mode 100644 index 000000000..8a28ebcd9 --- /dev/null +++ b/workflow-core-compose/dependencies/runtimeClasspath.txt @@ -0,0 +1,16 @@ +:workflow-core +:workflow-runtime +app.cash.molecule:molecule-runtime-jvm:0.3.0-SNAPSHOT +app.cash.molecule:molecule-runtime:0.3.0-SNAPSHOT +com.squareup.okio:okio-jvm:3.0.0 +com.squareup.okio:okio:3.0.0 +org.jetbrains.compose.runtime:runtime-desktop:1.1.0 +org.jetbrains.compose.runtime:runtime:1.1.0 +org.jetbrains.kotlin:kotlin-bom:1.6.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.6.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10 +org.jetbrains.kotlin:kotlin-stdlib:1.6.10 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2 +org.jetbrains:annotations:13.0 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..d6baed711 --- /dev/null +++ b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/BaseComposeRenderContext.kt @@ -0,0 +1,57 @@ +package com.squareup.workflow1.compose + +import androidx.compose.runtime.Composable +import com.squareup.workflow1.BaseRenderContext +import com.squareup.workflow1.Workflow +import com.squareup.workflow1.WorkflowAction +import com.squareup.workflow1.WorkflowAction.Companion.noAction + +/** + * @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 +} + +/** + * 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() } 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..4c2009164 --- /dev/null +++ b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/ChainedComposeWorkflowInterceptor.kt @@ -0,0 +1,124 @@ +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) + } + + 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..6eaa99440 --- /dev/null +++ b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/ComposeRuntimePlugin.kt @@ -0,0 +1,72 @@ +package com.squareup.workflow1.compose + +import androidx.compose.runtime.BroadcastFrameClock +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.flow.StateFlow +import kotlinx.coroutines.plus +import kotlinx.coroutines.yield + +/** + * [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 var composeWaitingForFrame = false + private val composeRuntimeClock = BroadcastFrameClock { + composeWaitingForFrame = true + } + + 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 { + (workflowRunner as WorkflowComposeRunner).nextComposedRendering() + } + } + + override suspend fun nextRendering() { + if (composeWaitingForFrame) { + composeWaitingForFrame = false + composeRuntimeClock.sendFrame(0L) + yield() + } + } + + 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/ComposeWorkflowInterceptor.kt b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/ComposeWorkflowInterceptor.kt new file mode 100644 index 000000000..b8f61806d --- /dev/null +++ b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/ComposeWorkflowInterceptor.kt @@ -0,0 +1,168 @@ +package com.squareup.workflow1.compose + +import androidx.compose.runtime.Composable +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) + } +} + +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) + } +} 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..55aa1ecac --- /dev/null +++ b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/RealComposeRenderContext.kt @@ -0,0 +1,46 @@ +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.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) + } +} 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..b43fbbf56 --- /dev/null +++ b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/WorkflowComposeChildNode.kt @@ -0,0 +1,50 @@ +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) } + @Suppress("UNCHECKED_CAST") + (workflowNode as WorkflowComposeNode).Rendering( + workflow as StatefulComposeWorkflow, + props as ChildPropsT, + rendering, + ) + return rendering.value!! + } +} 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..2f07f67da --- /dev/null +++ b/workflow-core-compose/src/commonMain/kotlin/com/squareup/workflow1/compose/WorkflowComposeNode.kt @@ -0,0 +1,140 @@ +package com.squareup.workflow1.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.key +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, + 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 + ) + + /** + * 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 + ) { + UpdatePropsAndState(workflow, props) + + val (baseRenderContext, renderContext) = remember( + state, + props, + workflow, + rendering.value + ) { + // Use the RenderContext once. After rendering successfully it is frozen until new state. + val base = RealComposeRenderContext( + renderer = subtreeManager, + sideEffectRunner = this, + eventActionsChannel = eventActionsChannel + ) + base to ComposeRenderContext(workflow = workflow, baseContext = base) + } + + rendering.value = interceptor.asComposeWorkflowInterceptor().intercept(workflow, this) + .Rendering(props, state, renderContext) + + SideEffect { + baseRenderContext.freeze() + commitAndUpdateScopes() + } + } + + @Composable + private fun UpdatePropsAndState( + workflow: StatefulComposeWorkflow, + newProps: PropsT + ) { + key(newProps) { + if (newProps != lastProps) { + state = interceptor + .asComposeWorkflowInterceptor() + .intercept(workflow, this@WorkflowComposeNode) + .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..fbbfff7b0 --- /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 { + // 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/api/workflow-core.api b/workflow-core/api/workflow-core.api index e7fe83557..b12e71f1f 100644 --- a/workflow-core/api/workflow-core.api +++ b/workflow-core/api/workflow-core.api @@ -122,7 +122,7 @@ 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; @@ -143,10 +143,11 @@ 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; diff --git a/workflow-core/dependencies/jvmRuntimeClasspath.txt b/workflow-core/dependencies/jvmRuntimeClasspath.txt index 81d8321d1..94a9cb2bd 100644 --- a/workflow-core/dependencies/jvmRuntimeClasspath.txt +++ b/workflow-core/dependencies/jvmRuntimeClasspath.txt @@ -4,6 +4,6 @@ org.jetbrains.kotlin:kotlin-bom:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10 org.jetbrains.kotlin:kotlin-stdlib:1.6.10 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2 org.jetbrains:annotations:13.0 diff --git a/workflow-core/dependencies/runtimeClasspath.txt b/workflow-core/dependencies/runtimeClasspath.txt index 19adaaffc..6a9b6ed46 100644 --- a/workflow-core/dependencies/runtimeClasspath.txt +++ b/workflow-core/dependencies/runtimeClasspath.txt @@ -5,6 +5,6 @@ org.jetbrains.kotlin:kotlin-stdlib-common:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10 org.jetbrains.kotlin:kotlin-stdlib:1.6.10 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2 org.jetbrains:annotations:13.0 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..670578a47 100644 --- a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/BaseRenderContext.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/BaseRenderContext.kt @@ -241,6 +241,7 @@ BaseRenderContext.renderChild( child: Workflow, key: String = "" ): ChildRenderingT = renderChild(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. 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..640e77445 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 @@ -105,6 +105,22 @@ public abstract class StatefulWorkflow< state: StateT ): StateT = state + /** + * Called whenever the state changes to generate a new [Snapshot] of the state. + * + * **Snapshots must be lazy.** + * + * Serialization must not be done at the time this method is called, + * since the state will be snapshotted frequently but the serialized form may only be needed very + * rarely. + * + * If the workflow does not have any state, or should always be started from scratch, return + * `null` from this method. + * + * @see initialState + */ + public abstract fun snapshotState(state: StateT): Snapshot? + /** * Called at least once† any time one of the following things happens: * - This workflow's [renderProps] changes (via the parent passing a different one in). @@ -129,22 +145,6 @@ public abstract class StatefulWorkflow< context: RenderContext ): RenderingT - /** - * Called whenever the state changes to generate a new [Snapshot] of the state. - * - * **Snapshots must be lazy.** - * - * Serialization must not be done at the time this method is called, - * since the state will be snapshotted frequently but the serialized form may only be needed very - * rarely. - * - * If the workflow does not have any state, or should always be started from scratch, return - * `null` from this method. - * - * @see initialState - */ - public abstract fun snapshotState(state: StateT): Snapshot? - /** * Satisfies the [Workflow] interface by returning `this`. */ @@ -194,7 +194,7 @@ public inline fun Workflow.Companion.state override fun render( renderProps: PropsT, renderState: StateT, - context: RenderContext + context: StatefulWorkflow.RenderContext ): RenderingT = render(context, renderProps, renderState) override fun snapshotState(state: StateT) = snapshot(state) @@ -207,11 +207,12 @@ public inline fun Workflow.Companion.stateful( crossinline initialState: (Snapshot?) -> StateT, crossinline render: BaseRenderContext.(state: StateT) -> RenderingT, crossinline snapshot: (StateT) -> Snapshot? -): StatefulWorkflow = stateful( - { _, initialSnapshot -> initialState(initialSnapshot) }, - { _, state -> render(state) }, - snapshot -) +): StatefulWorkflow = + stateful( + initialState = { _, initialSnapshot: Snapshot? -> initialState(initialSnapshot) }, + render = { _, state -> render(state) }, + snapshot = snapshot + ) /** * Returns a stateful [Workflow] implemented via the given functions. @@ -230,10 +231,10 @@ public inline fun Workflow.Companion.state state: StateT ) -> StateT = { _, _, state -> state } ): StatefulWorkflow = stateful( - { props, _ -> initialState(props) }, - render, - { null }, - onPropsChanged + initialState = { props, _ -> initialState(props) }, + render = render, + snapshot = { null }, + onPropsChanged = onPropsChanged ) /** @@ -243,10 +244,10 @@ public inline fun Workflow.Companion.state */ public inline fun Workflow.Companion.stateful( initialState: StateT, - crossinline render: BaseRenderContext.(state: StateT) -> RenderingT + crossinline render: BaseRenderContext.(state: StateT) -> RenderingT, ): StatefulWorkflow = stateful( - { initialState }, - { _, state -> render(state) } + initialState = { initialState }, + render = { _, state -> render(state) } ) /** 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..acb32a3ac 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,16 @@ 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: @@ -87,7 +87,7 @@ public fun RenderContext( * their own internal state. */ public inline fun Workflow.Companion.stateless( - crossinline render: BaseRenderContext.(props: PropsT) -> RenderingT + crossinline render: BaseRenderContext.(props: PropsT) -> RenderingT, ): Workflow = object : StatelessWorkflow() { override fun render( @@ -102,7 +102,7 @@ public inline fun Workflow.Companion.stateless( */ public fun Workflow.Companion.rendering( rendering: RenderingT -): Workflow = stateless { rendering } +): Workflow = stateless(render = { rendering }) /** * Convenience to create a [WorkflowAction] with parameter types matching those diff --git a/workflow-runtime/api/workflow-runtime.api b/workflow-runtime/api/workflow-runtime.api index bac7d553b..2daa13301 100644 --- a/workflow-runtime/api/workflow-runtime.api +++ b/workflow-runtime/api/workflow-runtime.api @@ -1,3 +1,79 @@ +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 forEachActive$wf1_workflow_runtime (Lkotlin/jvm/functions/Function1;)V + 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 retainOrCreate$wf1_workflow_runtime (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;)Lcom/squareup/workflow1/InlineLinkedList$InlineListNode; + 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/ChainedWorkflowInterceptorKt { + public static final fun chained (Ljava/util/List;)Lcom/squareup/workflow1/WorkflowInterceptor; +} + +public final class com/squareup/workflow1/IdCounter { + public fun ()V + public final fun createId ()J +} + +public final class com/squareup/workflow1/IdCounterKt { + public static final fun createId (Lcom/squareup/workflow1/IdCounter;)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 + 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 +83,40 @@ 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; + 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 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 { @@ -22,6 +129,7 @@ public final class com/squareup/workflow1/RenderingAndSnapshot { public abstract interface class com/squareup/workflow1/RuntimeConfig { public static final field Companion Lcom/squareup/workflow1/RuntimeConfig$Companion; + public abstract fun getUseComposeInRuntime ()Z } public final class com/squareup/workflow1/RuntimeConfig$Companion { @@ -30,19 +138,22 @@ public final class com/squareup/workflow1/RuntimeConfig$Companion { public final class com/squareup/workflow1/RuntimeConfig$FrameTimeout : com/squareup/workflow1/RuntimeConfig { public fun ()V - public fun (J)V - public synthetic fun (JILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (JZ)V + public synthetic fun (JZILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()J - public final fun copy (J)Lcom/squareup/workflow1/RuntimeConfig$FrameTimeout; - public static synthetic fun copy$default (Lcom/squareup/workflow1/RuntimeConfig$FrameTimeout;JILjava/lang/Object;)Lcom/squareup/workflow1/RuntimeConfig$FrameTimeout; + public final fun component2 ()Z + public final fun copy (JZ)Lcom/squareup/workflow1/RuntimeConfig$FrameTimeout; + public static synthetic fun copy$default (Lcom/squareup/workflow1/RuntimeConfig$FrameTimeout;JZILjava/lang/Object;)Lcom/squareup/workflow1/RuntimeConfig$FrameTimeout; public fun equals (Ljava/lang/Object;)Z public final fun getFrameTimeoutMs ()J + public fun getUseComposeInRuntime ()Z public fun hashCode ()I public fun toString ()Ljava/lang/String; } public final class com/squareup/workflow1/RuntimeConfig$RenderPerAction : com/squareup/workflow1/RuntimeConfig { public static final field INSTANCE Lcom/squareup/workflow1/RuntimeConfig$RenderPerAction; + public fun getUseComposeInRuntime ()Z } public class com/squareup/workflow1/SimpleLoggingWorkflowInterceptor : com/squareup/workflow1/WorkflowInterceptor { @@ -58,12 +169,31 @@ 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 fun tickChildren$wf1_workflow_runtime (Lkotlinx/coroutines/selects/SelectBuilder;)V +} + public final class com/squareup/workflow1/TreeSnapshot { public static final field Companion Lcom/squareup/workflow1/TreeSnapshot$Companion; public fun (Lcom/squareup/workflow1/Snapshot;Lkotlin/jvm/functions/Function0;)V public fun equals (Ljava/lang/Object;)Z - public final fun getChildTreeSnapshots$wf1_workflow_runtime ()Ljava/util/Map; - public final fun getWorkflowSnapshot$wf1_workflow_runtime ()Lcom/squareup/workflow1/Snapshot; + 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; } @@ -73,6 +203,21 @@ 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 final fun setHandler$wf1_workflow_runtime (Lkotlin/jvm/functions/Function1;)V + 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 { } @@ -115,175 +260,102 @@ 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 final class com/squareup/workflow1/internal/ActiveStagingList { - public fun ()V - public final fun commitStaging (Lkotlin/jvm/functions/Function1;)V - public final fun forEachActive (Lkotlin/jvm/functions/Function1;)V - public final fun forEachStaging (Lkotlin/jvm/functions/Function1;)V - public final fun retainOrCreate (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;)Lcom/squareup/workflow1/internal/InlineLinkedList$InlineListNode; -} - -public final class com/squareup/workflow1/internal/ChainedWorkflowInterceptor : com/squareup/workflow1/WorkflowInterceptor { - public fun (Ljava/util/List;)V - 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/internal/ChainedWorkflowInterceptorKt { - public static final fun chained (Ljava/util/List;)Lcom/squareup/workflow1/WorkflowInterceptor; -} - -public final class com/squareup/workflow1/internal/IdCounter { - public fun ()V - public final fun createId ()J -} - -public final class com/squareup/workflow1/internal/IdCounterKt { - public static final fun createId (Lcom/squareup/workflow1/internal/IdCounter;)J -} - -public final class com/squareup/workflow1/internal/InlineLinkedList { - public fun ()V - public final fun clear ()V - public final fun forEach (Lkotlin/jvm/functions/Function1;)V - public final fun getHead ()Lcom/squareup/workflow1/internal/InlineLinkedList$InlineListNode; - public final fun getTail ()Lcom/squareup/workflow1/internal/InlineLinkedList$InlineListNode; - public final fun plusAssign (Lcom/squareup/workflow1/internal/InlineLinkedList$InlineListNode;)V - public final fun removeFirst (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/internal/InlineLinkedList$InlineListNode; - public final fun setHead (Lcom/squareup/workflow1/internal/InlineLinkedList$InlineListNode;)V - public final fun setTail (Lcom/squareup/workflow1/internal/InlineLinkedList$InlineListNode;)V -} - -public abstract interface class com/squareup/workflow1/internal/InlineLinkedList$InlineListNode { - public abstract fun getNextListNode ()Lcom/squareup/workflow1/internal/InlineLinkedList$InlineListNode; - public abstract fun setNextListNode (Lcom/squareup/workflow1/internal/InlineLinkedList$InlineListNode;)V -} - -public final class com/squareup/workflow1/internal/RealRenderContext : com/squareup/workflow1/BaseRenderContext, com/squareup/workflow1/Sink { - public fun (Lcom/squareup/workflow1/internal/RealRenderContext$Renderer;Lcom/squareup/workflow1/internal/RealRenderContext$SideEffectRunner;Lkotlinx/coroutines/channels/SendChannel;)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; - 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 abstract interface class com/squareup/workflow1/internal/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/internal/RealRenderContext$SideEffectRunner { - public abstract fun runningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V -} - -public final class com/squareup/workflow1/internal/SideEffectNode : com/squareup/workflow1/internal/InlineLinkedList$InlineListNode { - public fun (Ljava/lang/String;Lkotlinx/coroutines/Job;)V - public final fun getJob ()Lkotlinx/coroutines/Job; - public final fun getKey ()Ljava/lang/String; - public synthetic fun getNextListNode ()Lcom/squareup/workflow1/internal/InlineLinkedList$InlineListNode; - public fun getNextListNode ()Lcom/squareup/workflow1/internal/SideEffectNode; - public synthetic fun setNextListNode (Lcom/squareup/workflow1/internal/InlineLinkedList$InlineListNode;)V - public fun setNextListNode (Lcom/squareup/workflow1/internal/SideEffectNode;)V -} - -public final class com/squareup/workflow1/internal/SubtreeManager : com/squareup/workflow1/internal/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/internal/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/internal/IdCounter;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun commitRenderedChildren ()V - public final fun createChildSnapshots ()Ljava/util/Map; - public fun render (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; - public final fun tickChildren (Lkotlinx/coroutines/selects/SelectBuilder;)V -} - -public final class com/squareup/workflow1/internal/SystemUtilsKt { - public static final fun currentTimeMillis ()J -} - -public final class com/squareup/workflow1/internal/ThrowablesKt { - public static final fun unwrapCancellationCause (Ljava/lang/Throwable;)Ljava/lang/Throwable; -} - -public final class com/squareup/workflow1/internal/WorkflowChildNode : com/squareup/workflow1/internal/InlineLinkedList$InlineListNode { - public fun (Lcom/squareup/workflow1/Workflow;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/internal/WorkflowNode;)V - public final fun acceptChildOutput (Ljava/lang/Object;)Lcom/squareup/workflow1/WorkflowAction; - public final fun getId ()Lcom/squareup/workflow1/internal/WorkflowNodeId; - public synthetic fun getNextListNode ()Lcom/squareup/workflow1/internal/InlineLinkedList$InlineListNode; - public fun getNextListNode ()Lcom/squareup/workflow1/internal/WorkflowChildNode; - public final fun getWorkflow ()Lcom/squareup/workflow1/Workflow; - public final fun getWorkflowNode ()Lcom/squareup/workflow1/internal/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 final fun setHandler (Lkotlin/jvm/functions/Function1;)V - public synthetic fun setNextListNode (Lcom/squareup/workflow1/internal/InlineLinkedList$InlineListNode;)V - public fun setNextListNode (Lcom/squareup/workflow1/internal/WorkflowChildNode;)V -} - -public final class com/squareup/workflow1/internal/WorkflowNode : com/squareup/workflow1/WorkflowInterceptor$WorkflowSession, com/squareup/workflow1/internal/RealRenderContext$SideEffectRunner, kotlinx/coroutines/CoroutineScope { - public fun (Lcom/squareup/workflow1/internal/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/internal/IdCounter;)V - public synthetic fun (Lcom/squareup/workflow1/internal/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/internal/IdCounter;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun cancel (Ljava/util/concurrent/CancellationException;)V - public static synthetic fun cancel$default (Lcom/squareup/workflow1/internal/WorkflowNode;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V +public class com/squareup/workflow1/WorkflowNode : com/squareup/workflow1/RealRenderContext$SideEffectRunner, com/squareup/workflow1/WorkflowInterceptor$WorkflowSession, kotlinx/coroutines/CoroutineScope { + 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; + public final fun cancel$wf1_workflow_runtime (Ljava/util/concurrent/CancellationException;)V + public static synthetic fun cancel$wf1_workflow_runtime$default (Lcom/squareup/workflow1/WorkflowNode;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V + protected final fun commitAndUpdateScopes ()V public fun getCoroutineContext ()Lkotlin/coroutines/CoroutineContext; - public final fun getId ()Lcom/squareup/workflow1/internal/WorkflowNodeId; + 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 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 final 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 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 final fun tick (Lkotlinx/coroutines/selects/SelectBuilder;)V + public final fun startSession ()V + public final fun tick$wf1_workflow_runtime (Lkotlinx/coroutines/selects/SelectBuilder;)V public fun toString ()Ljava/lang/String; } -public final class com/squareup/workflow1/internal/WorkflowNodeId { - public static final field Companion Lcom/squareup/workflow1/internal/WorkflowNodeId$Companion; +public final class com/squareup/workflow1/WorkflowNodeId { + public static final field Companion Lcom/squareup/workflow1/WorkflowNodeId$Companion; 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 component1$wf1_workflow_runtime ()Lcom/squareup/workflow1/WorkflowIdentifier; public final fun component2$wf1_workflow_runtime ()Ljava/lang/String; - public final fun copy (Lcom/squareup/workflow1/WorkflowIdentifier;Ljava/lang/String;)Lcom/squareup/workflow1/internal/WorkflowNodeId; - public static synthetic fun copy$default (Lcom/squareup/workflow1/internal/WorkflowNodeId;Lcom/squareup/workflow1/WorkflowIdentifier;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/workflow1/internal/WorkflowNodeId; + 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 final fun getIdentifier$wf1_workflow_runtime ()Lcom/squareup/workflow1/WorkflowIdentifier; public final fun getName$wf1_workflow_runtime ()Ljava/lang/String; public fun hashCode ()I - public final fun matches (Lcom/squareup/workflow1/Workflow;Ljava/lang/String;)Z + public final fun matches$wf1_workflow_runtime (Lcom/squareup/workflow1/Workflow;Ljava/lang/String;)Z public final fun toByteStringOrNull$wf1_workflow_runtime ()Lokio/ByteString; public fun toString ()Ljava/lang/String; } -public final class com/squareup/workflow1/internal/WorkflowNodeId$Companion { - public final fun parse (Lokio/ByteString;)Lcom/squareup/workflow1/internal/WorkflowNodeId; +public final class com/squareup/workflow1/WorkflowNodeId$Companion { + public final fun parse (Lokio/ByteString;)Lcom/squareup/workflow1/WorkflowNodeId; } -public final class com/squareup/workflow1/internal/WorkflowNodeIdKt { - public static final fun id (Lcom/squareup/workflow1/Workflow;Ljava/lang/String;)Lcom/squareup/workflow1/internal/WorkflowNodeId; - public static synthetic fun id$default (Lcom/squareup/workflow1/Workflow;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/workflow1/internal/WorkflowNodeId; +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 final class com/squareup/workflow1/internal/WorkflowRunner { +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 - public final fun cancelRuntime (Ljava/util/concurrent/CancellationException;)V - public static synthetic fun cancelRuntime$default (Lcom/squareup/workflow1/internal/WorkflowRunner;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V - public final fun nextRendering ()Lcom/squareup/workflow1/RenderingAndSnapshot; - public final fun processActions (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun cancelRuntime$wf1_workflow_runtime (Ljava/util/concurrent/CancellationException;)V + public static synthetic fun cancelRuntime$wf1_workflow_runtime$default (Lcom/squareup/workflow1/WorkflowRunner;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)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; + public final fun nextRendering$wf1_workflow_runtime ()Lcom/squareup/workflow1/RenderingAndSnapshot; + public final fun processActions$wf1_workflow_runtime (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + 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; +} + +public final class com/squareup/workflow1/internal/SideEffectNode : com/squareup/workflow1/InlineLinkedList$InlineListNode { + public fun (Ljava/lang/String;Lkotlinx/coroutines/Job;)V + public final fun getJob ()Lkotlinx/coroutines/Job; + public final fun getKey ()Ljava/lang/String; + public synthetic fun getNextListNode ()Lcom/squareup/workflow1/InlineLinkedList$InlineListNode; + public fun getNextListNode ()Lcom/squareup/workflow1/internal/SideEffectNode; + public synthetic fun setNextListNode (Lcom/squareup/workflow1/InlineLinkedList$InlineListNode;)V + public fun setNextListNode (Lcom/squareup/workflow1/internal/SideEffectNode;)V +} + +public final class com/squareup/workflow1/internal/SystemUtilsKt { + public static final fun currentTimeMillis ()J + public static final fun nanoTime ()J +} + +public final class com/squareup/workflow1/internal/ThrowablesKt { + public static final fun unwrapCancellationCause (Ljava/lang/Throwable;)Ljava/lang/Throwable; } diff --git a/workflow-runtime/build.gradle.kts b/workflow-runtime/build.gradle.kts index 57f66e1cc..9433f1c30 100644 --- a/workflow-runtime/build.gradle.kts +++ b/workflow-runtime/build.gradle.kts @@ -25,7 +25,8 @@ kotlin { } } } - ios() + // TODO: No native targets yet for Molecule until Compose 1.2.0 available in JB KMP runtime. + // ios() sourceSets { all { diff --git a/workflow-runtime/dependencies/jvmRuntimeClasspath.txt b/workflow-runtime/dependencies/jvmRuntimeClasspath.txt index 645cfead0..6b1f421e4 100644 --- a/workflow-runtime/dependencies/jvmRuntimeClasspath.txt +++ b/workflow-runtime/dependencies/jvmRuntimeClasspath.txt @@ -5,6 +5,6 @@ org.jetbrains.kotlin:kotlin-bom:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10 org.jetbrains.kotlin:kotlin-stdlib:1.6.10 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2 org.jetbrains:annotations:13.0 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..30a0ac30e 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): 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 78% 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 fdc42ecc7..a1abc595f 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, +public open class RealRenderContext( + protected open val renderer: Renderer, private 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 ) @@ -72,12 +68,12 @@ internal class RealRenderContext( /** * Freezes this context so that any further calls to this context will throw. */ - fun freeze() { + public fun freeze() { checkNotFrozen() frozen = true } - 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 476faefd3..767f37660 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt @@ -1,7 +1,5 @@ package com.squareup.workflow1 -import com.squareup.workflow1.internal.WorkflowRunner -import com.squareup.workflow1.internal.chained import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -97,6 +95,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. @@ -109,29 +110,44 @@ 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 = - WorkflowRunner(scope, workflow, props, initialSnapshot, chainedInterceptor, runtimeConfig) + val runner = workflowRuntimePlugin?.createWorkflowRunner( + scope, workflow, props, initialSnapshot, chainedInterceptor, runtimeConfig + ) + ?: WorkflowRunner(scope, workflow, props, initialSnapshot, chainedInterceptor, runtimeConfig) // Rendering is synchronous, so we can run the first render pass before launching the runtime // coroutine to calculate the initial rendering. - val renderingsAndSnapshots = 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 renderingsAndSnapshots = if (runtimeConfig.useComposeInRuntime) { + require(workflowRuntimePlugin != null) { + "Cannot use compose without plugging in" + + " the workflow-compose-core module." } - ) + workflowRuntimePlugin.initializeRenderingStream( + runner, + runtimeScope = scope + ) + } else { + 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 + } + ) + } scope.launch { while (isActive) { @@ -146,7 +162,13 @@ public fun renderWorkflowIn( // After receiving an output, the next render pass must be done before emitting that output, // so that the workflow states appear consistent to observers of the outputs and renderings. - renderingsAndSnapshots.value = runner.nextRendering() + if (runtimeConfig.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. + workflowRuntimePlugin?.nextRendering() + } else { + (renderingsAndSnapshots as MutableStateFlow).value = runner.nextRendering() + } output?.let { onOutput(it.value) } } } 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 b9e5f3435..478f251b4 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt @@ -19,18 +19,24 @@ public annotation class WorkflowExperimentalRuntime * A specification of the Workflow Runtime. */ public sealed interface RuntimeConfig { + public val useComposeInRuntime: Boolean /** * This version of the runtime will process as many actions as possible after one is received * until [frameTimeoutMs] has passed, at which point it will render(). */ @WorkflowExperimentalRuntime - public data class FrameTimeout(public val frameTimeoutMs: Long = 30L) : RuntimeConfig + public data class FrameTimeout( + public val frameTimeoutMs: Long = 30L, + public override val useComposeInRuntime: Boolean = false + ) : RuntimeConfig /** * 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 { + override val useComposeInRuntime: Boolean = false + } 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 82% 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 434b16c19..f4858c2fa 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,6 @@ -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.WorkflowInterceptor.WorkflowSession -import com.squareup.workflow1.identifier import kotlinx.coroutines.selects.SelectBuilder import kotlin.coroutines.CoroutineContext @@ -81,15 +74,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 +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 ) : RealRenderContext.Renderer { - private var children = ActiveStagingList>() + protected var children: ActiveStagingList> = ActiveStagingList() /** * Moves all the nodes that have been accumulated in the staging list to the active list, making @@ -97,7 +90,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 +107,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,20 +135,20 @@ internal class SubtreeManager( create = { createChildNode(child, props, key, handler) } ) stagedChild.setHandler(handler) - return stagedChild.render(child.asStatefulWorkflow(), props) + return stagedChild } /** * Uses [selector] to invoke [WorkflowNode.tick] for every running child workflow this instance * is managing. */ - fun tickChildren(selector: SelectBuilder) { + internal fun tickChildren(selector: SelectBuilder) { children.forEachActive { child -> child.workflowNode.tick(selector) } } - fun createChildSnapshots(): Map { + public fun createChildSnapshots(): Map { val snapshots = mutableMapOf() children.forEachActive { child -> val childWorkflow = child.workflow.asStatefulWorkflow() @@ -149,7 +157,7 @@ internal class SubtreeManager( return snapshots } - private fun createChildNode( + protected open fun createChildNode( child: Workflow, initialProps: ChildPropsT, key: String, @@ -175,7 +183,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..c2da75efe 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> { 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 73% 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 db4921d8c..75c3ec10f 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 @@ -37,15 +26,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 val workflow: StatefulWorkflow, + private val initialProps: PropsT, + private 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 { @@ -53,32 +42,50 @@ 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 - - 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) + + 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 fun startSession() { + interceptor.onSessionStarted(workflowScope = this, session = this) + state = interceptor.intercept(workflow = workflow, workflowSession = this) + .initialState(initialProps, initialSnapshot?.workflowSnapshot) } override fun toString(): String { @@ -97,7 +104,7 @@ internal class WorkflowNode( * render themselves and aggregate those child renderings. */ @Suppress("UNCHECKED_CAST") - fun render( + public fun render( workflow: StatefulWorkflow, input: PropsT ): RenderingT = @@ -107,7 +114,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() @@ -144,7 +152,7 @@ internal class WorkflowNode( * * It is an error to call this method after calling [cancel]. */ - fun tick(selector: SelectBuilder) { + internal fun tick(selector: SelectBuilder) { // Listen for any child workflow updates. subtreeManager.tickChildren(selector) @@ -162,7 +170,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 @@ -189,14 +197,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( @@ -215,7 +227,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 79% 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 9a7e70fec..f0ea59040 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,20 +1,12 @@ -package com.squareup.workflow1.internal +package com.squareup.workflow1 -import com.squareup.workflow1.ActionProcessingResult -import com.squareup.workflow1.PropsUpdated -import com.squareup.workflow1.RenderingAndSnapshot -import com.squareup.workflow1.RuntimeConfig import com.squareup.workflow1.RuntimeConfig.FrameTimeout -import com.squareup.workflow1.TimeoutForFrame -import com.squareup.workflow1.TreeSnapshot -import com.squareup.workflow1.Workflow -import com.squareup.workflow1.WorkflowExperimentalRuntime -import com.squareup.workflow1.WorkflowInterceptor -import com.squareup.workflow1.WorkflowOutput +import com.squareup.workflow1.internal.currentTimeMillis 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 @@ -22,17 +14,18 @@ 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() + 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, @@ -46,18 +39,23 @@ 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 } + protected val propsChannel: ReceiveChannel = props.dropWhile { it == currentProps } .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. @@ -65,7 +63,7 @@ internal class WorkflowRunner( * This method must be called before the first call to [processActions], and must be called again * between every subsequent call to [processActions]. */ - fun nextRendering(): RenderingAndSnapshot { + internal fun nextRendering(): RenderingAndSnapshot { val rendering = rootNode.render(workflow, currentProps) val snapshot = rootNode.snapshot(workflow) return RenderingAndSnapshot(rendering, snapshot) @@ -82,7 +80,7 @@ internal class WorkflowRunner( * In those cases we return null. */ @OptIn(WorkflowExperimentalRuntime::class) - suspend fun processActions(): WorkflowOutput? { + internal suspend fun processActions(): WorkflowOutput? { // First we block and wait until there is an action to process. var processingResult: ActionProcessingResult? = select { onPropsUpdated() @@ -139,7 +137,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/commonMain/kotlin/com/squareup/workflow1/internal/SystemUtils.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SystemUtils.kt index 6249da899..f701de085 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SystemUtils.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SystemUtils.kt @@ -5,3 +5,8 @@ package com.squareup.workflow1.internal * to use after we have processed some actions. Use this milliseconds since epoch for that. */ internal expect fun currentTimeMillis(): Long + +/** + * Current time in nanoseconds. + */ +internal expect fun nanoTime(): Long 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 95% 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 896f77570..ffc9814f5 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,9 @@ @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..d7339867c 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,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.assertNull 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 95% 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 0162798b5..fbb3fa51b 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,18 +1,10 @@ @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.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED 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 94% 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..b60e22c3d 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,17 +1,8 @@ @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 +package com.squareup.workflow1 + +import com.squareup.workflow1.SubtreeManagerTest.TestWorkflow.Rendering import kotlinx.coroutines.Dispatchers.Unconfined import kotlinx.coroutines.async import kotlinx.coroutines.runBlocking 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 93% 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 c526c9035..ba74a4707 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,30 +1,9 @@ @file:Suppress("EXPERIMENTAL_API_USAGE", "DEPRECATION") -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 @@ -103,6 +82,7 @@ internal class WorkflowNodeTest { return@PropsRenderingWorkflow state } val node = WorkflowNode(workflow.id(), workflow, "old", null, context) + .apply { startSession() } node.render(workflow, "new") @@ -116,6 +96,7 @@ internal class WorkflowNodeTest { return@PropsRenderingWorkflow state } val node = WorkflowNode(workflow.id(), workflow, "old", null, context) + .apply { startSession() } node.render(workflow, "old") @@ -127,6 +108,7 @@ internal class WorkflowNodeTest { "$old->$new" } val node = WorkflowNode(workflow.id(), workflow, "foo", null, context) + .apply { startSession() } val rendering = node.render(workflow, "foo2") @@ -170,7 +152,7 @@ internal class WorkflowNodeTest { val node = WorkflowNode( workflow.id(), workflow, "", null, context, emitOutputToParent = { WorkflowOutput("tick:$it") } - ) + ).apply { startSession() } node.render(workflow, "")("event") val result = runBlocking { @@ -204,7 +186,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") @@ -243,6 +225,7 @@ internal class WorkflowNodeTest { } } val node = WorkflowNode(workflow.id(), workflow, "", null, context) + .apply { startSession() } node.render(workflow, "") sink.send(action { setOutput("event") }) @@ -261,8 +244,8 @@ internal class WorkflowNodeTest { } val node = WorkflowNode( workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, baseContext = context - ) + initialSnapshot = null, baseContext = context + ).apply { startSession() } runBlocking { node.render(workflow.asStatefulWorkflow(), Unit) @@ -279,8 +262,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) @@ -298,8 +281,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) val result = runBlocking { @@ -328,8 +311,8 @@ internal class WorkflowNodeTest { } val node = WorkflowNode( workflow.id(), workflow.asStatefulWorkflow(), initialProps = true, - snapshot = null, baseContext = context - ) + initialSnapshot = null, baseContext = context + ).apply { startSession() } runBlocking { node.render(workflow.asStatefulWorkflow(), true) @@ -354,8 +337,8 @@ internal class WorkflowNodeTest { } val node = WorkflowNode( workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, baseContext = context - ) + initialSnapshot = null, baseContext = context + ).apply { startSession() } runBlocking { node.render(workflow.asStatefulWorkflow(), Unit) @@ -380,8 +363,8 @@ internal class WorkflowNodeTest { } val node = WorkflowNode( workflow.id(), workflow.asStatefulWorkflow(), initialProps = 0, - snapshot = null, baseContext = context - ) + initialSnapshot = null, baseContext = context + ).apply { startSession() } runBlocking { node.render(workflow.asStatefulWorkflow(), 0) @@ -405,8 +388,8 @@ internal class WorkflowNodeTest { } val node = WorkflowNode( workflow.id(), workflow.asStatefulWorkflow(), initialProps = 0, - snapshot = null, baseContext = context - ) + initialSnapshot = null, baseContext = context + ).apply { startSession() } runBlocking { node.render(workflow.asStatefulWorkflow(), 0) @@ -426,8 +409,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) @@ -453,9 +436,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) @@ -487,8 +470,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) @@ -516,9 +499,9 @@ internal class WorkflowNodeTest { workflow.id(), workflow, initialProps = "initial props", - snapshot = null, + initialSnapshot = null, baseContext = Unconfined - ) + ).apply { startSession() } assertEquals("initial props", originalNode.render(workflow, "foo")) val snapshot = originalNode.snapshot(workflow) @@ -529,9 +512,9 @@ internal class WorkflowNodeTest { workflow, // These props should be ignored, since snapshot is non-null. initialProps = "new props", - snapshot = snapshot, + initialSnapshot = snapshot, baseContext = Unconfined - ) + ).apply { startSession() } assertEquals("initial props", restoredNode.render(workflow, "foo")) } @@ -545,9 +528,9 @@ internal class WorkflowNodeTest { workflow.id(), workflow, initialProps = "initial props", - snapshot = null, + initialSnapshot = null, baseContext = Unconfined - ) + ).apply { startSession() } assertEquals("initial props", originalNode.render(workflow, "foo")) val snapshot = originalNode.snapshot(workflow) @@ -558,9 +541,9 @@ internal class WorkflowNodeTest { workflow, // These props should be ignored, since snapshot is non-null. initialProps = "new props", - snapshot = snapshot, + initialSnapshot = snapshot, baseContext = Unconfined - ) + ).apply { startSession() } assertEquals("restored", restoredNode.render(workflow, "foo")) } @@ -602,9 +585,9 @@ internal class WorkflowNodeTest { parentWorkflow.id(), parentWorkflow, initialProps = "initial props", - snapshot = null, + initialSnapshot = null, baseContext = Unconfined - ) + ).apply { startSession() } assertEquals("initial props|child props", originalNode.render(parentWorkflow, "foo")) val snapshot = originalNode.snapshot(parentWorkflow) @@ -615,9 +598,9 @@ internal class WorkflowNodeTest { parentWorkflow, // These props should be ignored, since snapshot is non-null. initialProps = "new props", - snapshot = snapshot, + initialSnapshot = snapshot, baseContext = Unconfined - ) + ).apply { startSession() } assertEquals("initial props|child props", restoredNode.render(parentWorkflow, "foo")) assertEquals("child props", restoredChildState) assertEquals("initial props", restoredParentState) @@ -642,6 +625,7 @@ internal class WorkflowNodeTest { } ) val node = WorkflowNode(workflow.id(), workflow, Unit, null, Unconfined) + .apply { startSession() } assertEquals(0, snapshotCalls) assertEquals(0, snapshotWrites) @@ -659,7 +643,7 @@ internal class WorkflowNodeTest { assertEquals(1, snapshotWrites) assertEquals(0, restoreCalls) - WorkflowNode(workflow.id(), workflow, Unit, snapshot, Unconfined) + WorkflowNode(workflow.id(), workflow, Unit, snapshot, Unconfined).apply { startSession() } assertEquals(1, snapshotCalls) assertEquals(1, snapshotWrites) @@ -682,9 +666,9 @@ internal class WorkflowNodeTest { workflow.id(), workflow, initialProps = "initial props", - snapshot = null, + initialSnapshot = null, baseContext = Unconfined - ) + ).apply { startSession() } assertEquals("initial props", originalNode.render(workflow, "foo")) val snapshot = originalNode.snapshot(workflow) @@ -694,9 +678,9 @@ internal class WorkflowNodeTest { workflow.id(), workflow, initialProps = "new props", - snapshot = snapshot, + initialSnapshot = snapshot, baseContext = Unconfined - ) + ).apply { startSession() } assertEquals("props:new props|state:initial props", restoredNode.render(workflow, "foo")) } @@ -706,10 +690,10 @@ internal class WorkflowNodeTest { id = workflow.id(key = "foo"), workflow = workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, + initialSnapshot = null, baseContext = Unconfined, parent = null - ) + ).apply { startSession() } assertEquals( "WorkflowInstance(identifier=${workflow.identifier}, renderKey=foo, " + @@ -724,10 +708,10 @@ internal class WorkflowNodeTest { id = workflow.id(key = "foo"), workflow = workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, + initialSnapshot = null, baseContext = Unconfined, parent = TestSession(42) - ) + ).apply { startSession() } assertEquals( "WorkflowInstance(identifier=${workflow.identifier}, renderKey=foo, " + @@ -757,11 +741,11 @@ internal class WorkflowNodeTest { id = workflow.id(key = "foo"), workflow = workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, + initialSnapshot = null, interceptor = interceptor, baseContext = Unconfined, parent = TestSession(42) - ) + ).apply { startSession() } assertSame(node.coroutineContext, interceptedScope.coroutineContext) assertEquals(workflow.identifier, interceptedSession.identifier) @@ -801,11 +785,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, parent = TestSession(42) - ) + ).apply { startSession() } assertEquals("props", interceptedProps) assertEquals(Snapshot.of("snapshot"), interceptedSnapshot) @@ -847,11 +831,11 @@ internal class WorkflowNodeTest { id = workflow.id(key = "foo"), workflow = workflow.asStatefulWorkflow(), initialProps = "old", - snapshot = null, + initialSnapshot = null, interceptor = interceptor, baseContext = Unconfined, parent = TestSession(42) - ) + ).apply { startSession() } val rendering = node.render(workflow, "new") assertEquals("old", interceptedOld) @@ -893,11 +877,11 @@ internal class WorkflowNodeTest { id = workflow.id(key = "foo"), workflow = workflow.asStatefulWorkflow(), initialProps = "props", - snapshot = null, + initialSnapshot = null, interceptor = interceptor, baseContext = Unconfined, parent = TestSession(42) - ) + ).apply { startSession() } val rendering = node.render(workflow, "props") assertEquals("props", interceptedProps) @@ -935,11 +919,11 @@ internal class WorkflowNodeTest { id = workflow.id(key = "foo"), workflow = workflow.asStatefulWorkflow(), initialProps = "old", - snapshot = null, + initialSnapshot = null, interceptor = interceptor, baseContext = Unconfined, parent = TestSession(42) - ) + ).apply { startSession() } val snapshot = node.snapshot(workflow) assertEquals("state", interceptedState) @@ -976,11 +960,11 @@ internal class WorkflowNodeTest { id = workflow.id(key = "foo"), workflow = workflow.asStatefulWorkflow(), initialProps = "old", - snapshot = null, + initialSnapshot = null, interceptor = interceptor, baseContext = Unconfined, parent = TestSession(42) - ) + ).apply { startSession() } val snapshot = node.snapshot(workflow) assertEquals("state", interceptedState) @@ -1017,12 +1001,12 @@ internal class WorkflowNodeTest { id = rootWorkflow.id(key = "foo"), workflow = rootWorkflow.asStatefulWorkflow(), initialProps = "props", - snapshot = null, + initialSnapshot = null, interceptor = interceptor, baseContext = Unconfined, parent = TestSession(42), idCounter = IdCounter() - ) + ).apply { startSession() } val rendering = node.render(rootWorkflow.asStatefulWorkflow(), "props") assertEquals("[root([leaf([[props]], [[props]])])]", rendering) @@ -1037,9 +1021,9 @@ internal class WorkflowNodeTest { workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, + initialSnapshot = null, baseContext = Unconfined - ) + ).apply { startSession() } val error = assertFailsWith { node.render(workflow.asStatefulWorkflow(), Unit) @@ -1065,9 +1049,9 @@ internal class WorkflowNodeTest { workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, + initialSnapshot = null, baseContext = Unconfined - ) + ).apply { startSession() } val error = assertFailsWith { node.render(workflow.asStatefulWorkflow(), Unit) @@ -1092,9 +1076,9 @@ internal class WorkflowNodeTest { workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, + initialSnapshot = null, baseContext = Unconfined - ) + ).apply { startSession() } val (_, sink) = node.render(workflow.asStatefulWorkflow(), Unit) sink.send("hello") @@ -1117,10 +1101,10 @@ internal class WorkflowNodeTest { workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, + initialSnapshot = null, baseContext = Unconfined, emitOutputToParent = { WorkflowOutput("output:$it") } - ) + ).apply { startSession() } val rendering = node.render(workflow.asStatefulWorkflow(), Unit) rendering.send("hello") @@ -1142,10 +1126,10 @@ internal class WorkflowNodeTest { workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, + initialSnapshot = null, baseContext = Unconfined, emitOutputToParent = { WorkflowOutput(it) } - ) + ).apply { startSession() } val rendering = node.render(workflow.asStatefulWorkflow(), Unit) rendering.send("hello") @@ -1173,9 +1157,9 @@ internal class WorkflowNodeTest { workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, + initialSnapshot = null, baseContext = Unconfined - ) + ).apply { startSession() } node.render(workflow.asStatefulWorkflow(), Unit) runBlocking { @@ -1198,10 +1182,10 @@ internal class WorkflowNodeTest { workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, + initialSnapshot = null, baseContext = Unconfined, emitOutputToParent = { WorkflowOutput("output:$it") } - ) + ).apply { startSession() } node.render(workflow.asStatefulWorkflow(), Unit) val output = runBlocking { @@ -1223,10 +1207,10 @@ internal class WorkflowNodeTest { workflow.id(), workflow.asStatefulWorkflow(), initialProps = Unit, - snapshot = null, + initialSnapshot = null, baseContext = Unconfined, emitOutputToParent = { WorkflowOutput(it) } - ) + ).apply { startSession() } node.render(workflow.asStatefulWorkflow(), Unit) val output = runBlocking { 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 95% 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 21c2d3a0b..faa7a984c 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,9 @@ -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.FrameTimeout 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 diff --git a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/SystemUtils.kt b/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/SystemUtils.kt index 354e7ef9c..b41392a09 100644 --- a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/SystemUtils.kt +++ b/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/SystemUtils.kt @@ -1,3 +1,5 @@ package com.squareup.workflow1.internal internal actual fun currentTimeMillis(): Long = System.currentTimeMillis() + +internal actual fun nanoTime(): Long = System.nanoTime() 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-rx2/dependencies/runtimeClasspath.txt b/workflow-rx2/dependencies/runtimeClasspath.txt index 2e945cf86..65a33a338 100644 --- a/workflow-rx2/dependencies/runtimeClasspath.txt +++ b/workflow-rx2/dependencies/runtimeClasspath.txt @@ -7,9 +7,9 @@ org.jetbrains.kotlin:kotlin-stdlib-common:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10 org.jetbrains.kotlin:kotlin-stdlib:1.6.10 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1 -org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.5.1 -org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.5.1 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.5.2 org.jetbrains:annotations:13.0 org.reactivestreams:reactive-streams:1.0.3 diff --git a/workflow-testing/dependencies/runtimeClasspath.txt b/workflow-testing/dependencies/runtimeClasspath.txt index e8d8cd8d0..48fa2bae3 100644 --- a/workflow-testing/dependencies/runtimeClasspath.txt +++ b/workflow-testing/dependencies/runtimeClasspath.txt @@ -12,8 +12,8 @@ org.jetbrains.kotlin:kotlin-stdlib-common:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10 org.jetbrains.kotlin:kotlin-stdlib:1.6.10 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1 -org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.5.1 -org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2 org.jetbrains:annotations:13.0 diff --git a/workflow-tracing/dependencies/runtimeClasspath.txt b/workflow-tracing/dependencies/runtimeClasspath.txt index c694d8f99..f32d25e61 100644 --- a/workflow-tracing/dependencies/runtimeClasspath.txt +++ b/workflow-tracing/dependencies/runtimeClasspath.txt @@ -10,6 +10,6 @@ org.jetbrains.kotlin:kotlin-stdlib-common:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10 org.jetbrains.kotlin:kotlin-stdlib:1.6.10 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2 org.jetbrains:annotations:13.0 diff --git a/workflow-ui/compose-tooling/build.gradle.kts b/workflow-ui/compose-tooling/build.gradle.kts index f7ffc1e37..d7906a0a9 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() } } diff --git a/workflow-ui/compose-tooling/dependencies/releaseRuntimeClasspath.txt b/workflow-ui/compose-tooling/dependencies/releaseRuntimeClasspath.txt index 846b8f021..1e41658f8 100644 --- a/workflow-ui/compose-tooling/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-ui/compose-tooling/dependencies/releaseRuntimeClasspath.txt @@ -16,24 +16,24 @@ androidx.arch.core:core-common:2.1.0 androidx.arch.core:core-runtime:2.1.0 androidx.autofill:autofill:1.0.0 androidx.collection:collection:1.1.0 -androidx.compose.animation:animation-core:1.1.0-rc01 -androidx.compose.animation:animation:1.1.0-rc01 -androidx.compose.foundation:foundation-layout:1.1.0-rc01 -androidx.compose.foundation:foundation:1.1.0-rc01 +androidx.compose.animation:animation-core:1.1.0 +androidx.compose.animation:animation:1.1.0 +androidx.compose.foundation:foundation-layout:1.1.0 +androidx.compose.foundation:foundation:1.1.0 androidx.compose.material:material-icons-core:1.0.0 androidx.compose.material:material-ripple:1.0.0 androidx.compose.material:material:1.0.0 -androidx.compose.runtime:runtime-saveable:1.1.0-rc01 -androidx.compose.runtime:runtime:1.1.0-rc01 -androidx.compose.ui:ui-geometry:1.1.0-rc01 -androidx.compose.ui:ui-graphics:1.1.0-rc01 -androidx.compose.ui:ui-text:1.1.0-rc01 -androidx.compose.ui:ui-tooling-data:1.1.0-rc01 -androidx.compose.ui:ui-tooling-preview:1.1.0-rc01 -androidx.compose.ui:ui-tooling:1.1.0-rc01 -androidx.compose.ui:ui-unit:1.1.0-rc01 -androidx.compose.ui:ui-util:1.1.0-rc01 -androidx.compose.ui:ui:1.1.0-rc01 +androidx.compose.runtime:runtime-saveable:1.1.0 +androidx.compose.runtime:runtime:1.1.0 +androidx.compose.ui:ui-geometry:1.1.0 +androidx.compose.ui:ui-graphics:1.1.0 +androidx.compose.ui:ui-text:1.1.0 +androidx.compose.ui:ui-tooling-data:1.1.0 +androidx.compose.ui:ui-tooling-preview:1.1.0 +androidx.compose.ui:ui-tooling:1.1.0 +androidx.compose.ui:ui-unit:1.1.0 +androidx.compose.ui:ui-util:1.1.0 +androidx.compose.ui:ui:1.1.0 androidx.core:core-ktx:1.6.0 androidx.core:core:1.6.0 androidx.cursoradapter:cursoradapter:1.0.0 @@ -51,7 +51,7 @@ androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0 androidx.lifecycle:lifecycle-viewmodel-savedstate:2.4.0 androidx.lifecycle:lifecycle-viewmodel:2.4.0 androidx.loader:loader:1.0.0 -androidx.profileinstaller:profileinstaller:1.1.0-rc01 +androidx.profileinstaller:profileinstaller:1.1.0 androidx.savedstate:savedstate-ktx:1.1.0 androidx.savedstate:savedstate:1.1.0 androidx.startup:startup-runtime:1.0.0 diff --git a/workflow-ui/compose/build.gradle.kts b/workflow-ui/compose/build.gradle.kts index ee921395c..468b71a81 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() } } diff --git a/workflow-ui/compose/dependencies/releaseRuntimeClasspath.txt b/workflow-ui/compose/dependencies/releaseRuntimeClasspath.txt index 3ec147f70..77eba9862 100644 --- a/workflow-ui/compose/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-ui/compose/dependencies/releaseRuntimeClasspath.txt @@ -13,18 +13,18 @@ androidx.arch.core:core-common:2.1.0 androidx.arch.core:core-runtime:2.1.0 androidx.autofill:autofill:1.0.0 androidx.collection:collection:1.1.0 -androidx.compose.animation:animation-core:1.1.0-rc01 -androidx.compose.animation:animation:1.1.0-rc01 -androidx.compose.foundation:foundation-layout:1.1.0-rc01 -androidx.compose.foundation:foundation:1.1.0-rc01 -androidx.compose.runtime:runtime-saveable:1.1.0-rc01 -androidx.compose.runtime:runtime:1.1.0-rc01 -androidx.compose.ui:ui-geometry:1.1.0-rc01 -androidx.compose.ui:ui-graphics:1.1.0-rc01 -androidx.compose.ui:ui-text:1.1.0-rc01 -androidx.compose.ui:ui-unit:1.1.0-rc01 -androidx.compose.ui:ui-util:1.1.0-rc01 -androidx.compose.ui:ui:1.1.0-rc01 +androidx.compose.animation:animation-core:1.1.0 +androidx.compose.animation:animation:1.1.0 +androidx.compose.foundation:foundation-layout:1.1.0 +androidx.compose.foundation:foundation:1.1.0 +androidx.compose.runtime:runtime-saveable:1.1.0 +androidx.compose.runtime:runtime:1.1.0 +androidx.compose.ui:ui-geometry:1.1.0 +androidx.compose.ui:ui-graphics:1.1.0 +androidx.compose.ui:ui-text:1.1.0 +androidx.compose.ui:ui-unit:1.1.0 +androidx.compose.ui:ui-util:1.1.0 +androidx.compose.ui:ui:1.1.0 androidx.core:core-ktx:1.6.0 androidx.core:core:1.6.0 androidx.cursoradapter:cursoradapter:1.0.0 @@ -42,7 +42,7 @@ androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0 androidx.lifecycle:lifecycle-viewmodel-savedstate:2.4.0 androidx.lifecycle:lifecycle-viewmodel:2.4.0 androidx.loader:loader:1.0.0 -androidx.profileinstaller:profileinstaller:1.1.0-rc01 +androidx.profileinstaller:profileinstaller:1.1.0 androidx.savedstate:savedstate:1.1.0 androidx.startup:startup-runtime:1.0.0 androidx.tracing:tracing:1.0.0 diff --git a/workflow-ui/container-android/dependencies/releaseRuntimeClasspath.txt b/workflow-ui/container-android/dependencies/releaseRuntimeClasspath.txt index 1f15c4407..75b032cd5 100644 --- a/workflow-ui/container-android/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-ui/container-android/dependencies/releaseRuntimeClasspath.txt @@ -41,7 +41,7 @@ org.jetbrains.kotlin:kotlin-stdlib-common:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10 org.jetbrains.kotlin:kotlin-stdlib:1.6.10 -org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1 +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2 org.jetbrains:annotations:13.0 diff --git a/workflow-ui/container-common/dependencies/runtimeClasspath.txt b/workflow-ui/container-common/dependencies/runtimeClasspath.txt index 5a9d00718..d232d2e3e 100644 --- a/workflow-ui/container-common/dependencies/runtimeClasspath.txt +++ b/workflow-ui/container-common/dependencies/runtimeClasspath.txt @@ -6,6 +6,6 @@ org.jetbrains.kotlin:kotlin-stdlib-common:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10 org.jetbrains.kotlin:kotlin-stdlib:1.6.10 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2 org.jetbrains:annotations:13.0 diff --git a/workflow-ui/core-android/api/core-android.api b/workflow-ui/core-android/api/core-android.api index db003b629..fa39e523f 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/dependencies/releaseRuntimeClasspath.txt b/workflow-ui/core-android/dependencies/releaseRuntimeClasspath.txt index ee28cf72b..c53b3611c 100644 --- a/workflow-ui/core-android/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-ui/core-android/dependencies/releaseRuntimeClasspath.txt @@ -32,7 +32,7 @@ org.jetbrains.kotlin:kotlin-stdlib-common:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10 org.jetbrains.kotlin:kotlin-stdlib:1.6.10 -org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1 +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2 org.jetbrains:annotations:13.0 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..5acbaab41 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 = workflow, + scope = scope, + props = props, + initialSnapshot = restoredSnap, + interceptors = interceptors, + runtimeConfig = runtimeConfig, + workflowRuntimePlugin = workflowRuntimePlugin, + onOutput = onOutput ) return renderingsAndSnapshots diff --git a/workflow-ui/core-common/dependencies/runtimeClasspath.txt b/workflow-ui/core-common/dependencies/runtimeClasspath.txt index 19adaaffc..6a9b6ed46 100644 --- a/workflow-ui/core-common/dependencies/runtimeClasspath.txt +++ b/workflow-ui/core-common/dependencies/runtimeClasspath.txt @@ -5,6 +5,6 @@ org.jetbrains.kotlin:kotlin-stdlib-common:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10 org.jetbrains.kotlin:kotlin-stdlib:1.6.10 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2 org.jetbrains:annotations:13.0 diff --git a/workflow-ui/radiography/dependencies/releaseRuntimeClasspath.txt b/workflow-ui/radiography/dependencies/releaseRuntimeClasspath.txt index a5efc6858..7781d360b 100644 --- a/workflow-ui/radiography/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-ui/radiography/dependencies/releaseRuntimeClasspath.txt @@ -35,7 +35,7 @@ org.jetbrains.kotlin:kotlin-stdlib-common:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10 org.jetbrains.kotlin:kotlin-stdlib:1.6.10 -org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1 +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2 org.jetbrains:annotations:13.0