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 ece4623963..e52512c534 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
@@ -22,10 +22,10 @@ import com.squareup.sample.poetry.model.Poem
import com.squareup.workflow1.RuntimeConfig
import com.squareup.workflow1.RuntimeConfigOptions.Companion.RENDER_PER_ACTION
import com.squareup.workflow1.WorkflowInterceptor
+import com.squareup.workflow1.android.renderWorkflowIn
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ViewEnvironment.Companion.EMPTY
import com.squareup.workflow1.ui.ViewRegistry
-import com.squareup.workflow1.ui.renderWorkflowIn
import com.squareup.workflow1.ui.withEnvironment
import com.squareup.workflow1.ui.workflowContentView
import kotlinx.coroutines.flow.StateFlow
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 530df49c74..bc01c24e59 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -92,6 +92,7 @@ timber = "5.0.1"
truth = "1.4.4"
turbine = "1.0.0"
vanniktech-publish = "0.32.0"
+uiAndroidVersion = "1.8.2"
[plugins]
@@ -135,6 +136,7 @@ androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" }
androidx-compose-runtime-saveable = { module = "androidx.compose.runtime:runtime-saveable" }
androidx-compose-ui = { module = "androidx.compose.ui:ui" }
+androidx-compose-ui-android = { module = "androidx.compose.ui:ui-android" }
androidx-compose-ui-geometry = { module = "androidx.compose.ui:ui-geometry" }
androidx-compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" }
androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposebinding/HelloBindingActivity.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposebinding/HelloBindingActivity.kt
index 69efe093cb..9d368ad56b 100644
--- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposebinding/HelloBindingActivity.kt
+++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposebinding/HelloBindingActivity.kt
@@ -10,6 +10,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.squareup.workflow1.WorkflowExperimentalRuntime
+import com.squareup.workflow1.android.renderWorkflowIn
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
import com.squareup.workflow1.mapRendering
import com.squareup.workflow1.ui.Screen
@@ -17,7 +18,6 @@ import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.ViewRegistry
import com.squareup.workflow1.ui.compose.withComposeInteropSupport
import com.squareup.workflow1.ui.plus
-import com.squareup.workflow1.ui.renderWorkflowIn
import com.squareup.workflow1.ui.withEnvironment
import com.squareup.workflow1.ui.workflowContentView
import kotlinx.coroutines.flow.StateFlow
diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflowActivity.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflowActivity.kt
index 5f6c7f537b..2c5ea4f0bf 100644
--- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflowActivity.kt
+++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflowActivity.kt
@@ -9,12 +9,12 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.squareup.workflow1.WorkflowExperimentalRuntime
+import com.squareup.workflow1.android.renderWorkflowIn
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
import com.squareup.workflow1.mapRendering
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.compose.withComposeInteropSupport
-import com.squareup.workflow1.ui.renderWorkflowIn
import com.squareup.workflow1.ui.withEnvironment
import com.squareup.workflow1.ui.workflowContentView
import kotlinx.coroutines.flow.StateFlow
diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingActivity.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingActivity.kt
index 8047cf7a47..47222c60cc 100644
--- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingActivity.kt
+++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingActivity.kt
@@ -9,12 +9,12 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.squareup.workflow1.WorkflowExperimentalRuntime
+import com.squareup.workflow1.android.renderWorkflowIn
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
import com.squareup.workflow1.mapRendering
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.compose.withComposeInteropSupport
-import com.squareup.workflow1.ui.renderWorkflowIn
import com.squareup.workflow1.ui.withEnvironment
import com.squareup.workflow1.ui.workflowContentView
import kotlinx.coroutines.flow.StateFlow
diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/NestedRenderingsActivity.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/NestedRenderingsActivity.kt
index 3873850063..c39c42c787 100644
--- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/NestedRenderingsActivity.kt
+++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/NestedRenderingsActivity.kt
@@ -11,6 +11,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.squareup.workflow1.WorkflowExperimentalRuntime
+import com.squareup.workflow1.android.renderWorkflowIn
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
import com.squareup.workflow1.mapRendering
import com.squareup.workflow1.ui.Screen
@@ -18,7 +19,6 @@ import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.ViewRegistry
import com.squareup.workflow1.ui.compose.withComposeInteropSupport
import com.squareup.workflow1.ui.plus
-import com.squareup.workflow1.ui.renderWorkflowIn
import com.squareup.workflow1.ui.withEnvironment
import com.squareup.workflow1.ui.workflowContentView
import kotlinx.coroutines.flow.StateFlow
diff --git a/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoetryActivity.kt b/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoetryActivity.kt
index 6d4d8cd4d6..f0b555883c 100644
--- a/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoetryActivity.kt
+++ b/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoetryActivity.kt
@@ -13,10 +13,10 @@ import com.squareup.sample.poetry.RealPoemWorkflow
import com.squareup.sample.poetry.RealPoemsBrowserWorkflow
import com.squareup.sample.poetry.model.Poem
import com.squareup.workflow1.WorkflowExperimentalRuntime
+import com.squareup.workflow1.android.renderWorkflowIn
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.navigation.reportNavigation
-import com.squareup.workflow1.ui.renderWorkflowIn
import com.squareup.workflow1.ui.withRegistry
import com.squareup.workflow1.ui.workflowContentView
import kotlinx.coroutines.flow.Flow
diff --git a/samples/containers/app-raven/src/main/java/com/squareup/sample/ravenapp/RavenActivity.kt b/samples/containers/app-raven/src/main/java/com/squareup/sample/ravenapp/RavenActivity.kt
index e91d38043f..e6900be1ae 100644
--- a/samples/containers/app-raven/src/main/java/com/squareup/sample/ravenapp/RavenActivity.kt
+++ b/samples/containers/app-raven/src/main/java/com/squareup/sample/ravenapp/RavenActivity.kt
@@ -13,10 +13,10 @@ import com.squareup.sample.container.SampleContainers
import com.squareup.sample.poetry.RealPoemWorkflow
import com.squareup.sample.poetry.model.Raven
import com.squareup.workflow1.WorkflowExperimentalRuntime
+import com.squareup.workflow1.android.renderWorkflowIn
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.navigation.reportNavigation
-import com.squareup.workflow1.ui.renderWorkflowIn
import com.squareup.workflow1.ui.withRegistry
import com.squareup.workflow1.ui.workflowContentView
import kotlinx.coroutines.Job
diff --git a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonActivity.kt b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonActivity.kt
index 50cb56c6e1..76ef950524 100644
--- a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonActivity.kt
+++ b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonActivity.kt
@@ -11,10 +11,10 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
import com.squareup.sample.container.SampleContainers
import com.squareup.workflow1.WorkflowExperimentalRuntime
+import com.squareup.workflow1.android.renderWorkflowIn
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.navigation.reportNavigation
-import com.squareup.workflow1.ui.renderWorkflowIn
import com.squareup.workflow1.ui.withRegistry
import com.squareup.workflow1.ui.workflowContentView
import kotlinx.coroutines.Job
diff --git a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/TimeMachineModel.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/TimeMachineModel.kt
index 7ec6d57175..ed9865f8ca 100644
--- a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/TimeMachineModel.kt
+++ b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/TimeMachineModel.kt
@@ -6,10 +6,10 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.savedstate.SavedStateRegistryOwner
import com.squareup.workflow1.WorkflowExperimentalRuntime
+import com.squareup.workflow1.android.renderWorkflowIn
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
import com.squareup.workflow1.diagnostic.tracing.TracingWorkflowInterceptor
import com.squareup.workflow1.ui.Screen
-import com.squareup.workflow1.ui.renderWorkflowIn
import kotlinx.coroutines.flow.StateFlow
import java.io.File
import kotlin.time.ExperimentalTime
diff --git a/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragment.kt b/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragment.kt
index 5bca0cdee3..20512befca 100644
--- a/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragment.kt
+++ b/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragment.kt
@@ -11,9 +11,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.squareup.workflow1.WorkflowExperimentalRuntime
+import com.squareup.workflow1.android.renderWorkflowIn
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
import com.squareup.workflow1.ui.WorkflowLayout
-import com.squareup.workflow1.ui.renderWorkflowIn
import kotlinx.coroutines.flow.StateFlow
class HelloWorkflowFragment : Fragment() {
diff --git a/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflowActivity.kt b/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflowActivity.kt
index 6a6a702b82..fa6623e04e 100644
--- a/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflowActivity.kt
+++ b/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflowActivity.kt
@@ -9,8 +9,8 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.squareup.workflow1.WorkflowExperimentalRuntime
+import com.squareup.workflow1.android.renderWorkflowIn
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
-import com.squareup.workflow1.ui.renderWorkflowIn
import com.squareup.workflow1.ui.workflowContentView
import kotlinx.coroutines.flow.StateFlow
diff --git a/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysActivity.kt b/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysActivity.kt
index 9c8c16ac1f..9442ffff14 100644
--- a/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysActivity.kt
+++ b/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysActivity.kt
@@ -9,9 +9,9 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.squareup.workflow1.WorkflowExperimentalRuntime
+import com.squareup.workflow1.android.renderWorkflowIn
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
import com.squareup.workflow1.ui.Screen
-import com.squareup.workflow1.ui.renderWorkflowIn
import com.squareup.workflow1.ui.workflowContentView
import kotlinx.coroutines.flow.StateFlow
diff --git a/samples/stub-visibility/src/main/java/com/squareup/sample/stubvisibility/StubVisibilityActivity.kt b/samples/stub-visibility/src/main/java/com/squareup/sample/stubvisibility/StubVisibilityActivity.kt
index 50b6fe7aa4..aa8a4fe52a 100644
--- a/samples/stub-visibility/src/main/java/com/squareup/sample/stubvisibility/StubVisibilityActivity.kt
+++ b/samples/stub-visibility/src/main/java/com/squareup/sample/stubvisibility/StubVisibilityActivity.kt
@@ -9,9 +9,9 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.squareup.workflow1.WorkflowExperimentalRuntime
+import com.squareup.workflow1.android.renderWorkflowIn
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
import com.squareup.workflow1.ui.Screen
-import com.squareup.workflow1.ui.renderWorkflowIn
import com.squareup.workflow1.ui.workflowContentView
import kotlinx.coroutines.flow.StateFlow
diff --git a/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/TicTacToeModel.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/TicTacToeModel.kt
index 3d4ae097c2..8fe7014641 100644
--- a/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/TicTacToeModel.kt
+++ b/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/TicTacToeModel.kt
@@ -9,10 +9,10 @@ import androidx.lifecycle.viewModelScope
import androidx.savedstate.SavedStateRegistryOwner
import com.squareup.sample.mainworkflow.TicTacToeWorkflow
import com.squareup.workflow1.WorkflowExperimentalRuntime
+import com.squareup.workflow1.android.renderWorkflowIn
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
import com.squareup.workflow1.diagnostic.tracing.TracingWorkflowInterceptor
import com.squareup.workflow1.ui.Screen
-import com.squareup.workflow1.ui.renderWorkflowIn
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.StateFlow
import java.io.File
diff --git a/samples/todo-android/app/src/main/java/com/squareup/sample/todo/ToDoActivity.kt b/samples/todo-android/app/src/main/java/com/squareup/sample/todo/ToDoActivity.kt
index c957d2674c..ff0e88bda9 100644
--- a/samples/todo-android/app/src/main/java/com/squareup/sample/todo/ToDoActivity.kt
+++ b/samples/todo-android/app/src/main/java/com/squareup/sample/todo/ToDoActivity.kt
@@ -10,11 +10,11 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.squareup.sample.container.overviewdetail.OverviewDetailContainer
import com.squareup.workflow1.WorkflowExperimentalRuntime
+import com.squareup.workflow1.android.renderWorkflowIn
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
import com.squareup.workflow1.diagnostic.tracing.TracingWorkflowInterceptor
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ViewRegistry
-import com.squareup.workflow1.ui.renderWorkflowIn
import com.squareup.workflow1.ui.withRegistry
import com.squareup.workflow1.ui.workflowContentView
import kotlinx.coroutines.flow.StateFlow
diff --git a/settings.gradle.kts b/settings.gradle.kts
index f4982b30f7..00f951b069 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -64,6 +64,7 @@ include(
":workflow-config:config-jvm",
":workflow-core",
":workflow-runtime",
+ ":workflow-runtime-android",
":workflow-rx2",
":workflow-testing",
":workflow-tracing",
diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/HandlerBox.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/HandlerBox.kt
index 240699c8fa..067dcfeb2b 100644
--- a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/HandlerBox.kt
+++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/HandlerBox.kt
@@ -12,7 +12,14 @@ internal fun
BaseRenderContext
.eventHandler0(
remember: Boolean,
update: Updater
.() -> Unit
): () -> Unit {
- val handler = { actionSink.send(action("eH: $name", update)) }
+ val handler = {
+ actionSink.send(
+ action(
+ name = "eH: $name",
+ apply = update,
+ )
+ )
+ }
return if (remember) {
val box = remember(name) { HandlerBox0() }
box.handler = handler
@@ -34,7 +41,13 @@ internal inline fun
BaseRenderContext
.eventHa
remember: Boolean,
noinline update: Updater
.(EventT) -> Unit
): (EventT) -> Unit {
- val handler = { e: EventT -> actionSink.send(action("eH: $name") { update(e) }) }
+ val handler = { e: EventT ->
+ actionSink.send(
+ action(
+ name = "eH: $name",
+ ) { update(e) }
+ )
+ }
return if (remember) {
val box = remember(name, typeOf()) { HandlerBox1() }
box.handler = handler
@@ -56,7 +69,13 @@ internal inline fun BaseRenderContext
remember: Boolean,
noinline update: Updater
.(E1, E2) -> Unit
): (E1, E2) -> Unit {
- val handler = { e1: E1, e2: E2 -> actionSink.send(action("eH: $name") { update(e1, e2) }) }
+ val handler = { e1: E1, e2: E2 ->
+ actionSink.send(
+ action(
+ name = "eH: $name",
+ ) { update(e1, e2) }
+ )
+ }
return if (remember) {
val box = remember(name, typeOf(), typeOf()) { HandlerBox2() }
box.handler = handler
@@ -86,7 +105,13 @@ internal inline fun <
noinline update: Updater.(E1, E2, E3) -> Unit
): (E1, E2, E3) -> Unit {
val handler =
- { e1: E1, e2: E2, e3: E3 -> actionSink.send(action("eH: $name") { update(e1, e2, e3) }) }
+ { e1: E1, e2: E2, e3: E3 ->
+ actionSink.send(
+ action(
+ name = "eH: $name",
+ ) { update(e1, e2, e3) }
+ )
+ }
return if (remember) {
val box =
remember(name, typeOf(), typeOf(), typeOf()) { HandlerBox3() }
@@ -118,7 +143,11 @@ internal inline fun <
noinline update: Updater.(E1, E2, E3, E4) -> Unit
): (E1, E2, E3, E4) -> Unit {
val handler = { e1: E1, e2: E2, e3: E3, e4: E4 ->
- actionSink.send(action("eH: $name") { update(e1, e2, e3, e4) })
+ actionSink.send(
+ action(
+ name = "eH: $name",
+ ) { update(e1, e2, e3, e4) }
+ )
}
return if (remember) {
val box = remember(
@@ -158,7 +187,11 @@ internal inline fun <
noinline update: Updater
.(E1, E2, E3, E4, E5) -> Unit
): (E1, E2, E3, E4, E5) -> Unit {
val handler = { e1: E1, e2: E2, e3: E3, e4: E4, e5: E5 ->
- actionSink.send(action("eH: $name") { update(e1, e2, e3, e4, e5) })
+ actionSink.send(
+ action(
+ name = "eH: $name",
+ ) { update(e1, e2, e3, e4, e5) }
+ )
}
return if (remember) {
val box = remember(
@@ -200,7 +233,11 @@ internal inline fun <
noinline update: Updater
.(E1, E2, E3, E4, E5, E6) -> Unit
): (E1, E2, E3, E4, E5, E6) -> Unit {
val handler = { e1: E1, e2: E2, e3: E3, e4: E4, e5: E5, e6: E6 ->
- actionSink.send(action("eH: $name") { update(e1, e2, e3, e4, e5, e6) })
+ actionSink.send(
+ action(
+ name = "eH: $name",
+ ) { update(e1, e2, e3, e4, e5, e6) }
+ )
}
return if (remember) {
val box = remember(
@@ -244,7 +281,11 @@ internal inline fun <
noinline update: Updater
.(E1, E2, E3, E4, E5, E6, E7) -> Unit
): (E1, E2, E3, E4, E5, E6, E7) -> Unit {
val handler = { e1: E1, e2: E2, e3: E3, e4: E4, e5: E5, e6: E6, e7: E7 ->
- actionSink.send(action("eH: $name") { update(e1, e2, e3, e4, e5, e6, e7) })
+ actionSink.send(
+ action(
+ name = "eH: $name",
+ ) { update(e1, e2, e3, e4, e5, e6, e7) }
+ )
}
return if (remember) {
val box = remember(
@@ -290,7 +331,11 @@ internal inline fun <
noinline update: Updater
.(E1, E2, E3, E4, E5, E6, E7, E8) -> Unit
): (E1, E2, E3, E4, E5, E6, E7, E8) -> Unit {
val handler = { e1: E1, e2: E2, e3: E3, e4: E4, e5: E5, e6: E6, e7: E7, e8: E8 ->
- actionSink.send(action("eH: $name") { update(e1, e2, e3, e4, e5, e6, e7, e8) })
+ actionSink.send(
+ action(
+ name = "eH: $name",
+ ) { update(e1, e2, e3, e4, e5, e6, e7, e8) }
+ )
}
return if (remember) {
val box = remember(
@@ -338,7 +383,11 @@ internal inline fun <
noinline update: Updater
.(E1, E2, E3, E4, E5, E6, E7, E8, E9) -> Unit
): (E1, E2, E3, E4, E5, E6, E7, E8, E9) -> Unit {
val handler = { e1: E1, e2: E2, e3: E3, e4: E4, e5: E5, e6: E6, e7: E7, e8: E8, e9: E9 ->
- actionSink.send(action("eH: $name") { update(e1, e2, e3, e4, e5, e6, e7, e8, e9) })
+ actionSink.send(
+ action(
+ name = "eH: $name",
+ ) { update(e1, e2, e3, e4, e5, e6, e7, e8, e9) }
+ )
}
return if (remember) {
val box = remember(
@@ -389,7 +438,11 @@ internal inline fun <
): (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10) -> Unit {
val handler =
{ e1: E1, e2: E2, e3: E3, e4: E4, e5: E5, e6: E6, e7: E7, e8: E8, e9: E9, e10: E10 ->
- actionSink.send(action("eH: $name") { update(e1, e2, e3, e4, e5, e6, e7, e8, e9, e10) })
+ actionSink.send(
+ action(
+ name = "eH: $name",
+ ) { update(e1, e2, e3, e4, e5, e6, e7, e8, e9, e10) }
+ )
}
return if (remember) {
val box = remember(
diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt
index 5c31f2ac15..f3596935ee 100644
--- a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt
+++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt
@@ -19,6 +19,16 @@ public annotation class WorkflowExperimentalRuntime
public typealias RuntimeConfig = Set
+/**
+ * Whether or not we have an optimization enabled that should cause us to consider 'deferring'
+ * the application of the first action received after resuming from suspension in the runtime
+ * loop.
+ */
+// @WorkflowExperimentalRuntime
+// public fun RuntimeConfig.shouldDeferFirstAction(): Boolean {
+// return contains(RuntimeConfigOptions.CONFLATE_STALE_RENDERINGS)
+// }
+
/**
* A specification of the possible Workflow Runtime options.
*/
diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt
index 64c2cd0122..7ed0c48309 100644
--- a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt
+++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt
@@ -59,9 +59,10 @@ internal class WorkerWorkflow(
renderState: Int,
context: RenderContext, Int, OutputT>
) {
+ val localKey = renderState.toString()
// Scope the side effect coroutine to the state value, so the worker will be re-started when
// it changes (such that doesSameWorkAs returns false above).
- context.runningSideEffect(renderState.toString()) {
+ context.runningSideEffect(localKey) {
runWorker(renderProps, key, context.actionSink)
}
}
diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowAction.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowAction.kt
index f2cbd04699..490b3850a2 100644
--- a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowAction.kt
+++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowAction.kt
@@ -3,7 +3,6 @@
package com.squareup.workflow1
-import com.squareup.workflow1.WorkflowAction.Companion.toString
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.jvm.JvmOverloads
diff --git a/workflow-runtime-android/README.md b/workflow-runtime-android/README.md
new file mode 100644
index 0000000000..0631e0aed4
--- /dev/null
+++ b/workflow-runtime-android/README.md
@@ -0,0 +1,4 @@
+# Module Workflow Runtime Android
+
+This module is an Android library that is used to test the Workflow Runtime with Android specific
+coroutine dispatchers. These are headless android-tests that run on device without UI.
diff --git a/workflow-runtime-android/api/workflow-runtime-android.api b/workflow-runtime-android/api/workflow-runtime-android.api
new file mode 100644
index 0000000000..341ac870ad
--- /dev/null
+++ b/workflow-runtime-android/api/workflow-runtime-android.api
@@ -0,0 +1,15 @@
+public final class com/squareup/workflow1/android/AndroidFrameClock : com/squareup/workflow1/WorkflowFrameClock {
+ public static final field INSTANCE Lcom/squareup/workflow1/android/AndroidFrameClock;
+ public fun resumeOnFrame (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public final class com/squareup/workflow1/android/AndroidRenderWorkflowKt {
+ public static final fun removeWorkflowState (Landroidx/lifecycle/SavedStateHandle;)V
+ public static final fun renderWorkflowIn (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;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;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;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;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;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;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;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;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;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;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow;
+}
+
diff --git a/workflow-runtime-android/build.gradle.kts b/workflow-runtime-android/build.gradle.kts
new file mode 100644
index 0000000000..16396c561c
--- /dev/null
+++ b/workflow-runtime-android/build.gradle.kts
@@ -0,0 +1,33 @@
+plugins {
+ id("com.android.library")
+ id("kotlin-android")
+ id("android-defaults")
+ id("android-ui-tests")
+}
+
+android {
+ namespace = "com.squareup.workflow1"
+ testNamespace = "$namespace.test"
+}
+
+dependencies {
+ val composeBom = platform(libs.androidx.compose.bom)
+
+ api(project(":workflow-runtime"))
+ api(libs.androidx.lifecycle.viewmodel.savedstate)
+
+ implementation(project(":workflow-core"))
+ implementation(composeBom)
+ implementation(libs.androidx.compose.ui.android)
+
+ androidTestImplementation(libs.androidx.activity.ktx)
+ androidTestImplementation(libs.androidx.lifecycle.viewmodel.ktx)
+ androidTestImplementation(libs.androidx.test.core)
+ androidTestImplementation(libs.androidx.test.truth)
+ androidTestImplementation(libs.kotlin.test.core)
+ androidTestImplementation(libs.kotlin.test.jdk)
+ androidTestImplementation(libs.kotlinx.coroutines.android)
+ androidTestImplementation(libs.kotlinx.coroutines.core)
+ androidTestImplementation(libs.kotlinx.coroutines.test)
+ androidTestImplementation(libs.truth)
+}
diff --git a/workflow-runtime-android/gradle.properties b/workflow-runtime-android/gradle.properties
new file mode 100644
index 0000000000..5f09c5c151
--- /dev/null
+++ b/workflow-runtime-android/gradle.properties
@@ -0,0 +1,3 @@
+POM_ARTIFACT_ID=workflow-runtime-android
+POM_NAME=Workflow Runtime Android
+POM_PACKAGING=aar
diff --git a/workflow-runtime-android/src/androidTest/AndroidManifest.xml b/workflow-runtime-android/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000000..1258204722
--- /dev/null
+++ b/workflow-runtime-android/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/workflow-runtime-android/src/androidTest/java/com/squareup/workflow1/android/AndroidDispatchersRenderWorkflowInTest.kt b/workflow-runtime-android/src/androidTest/java/com/squareup/workflow1/android/AndroidDispatchersRenderWorkflowInTest.kt
new file mode 100644
index 0000000000..300f561c70
--- /dev/null
+++ b/workflow-runtime-android/src/androidTest/java/com/squareup/workflow1/android/AndroidDispatchersRenderWorkflowInTest.kt
@@ -0,0 +1,429 @@
+package com.squareup.workflow1.android
+
+import android.view.Choreographer
+import androidx.compose.ui.platform.AndroidUiDispatcher
+import androidx.test.platform.app.InstrumentationRegistry
+import com.squareup.workflow1.RuntimeConfig
+import com.squareup.workflow1.RuntimeConfigOptions
+import com.squareup.workflow1.RuntimeConfigOptions.CONFLATE_STALE_RENDERINGS
+import com.squareup.workflow1.RuntimeConfigOptions.PARTIAL_TREE_RENDERING
+import com.squareup.workflow1.RuntimeConfigOptions.RENDER_ONLY_WHEN_STATE_CHANGES
+import com.squareup.workflow1.RuntimeConfigOptions.STABLE_EVENT_HANDLERS
+import com.squareup.workflow1.Workflow
+import com.squareup.workflow1.WorkflowExperimentalRuntime
+import com.squareup.workflow1.WorkflowInterceptor
+import com.squareup.workflow1.WorkflowInterceptor.RenderPassesComplete
+import com.squareup.workflow1.WorkflowInterceptor.RuntimeLoopOutcome
+import com.squareup.workflow1.action
+import com.squareup.workflow1.asWorker
+import com.squareup.workflow1.renderChild
+import com.squareup.workflow1.runningWorker
+import com.squareup.workflow1.stateful
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.plus
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+
+@OptIn(WorkflowExperimentalRuntime::class, ExperimentalCoroutinesApi::class)
+class AndroidDispatchersRenderWorkflowInTest {
+
+ @Test
+ fun conflate_renderings_for_multiple_worker_actions_same_trigger() =
+ runTest(UnconfinedTestDispatcher()) {
+
+ val trigger = MutableStateFlow("unchanged state")
+ val emitted = mutableListOf()
+ var renderingsPassed = 0
+ val countInterceptor = object : WorkflowInterceptor {
+ override fun onRuntimeLoopTick(outcome: RuntimeLoopOutcome) {
+ if (outcome is RenderPassesComplete<*>) {
+ renderingsPassed++
+ }
+ }
+ }
+
+ val childWorkflow = Workflow.stateful(
+ initialState = "unchanged state",
+ render = { renderState ->
+ runningWorker(
+ worker = trigger.drop(1).asWorker(),
+ key = "Worker1"
+ ) {
+ action("") {
+ val newState = "$it+u1"
+ state = newState
+ setOutput(newState)
+ }
+ }
+ renderState
+ }
+ )
+ val workflow = Workflow.stateful(
+ initialState = "unchanged state",
+ render = { renderState ->
+ renderChild(childWorkflow) { childOutput ->
+ action("childHandler") {
+ state = childOutput
+ }
+ }
+ runningWorker(
+ worker = trigger.drop(1).asWorker(),
+ key = "Worker2"
+ ) {
+ action("") {
+ // Update the state in order to show conflation.
+ state = "$state+u2"
+ }
+ }
+ runningWorker(
+ worker = trigger.drop(1).asWorker(),
+ key = "Worker3"
+ ) {
+ action("") {
+ // Update the state in order to show conflation.
+ state = "$state+u3"
+ }
+ }
+ runningWorker(
+ worker = trigger.drop(1).asWorker(),
+ key = "Worker4"
+ ) {
+ action("") {
+ // Update the state in order to show conflation.
+ state = "$state+u4"
+ // Output only on the last one!
+ setOutput(state)
+ }
+ }
+ renderState
+ }
+ )
+ val props = MutableStateFlow(Unit)
+ // Render this on Compose's AndroidUiDispatcher.Main
+ val renderings = renderWorkflowIn(
+ workflow = workflow,
+ scope = backgroundScope +
+ AndroidUiDispatcher.Main,
+ props = props,
+ runtimeConfig = setOf(CONFLATE_STALE_RENDERINGS),
+ workflowTracer = null,
+ interceptors = listOf(countInterceptor)
+ ) { }
+
+ val renderedMutex = Mutex(locked = true)
+
+ val collectionJob = launch {
+ // Collect this unconfined so we can get all the renderings faster than actions can
+ // be processed.
+ renderings.collect {
+ emitted += it
+ println("SAE: $it")
+ if (it == "state change+u1+u2+u3+u4") {
+ renderedMutex.unlock()
+ }
+ }
+ }
+
+ trigger.value = "state change"
+
+ renderedMutex.lock()
+
+ collectionJob.cancel()
+
+ // 2 renderings (initial and then the update.) Not *5* renderings.
+ assertEquals(2, emitted.size, "Expected only 2 emitted renderings when conflating actions.")
+ assertEquals(
+ 2,
+ renderingsPassed,
+ "Expected only 2 renderings passed to interceptor when conflating actions."
+ )
+ assertEquals("state change+u1+u2+u3+u4", emitted.last())
+ }
+
+ @Test
+ fun conflate_renderings_for_multiple_side_effect_actions() =
+ runTest(UnconfinedTestDispatcher()) {
+
+ val trigger = MutableStateFlow("unchanged state")
+ val emitted = mutableListOf()
+ var renderingsPassed = 0
+ val countInterceptor = object : WorkflowInterceptor {
+ override fun onRuntimeLoopTick(outcome: RuntimeLoopOutcome) {
+ if (outcome is RenderPassesComplete<*>) {
+ renderingsPassed++
+ }
+ }
+ }
+
+ val childWorkflow = Workflow.stateful(
+ initialState = "unchanged state",
+ render = { renderState ->
+ runningSideEffect("childSideEffect") {
+ trigger.drop(1).collect {
+ actionSink.send(
+ action(
+ name = "handleChildSideEffectAction",
+ ) {
+ val newState = "$it+u1"
+ state = newState
+ setOutput(newState)
+ }
+ )
+ }
+ }
+ renderState
+ }
+ )
+ val workflow = Workflow.stateful(
+ initialState = "unchanged state",
+ render = { renderState ->
+ renderChild(childWorkflow) { childOutput ->
+ action("childHandler") {
+ state = childOutput
+ }
+ }
+ runningSideEffect("parentSideEffect") {
+ trigger.drop(1).collect {
+ actionSink.send(
+ action(
+ name = "handleParentSideEffectAction",
+ ) {
+ state = "$state+u2"
+ }
+ )
+ }
+ }
+ renderState
+ }
+ )
+ val props = MutableStateFlow(Unit)
+ // Render this on the Main.immediate dispatcher from Android.
+ val renderings = renderWorkflowIn(
+ workflow = workflow,
+ scope = backgroundScope +
+ AndroidUiDispatcher.Main,
+ props = props,
+ runtimeConfig = setOf(CONFLATE_STALE_RENDERINGS),
+ workflowTracer = null,
+ interceptors = listOf(countInterceptor)
+ ) { }
+
+ val renderedMutex = Mutex(locked = true)
+
+ val collectionJob = launch {
+ // Collect this unconfined so we can get all the renderings faster than actions can
+ // be processed.
+ renderings.collect {
+ emitted += it
+ if (it == "state change+u1+u2") {
+ renderedMutex.unlock()
+ }
+ }
+ }
+
+ trigger.value = "state change"
+
+ renderedMutex.lock()
+
+ collectionJob.cancel()
+
+ // 2 renderings (initial and then the update.) Not *3* renderings.
+ assertEquals(2, emitted.size, "Expected only 2 emitted renderings when conflating actions.")
+ assertEquals(
+ 2,
+ renderingsPassed,
+ "Expected only 2 renderings passed to interceptor when conflating actions."
+ )
+ assertEquals("state change+u1+u2", emitted.last())
+ }
+
+ private val runtimes = setOf(
+ RuntimeConfigOptions.RENDER_PER_ACTION,
+ setOf(RENDER_ONLY_WHEN_STATE_CHANGES),
+ setOf(CONFLATE_STALE_RENDERINGS),
+ setOf(STABLE_EVENT_HANDLERS),
+ setOf(CONFLATE_STALE_RENDERINGS, RENDER_ONLY_WHEN_STATE_CHANGES),
+ setOf(RENDER_ONLY_WHEN_STATE_CHANGES, PARTIAL_TREE_RENDERING),
+ setOf(CONFLATE_STALE_RENDERINGS, RENDER_ONLY_WHEN_STATE_CHANGES, STABLE_EVENT_HANDLERS),
+ setOf(RENDER_ONLY_WHEN_STATE_CHANGES, PARTIAL_TREE_RENDERING, STABLE_EVENT_HANDLERS),
+ setOf(CONFLATE_STALE_RENDERINGS, RENDER_ONLY_WHEN_STATE_CHANGES, PARTIAL_TREE_RENDERING),
+ setOf(
+ CONFLATE_STALE_RENDERINGS,
+ RENDER_ONLY_WHEN_STATE_CHANGES,
+ PARTIAL_TREE_RENDERING,
+ STABLE_EVENT_HANDLERS
+ ),
+ )
+
+ private class SimpleScreen(
+ val name: String = "Empty",
+ val callback: () -> Unit,
+ )
+
+ @Test
+ fun all_runtimes_handle_rendering_events_before_next_frame() {
+
+ var mainChoreographer: Choreographer? = null
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ mainChoreographer = Choreographer.getInstance()
+ }
+
+ var theFrameWasRun = false
+ val frameCallback = Choreographer.FrameCallback {
+ theFrameWasRun = true
+ println("SAE: Frame callback run.")
+ }
+
+ runtimes.forEach { runtimeConfig ->
+ runTest(UnconfinedTestDispatcher()) {
+
+ println("SAE: TEST CONFIG: $runtimeConfig")
+
+ theFrameWasRun = false
+ val mutex = Mutex(locked = true)
+
+ val workflow = Workflow.stateful(
+ initialState = "neverends",
+ render = { renderState ->
+ SimpleScreen(
+ name = renderState,
+ callback = {
+ println("SAE: CALLBACK FIRED")
+ actionSink.send(
+ action(
+ name = "handleInput"
+ ) {
+ println("SAE: handleInput action applied")
+ state = "$state+$state"
+ }
+ )
+ mainChoreographer!!.postFrameCallback(frameCallback)
+ println("SAE: set up frame callback")
+ }
+ )
+ }
+ )
+
+ val renderings = renderWorkflowIn(
+ workflow = workflow,
+ scope = backgroundScope +
+ AndroidUiDispatcher.Main,
+ props = MutableStateFlow(Unit).asStateFlow(),
+ runtimeConfig = runtimeConfig,
+ workflowTracer = null,
+ interceptors = emptyList()
+ ) {}
+
+ val collectionJob = launch {
+ renderings.collect {
+ println("SAE: got rendering: ${it.name}")
+ if (it.name == "neverends+neverends") {
+ // The rendering we were looking for after the event!
+ assertFalse(
+ theFrameWasRun,
+ "The callback on this frame was run before we" +
+ "got our rendering!"
+ )
+ mainChoreographer!!.removeFrameCallback(frameCallback)
+ mutex.unlock()
+ } else {
+ it.callback()
+ }
+ }
+ }
+
+ mutex.lock()
+ collectionJob.cancel()
+ }
+ }
+ }
+
+ @Test
+ fun all_runtimes_handle_actions_before_the_next_frame() {
+ var mainChoreographer: Choreographer? = null
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ mainChoreographer = Choreographer.getInstance()
+ }
+
+ var theFrameWasRun = false
+ val frameCallback = Choreographer.FrameCallback {
+ theFrameWasRun = true
+ println("SAE: Frame callback run.")
+ }
+
+ runtimes.forEach { runtimeConfig ->
+ runTest(UnconfinedTestDispatcher()) {
+
+ println("SAE: Running test with config: $runtimeConfig")
+
+ val trigger = MutableStateFlow("unchanged state")
+ val mutex = Mutex(locked = true)
+ theFrameWasRun = false
+
+ val workflow = Workflow.stateful(
+ initialState = "unchanged state",
+ render = { renderState ->
+ runningSideEffect("only1") {
+ trigger.drop(1).collect {
+ println("SAE: Enqueued handler message")
+ actionSink.send(
+ action(
+ name = "triggerCollect",
+ ) {
+ println("SAE: Trigger handler action")
+ state = it
+ }
+ )
+ mainChoreographer!!.postFrameCallback(frameCallback)
+ }
+ }
+ renderState
+ }
+ )
+
+ val renderings = renderWorkflowIn(
+ workflow = workflow,
+ scope = backgroundScope +
+ AndroidUiDispatcher.Main,
+ props = MutableStateFlow(Unit).asStateFlow(),
+ runtimeConfig = runtimeConfig,
+ workflowTracer = null,
+ interceptors = emptyList()
+ ) {}
+
+ val collectionJob = launch {
+ // Collect this unconfined so we can get all the renderings faster than actions can
+ // be processed.
+ renderings.collect {
+ println("SAE: Got rendering: $it, theFrameWasRun? $theFrameWasRun")
+ if (it == "changed state") {
+ // The rendering we were looking for!
+ assertFalse(
+ theFrameWasRun,
+ "The callback on this frame was run before we" +
+ "got our rendering!"
+ )
+ mainChoreographer!!.removeFrameCallback(frameCallback)
+ mutex.unlock()
+ }
+ }
+ }
+
+ launch {
+ trigger.value = "changed state"
+ }
+
+ mutex.lock()
+ collectionJob.cancel()
+ }
+ }
+ }
+}
diff --git a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/AndroidRenderWorkflowInTest.kt b/workflow-runtime-android/src/androidTest/java/com/squareup/workflow1/android/AndroidRenderWorkflowInTest.kt
similarity index 91%
rename from workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/AndroidRenderWorkflowInTest.kt
rename to workflow-runtime-android/src/androidTest/java/com/squareup/workflow1/android/AndroidRenderWorkflowInTest.kt
index 9f93db7471..4404f65713 100644
--- a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/AndroidRenderWorkflowInTest.kt
+++ b/workflow-runtime-android/src/androidTest/java/com/squareup/workflow1/android/AndroidRenderWorkflowInTest.kt
@@ -1,4 +1,4 @@
-package com.squareup.workflow1.ui
+package com.squareup.workflow1.android
import android.widget.FrameLayout
import androidx.activity.ComponentActivity
@@ -10,7 +10,12 @@ import androidx.lifecycle.viewModelScope
import androidx.test.ext.junit.rules.ActivityScenarioRule
import com.google.common.truth.Truth.assertThat
import com.squareup.workflow1.StatelessWorkflow
+import com.squareup.workflow1.ui.AndroidScreen
+import com.squareup.workflow1.ui.Screen
+import com.squareup.workflow1.ui.ScreenViewFactory
+import com.squareup.workflow1.ui.ScreenViewHolder
import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule
+import com.squareup.workflow1.ui.workflowContentView
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.StateFlow
import leakcanary.DetectLeaksAfterTestSuccess
diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidRenderWorkflow.kt b/workflow-runtime-android/src/main/java/com/squareup/workflow1/android/AndroidRenderWorkflow.kt
similarity index 95%
rename from workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidRenderWorkflow.kt
rename to workflow-runtime-android/src/main/java/com/squareup/workflow1/android/AndroidRenderWorkflow.kt
index 02e5bc00fd..433f06d8f1 100644
--- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidRenderWorkflow.kt
+++ b/workflow-runtime-android/src/main/java/com/squareup/workflow1/android/AndroidRenderWorkflow.kt
@@ -1,4 +1,4 @@
-package com.squareup.workflow1.ui
+package com.squareup.workflow1.android
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.SavedStateHandle
@@ -172,14 +172,14 @@ public fun renderWorkflowIn(
workflowTracer: WorkflowTracer? = null,
onOutput: suspend (OutputT) -> Unit = {}
): StateFlow = renderWorkflowIn(
- workflow,
- scope,
- MutableStateFlow(prop),
- savedStateHandle,
- interceptors,
- runtimeConfig,
- workflowTracer,
- onOutput
+ workflow = workflow,
+ scope = scope,
+ props = MutableStateFlow(prop),
+ savedStateHandle = savedStateHandle,
+ interceptors = interceptors,
+ runtimeConfig = runtimeConfig,
+ workflowTracer = workflowTracer,
+ onOutput = onOutput
)
/**
@@ -273,14 +273,15 @@ public fun renderWorkflowIn(
): StateFlow {
val restoredSnap = savedStateHandle?.get(KEY)?.snapshot
val renderingsAndSnapshots = renderWorkflowIn(
- workflow,
- scope,
- props,
- restoredSnap,
- interceptors,
- runtimeConfig,
- workflowTracer,
- onOutput
+ workflow = workflow,
+ scope = scope,
+ props = props,
+ initialSnapshot = restoredSnap,
+ interceptors = interceptors,
+ runtimeConfig = runtimeConfig,
+ workflowTracer = workflowTracer,
+ workflowFrameClock = AndroidFrameClock,
+ onOutput = onOutput,
)
return renderingsAndSnapshots
diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/PickledTreesnapshot.kt b/workflow-runtime-android/src/main/java/com/squareup/workflow1/android/PickledTreesnapshot.kt
similarity index 95%
rename from workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/PickledTreesnapshot.kt
rename to workflow-runtime-android/src/main/java/com/squareup/workflow1/android/PickledTreesnapshot.kt
index 3936e04164..b9f229725c 100644
--- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/PickledTreesnapshot.kt
+++ b/workflow-runtime-android/src/main/java/com/squareup/workflow1/android/PickledTreesnapshot.kt
@@ -1,4 +1,4 @@
-package com.squareup.workflow1.ui
+package com.squareup.workflow1.android
import android.os.Parcel
import android.os.Parcelable
diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/TreeSnapshotSaver.kt b/workflow-runtime-android/src/main/java/com/squareup/workflow1/android/TreeSnapshotSaver.kt
similarity index 94%
rename from workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/TreeSnapshotSaver.kt
rename to workflow-runtime-android/src/main/java/com/squareup/workflow1/android/TreeSnapshotSaver.kt
index 4c78a97073..4ce542b8de 100644
--- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/TreeSnapshotSaver.kt
+++ b/workflow-runtime-android/src/main/java/com/squareup/workflow1/android/TreeSnapshotSaver.kt
@@ -1,11 +1,11 @@
-package com.squareup.workflow1.ui
+package com.squareup.workflow1.android
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Bundle
import androidx.savedstate.SavedStateRegistry
import com.squareup.workflow1.TreeSnapshot
-import com.squareup.workflow1.ui.TreeSnapshotSaver.Companion.fromSavedStateRegistry
+import com.squareup.workflow1.android.TreeSnapshotSaver.Companion.fromSavedStateRegistry
/**
* Persistence aid for [TreeSnapshot]. Use [fromSavedStateRegistry] to create one
diff --git a/workflow-runtime-android/src/main/java/com/squareup/workflow1/android/WorkflowFrameClock.kt b/workflow-runtime-android/src/main/java/com/squareup/workflow1/android/WorkflowFrameClock.kt
new file mode 100644
index 0000000000..10ab4a831b
--- /dev/null
+++ b/workflow-runtime-android/src/main/java/com/squareup/workflow1/android/WorkflowFrameClock.kt
@@ -0,0 +1,20 @@
+package com.squareup.workflow1.android
+
+import androidx.compose.runtime.ExperimentalComposeApi
+import androidx.compose.runtime.monotonicFrameClock
+import androidx.compose.ui.platform.AndroidUiDispatcher
+import com.squareup.workflow1.WorkflowFrameClock
+
+@OptIn(ExperimentalComposeApi::class)
+public object AndroidFrameClock : WorkflowFrameClock {
+
+ private val composeAndroidFrameClock = AndroidUiDispatcher.Main.monotonicFrameClock
+
+ override suspend fun resumeOnFrame() {
+ composeAndroidFrameClock.withFrameNanos {
+ println("SAE: With Frame Nanos Callback! at time: $it")
+ // no-op, we just need to resume at the frame barrier!
+ }
+ println("SAE: Resumed from `withFrameNanos`")
+ }
+}
diff --git a/workflow-runtime/api/workflow-runtime.api b/workflow-runtime/api/workflow-runtime.api
index 150f639768..999e427923 100644
--- a/workflow-runtime/api/workflow-runtime.api
+++ b/workflow-runtime/api/workflow-runtime.api
@@ -11,8 +11,8 @@ public final class com/squareup/workflow1/NoopWorkflowInterceptor : com/squareup
}
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;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;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;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;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;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;Lcom/squareup/workflow1/WorkflowFrameClock;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;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;Lcom/squareup/workflow1/WorkflowFrameClock;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow;
}
public final class com/squareup/workflow1/RenderingAndSnapshot {
@@ -51,6 +51,15 @@ public final class com/squareup/workflow1/TreeSnapshot$Companion {
public final fun parse (Lokio/ByteString;)Lcom/squareup/workflow1/TreeSnapshot;
}
+public abstract interface class com/squareup/workflow1/WorkflowFrameClock {
+ public static final field Companion Lcom/squareup/workflow1/WorkflowFrameClock$Companion;
+ public abstract fun resumeOnFrame (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public final class com/squareup/workflow1/WorkflowFrameClock$Companion {
+ public final fun getDEFAULT_FRAME_CLOCK ()Lcom/squareup/workflow1/WorkflowFrameClock;
+}
+
public abstract interface class com/squareup/workflow1/WorkflowInterceptor {
public abstract fun onInitialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
public abstract fun onPropsChanged (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
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 78a25f097f..6cdbd08c06 100644
--- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt
+++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt
@@ -100,6 +100,9 @@ import kotlinx.coroutines.launch
* @param runtimeConfig
* Configuration parameters for the Workflow Runtime.
*
+ * @param workflowFrameClock
+ * The frame clock that will be used to synchronize this Workflow Runtime.
+ *
* @return
* A [StateFlow] of [RenderingAndSnapshot]s that will emit any time the root workflow creates a new
* rendering.
@@ -113,18 +116,19 @@ public fun renderWorkflowIn(
interceptors: List = emptyList(),
runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG,
workflowTracer: WorkflowTracer? = null,
+ workflowFrameClock: WorkflowFrameClock = WorkflowFrameClock.DEFAULT_FRAME_CLOCK,
onOutput: suspend (OutputT) -> Unit
): StateFlow> {
val chainedInterceptor = interceptors.chained()
val runner = WorkflowRunner(
- scope,
- workflow,
- props,
- initialSnapshot,
- chainedInterceptor,
- runtimeConfig,
- workflowTracer
+ scope = scope,
+ protoWorkflow = workflow,
+ props = props,
+ snapshot = initialSnapshot,
+ interceptor = chainedInterceptor,
+ runtimeConfig = runtimeConfig,
+ workflowTracer = workflowTracer
)
// Rendering is synchronous, so we can run the first render pass before launching the runtime
@@ -178,10 +182,16 @@ public fun renderWorkflowIn(
scope.launch {
outer@ while (isActive) {
- // It might look weird to start by processing an action before getting the rendering below,
+ // It might look weird to start by waiting for an action before getting the rendering below,
// but remember the first render pass already occurred above, before this coroutine was even
// launched.
- var actionResult: ActionProcessingResult = runner.processAction()
+ var actionResult: ActionProcessingResult = runner.waitForAction()
+ println("SAE: After direct apply action: $actionResult.")
+ //
+ // if (actionResult is DeferredActionToBeApplied) {
+ // actionResult = actionResult.applyAction.await()
+ // println("SAE: After deferred apply action: $actionResult.")
+ // }
if (shouldShortCircuitForUnchangedState(actionResult)) {
chainedInterceptor.onRuntimeLoopTick(RenderPassSkipped())
@@ -200,8 +210,11 @@ public fun renderWorkflowIn(
var conflationHasChangedState = false
conflate@ while (isActive && actionResult is ActionApplied<*> && actionResult.output == null) {
conflationHasChangedState = conflationHasChangedState || actionResult.stateChanged
- // We may have more actions we can process, this rendering could be stale.
- actionResult = runner.processAction(waitForAnAction = false)
+
+ println("SAE: Before conflate applies another action.")
+ // We may have more actions we can apply, this rendering could be stale.
+ actionResult = runner.applyNextAvailableAction()
+ println("SAE: After conflate applies another action: $actionResult.")
// If no actions processed, then no new rendering needed. Pass on to UI.
if (actionResult == ActionsExhausted) break@conflate
@@ -222,7 +235,7 @@ public fun renderWorkflowIn(
continue@outer
}
- // Make sure the runtime has not been cancelled from runner.processAction()
+ // Make sure the runtime has not been cancelled.
if (!isActive) return@launch
nextRenderAndSnapshot = runner.nextRendering()
diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowFrameClock.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowFrameClock.kt
new file mode 100644
index 0000000000..25f7ad29fe
--- /dev/null
+++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowFrameClock.kt
@@ -0,0 +1,23 @@
+package com.squareup.workflow1
+
+import kotlinx.coroutines.yield
+
+/**
+ * Basic frame clock providing synchronization for the Workflow Runtime.
+ */
+public fun interface WorkflowFrameClock {
+
+ /**
+ * Resumes before the next 'frame' is processed.
+ */
+ public suspend fun resumeOnFrame(): Unit
+
+ companion object {
+ /**
+ * The default 'frame clock' is simply to yield the dispatcher to let actions queue up.
+ */
+ val DEFAULT_FRAME_CLOCK = WorkflowFrameClock {
+ yield()
+ }
+ }
+}
diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt
index 09fb7608a3..688e96fe73 100644
--- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt
+++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt
@@ -2,6 +2,7 @@ package com.squareup.workflow1.internal
import com.squareup.workflow1.ActionApplied
import com.squareup.workflow1.ActionProcessingResult
+import com.squareup.workflow1.ActionsExhausted
import com.squareup.workflow1.NoopWorkflowInterceptor
import com.squareup.workflow1.RuntimeConfig
import com.squareup.workflow1.TreeSnapshot
@@ -146,19 +147,26 @@ internal class SubtreeManager(
}
/**
- * Uses [selector] to invoke [WorkflowNode.onNextAction] for every running child workflow this instance
+ * Uses [selector] to invoke [WorkflowNode.selectNextAction] for every running child workflow this instance
* is managing.
*
- * @return [Boolean] whether or not the children action queues are empty.
*/
- fun onNextChildAction(selector: SelectBuilder): Boolean {
- var empty = true
+ fun selectNextChildAction(
+ selector: SelectBuilder,
+ ) {
children.forEachActive { child ->
- // Do this separately so the compiler doesn't avoid it if empty is already false.
- val childEmpty = child.workflowNode.onNextAction(selector)
- empty = childEmpty && empty
+ child.workflowNode.selectNextAction(selector)
}
- return empty
+ }
+
+ fun applyNextAvailableChildAction(): ActionProcessingResult {
+ children.forEachActive { child ->
+ val result = child.workflowNode.applyNextAvailableAction()
+ if (result != ActionsExhausted) {
+ return result
+ }
+ }
+ return ActionsExhausted
}
fun createChildSnapshots(): Map {
diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt
index 04c680fb20..bf15db9fa1 100644
--- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt
+++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt
@@ -2,6 +2,7 @@ package com.squareup.workflow1.internal
import com.squareup.workflow1.ActionApplied
import com.squareup.workflow1.ActionProcessingResult
+import com.squareup.workflow1.ActionsExhausted
import com.squareup.workflow1.NoopWorkflowInterceptor
import com.squareup.workflow1.NullableInitBox
import com.squareup.workflow1.RenderContext
@@ -27,8 +28,6 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart.LAZY
-import kotlinx.coroutines.DelicateCoroutinesApi
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
@@ -208,29 +207,44 @@ internal class WorkflowNode(
*
* It is an error to call this method after calling [cancel].
*
- * @return [Boolean] whether or not the queues were empty for this node and its children at the
- * time of suspending.
*/
- @OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class)
- fun onNextAction(selector: SelectBuilder): Boolean {
+ fun selectNextAction(
+ selector: SelectBuilder,
+ ) {
// Listen for any child workflow updates.
- var empty = subtreeManager.onNextChildAction(selector)
-
- empty = empty && (eventActionsChannel.isEmpty || eventActionsChannel.isClosedForReceive)
+ subtreeManager.selectNextChildAction(selector)
// Listen for any events.
with(selector) {
eventActionsChannel.onReceive { action ->
+ // if (runtimeConfig.shouldDeferFirstAction()) {
+ // return@onReceive DeferredActionToBeApplied(
+ // applyAction = async {
+ // applyAction(action)
+ // }
+ // )
+ // }
+
return@onReceive applyAction(action)
}
}
- return empty
+ }
+
+ fun applyNextAvailableAction(): ActionProcessingResult {
+ val result = subtreeManager.applyNextAvailableChildAction()
+
+ if (result == ActionsExhausted) {
+ return eventActionsChannel.tryReceive().getOrNull()?.let { action ->
+ applyAction(action)
+ } ?: ActionsExhausted
+ }
+ return result
}
/**
* Cancels this state machine host, and any coroutines started as children of it.
*
- * This must be called when the caller will no longer call [onNextAction]. It is an error to call [onNextAction]
+ * This must be called when the caller will no longer call [selectNextAction]. It is an error to call [selectNextAction]
* after calling this method.
*/
fun cancel(cause: CancellationException? = null) {
diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt
index 9eb66bb1ba..6ff146e0c0 100644
--- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt
+++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt
@@ -1,14 +1,11 @@
package com.squareup.workflow1.internal
import com.squareup.workflow1.ActionProcessingResult
-import com.squareup.workflow1.ActionsExhausted
import com.squareup.workflow1.PropsUpdated
import com.squareup.workflow1.RenderingAndSnapshot
import com.squareup.workflow1.RuntimeConfig
-import com.squareup.workflow1.RuntimeConfigOptions.CONFLATE_STALE_RENDERINGS
import com.squareup.workflow1.TreeSnapshot
import com.squareup.workflow1.Workflow
-import com.squareup.workflow1.WorkflowExperimentalRuntime
import com.squareup.workflow1.WorkflowInterceptor
import com.squareup.workflow1.WorkflowTracer
import kotlinx.coroutines.CancellationException
@@ -19,7 +16,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.dropWhile
import kotlinx.coroutines.flow.produceIn
import kotlinx.coroutines.selects.SelectBuilder
-import kotlinx.coroutines.selects.onTimeout
import kotlinx.coroutines.selects.select
@OptIn(ExperimentalCoroutinesApi::class)
@@ -65,8 +61,8 @@ internal class WorkflowRunner(
/**
* Perform a render pass and a snapshot pass and return the results.
*
- * This method must be called before the first call to [processAction], and must be called again
- * between every subsequent call to [processAction].
+ * This method must be called before the first call to [waitForAction], and must be called again
+ * between every subsequent call to [waitForAction].
*/
fun nextRendering(): RenderingAndSnapshot {
return interceptor.onRenderAndSnapshot(currentProps, { props ->
@@ -77,30 +73,26 @@ internal class WorkflowRunner(
}
/**
- * Process the first action from anywhere in the Workflow tree, or process the updated props.
+ * Suspends waiting to process the next action from anywhere in the Workflow tree, or process
+ * the updated props.
*
* [select] is used which suspends on multiple coroutines, executing the first to be scheduled
* and resume (breaking ties with order of declaration). Guarantees only continuing on the winning
* coroutine and no others.
*/
- @OptIn(WorkflowExperimentalRuntime::class)
- suspend fun processAction(waitForAnAction: Boolean = true): ActionProcessingResult {
- // If waitForAction is true we block and wait until there is an action to process.
+ suspend fun waitForAction(): ActionProcessingResult {
+ // If firstAction is true we block and wait until there is an action to process.
return select {
onPropsUpdated()
// Have the workflow tree build the select to wait for an event/output from Worker.
- val empty = rootNode.onNextAction(this)
- if (!waitForAnAction && runtimeConfig.contains(CONFLATE_STALE_RENDERINGS) && empty) {
- // With CONFLATE_STALE_RENDERINGS if there are no queued actions and we are not
- // waiting for one, then return ActionsExhausted and pass the rendering on.
- onTimeout(timeMillis = 0) {
- // This will select synchronously since time is 0.
- ActionsExhausted
- }
- }
+ rootNode.selectNextAction(this)
}
}
+ fun applyNextAvailableAction(): ActionProcessingResult {
+ return rootNode.applyNextAvailableAction()
+ }
+
@OptIn(DelicateCoroutinesApi::class)
private fun SelectBuilder.onPropsUpdated() {
// Stop trying to read from the inputs channel after it's closed.
diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/SubtreeManagerTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/SubtreeManagerTest.kt
index 909bfff5c7..1d68d4f6f5 100644
--- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/SubtreeManagerTest.kt
+++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/SubtreeManagerTest.kt
@@ -305,7 +305,7 @@ internal class SubtreeManagerTest {
@Suppress("UNCHECKED_CAST")
private suspend fun SubtreeManager
.applyNextAction() =
select {
- onNextChildAction(this)
+ selectNextChildAction(this)
} as ActionApplied?>
private fun subtreeManagerForTest(
diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt
index 616630284a..01fa388a72 100644
--- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt
+++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt
@@ -190,7 +190,7 @@ internal class WorkflowNodeTest {
runTest {
val result = withTimeout(10) {
select {
- node.onNextAction(this)
+ node.selectNextAction(this)
} as ActionApplied
}
assertEquals("applyActionOutput:event", result.output!!.value)
@@ -236,7 +236,7 @@ internal class WorkflowNodeTest {
val result = withTimeout(10) {
List(2) {
select {
- node.onNextAction(this)
+ node.selectNextAction(this)
} as ActionApplied
}
}
@@ -340,7 +340,7 @@ internal class WorkflowNodeTest {
// Result should be available instantly, any delay at all indicates something is broken.
val result = withTimeout(1) {
select {
- node.onNextAction(this)
+ node.selectNextAction(this)
} as ActionApplied
}
assertEquals("result", result.output!!.value)
@@ -1198,7 +1198,7 @@ internal class WorkflowNodeTest {
sink.send("hello")
val result = select {
- node.onNextAction(this)
+ node.selectNextAction(this)
} as ActionApplied
assertNull(result.output)
assertTrue(result.stateChanged)
@@ -1227,7 +1227,7 @@ internal class WorkflowNodeTest {
runTest {
val result = select {
- node.onNextAction(this)
+ node.selectNextAction(this)
} as ActionApplied
assertEquals("output:hello", result.output!!.value)
assertFalse(result.stateChanged)
@@ -1252,7 +1252,7 @@ internal class WorkflowNodeTest {
runTest {
val result = select {
- node.onNextAction(this)
+ node.selectNextAction(this)
} as ActionApplied
assertNull(result.output!!.value)
assertFalse(result.stateChanged)
@@ -1279,7 +1279,7 @@ internal class WorkflowNodeTest {
node.render(workflow.asStatefulWorkflow(), Unit)
select {
- node.onNextAction(this)
+ node.selectNextAction(this)
} as ActionApplied
val state = node.render(workflow.asStatefulWorkflow(), Unit)
@@ -1306,7 +1306,7 @@ internal class WorkflowNodeTest {
runTest {
val result = select {
- node.onNextAction(this)
+ node.selectNextAction(this)
} as ActionApplied
assertEquals("output:child:hello", result.output!!.value)
}
@@ -1330,7 +1330,7 @@ internal class WorkflowNodeTest {
runTest {
val result = select {
- node.onNextAction(this)
+ node.selectNextAction(this)
} as ActionApplied
assertNull(result.output!!.value)
}
diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowRunnerTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowRunnerTest.kt
index 18e657ce44..e045b58a70 100644
--- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowRunnerTest.kt
+++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowRunnerTest.kt
@@ -86,7 +86,7 @@ internal class WorkflowRunnerTest {
}
}
- @Test fun initial_processActions_does_not_handle_initial_props() {
+ @Test fun initial_waitForActions_does_not_handle_initial_props() {
runtimeTestRunner.runParametrizedTest(
paramSource = runtimeOptions,
before = ::setup,
@@ -102,14 +102,14 @@ internal class WorkflowRunnerTest {
)
runner.nextRendering()
- val outputDeferred = scope.async { runner.processAction() }
+ val outputDeferred = scope.async { runner.waitForAction() }
scope.runCurrent()
assertTrue(outputDeferred.isActive)
}
}
- @Test fun initial_processActions_handles_props_changed_after_initialization() {
+ @Test fun initial_waitForActions_handles_props_changed_after_initialization() {
runtimeTestRunner.runParametrizedTest(
paramSource = runtimeOptions,
before = ::setup,
@@ -131,7 +131,7 @@ internal class WorkflowRunnerTest {
// Get the runner into the state where it's waiting for a props update.
val initialRendering = runner.nextRendering().rendering
assertEquals("initial", initialRendering)
- val output = scope.async { runner.processAction() }
+ val output = scope.async { runner.waitForAction() }
assertTrue(output.isActive)
// Resume the dispatcher to start the coroutines and process the new props value.
@@ -146,7 +146,7 @@ internal class WorkflowRunnerTest {
}
}
- @Test fun processActions_handles_workflow_update() {
+ @Test fun waitForActions_handles_workflow_update() {
runtimeTestRunner.runParametrizedTest(
paramSource = runtimeOptions,
before = ::setup,
@@ -179,7 +179,7 @@ internal class WorkflowRunnerTest {
}
}
- @Test fun processActions_handles_concurrent_props_change_and_workflow_update() {
+ @Test fun waitForActions_handles_concurrent_props_change_and_workflow_update() {
runtimeTestRunner.runParametrizedTest(
paramSource = runtimeOptions,
before = ::setup,
@@ -219,7 +219,7 @@ internal class WorkflowRunnerTest {
}
}
- @Test fun cancelRuntime_does_not_interrupt_processActions() {
+ @Test fun cancelRuntime_does_not_interrupt_waitForActions() {
runtimeTestRunner.runParametrizedTest(
paramSource = runtimeOptions,
before = ::setup,
@@ -229,7 +229,7 @@ internal class WorkflowRunnerTest {
val runner =
WorkflowRunner(workflow, MutableStateFlow(Unit), runtimeConfig)
runner.nextRendering()
- val output = scope.async { runner.processAction() }
+ val output = scope.async { runner.waitForAction() }
scope.runCurrent()
assertTrue(output.isActive)
@@ -272,7 +272,7 @@ internal class WorkflowRunnerTest {
}
}
- @Test fun cancelling_scope_interrupts_processActions() {
+ @Test fun cancelling_scope_interrupts_waitForActions() {
runtimeTestRunner.runParametrizedTest(
paramSource = runtimeOptions,
before = ::setup,
@@ -283,7 +283,7 @@ internal class WorkflowRunnerTest {
val runner =
WorkflowRunner(workflow, MutableStateFlow(Unit), runtimeConfig)
runner.nextRendering()
- val actionResult = scope.async { runner.processAction() }
+ val actionResult = scope.async { runner.waitForAction() }
scope.runCurrent()
assertTrue(actionResult.isActive)
@@ -314,7 +314,7 @@ internal class WorkflowRunnerTest {
val runner =
WorkflowRunner(workflow, MutableStateFlow(Unit), runtimeConfig)
runner.nextRendering()
- val actionResult = scope.async { runner.processAction() }
+ val actionResult = scope.async { runner.waitForAction() }
scope.runCurrent()
assertTrue(actionResult.isActive)
assertNull(cancellationException)
@@ -330,10 +330,11 @@ internal class WorkflowRunnerTest {
@Suppress("UNCHECKED_CAST")
private fun WorkflowRunner<*, T, *>.runTillNextActionResult(): ActionApplied? = scope.run {
- val firstOutputDeferred = async { processAction() }
+ val firstOutputDeferred = async { waitForAction() }
runCurrent()
// If it is [ PropsUpdated] or any other ActionProcessingResult, will return as null.
- firstOutputDeferred.getCompleted() as? ActionApplied
+ val actionResult = firstOutputDeferred.getCompleted() as? ActionApplied
+ return@run actionResult
}
@Suppress("TestFunctionName")
diff --git a/workflow-runtime/src/iosMain/kotlin/com.squareup.workflow1.internal/SystemUtils.kt b/workflow-runtime/src/iosMain/kotlin/com/squareup/workflow1/internal/SystemUtils.kt
similarity index 100%
rename from workflow-runtime/src/iosMain/kotlin/com.squareup.workflow1.internal/SystemUtils.kt
rename to workflow-runtime/src/iosMain/kotlin/com/squareup/workflow1/internal/SystemUtils.kt
diff --git a/workflow-runtime/src/jsMain/kotlin/com.squareup.workflow1.internal/SystemUtils.kt b/workflow-runtime/src/jsMain/kotlin/com/squareup/workflow1/internal/SystemUtils.kt
similarity index 100%
rename from workflow-runtime/src/jsMain/kotlin/com.squareup.workflow1.internal/SystemUtils.kt
rename to workflow-runtime/src/jsMain/kotlin/com/squareup/workflow1/internal/SystemUtils.kt
diff --git a/workflow-ui/core-android/api/core-android.api b/workflow-ui/core-android/api/core-android.api
index 9e6c49969e..1f462ae976 100644
--- a/workflow-ui/core-android/api/core-android.api
+++ b/workflow-ui/core-android/api/core-android.api
@@ -3,16 +3,6 @@ public final class com/squareup/workflow1/ui/ActivityWorkflowContentViewKt {
public static final fun getWorkflowContentViewOrNull (Landroid/app/Activity;)Lcom/squareup/workflow1/ui/WorkflowLayout;
}
-public final class com/squareup/workflow1/ui/AndroidRenderWorkflowKt {
- public static final fun removeWorkflowState (Landroidx/lifecycle/SavedStateHandle;)V
- public static final fun renderWorkflowIn (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;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;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;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;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;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;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;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;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;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;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;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 {
public abstract fun getViewFactory ()Lcom/squareup/workflow1/ui/ScreenViewFactory;
}
diff --git a/workflow-ui/core-android/build.gradle.kts b/workflow-ui/core-android/build.gradle.kts
index 89adc8b266..1d489d7e81 100644
--- a/workflow-ui/core-android/build.gradle.kts
+++ b/workflow-ui/core-android/build.gradle.kts
@@ -23,6 +23,7 @@ dependencies {
// Needs to be API for the WorkflowInterceptor argument to WorkflowRunner.Config.
api(project(":workflow-runtime"))
+ api(project(":workflow-runtime-android"))
api(project(":workflow-ui:core-common"))
compileOnly(libs.androidx.viewbinding)
diff --git a/workflow-ui/core-android/dependencies/releaseRuntimeClasspath.txt b/workflow-ui/core-android/dependencies/releaseRuntimeClasspath.txt
index 8ad63aa2f1..ab2f7f1db0 100644
--- a/workflow-ui/core-android/dependencies/releaseRuntimeClasspath.txt
+++ b/workflow-ui/core-android/dependencies/releaseRuntimeClasspath.txt
@@ -1,15 +1,38 @@
+androidx.activity:activity-ktx:1.8.2
androidx.activity:activity:1.8.2
-androidx.annotation:annotation-experimental:1.4.0
+androidx.annotation:annotation-experimental:1.4.1
androidx.annotation:annotation-jvm:1.8.1
androidx.annotation:annotation:1.8.1
androidx.arch.core:core-common:2.2.0
androidx.arch.core:core-runtime:2.2.0
-androidx.collection:collection:1.1.0
+androidx.autofill:autofill:1.0.0
+androidx.collection:collection-jvm:1.4.4
+androidx.collection:collection-ktx:1.4.4
+androidx.collection:collection:1.4.4
+androidx.compose.runtime:runtime-android:1.7.2
+androidx.compose.runtime:runtime-saveable-android:1.7.2
+androidx.compose.runtime:runtime-saveable:1.7.2
+androidx.compose.runtime:runtime:1.7.2
+androidx.compose.ui:ui-android:1.7.2
+androidx.compose.ui:ui-geometry-android:1.7.2
+androidx.compose.ui:ui-geometry:1.7.2
+androidx.compose.ui:ui-graphics-android:1.7.2
+androidx.compose.ui:ui-graphics:1.7.2
+androidx.compose.ui:ui-text-android:1.7.2
+androidx.compose.ui:ui-text:1.7.2
+androidx.compose.ui:ui-unit-android:1.7.2
+androidx.compose.ui:ui-unit:1.7.2
+androidx.compose.ui:ui-util-android:1.7.2
+androidx.compose.ui:ui-util:1.7.2
+androidx.compose:compose-bom:2024.09.02
androidx.concurrent:concurrent-futures:1.1.0
androidx.core:core-ktx:1.13.1
androidx.core:core:1.13.1
+androidx.customview:customview-poolingcontainer:1.0.0
androidx.documentfile:documentfile:1.0.0
androidx.dynamicanimation:dynamicanimation:1.0.0
+androidx.emoji2:emoji2:1.2.0
+androidx.graphics:graphics-path:1.0.1
androidx.interpolator:interpolator:1.0.0
androidx.legacy:legacy-support-core-utils:1.0.0
androidx.lifecycle:lifecycle-common-jvm:2.8.7
@@ -17,17 +40,22 @@ androidx.lifecycle:lifecycle-common:2.8.7
androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.7
androidx.lifecycle:lifecycle-livedata-core:2.8.7
androidx.lifecycle:lifecycle-livedata:2.8.7
+androidx.lifecycle:lifecycle-process:2.8.7
androidx.lifecycle:lifecycle-runtime-android:2.8.7
+androidx.lifecycle:lifecycle-runtime-compose-android:2.8.7
+androidx.lifecycle:lifecycle-runtime-compose:2.8.7
androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.7
androidx.lifecycle:lifecycle-runtime-ktx:2.8.7
androidx.lifecycle:lifecycle-runtime:2.8.7
androidx.lifecycle:lifecycle-viewmodel-android:2.8.7
+androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.7
androidx.lifecycle:lifecycle-viewmodel:2.8.7
androidx.loader:loader:1.0.0
androidx.localbroadcastmanager:localbroadcastmanager:1.0.0
androidx.print:print:1.0.0
androidx.profileinstaller:profileinstaller:1.3.1
+androidx.savedstate:savedstate-ktx:1.2.1
androidx.savedstate:savedstate:1.2.1
androidx.startup:startup-runtime:1.1.1
androidx.tracing:tracing:1.0.0
diff --git a/workflow-ui/radiography/dependencies/releaseRuntimeClasspath.txt b/workflow-ui/radiography/dependencies/releaseRuntimeClasspath.txt
index 48859fad88..2541a3d3ac 100644
--- a/workflow-ui/radiography/dependencies/releaseRuntimeClasspath.txt
+++ b/workflow-ui/radiography/dependencies/releaseRuntimeClasspath.txt
@@ -1,15 +1,38 @@
+androidx.activity:activity-ktx:1.8.2
androidx.activity:activity:1.8.2
-androidx.annotation:annotation-experimental:1.4.0
+androidx.annotation:annotation-experimental:1.4.1
androidx.annotation:annotation-jvm:1.8.1
androidx.annotation:annotation:1.8.1
androidx.arch.core:core-common:2.2.0
androidx.arch.core:core-runtime:2.2.0
-androidx.collection:collection:1.1.0
+androidx.autofill:autofill:1.0.0
+androidx.collection:collection-jvm:1.4.4
+androidx.collection:collection-ktx:1.4.4
+androidx.collection:collection:1.4.4
+androidx.compose.runtime:runtime-android:1.7.2
+androidx.compose.runtime:runtime-saveable-android:1.7.2
+androidx.compose.runtime:runtime-saveable:1.7.2
+androidx.compose.runtime:runtime:1.7.2
+androidx.compose.ui:ui-android:1.7.2
+androidx.compose.ui:ui-geometry-android:1.7.2
+androidx.compose.ui:ui-geometry:1.7.2
+androidx.compose.ui:ui-graphics-android:1.7.2
+androidx.compose.ui:ui-graphics:1.7.2
+androidx.compose.ui:ui-text-android:1.7.2
+androidx.compose.ui:ui-text:1.7.2
+androidx.compose.ui:ui-unit-android:1.7.2
+androidx.compose.ui:ui-unit:1.7.2
+androidx.compose.ui:ui-util-android:1.7.2
+androidx.compose.ui:ui-util:1.7.2
+androidx.compose:compose-bom:2024.09.02
androidx.concurrent:concurrent-futures:1.1.0
androidx.core:core-ktx:1.13.1
androidx.core:core:1.13.1
+androidx.customview:customview-poolingcontainer:1.0.0
androidx.documentfile:documentfile:1.0.0
androidx.dynamicanimation:dynamicanimation:1.0.0
+androidx.emoji2:emoji2:1.2.0
+androidx.graphics:graphics-path:1.0.1
androidx.interpolator:interpolator:1.0.0
androidx.legacy:legacy-support-core-utils:1.0.0
androidx.lifecycle:lifecycle-common-jvm:2.8.7
@@ -17,17 +40,22 @@ androidx.lifecycle:lifecycle-common:2.8.7
androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.7
androidx.lifecycle:lifecycle-livedata-core:2.8.7
androidx.lifecycle:lifecycle-livedata:2.8.7
+androidx.lifecycle:lifecycle-process:2.8.7
androidx.lifecycle:lifecycle-runtime-android:2.8.7
+androidx.lifecycle:lifecycle-runtime-compose-android:2.8.7
+androidx.lifecycle:lifecycle-runtime-compose:2.8.7
androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.7
androidx.lifecycle:lifecycle-runtime-ktx:2.8.7
androidx.lifecycle:lifecycle-runtime:2.8.7
androidx.lifecycle:lifecycle-viewmodel-android:2.8.7
+androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.7
androidx.lifecycle:lifecycle-viewmodel:2.8.7
androidx.loader:loader:1.0.0
androidx.localbroadcastmanager:localbroadcastmanager:1.0.0
androidx.print:print:1.0.0
androidx.profileinstaller:profileinstaller:1.3.1
+androidx.savedstate:savedstate-ktx:1.2.1
androidx.savedstate:savedstate:1.2.1
androidx.startup:startup-runtime:1.1.1
androidx.tracing:tracing:1.0.0