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