diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocompose/HelloBinding.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocompose/HelloBinding.kt index 0309a095b4..ee1299675c 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocompose/HelloBinding.kt +++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocompose/HelloBinding.kt @@ -8,10 +8,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.squareup.sample.compose.hellocompose.HelloWorkflow.Rendering import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.compose.composeScreenViewFactory +import com.squareup.workflow1.ui.compose.screenComposableFactory @OptIn(WorkflowUiExperimentalApi::class) -val HelloBinding = composeScreenViewFactory { rendering, _ -> +val HelloBinding = screenComposableFactory { rendering, _ -> Text( rendering.message, modifier = Modifier diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposebinding/HelloBinding.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposebinding/HelloBinding.kt index be9dac792a..a04e1f4451 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposebinding/HelloBinding.kt +++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposebinding/HelloBinding.kt @@ -9,11 +9,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.squareup.sample.compose.hellocomposebinding.HelloWorkflow.Rendering import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.compose.composeScreenViewFactory +import com.squareup.workflow1.ui.compose.screenComposableFactory import com.squareup.workflow1.ui.compose.tooling.Preview @OptIn(WorkflowUiExperimentalApi::class) -val HelloBinding = composeScreenViewFactory { rendering, _ -> +val HelloBinding = screenComposableFactory { rendering, _ -> Text( rendering.message, modifier = Modifier 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 84e8bd99d6..1bfd4c2ec4 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 @@ -17,6 +17,7 @@ import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowLayout import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.compose.withComposeInteropSupport import com.squareup.workflow1.ui.compose.withCompositionRoot import com.squareup.workflow1.ui.container.withEnvironment import com.squareup.workflow1.ui.plus @@ -25,13 +26,15 @@ import kotlinx.coroutines.flow.StateFlow @OptIn(WorkflowUiExperimentalApi::class) private val viewEnvironment = - (ViewEnvironment.EMPTY + ViewRegistry(HelloBinding)).withCompositionRoot { content -> - MaterialTheme(content = content) - } + (ViewEnvironment.EMPTY + ViewRegistry(HelloBinding)) + .withCompositionRoot { content -> + MaterialTheme(content = content) + } + .withComposeInteropSupport() /** * Demonstrates how to create and display a view factory with - * [composeScreenViewFactory][com.squareup.workflow1.ui.compose.composeScreenViewFactory]. + * [screenComposableFactory][com.squareup.workflow1.ui.compose.screenComposableFactory]. */ class HelloBindingActivity : AppCompatActivity() { @OptIn(WorkflowUiExperimentalApi::class) 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 8526eb7b6a..16555b235e 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 @@ -10,9 +10,13 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.squareup.workflow1.WorkflowExperimentalRuntime import com.squareup.workflow1.config.AndroidRuntimeConfigTools +import com.squareup.workflow1.mapRendering import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowLayout import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.compose.withComposeInteropSupport +import com.squareup.workflow1.ui.container.withEnvironment import com.squareup.workflow1.ui.renderWorkflowIn import kotlinx.coroutines.flow.StateFlow @@ -30,7 +34,9 @@ class HelloComposeWorkflowActivity : AppCompatActivity() { @OptIn(WorkflowUiExperimentalApi::class) val renderings: StateFlow by lazy { renderWorkflowIn( - workflow = HelloWorkflow, + workflow = HelloWorkflow.mapRendering { + it.withEnvironment(ViewEnvironment.EMPTY.withComposeInteropSupport()) + }, scope = viewModelScope, savedStateHandle = savedState, runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig() 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 b6f50e7dc2..8f67bc18e5 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 @@ -10,9 +10,13 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.squareup.workflow1.WorkflowExperimentalRuntime import com.squareup.workflow1.config.AndroidRuntimeConfigTools +import com.squareup.workflow1.mapRendering import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowLayout import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.compose.withComposeInteropSupport +import com.squareup.workflow1.ui.container.withEnvironment import com.squareup.workflow1.ui.renderWorkflowIn import kotlinx.coroutines.flow.StateFlow @@ -34,7 +38,9 @@ class InlineRenderingActivity : AppCompatActivity() { @OptIn(WorkflowUiExperimentalApi::class) val renderings: StateFlow by lazy { renderWorkflowIn( - workflow = InlineRenderingWorkflow, + workflow = InlineRenderingWorkflow.mapRendering { + it.withEnvironment(ViewEnvironment.EMPTY.withComposeInteropSupport()) + }, scope = viewModelScope, savedStateHandle = savedState, runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig() diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingWorkflow.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingWorkflow.kt index 8e53b70265..3e77d96fb1 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingWorkflow.kt +++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingWorkflow.kt @@ -21,14 +21,14 @@ import com.squareup.workflow1.StatefulWorkflow import com.squareup.workflow1.WorkflowExperimentalRuntime import com.squareup.workflow1.config.AndroidRuntimeConfigTools import com.squareup.workflow1.parse -import com.squareup.workflow1.ui.AndroidScreen +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.compose.ComposeScreen import com.squareup.workflow1.ui.compose.WorkflowRendering import com.squareup.workflow1.ui.compose.renderAsState -object InlineRenderingWorkflow : StatefulWorkflow>() { +object InlineRenderingWorkflow : StatefulWorkflow() { override fun initialState( props: Unit, @@ -39,7 +39,7 @@ object InlineRenderingWorkflow : StatefulWorkflow = ComposeScreen { + ) = ComposeScreen { Box { Button(onClick = context.eventHandler { state += 1 }) { Text("Counter: ") diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/launcher/Samples.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/launcher/Samples.kt index 66bb94a2ae..792e5e1e8b 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/launcher/Samples.kt +++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/launcher/Samples.kt @@ -37,7 +37,7 @@ val samples = listOf( Sample( "Hello Compose Binding", HelloBindingActivity::class, - "Creates a ViewFactory using composeViewFactory." + "Creates a ScreenComposableFactory using screenComposableFactory()." ) { DrawHelloRenderingPreview() }, Sample( "Nested Renderings", diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/LegacyRunner.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/LegacyRunner.kt index e9296e3dd3..bab4162933 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/LegacyRunner.kt +++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/LegacyRunner.kt @@ -11,6 +11,7 @@ import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromViewBinding import com.squareup.workflow1.ui.ScreenViewRunner import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.compose.asComposableFactory import com.squareup.workflow1.ui.compose.tooling.Preview /** @@ -36,7 +37,7 @@ class LegacyRunner(private val binding: LegacyViewBinding) : ScreenViewRunner - CompositionLocalProvider(LocalBackgroundColor provides Color.Green) { - content() + (ViewEnvironment.EMPTY + viewRegistry) + .withCompositionRoot { content -> + CompositionLocalProvider(LocalBackgroundColor provides Color.Green) { + content() + } } - } + .withComposeInteropSupport() @WorkflowUiExperimentalApi class NestedRenderingsActivity : AppCompatActivity() { diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/RecursiveViewFactory.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/RecursiveViewFactory.kt index 648596c3b2..bea6b6b807 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/RecursiveViewFactory.kt +++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/RecursiveViewFactory.kt @@ -27,7 +27,7 @@ import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.compose.WorkflowRendering -import com.squareup.workflow1.ui.compose.composeScreenViewFactory +import com.squareup.workflow1.ui.compose.screenComposableFactory import com.squareup.workflow1.ui.compose.tooling.Preview /** @@ -39,7 +39,7 @@ val LocalBackgroundColor = compositionLocalOf { error("No background colo * A `ViewFactory` that renders [RecursiveWorkflow.Rendering]s. */ @OptIn(WorkflowUiExperimentalApi::class) -val RecursiveViewFactory = composeScreenViewFactory { rendering, viewEnvironment -> +val RecursiveViewFactory = screenComposableFactory { rendering, viewEnvironment -> // Every child should be drawn with a slightly-darker background color. val color = LocalBackgroundColor.current val childColor = remember(color) { diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/preview/PreviewActivity.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/preview/PreviewActivity.kt index 51872aacd9..1b923f65c6 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/preview/PreviewActivity.kt +++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/preview/PreviewActivity.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.unit.dp import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.compose.WorkflowRendering -import com.squareup.workflow1.ui.compose.composeScreenViewFactory +import com.squareup.workflow1.ui.compose.screenComposableFactory import com.squareup.workflow1.ui.compose.tooling.Preview class PreviewActivity : AppCompatActivity() { @@ -65,7 +65,7 @@ data class ContactDetailsRendering( ) : Screen private val contactViewFactory = - composeScreenViewFactory { rendering, environment -> + screenComposableFactory { rendering, environment -> Card( modifier = Modifier .padding(8.dp) @@ -78,7 +78,7 @@ private val contactViewFactory = Text(rendering.name, style = MaterialTheme.typography.body1) WorkflowRendering( rendering = rendering.details, - viewEnvironment = environment, + environment = environment, modifier = Modifier .aspectRatio(1f) .border(0.dp, Color.LightGray) diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/textinput/TextInputViewFactory.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/textinput/TextInputViewFactory.kt index 994e7db4c6..df3a646e02 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/textinput/TextInputViewFactory.kt +++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/textinput/TextInputViewFactory.kt @@ -20,11 +20,11 @@ import com.squareup.sample.compose.textinput.TextInputWorkflow.Rendering import com.squareup.workflow1.ui.TextController import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.compose.asMutableState -import com.squareup.workflow1.ui.compose.composeScreenViewFactory +import com.squareup.workflow1.ui.compose.screenComposableFactory import com.squareup.workflow1.ui.compose.tooling.Preview @OptIn(WorkflowUiExperimentalApi::class) -val TextInputViewFactory = composeScreenViewFactory { rendering, _ -> +val TextInputViewFactory = screenComposableFactory { rendering, _ -> Column( modifier = Modifier .fillMaxSize() diff --git a/workflow-ui/compose-tooling/api/compose-tooling.api b/workflow-ui/compose-tooling/api/compose-tooling.api index a3f8dd69ad..da5e807a3e 100644 --- a/workflow-ui/compose-tooling/api/compose-tooling.api +++ b/workflow-ui/compose-tooling/api/compose-tooling.api @@ -6,6 +6,6 @@ public final class com/squareup/workflow1/ui/compose/tooling/ComposableSingleton } public final class com/squareup/workflow1/ui/compose/tooling/ViewFactoriesKt { - public static final fun Preview (Lcom/squareup/workflow1/ui/ScreenViewFactory;Lcom/squareup/workflow1/ui/Screen;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun Preview (Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory;Lcom/squareup/workflow1/ui/Screen;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V } diff --git a/workflow-ui/compose-tooling/src/androidTest/java/com/squareup/workflow1/ui/compose/tooling/PreviewViewFactoryTest.kt b/workflow-ui/compose-tooling/src/androidTest/java/com/squareup/workflow1/ui/compose/tooling/PreviewViewFactoryTest.kt index 5a9012044a..330a9d3872 100644 --- a/workflow-ui/compose-tooling/src/androidTest/java/com/squareup/workflow1/ui/compose/tooling/PreviewViewFactoryTest.kt +++ b/workflow-ui/compose-tooling/src/androidTest/java/com/squareup/workflow1/ui/compose/tooling/PreviewViewFactoryTest.kt @@ -18,7 +18,7 @@ import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ViewEnvironmentKey import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.compose.WorkflowRendering -import com.squareup.workflow1.ui.compose.composeScreenViewFactory +import com.squareup.workflow1.ui.compose.screenComposableFactory import com.squareup.workflow1.ui.internal.test.IdleAfterTestRule import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule import leakcanary.DetectLeaksAfterTestSuccess @@ -96,7 +96,7 @@ internal class PreviewViewFactoryTest { } private val ParentWithOneChild = - composeScreenViewFactory { rendering, environment -> + screenComposableFactory { rendering, environment -> Column { BasicText(rendering.first.text) WorkflowRendering(rendering.second, environment) @@ -109,7 +109,7 @@ internal class PreviewViewFactoryTest { } private val ParentWithTwoChildren = - composeScreenViewFactory { rendering, environment -> + screenComposableFactory { rendering, environment -> Column { WorkflowRendering(rendering.first, environment) BasicText(rendering.second.text) @@ -156,11 +156,11 @@ internal class PreviewViewFactoryTest { ) : Screen private val ParentRecursive = - composeScreenViewFactory { rendering, environment -> + screenComposableFactory { rendering, environment -> Column { BasicText(rendering.text) rendering.child?.let { child -> - WorkflowRendering(rendering = child, viewEnvironment = environment) + WorkflowRendering(rendering = child, environment = environment) } } } @@ -198,7 +198,7 @@ internal class PreviewViewFactoryTest { override val default: String get() = error("Not specified") } - private val ParentConsumesCustomKey = composeScreenViewFactory { _, environment -> + private val ParentConsumesCustomKey = screenComposableFactory { _, environment -> BasicText(environment[TestEnvironmentKey]) } diff --git a/workflow-ui/compose-tooling/src/main/java/com/squareup/workflow1/ui/compose/tooling/PlaceholderViewFactory.kt b/workflow-ui/compose-tooling/src/main/java/com/squareup/workflow1/ui/compose/tooling/PlaceholderViewFactory.kt index e2b544b6f7..30b0da5575 100644 --- a/workflow-ui/compose-tooling/src/main/java/com/squareup/workflow1/ui/compose/tooling/PlaceholderViewFactory.kt +++ b/workflow-ui/compose-tooling/src/main/java/com/squareup/workflow1/ui/compose/tooling/PlaceholderViewFactory.kt @@ -25,15 +25,18 @@ import androidx.compose.ui.unit.dp import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.compose.composeScreenViewFactory +import com.squareup.workflow1.ui.compose.ScreenComposableFactory +import com.squareup.workflow1.ui.compose.screenComposableFactory /** * A [ScreenViewFactory] that will be used any time a [PreviewScreenViewFactoryFinder] * is asked to show a rendering. It displays a placeholder graphic and the rendering's * `toString()` result. */ -internal fun placeholderScreenViewFactory(modifier: Modifier): ScreenViewFactory = - composeScreenViewFactory { rendering, _ -> +internal fun placeholderScreenComposableFactory( + modifier: Modifier +): ScreenComposableFactory = + screenComposableFactory { rendering, _ -> BoxWithConstraints { BasicText( modifier = modifier diff --git a/workflow-ui/compose-tooling/src/main/java/com/squareup/workflow1/ui/compose/tooling/PreviewViewEnvironment.kt b/workflow-ui/compose-tooling/src/main/java/com/squareup/workflow1/ui/compose/tooling/PreviewViewEnvironment.kt index 6201160773..fd812bafa6 100644 --- a/workflow-ui/compose-tooling/src/main/java/com/squareup/workflow1/ui/compose/tooling/PreviewViewEnvironment.kt +++ b/workflow-ui/compose-tooling/src/main/java/com/squareup/workflow1/ui/compose/tooling/PreviewViewEnvironment.kt @@ -7,29 +7,33 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.squareup.workflow1.ui.Screen -import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.ScreenViewFactoryFinder import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.compose.ScreenComposableFactory +import com.squareup.workflow1.ui.compose.ScreenComposableFactoryFinder /** * Creates and [remember]s a [ViewEnvironment] that has a special [ScreenViewFactoryFinder] * and any additional elements as configured by [viewEnvironmentUpdater]. * * The [ScreenViewFactoryFinder] will contain [mainFactory] if specified, as well as a - * [placeholderScreenViewFactory] that will be used to show any renderings that don't match + * [placeholderScreenComposableFactory] that will be used to show any renderings that don't match * [mainFactory]'s type. All placeholders will have [placeholderModifier] applied. */ @Composable internal fun rememberPreviewViewEnvironment( placeholderModifier: Modifier, viewEnvironmentUpdater: ((ViewEnvironment) -> ViewEnvironment)? = null, - mainFactory: ScreenViewFactory<*>? = null + mainFactory: ScreenComposableFactory<*>? = null ): ViewEnvironment { val finder = remember(mainFactory, placeholderModifier) { - PreviewScreenViewFactoryFinder(mainFactory, placeholderScreenViewFactory(placeholderModifier)) + PreviewScreenComposableFactoryFinder( + mainFactory, + placeholderScreenComposableFactory(placeholderModifier) + ) } return remember(finder, viewEnvironmentUpdater) { - (ViewEnvironment.EMPTY + (ScreenViewFactoryFinder to finder)).let { environment -> + (ViewEnvironment.EMPTY + (ScreenComposableFactoryFinder to finder)).let { environment -> // Give the preview a chance to add its own elements to the ViewEnvironment. viewEnvironmentUpdater?.let { it(environment) } ?: environment } @@ -37,22 +41,22 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi } /** - * A [ScreenViewFactoryFinder] that uses [mainFactory] for rendering [RenderingT]s, + * A [ScreenComposableFactoryFinder] that uses [mainFactory] for rendering [RenderingT]s, * and [placeholderFactory] for all other * [WorkflowRendering][com.squareup.workflow1.ui.compose.WorkflowRendering] calls. */ @Immutable -private class PreviewScreenViewFactoryFinder( - private val mainFactory: ScreenViewFactory? = null, - private val placeholderFactory: ScreenViewFactory -) : ScreenViewFactoryFinder { - override fun getViewFactoryForRendering( +private class PreviewScreenComposableFactoryFinder( + private val mainFactory: ScreenComposableFactory? = null, + private val placeholderFactory: ScreenComposableFactory +) : ScreenComposableFactoryFinder { + override fun getComposableFactoryForRendering( environment: ViewEnvironment, rendering: ScreenT - ): ScreenViewFactory = + ): ScreenComposableFactory = @Suppress("UNCHECKED_CAST") if (rendering::class == mainFactory?.type) { - mainFactory as ScreenViewFactory + mainFactory as ScreenComposableFactory } else { placeholderFactory } diff --git a/workflow-ui/compose-tooling/src/main/java/com/squareup/workflow1/ui/compose/tooling/ViewFactories.kt b/workflow-ui/compose-tooling/src/main/java/com/squareup/workflow1/ui/compose/tooling/ViewFactories.kt index 334b76204f..0bf705c02a 100644 --- a/workflow-ui/compose-tooling/src/main/java/com/squareup/workflow1/ui/compose/tooling/ViewFactories.kt +++ b/workflow-ui/compose-tooling/src/main/java/com/squareup/workflow1/ui/compose/tooling/ViewFactories.kt @@ -14,8 +14,9 @@ import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.ScreenViewFactoryFinder import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.compose.ScreenComposableFactory import com.squareup.workflow1.ui.compose.WorkflowRendering -import com.squareup.workflow1.ui.compose.composeScreenViewFactory +import com.squareup.workflow1.ui.compose.screenComposableFactory /** * Draws this [ScreenViewFactory] using a special preview [ScreenViewFactoryFinder]. @@ -34,7 +35,7 @@ import com.squareup.workflow1.ui.compose.composeScreenViewFactory */ @WorkflowUiExperimentalApi @Composable -public fun ScreenViewFactory.Preview( +public fun ScreenComposableFactory.Preview( rendering: RenderingT, modifier: Modifier = Modifier, placeholderModifier: Modifier = Modifier, @@ -49,7 +50,7 @@ public fun ScreenViewFactory.Preview( @Preview(showBackground = true) @Composable private fun ViewFactoryPreviewPreview() { - val factory = composeScreenViewFactory { _, environment -> + val factory = screenComposableFactory { _, environment -> Column( verticalArrangement = spacedBy(8.dp), modifier = Modifier.padding(8.dp) @@ -59,7 +60,7 @@ private fun ViewFactoryPreviewPreview() { rendering = TextRendering( "Child rendering with very long text to suss out cross-hatch rendering edge cases", ), - viewEnvironment = environment, + environment = environment, modifier = Modifier .aspectRatio(1f) .padding(8.dp) diff --git a/workflow-ui/compose/api/compose.api b/workflow-ui/compose/api/compose.api index eede71a594..264f550198 100644 --- a/workflow-ui/compose/api/compose.api +++ b/workflow-ui/compose/api/compose.api @@ -1,40 +1,67 @@ -public abstract interface class com/squareup/workflow1/ui/compose/ComposeScreen : com/squareup/workflow1/ui/AndroidScreen { +public abstract interface class com/squareup/workflow1/ui/compose/ComposeScreen : com/squareup/workflow1/ui/Screen { public abstract fun Content (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroidx/compose/runtime/Composer;I)V - public abstract fun getViewFactory ()Lcom/squareup/workflow1/ui/ScreenViewFactory; -} - -public final class com/squareup/workflow1/ui/compose/ComposeScreen$DefaultImpls { - public static fun getViewFactory (Lcom/squareup/workflow1/ui/compose/ComposeScreen;)Lcom/squareup/workflow1/ui/ScreenViewFactory; } public final class com/squareup/workflow1/ui/compose/ComposeScreenKt { public static final fun ComposeScreen (Lkotlin/jvm/functions/Function3;)Lcom/squareup/workflow1/ui/compose/ComposeScreen; } -public abstract class com/squareup/workflow1/ui/compose/ComposeScreenViewFactory : com/squareup/workflow1/ui/ScreenViewFactory { - public static final field $stable I - public fun ()V +public final class com/squareup/workflow1/ui/compose/CompositionRootKt { + public static final fun withCompositionRoot (Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function3;)Lcom/squareup/workflow1/ui/ViewEnvironment; + public static final fun withCompositionRoot (Lcom/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder;Lkotlin/jvm/functions/Function3;)Lcom/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder; +} + +public final class com/squareup/workflow1/ui/compose/RenderAsStateKt { + public static final fun renderAsState (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/util/List;Lkotlinx/coroutines/CoroutineScope;Ljava/util/Set;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/State; +} + +public abstract interface class com/squareup/workflow1/ui/compose/ScreenComposableFactory : com/squareup/workflow1/ui/ViewRegistry$Entry { + public static final field Companion Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory$Companion; public abstract fun Content (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroidx/compose/runtime/Composer;I)V - public final fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Lcom/squareup/workflow1/ui/ScreenViewHolder; + public abstract fun getKey ()Lcom/squareup/workflow1/ui/ViewRegistry$Key; + public abstract fun getType ()Lkotlin/reflect/KClass; } -public final class com/squareup/workflow1/ui/compose/ComposeScreenViewFactoryKt { - public static final fun composeScreenViewFactory (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function4;)Lcom/squareup/workflow1/ui/ScreenViewFactory; +public final class com/squareup/workflow1/ui/compose/ScreenComposableFactory$Companion { } -public final class com/squareup/workflow1/ui/compose/CompositionRootKt { - public static final fun withCompositionRoot (Lcom/squareup/workflow1/ui/ScreenViewFactoryFinder;Lkotlin/jvm/functions/Function3;)Lcom/squareup/workflow1/ui/ScreenViewFactoryFinder; - public static final fun withCompositionRoot (Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function3;)Lcom/squareup/workflow1/ui/ViewEnvironment; +public final class com/squareup/workflow1/ui/compose/ScreenComposableFactory$DefaultImpls { + public static fun getKey (Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory;)Lcom/squareup/workflow1/ui/ViewRegistry$Key; } -public final class com/squareup/workflow1/ui/compose/RenderAsStateKt { - public static final fun renderAsState (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/util/List;Lkotlinx/coroutines/CoroutineScope;Ljava/util/Set;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/State; +public abstract interface class com/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder { + public static final field Companion Lcom/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder$Companion; + public abstract fun getComposableFactoryForRendering (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/Screen;)Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory; +} + +public final class com/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder$Companion : com/squareup/workflow1/ui/ViewEnvironmentKey { + public fun getDefault ()Lcom/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder; + public synthetic fun getDefault ()Ljava/lang/Object; +} + +public final class com/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder$DefaultImpls { + public static fun getComposableFactoryForRendering (Lcom/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder;Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/Screen;)Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory; +} + +public final class com/squareup/workflow1/ui/compose/ScreenComposableFactoryFinderKt { + public static final fun requireComposableFactoryForRendering (Lcom/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder;Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/Screen;)Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory; +} + +public final class com/squareup/workflow1/ui/compose/ScreenComposableFactoryKt { + public static final fun asComposableFactory (Lcom/squareup/workflow1/ui/ScreenViewFactory;)Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory; + public static final fun asViewFactory (Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory;)Lcom/squareup/workflow1/ui/ScreenViewFactory; + public static final fun screenComposableFactory (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function4;)Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory; + public static final fun toComposableFactory (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory; } public final class com/squareup/workflow1/ui/compose/TextControllerAsMutableStateKt { public static final fun asMutableState (Lcom/squareup/workflow1/ui/TextController;Landroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/MutableState; } +public final class com/squareup/workflow1/ui/compose/ViewEnvironmentWithComposeSupportKt { + public static final fun withComposeInteropSupport (Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/ViewEnvironment; +} + public final class com/squareup/workflow1/ui/compose/WorkflowRenderingKt { public static final fun WorkflowRendering (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V } diff --git a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeScreenViewFactoryTest.kt b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ScreenComposableFactoryTest.kt similarity index 85% rename from workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeScreenViewFactoryTest.kt rename to workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ScreenComposableFactoryTest.kt index 27f7ab2cca..341f877660 100644 --- a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeScreenViewFactoryTest.kt +++ b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ScreenComposableFactoryTest.kt @@ -33,7 +33,7 @@ import org.junit.runner.RunWith @OptIn(WorkflowUiExperimentalApi::class) @RunWith(AndroidJUnit4::class) -internal class ComposeScreenViewFactoryTest { +internal class ScreenComposableFactoryTest { private val composeRule = createComposeRule() @@ -44,10 +44,11 @@ internal class ComposeScreenViewFactoryTest { .around(IdlingDispatcherRule) @Test fun showsComposeContent() { - val viewFactory = composeScreenViewFactory { _, _ -> + val viewFactory = screenComposableFactory { _, _ -> BasicText("Hello, world!") } - val viewEnvironment = ViewEnvironment.EMPTY + ViewRegistry(viewFactory) + val viewEnvironment = (ViewEnvironment.EMPTY + ViewRegistry(viewFactory)) + .withComposeInteropSupport() composeRule.setContent { AndroidView(::RootView) { @@ -59,10 +60,11 @@ internal class ComposeScreenViewFactoryTest { } @Test fun getsRenderingUpdates() { - val viewFactory = composeScreenViewFactory { rendering, _ -> + val viewFactory = screenComposableFactory { rendering, _ -> BasicText(rendering.text, Modifier.testTag("text")) } - val viewEnvironment = ViewEnvironment.EMPTY + ViewRegistry(viewFactory) + val viewEnvironment = (ViewEnvironment.EMPTY + ViewRegistry(viewFactory)) + .withComposeInteropSupport() var rendering by mutableStateOf(TestRendering("hello")) composeRule.setContent { @@ -82,13 +84,14 @@ internal class ComposeScreenViewFactoryTest { override val default: String get() = error("No default") } - val viewFactory = composeScreenViewFactory { _, environment -> + val viewFactory = screenComposableFactory { _, environment -> val text = environment[testEnvironmentKey] BasicText(text, Modifier.testTag("text")) } val viewRegistry = ViewRegistry(viewFactory) var viewEnvironment by mutableStateOf( - ViewEnvironment.EMPTY + viewRegistry + (testEnvironmentKey to "hello") + (ViewEnvironment.EMPTY + viewRegistry + (testEnvironmentKey to "hello")) + .withComposeInteropSupport() ) composeRule.setContent { @@ -112,6 +115,7 @@ internal class ComposeScreenViewFactoryTest { content() } } + .withComposeInteropSupport() composeRule.setContent { AndroidView(::RootView) { @@ -137,7 +141,7 @@ internal class ComposeScreenViewFactoryTest { private data class TestRendering(val text: String = "") : Screen private companion object { - val TestFactory = composeScreenViewFactory { rendering, _ -> + val TestFactory = screenComposableFactory { rendering, _ -> BasicText(rendering.text) } } diff --git a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/WorkflowRenderingTest.kt b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/WorkflowRenderingTest.kt index 107ed58f56..2fc92c85df 100644 --- a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/WorkflowRenderingTest.kt +++ b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/WorkflowRenderingTest.kt @@ -94,12 +94,12 @@ internal class WorkflowRenderingTest { ) : Screen val registry1 = ViewRegistry( - composeScreenViewFactory { rendering, _ -> + screenComposableFactory { rendering, _ -> BasicText(rendering.text) } ) val registry2 = ViewRegistry( - composeScreenViewFactory { rendering, _ -> + screenComposableFactory { rendering, _ -> BasicText(rendering.text.reversed()) } ) @@ -115,33 +115,10 @@ internal class WorkflowRenderingTest { composeRule.onNodeWithText("olleh").assertDoesNotExist() } - /** - * Ensures we match the behavior of WorkflowViewStub and other containers, which only check for - * a new factory when a new rendering is incompatible with the current one. - */ - @Test fun doesNotRecompose_whenAndroidViewRendering_factoryChanged() { - data class ShiftyRendering(val whichFactory: Boolean) : AndroidScreen { - override val viewFactory: ScreenViewFactory = when (whichFactory) { - true -> composeScreenViewFactory { _, _ -> BasicText("one") } - false -> composeScreenViewFactory { _, _ -> BasicText("two") } - } - } - - var rendering by mutableStateOf(ShiftyRendering(true)) - - composeRule.setContent { - WorkflowRendering(rendering, ViewEnvironment.EMPTY) - } - - composeRule.onNodeWithText("one").assertIsDisplayed() - rendering = ShiftyRendering(false) - composeRule.onNodeWithText("one").assertIsDisplayed() - } - @Test fun wrapsFactoryWithRoot_whenAlreadyInComposition() { data class TestRendering(val text: String) : Screen - val testFactory = composeScreenViewFactory { rendering, _ -> + val testFactory = screenComposableFactory { rendering, _ -> BasicText(rendering.text) } val viewEnvironment = (ViewEnvironment.EMPTY + ViewRegistry(testFactory)) @@ -164,7 +141,7 @@ internal class WorkflowRenderingTest { val wrapperText = mutableStateOf("two") composeRule.setContent { - WorkflowRendering(LegacyViewRendering(wrapperText.value), ViewEnvironment.EMPTY) + WorkflowRendering(LegacyViewRendering(wrapperText.value), env) } onView(withText("two")).check(matches(isDisplayed())) @@ -178,7 +155,7 @@ internal class WorkflowRenderingTest { composeRule.setContent { val rendering = NamedScreen(LegacyViewRendering(wrapperText.value), "fnord") - WorkflowRendering(rendering, ViewEnvironment.EMPTY) + WorkflowRendering(rendering, env) } onView(withText("two")).check(matches(isDisplayed())) @@ -189,7 +166,7 @@ internal class WorkflowRenderingTest { @Test fun destroysChildLifecycle_fromCompose_whenIncompatibleRendering() { val lifecycleEvents = mutableListOf() - class LifecycleRecorder : ComposableRendering { + class LifecycleRecorder : ComposableRendering { @Composable override fun Content(viewEnvironment: ViewEnvironment) { val lifecycle = LocalLifecycleOwner.current.lifecycle DisposableEffect(lifecycle) { @@ -206,13 +183,13 @@ internal class WorkflowRenderingTest { } } - class EmptyRendering : ComposableRendering { + class EmptyRendering : ComposableRendering { @Composable override fun Content(viewEnvironment: ViewEnvironment) {} } var rendering: Screen by mutableStateOf(LifecycleRecorder()) composeRule.setContent { - WorkflowRendering(rendering, ViewEnvironment.EMPTY) + WorkflowRendering(rendering, env) } composeRule.runOnIdle { @@ -248,13 +225,13 @@ internal class WorkflowRenderingTest { } } - class EmptyRendering : ComposableRendering { + class EmptyRendering : ComposableRendering { @Composable override fun Content(viewEnvironment: ViewEnvironment) {} } var rendering: Screen by mutableStateOf(LifecycleRecorder()) composeRule.setContent { - WorkflowRendering(rendering, ViewEnvironment.EMPTY) + WorkflowRendering(rendering, env) } composeRule.runOnIdle { @@ -279,7 +256,7 @@ internal class WorkflowRenderingTest { composeRule.setContent { CompositionLocalProvider(LocalLifecycleOwner provides parentOwner) { - WorkflowRendering(LifecycleRecorder(states), ViewEnvironment.EMPTY) + WorkflowRendering(LifecycleRecorder(states), env) } } @@ -327,7 +304,7 @@ internal class WorkflowRenderingTest { composeRule.setContent { CompositionLocalProvider(LocalLifecycleOwner provides parentOwner) { - WorkflowRendering(LifecycleRecorder(states), ViewEnvironment.EMPTY) + WorkflowRendering(LifecycleRecorder(states), env) } } @@ -337,7 +314,7 @@ internal class WorkflowRenderingTest { } @Test fun appliesModifierToComposableContent() { - class Rendering : ComposableRendering { + class Rendering : ComposableRendering { @Composable override fun Content(viewEnvironment: ViewEnvironment) { Box( Modifier @@ -350,7 +327,7 @@ internal class WorkflowRenderingTest { composeRule.setContent { WorkflowRendering( Rendering(), - ViewEnvironment.EMPTY, + env, Modifier.size(width = 42.dp, height = 43.dp) ) } @@ -361,7 +338,7 @@ internal class WorkflowRenderingTest { } @Test fun propagatesMinConstraints() { - class Rendering : ComposableRendering { + class Rendering : ComposableRendering { @Composable override fun Content(viewEnvironment: ViewEnvironment) { Box(Modifier.testTag("box")) } @@ -370,7 +347,7 @@ internal class WorkflowRenderingTest { composeRule.setContent { WorkflowRendering( Rendering(), - ViewEnvironment.EMPTY, + env, Modifier.sizeIn(minWidth = 42.dp, minHeight = 43.dp) ) } @@ -397,7 +374,7 @@ internal class WorkflowRenderingTest { with(LocalDensity.current) { WorkflowRendering( LegacyRendering(viewId), - ViewEnvironment.EMPTY, + env, Modifier.size(42.toDp(), 43.toDp()) ) } @@ -411,7 +388,7 @@ internal class WorkflowRenderingTest { class Rendering( override val compatibilityKey: String - ) : ComposableRendering, Compatible { + ) : ComposableRendering, Compatible { @Composable override fun Content(viewEnvironment: ViewEnvironment) { var counter by rememberSaveable { mutableStateOf(0) } Column { @@ -432,7 +409,7 @@ internal class WorkflowRenderingTest { var key by mutableStateOf("one") composeRule.setContent { - WorkflowRendering(Rendering(key), ViewEnvironment.EMPTY) + WorkflowRendering(Rendering(key), env) } composeRule.onNodeWithTag("tag") @@ -461,7 +438,7 @@ internal class WorkflowRenderingTest { @Test fun doesNotSkipPreviousContentWhenCompatible() { var disposeCount = 0 - class Rendering(val text: String) : ComposableRendering { + class Rendering(val text: String) : ComposableRendering { @Composable override fun Content(viewEnvironment: ViewEnvironment) { var counter by rememberSaveable { mutableStateOf(0) } Column { @@ -482,7 +459,7 @@ internal class WorkflowRenderingTest { var text by mutableStateOf("one") composeRule.setContent { - WorkflowRendering(Rendering(text), ViewEnvironment.EMPTY) + WorkflowRendering(Rendering(text), env) } composeRule.onNodeWithTag("tag") @@ -517,7 +494,7 @@ internal class WorkflowRenderingTest { private class LifecycleRecorder( // For some reason, if we just capture the states val, it is null in the composable. private val states: MutableList - ) : ComposableRendering { + ) : ComposableRendering { @Composable override fun Content(viewEnvironment: ViewEnvironment) { val lifecycle = LocalLifecycleOwner.current.lifecycle DisposableEffect(lifecycle) { @@ -535,25 +512,40 @@ internal class WorkflowRenderingTest { } } - private interface ComposableRendering> : - AndroidScreen { - - /** - * It is significant that this returns a new instance on every call, since we can't rely on real - * implementations in the wild to reuse the same factory instance across rendering instances. - */ - override val viewFactory: ScreenViewFactory - get() = object : ComposeScreenViewFactory>() { - override val type: KClass> = ComposableRendering::class - - @Composable override fun Content( - rendering: ComposableRendering<*>, - viewEnvironment: ViewEnvironment - ) { - rendering.Content(viewEnvironment) + /** + * It is significant that this returns a new instance on every call, since we can't rely on real + * implementations in the wild to reuse the same factory instance across rendering instances. + */ + private object InefficientComposableFinder : ScreenComposableFactoryFinder { + override fun getComposableFactoryForRendering( + environment: ViewEnvironment, + rendering: ScreenT + ): ScreenComposableFactory? { + return if (rendering is ComposableRendering) { + object : ScreenComposableFactory { + override val type: KClass get() = error("whatever") + + @Composable override fun Content( + rendering: ScreenT, + environment: ViewEnvironment + ) { + (rendering as ComposableRendering).Content(environment) + } } + } else { + super.getComposableFactoryForRendering( + environment, + rendering + ) } + } + } + + private val env = + (ViewEnvironment.EMPTY + (ScreenComposableFactoryFinder to InefficientComposableFinder)) + .withComposeInteropSupport() + private interface ComposableRendering : Screen { @Composable fun Content(viewEnvironment: ViewEnvironment) } diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeScreen.kt b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeScreen.kt index 0d5593074a..9b3b963721 100644 --- a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeScreen.kt +++ b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeScreen.kt @@ -2,15 +2,14 @@ package com.squareup.workflow1.ui.compose import androidx.compose.runtime.Composable import com.squareup.workflow1.ui.AndroidScreen -import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import kotlin.reflect.KClass /** * Interface implemented by a rendering class to allow it to drive a composable UI via an - * appropriate [ComposeScreenViewFactory] implementation, by simply overriding the [Content] method. + * appropriate [ScreenComposableFactory] implementation, by simply overriding the [Content] method. * This is the compose analog to [AndroidScreen]. * * Note that unlike most workflow view functions, [Content] does not take the rendering as a @@ -36,13 +35,10 @@ import kotlin.reflect.KClass * This is the simplest way to bridge the gap between your workflows and the UI, but using it * requires your workflows code to reside in Android modules, instead of pure Kotlin. If this is a * problem, or you need more flexibility for any other reason, you can use [ViewRegistry] to bind - * your renderings to [ComposeScreenViewFactory] implementations at runtime. + * your renderings to [ScreenComposableFactoryFinder] implementations at runtime. */ @WorkflowUiExperimentalApi -public interface ComposeScreen : AndroidScreen { - - /** Don't override this, override [Content] instead. */ - override val viewFactory: ScreenViewFactory get() = Companion +public interface ComposeScreen : Screen { /** * The composable content of this rendering. This method will be called with the current rendering @@ -50,21 +46,6 @@ public interface ComposeScreen : AndroidScreen { * changes. */ @Composable public fun Content(viewEnvironment: ViewEnvironment) - - private companion object : ComposeScreenViewFactory() { - /** - * Just returns [ComposeScreen]'s class, since this factory isn't for using with a view - * registry it doesn't matter. - */ - override val type: KClass = ComposeScreen::class - - @Composable override fun Content( - rendering: ComposeScreen, - viewEnvironment: ViewEnvironment - ) { - rendering.Content(viewEnvironment) - } - } } /** diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeScreenViewFactory.kt b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeScreenViewFactory.kt deleted file mode 100644 index 1298ae40bd..0000000000 --- a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeScreenViewFactory.kt +++ /dev/null @@ -1,144 +0,0 @@ -// See https://youtrack.jetbrains.com/issue/KT-31734 -@file:Suppress("RemoveEmptyParenthesesFromAnnotationEntry") - -package com.squareup.workflow1.ui.compose - -import android.content.Context -import android.view.ViewGroup -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.ComposeView -import com.squareup.workflow1.ui.Screen -import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.ScreenViewHolder -import com.squareup.workflow1.ui.ViewEnvironment -import com.squareup.workflow1.ui.ViewRegistry.Key -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import kotlin.reflect.KClass - -/** - * Creates a [ScreenViewFactory] that uses a [Composable] function to display the rendering. - * - * Simple usage: - * - * ``` - * val FooViewFactory = composeScreenViewFactory { rendering, _ -> - * Text(rendering.message) - * } - * - * … - * - * val viewRegistry = ViewRegistry(FooViewFactory, …) - * ``` - * - * If you need to write a class instead of a function, for example to support dependency injection, - * see [ComposeScreenViewFactory]. - * - * For more details about how to write composable view factories, see [ComposeScreenViewFactory]. - */ -@WorkflowUiExperimentalApi -public inline fun composeScreenViewFactory( - noinline content: @Composable ( - rendering: RenderingT, - environment: ViewEnvironment - ) -> Unit -): ScreenViewFactory = composeScreenViewFactory(RenderingT::class, content) - -@PublishedApi -@WorkflowUiExperimentalApi -internal fun composeScreenViewFactory( - type: KClass, - content: @Composable ( - rendering: RenderingT, - environment: ViewEnvironment - ) -> Unit -): ScreenViewFactory = object : ComposeScreenViewFactory() { - override val type: KClass = type - - @Composable override fun Content( - rendering: RenderingT, - viewEnvironment: ViewEnvironment - ) { - content(rendering, viewEnvironment) - } -} - -/** - * A [ScreenViewFactory] that uses a [Composable] function to display the rendering. It is the - * Compose-based analogue of [ScreenViewRunner][com.squareup.workflow1.ui.ScreenViewRunner]. - * - * Simple usage: - * - * ``` - * class FooViewFactory : ComposeScreenViewFactory() { - * override val type = FooScreen::class - * - * @Composable override fun Content( - * rendering: FooScreen, - * viewEnvironment: ViewEnvironment - * ) { - * Text(rendering.message) - * } - * } - * - * … - * - * val viewRegistry = ViewRegistry(FooViewFactory, …) - * ``` - * - * ## Nesting child renderings - * - * Workflows can render other workflows, and renderings from one workflow can contain renderings - * from other workflows. These renderings may all be bound to their own [ScreenViewFactory]s. - * Regular [ScreenViewFactory]s and [ScreenViewRunner][com.squareup.workflow1.ui.ScreenViewRunner]s - * use [WorkflowViewStub][com.squareup.workflow1.ui.WorkflowViewStub] to recursively show nested - * renderings using the [ViewRegistry][com.squareup.workflow1.ui.ViewRegistry]. - * - * View factories defined using this function may also show nested renderings. Doing so is as simple - * as calling [WorkflowRendering] and passing in the nested rendering. See the kdoc on that function - * for an example. - * - * Nested renderings will have access to any - * [composition locals][androidx.compose.runtime.CompositionLocal] defined in outer composable, even - * if there are legacy views in between them, as long as the [ViewEnvironment] is propagated - * continuously between the two factories. - * - * ## Initializing Compose context - * - * Often all the [composeScreenViewFactory] factories in an app need to share some context – - * for example, certain composition locals need to be provided, such as `MaterialTheme`. - * To configure this shared context, call [withCompositionRoot] on your top-level [ViewEnvironment]. - * The first time a [composeScreenViewFactory] is used to show a rendering, its [Content] function will - * be wrapped with the [CompositionRoot]. See the documentation on [CompositionRoot] for - * more information. - */ -@WorkflowUiExperimentalApi -public abstract class ComposeScreenViewFactory : - ScreenViewFactory { - - final override val key: Key> - get() = Key(type, ComposeScreenViewFactory::class) - - /** - * The composable content of this [ScreenViewFactory]. This method will be called - * any time [rendering] or [viewEnvironment] change. It is the Compose-based analogue of - * [ScreenViewRunner.showRendering][com.squareup.workflow1.ui.ScreenViewRunner.showRendering]. - */ - @Composable public abstract fun Content( - rendering: RenderingT, - viewEnvironment: ViewEnvironment - ) - - final override fun buildView( - initialRendering: RenderingT, - initialEnvironment: ViewEnvironment, - context: Context, - container: ViewGroup? - ): ScreenViewHolder { - val view = ComposeView(context) - return ScreenViewHolder(initialEnvironment, view) { rendering, environment -> - // Update the state whenever a new rendering is emitted. - // This lambda will be executed synchronously before ScreenViewHolder.show returns. - view.setContent { Content(rendering, environment) } - } - } -} diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/CompositionRoot.kt b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/CompositionRoot.kt index 07f65424d1..28f947f31c 100644 --- a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/CompositionRoot.kt +++ b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/CompositionRoot.kt @@ -8,7 +8,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.staticCompositionLocalOf import com.squareup.workflow1.ui.Screen -import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.ScreenViewFactoryFinder import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi @@ -20,44 +19,45 @@ private val LocalHasViewFactoryRootBeenApplied = staticCompositionLocalOf { fals /** * A composable function that will be used to wrap the first (highest-level) - * [composeScreenViewFactory] view factory in a composition. This can be used to setup any + * [screenComposableFactory] view factory in a composition. This can be used to setup any * [composition locals][androidx.compose.runtime.CompositionLocal] that all - * [composeScreenViewFactory] factories need access to, such as UI themes. + * [screenComposableFactory] factories need access to, such as UI themes. * - * This function will called once, to wrap the _highest-level_ [composeScreenViewFactory] - * in the tree. However, composition locals are propagated down to child [composeScreenViewFactory] - * compositions, so any locals provided here will be available in _all_ [composeScreenViewFactory] + * This function will called once, to wrap the _highest-level_ [screenComposableFactory] + * in the tree. However, composition locals are propagated down to child [screenComposableFactory] + * compositions, so any locals provided here will be available in _all_ [screenComposableFactory] * compositions. */ public typealias CompositionRoot = @Composable (content: @Composable () -> Unit) -> Unit /** * Convenience function for applying a [CompositionRoot] to this [ViewEnvironment]'s - * [ScreenViewFactoryFinder]. See [ScreenViewFactoryFinder.withCompositionRoot]. + * [ScreenComposableFactoryFinder]. See [ScreenComposableFactoryFinder.withCompositionRoot]. */ @WorkflowUiExperimentalApi public fun ViewEnvironment.withCompositionRoot(root: CompositionRoot): ViewEnvironment { return this + - (ScreenViewFactoryFinder to this[ScreenViewFactoryFinder].withCompositionRoot(root)) + (ScreenComposableFactoryFinder to this[ScreenComposableFactoryFinder].withCompositionRoot(root)) } /** - * Returns a [ScreenViewFactoryFinder] that ensures that any [composeScreenViewFactory] + * Returns a [ScreenViewFactoryFinder] that ensures that any [screenComposableFactory] * factories registered in this registry will be wrapped exactly once with a [CompositionRoot] * wrapper. See [CompositionRoot] for more information. */ @WorkflowUiExperimentalApi -public fun ScreenViewFactoryFinder.withCompositionRoot( +public fun ScreenComposableFactoryFinder.withCompositionRoot( root: CompositionRoot -): ScreenViewFactoryFinder = - mapFactories { factory -> +): ScreenComposableFactoryFinder { + return mapFactories { factory -> @Suppress("UNCHECKED_CAST") - (factory as? ComposeScreenViewFactory)?.let { composeFactory -> - composeScreenViewFactory(composeFactory.type) { rendering, environment -> + (factory as? ScreenComposableFactory)?.let { composeFactory -> + screenComposableFactory(composeFactory.type) { rendering, environment -> WrappedWithRootIfNecessary(root) { composeFactory.Content(rendering, environment) } } } ?: factory } +} /** * Adds [content] to the composition, ensuring that [CompositionRoot] has been applied. Will only @@ -85,20 +85,21 @@ internal fun WrappedWithRootIfNecessary( } @WorkflowUiExperimentalApi -private fun ScreenViewFactoryFinder.mapFactories( - transform: (ScreenViewFactory<*>) -> ScreenViewFactory<*> -): ScreenViewFactoryFinder = object : ScreenViewFactoryFinder { - override fun getViewFactoryForRendering( +private fun ScreenComposableFactoryFinder.mapFactories( + transform: (ScreenComposableFactory<*>) -> ScreenComposableFactory<*> +): ScreenComposableFactoryFinder = object : ScreenComposableFactoryFinder { + override fun getComposableFactoryForRendering( environment: ViewEnvironment, rendering: ScreenT - ): ScreenViewFactory { - val factoryFor = this@mapFactories.getViewFactoryForRendering(environment, rendering) + ): ScreenComposableFactory? { + val factoryFor = this@mapFactories.getComposableFactoryForRendering(environment, rendering) + ?: return null val transformedFactory = transform(factoryFor) check(transformedFactory.type == rendering::class) { - "Expected transform to return a ScreenViewFactory that is compatible " + + "Expected transform to return a ScreenComposableFactory that is compatible " + "with ${rendering::class}, but got one with type ${transformedFactory.type}" } @Suppress("UNCHECKED_CAST") - return transformedFactory as ScreenViewFactory + return transformedFactory as ScreenComposableFactory } } diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ScreenComposableFactory.kt b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ScreenComposableFactory.kt new file mode 100644 index 0000000000..ecaa5b47ca --- /dev/null +++ b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ScreenComposableFactory.kt @@ -0,0 +1,280 @@ +package com.squareup.workflow1.ui.compose + +import android.content.Context +import android.view.ViewGroup +import androidx.activity.OnBackPressedDispatcherOwner +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner +import androidx.activity.setViewTreeOnBackPressedDispatcherOwner +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.setViewTreeLifecycleOwner +import com.squareup.workflow1.ui.OnBackPressedDispatcherOwnerKey +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewHolder +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.ViewRegistry +import com.squareup.workflow1.ui.ViewRegistry.Key +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.show +import com.squareup.workflow1.ui.startShowing +import kotlin.reflect.KClass + +@WorkflowUiExperimentalApi +public inline fun screenComposableFactory( + noinline content: @Composable ( + rendering: ScreenT, + environment: ViewEnvironment + ) -> Unit +): ScreenComposableFactory = screenComposableFactory(ScreenT::class, content) + +@PublishedApi +@WorkflowUiExperimentalApi +internal fun screenComposableFactory( + type: KClass, + content: @Composable ( + rendering: ScreenT, + environment: ViewEnvironment + ) -> Unit +): ScreenComposableFactory = object : ScreenComposableFactory { + override val type: KClass = type + + @Composable override fun Content( + rendering: ScreenT, + environment: ViewEnvironment + ) { + content(rendering, environment) + } +} + +// Can't get replace to work. +@Suppress("DeprecatedCallableAddReplaceWith") +@WorkflowUiExperimentalApi +@Deprecated("Use screenComposableFactory") +public inline fun composeScreenViewFactory( + noinline content: @Composable ( + rendering: ScreenT, + environment: ViewEnvironment + ) -> Unit +): ScreenComposableFactory = screenComposableFactory(content) + +/** + * TODO update + * + * A [ViewRegistry.Entry] that uses a [Composable] function to display [ScreenT]. + * It is the Compose-based analogue of [ScreenViewRunner][com.squareup.workflow1.ui.ScreenViewRunner]. + * + * Simple usage: + * + * ``` + * class FooViewFactory : screenComposableFactory() { + * override val type = FooScreen::class + * + * @Composable override fun Content( + * rendering: FooScreen, + * viewEnvironment: ViewEnvironment + * ) { + * Text(rendering.message) + * } + * } + * + * … + * + * val viewRegistry = ViewRegistry(FooViewFactory, …) + * ``` + * + * ## Nesting child renderings + * + * Workflows can render other workflows, and renderings from one workflow can contain renderings + * from other workflows. These renderings may all be bound to their own [ScreenViewFactory]s. + * Regular [ScreenViewFactory]s and [ScreenViewRunner][com.squareup.workflow1.ui.ScreenViewRunner]s + * use [WorkflowViewStub][com.squareup.workflow1.ui.WorkflowViewStub] to recursively show nested + * renderings using the [ViewRegistry][com.squareup.workflow1.ui.ViewRegistry]. + * + * View factories defined using this function may also show nested renderings. Doing so is as simple + * as calling [WorkflowRendering] and passing in the nested rendering. See the kdoc on that function + * for an example. + * + * Nested renderings will have access to any + * [composition locals][androidx.compose.runtime.CompositionLocal] defined in outer composable, even + * if there are legacy views in between them, as long as the [ViewEnvironment] is propagated + * continuously between the two factories. + * + * ## Initializing Compose context + * + * Often all the [screenComposableFactory] factories in an app need to share some context – + * for example, certain composition locals need to be provided, such as `MaterialTheme`. + * To configure this shared context, call [withCompositionRoot] on your top-level [ViewEnvironment]. + * The first time a [screenComposableFactory] is used to show a rendering, its [Content] function + * will be wrapped with the [CompositionRoot]. See the documentation on [CompositionRoot] for + * more information. + */ +@WorkflowUiExperimentalApi +public interface ScreenComposableFactory : ViewRegistry.Entry { + public val type: KClass + + override val key: Key> + get() = Key(type, ScreenComposableFactory::class) + + /** + * The composable content of this [ScreenComposableFactory]. This method will be called + * any time [rendering] or [environment] change. It is the Compose-based analogue of + * [ScreenViewRunner.showRendering][com.squareup.workflow1.ui.ScreenViewRunner.showRendering]. + */ + @Composable public fun Content( + rendering: ScreenT, + environment: ViewEnvironment + ) + + public companion object { + public inline operator fun invoke( + crossinline block: @Composable (rendering: ScreenT, environment: ViewEnvironment) -> Unit + ): ScreenComposableFactory { + return object : ScreenComposableFactory { + override val type = ScreenT::class + + @Composable override fun Content( + rendering: ScreenT, + environment: ViewEnvironment + ) { + block(rendering, environment) + } + } + } + } +} + +/** + * Returns a [ScreenComposableFactory] to display [Screen]s that match + * the receiver in a Compose context. Delegates to the [ScreenComposableFactoryFinder] + * found in [environment]. + * + * TODO more + * * How to customize it + * + * @see [ViewEnvironment.withComposeInteropSupport] + */ +@WorkflowUiExperimentalApi +public fun ScreenT.toComposableFactory( + environment: ViewEnvironment +): ScreenComposableFactory { + return environment[ScreenComposableFactoryFinder] + .requireComposableFactoryForRendering(environment, this) +} + +/** + * Convert a [ScreenComposableFactory] into a [ScreenViewFactory] + * by using a [ComposeView] to host [ScreenComposableFactory.Content]. + */ +@WorkflowUiExperimentalApi +public fun ScreenComposableFactory.asViewFactory(): + ScreenViewFactory { + + return object : ScreenViewFactory { + override val type = this@asViewFactory.type + + override fun buildView( + initialRendering: ScreenT, + initialEnvironment: ViewEnvironment, + context: Context, + container: ViewGroup? + ): ScreenViewHolder { + val view = ComposeView(context) + return ScreenViewHolder(initialEnvironment, view) { newRendering, environment -> + // Update the state whenever a new rendering is emitted. + // This lambda will be executed synchronously before ScreenViewHolder.show returns. + view.setContent { Content(newRendering, environment) } + } + } + } +} + +/** + * Convert a [ScreenViewFactory] to a [ScreenComposableFactory], + * using [AndroidView] to host the `View` it builds. + */ +@WorkflowUiExperimentalApi +public fun ScreenViewFactory.asComposableFactory(): + ScreenComposableFactory { + return object : ScreenComposableFactory { + private val viewFactory = this@asComposableFactory + + override val type: KClass get() = viewFactory.type + + /** + * This is effectively the logic of `WorkflowViewStub`, but translated into Compose idioms. + * This approach has a few advantages: + * + * - Avoids extra custom views required to host `WorkflowViewStub` inside a Composition. Its trick + * of replacing itself in its parent doesn't play nicely with Compose. + * - Allows us to pass the correct parent view for inflation (the root of the composition). + * - Avoids `WorkflowViewStub` having to do its own lookup to find the correct + * [ScreenViewFactory], since we already have the correct one. + * - Propagate the current `LifecycleOwner` from [LocalLifecycleOwner] by setting it as the + * [ViewTreeLifecycleOwner] on the view. + * - Propagate the current [OnBackPressedDispatcherOwner] from either + * [LocalOnBackPressedDispatcherOwner] or the [viewEnvironment], + * both on the [AndroidView] via [setViewTreeOnBackPressedDispatcherOwner], + * and in the [ViewEnvironment] for use by any nested [WorkflowViewStub] + * + * Like `WorkflowViewStub`, this function uses the [viewFactory] to create and memoize a + * `View` to display the [rendering], keeps it updated with the latest [rendering] and + * [environment], and adds it to the composition. + */ + @Composable override fun Content( + rendering: ScreenT, + environment: ViewEnvironment + ) { + val lifecycleOwner = LocalLifecycleOwner.current + + // Make sure any nested WorkflowViewStub will be able to propagate the + // OnBackPressedDispatcherOwner, if we found one. No need to fail fast here. + // It's only an issue if someone tries to use it, and the error message + // at those call sites should be clear enough. + val onBackOrNull = LocalOnBackPressedDispatcherOwner.current + ?: environment.map[OnBackPressedDispatcherOwnerKey] as? OnBackPressedDispatcherOwner + + val envWithOnBack = onBackOrNull + ?.let { environment + (OnBackPressedDispatcherOwnerKey to it) } + ?: environment + + AndroidView( + factory = { context -> + + // We pass in a null container because the container isn't a View, it's a composable. The + // compose machinery will generate an intermediate view that it ends up adding this to but + // we don't have access to that. + viewFactory + .startShowing(rendering, envWithOnBack, context, container = null) + .let { viewHolder -> + // Put the viewHolder in a tag so that we can find it in the update lambda, below. + viewHolder.view.setTag(R.id.workflow_screen_view_holder, viewHolder) + + // Unfortunately AndroidView doesn't propagate these itself. + viewHolder.view.setViewTreeLifecycleOwner(lifecycleOwner) + onBackOrNull?.let { + viewHolder.view.setViewTreeOnBackPressedDispatcherOwner(it) + } + + // We don't propagate the (non-compose) SavedStateRegistryOwner, or the (compose) + // SaveableStateRegistry, because currently all our navigation is implemented as + // Android views, which ensures there is always an Android view between any state + // registry and any Android view shown as a child of it, even if there's a compose + // view in between. + viewHolder.view + } + }, + // This function will be invoked every time this composable is recomposed, which means that + // any time a new rendering or view environment are passed in we'll send them to the view. + update = { view -> + @Suppress("UNCHECKED_CAST") + val viewHolder = + view.getTag(R.id.workflow_screen_view_holder) as ScreenViewHolder + viewHolder.show(rendering, envWithOnBack) + } + ) + } + } +} diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder.kt b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder.kt new file mode 100644 index 0000000000..08830a46f5 --- /dev/null +++ b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder.kt @@ -0,0 +1,73 @@ +package com.squareup.workflow1.ui.compose + +import com.squareup.workflow1.ui.NamedScreen +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.ViewEnvironmentKey +import com.squareup.workflow1.ui.ViewRegistry +import com.squareup.workflow1.ui.ViewRegistry.Key +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.container.EnvironmentScreen +import com.squareup.workflow1.ui.getFactoryFor + +@WorkflowUiExperimentalApi +public interface ScreenComposableFactoryFinder { + public fun getComposableFactoryForRendering( + environment: ViewEnvironment, + rendering: ScreenT + ): ScreenComposableFactory? { + val factoryOrNull: ScreenComposableFactory? = + environment[ViewRegistry].getFactoryFor(rendering) + + @Suppress("UNCHECKED_CAST") + return factoryOrNull + ?: (rendering as? ComposeScreen)?.let { + ScreenComposableFactory { rendering, environment -> + // Do we need to key on either, both params? + rendering.Content(environment) + } as ScreenComposableFactory + } + // ?: (rendering as? BackStackScreen<*>)?.let { + // BackStackScreenViewFactory as ScreenViewFactory + // } + // ?: (rendering as? BodyAndOverlaysScreen<*, *>)?.let { + // BodyAndOverlaysContainer as ScreenViewFactory + // } + ?: (rendering as? NamedScreen<*>)?.let { + ScreenComposableFactory> { rendering, environment -> + val innerFactory = rendering.content.toComposableFactory(environment) + innerFactory.Content(rendering.content, environment) + // WorkflowRendering(rendering.content, environment) + } as ScreenComposableFactory + } + ?: (rendering as? EnvironmentScreen<*>)?.let { + ScreenComposableFactory> { rendering, environment -> + val comboEnv = environment + rendering.environment + val innerFactory = rendering.content.toComposableFactory(comboEnv) + innerFactory.Content(rendering.content, comboEnv) + // WorkflowRendering(rendering.content, comboEnv) + } as ScreenComposableFactory + } + } + + public companion object : ViewEnvironmentKey() { + override val default: ScreenComposableFactoryFinder + get() = object : ScreenComposableFactoryFinder {} + } +} + +@WorkflowUiExperimentalApi +public fun ScreenComposableFactoryFinder.requireComposableFactoryForRendering( + environment: ViewEnvironment, + rendering: ScreenT +): ScreenComposableFactory { + return getComposableFactoryForRendering(environment, rendering) + ?: throw IllegalArgumentException( + "A ScreenComposableFactory should have been registered to display $rendering, " + + "or that class should implement ComposeScreen. Instead found " + + "${ + environment[ViewRegistry] + .getEntryFor(Key(rendering::class, ScreenComposableFactory::class)) + }." + ) +} diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ViewEnvironmentWithComposeSupport.kt b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ViewEnvironmentWithComposeSupport.kt new file mode 100644 index 0000000000..7acf1ab748 --- /dev/null +++ b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ViewEnvironmentWithComposeSupport.kt @@ -0,0 +1,54 @@ +package com.squareup.workflow1.ui.compose + +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewFactoryFinder +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +/** + * Replaces the [ScreenComposableFactoryFinder] and [ScreenViewFactoryFinder] + * found in the receiving [ViewEnvironment] with wrappers that are able to + * delegate from one platform to the other. Required to allow + * [WorkflowViewStub][com.squareup.workflow1.ui.WorkflowViewStub] + * to handle renderings bound to `@Composable` functions, and to allow + * [WorkflowRendering] to handle renderings bound to [ScreenViewFactory]. + * + * TODO MORE + * + * App-specific customizations of those objects must be placed in the + * [ViewEnvironment] before calling this method. + */ +@WorkflowUiExperimentalApi +public fun ViewEnvironment.withComposeInteropSupport(): ViewEnvironment { + val rawViewFactoryFinder = get(ScreenViewFactoryFinder) + val rawComposableFactoryFinder = get(ScreenComposableFactoryFinder) + + val convertingViewFactoryFinder = object : ScreenViewFactoryFinder { + override fun getViewFactoryForRendering( + environment: ViewEnvironment, + rendering: ScreenT + ): ScreenViewFactory? { + return rawViewFactoryFinder.getViewFactoryForRendering(environment, rendering) + ?: rawComposableFactoryFinder.getComposableFactoryForRendering(environment, rendering) + ?.asViewFactory() + } + } + + val convertingComposableFactoryFinder = object : ScreenComposableFactoryFinder { + override fun getComposableFactoryForRendering( + environment: ViewEnvironment, + rendering: ScreenT + ): ScreenComposableFactory? { + return rawComposableFactoryFinder.getComposableFactoryForRendering(environment, rendering) + ?: rawViewFactoryFinder.getViewFactoryForRendering(environment, rendering) + ?.asComposableFactory() + } + } + + // TODO consider something like LocalHasViewFactoryRootBeenApplied to enforce + // no duplicate calls + + return this + (ScreenViewFactoryFinder to convertingViewFactoryFinder) + + (ScreenComposableFactoryFinder to convertingComposableFactoryFinder) +} diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt index f746010648..ece128fa1c 100644 --- a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt +++ b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt @@ -1,9 +1,5 @@ package com.squareup.workflow1.ui.compose -import android.view.View -import androidx.activity.OnBackPressedDispatcherOwner -import androidx.activity.compose.LocalOnBackPressedDispatcherOwner -import androidx.activity.setViewTreeOnBackPressedDispatcherOwner import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -12,28 +8,20 @@ import androidx.compose.runtime.key import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle.State.DESTROYED import androidx.lifecycle.Lifecycle.State.INITIALIZED import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry -import androidx.lifecycle.setViewTreeLifecycleOwner import com.squareup.workflow1.ui.Compatible -import com.squareup.workflow1.ui.OnBackPressedDispatcherOwnerKey import com.squareup.workflow1.ui.Screen -import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.ScreenViewFactoryFinder import com.squareup.workflow1.ui.ScreenViewHolder import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub import com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner -import com.squareup.workflow1.ui.show -import com.squareup.workflow1.ui.startShowing -import com.squareup.workflow1.ui.toViewFactory -import kotlin.reflect.KClass /** * Renders [rendering] into the composition using this [ViewEnvironment]'s @@ -69,7 +57,7 @@ import kotlin.reflect.KClass @Composable public fun WorkflowRendering( rendering: Screen, - viewEnvironment: ViewEnvironment, + environment: ViewEnvironment, modifier: Modifier = Modifier ) { // This will fetch a new view factory any time the new rendering is incompatible with the previous @@ -83,13 +71,13 @@ public fun WorkflowRendering( // factory created an Android view, this will also remove the old one from the view hierarchy // before replacing it with the new one. key(renderingCompatibilityKey) { - val viewFactory = remember { + val composableFactory = remember { // The view registry may return a new factory instance for a rendering every time we ask it, for // example if an AndroidScreen doesn't share its factory between rendering instances. We // intentionally don't ask it for a new instance every time to match the behavior of // WorkflowViewStub and other containers, which only ask for a new factory when the rendering is // incompatible. - rendering.toViewFactory(viewEnvironment).asComposeViewFactory() + rendering.toComposableFactory(environment) } // Just like WorkflowViewStub, we need to manage a Lifecycle for the child view. We just provide @@ -103,7 +91,7 @@ public fun WorkflowRendering( // into this function is to directly control the layout of the child view – which means // minimum constraints are likely to be significant. Box(modifier, propagateMinConstraints = true) { - viewFactory.Content(rendering, viewEnvironment) + composableFactory.Content(rendering, environment) } } } @@ -146,90 +134,3 @@ public fun WorkflowRendering( return lifecycleOwner } - -/** - * Returns a [ComposeScreenViewFactory] that makes it convenient to display this [ScreenViewFactory] - * as a composable. If this is a [ComposeScreenViewFactory] already it just returns `this`, - * otherwise it wraps the factory in one that manages a classic Android view. - */ -@OptIn(WorkflowUiExperimentalApi::class) -private fun ScreenViewFactory.asComposeViewFactory() = - (this as? ComposeScreenViewFactory) ?: object : ComposeScreenViewFactory() { - - private val originalFactory = this@asComposeViewFactory - override val type: KClass get() = originalFactory.type - - /** - * This is effectively the logic of [WorkflowViewStub], but translated into Compose idioms. - * This approach has a few advantages: - * - * - Avoids extra custom views required to host `WorkflowViewStub` inside a Composition. Its trick - * of replacing itself in its parent doesn't play nicely with Compose. - * - Allows us to pass the correct parent view for inflation (the root of the composition). - * - Avoids `WorkflowViewStub` having to do its own lookup to find the correct - * [ScreenViewFactory], since we already have the correct one. - * - Propagate the current [LifecycleOwner] from [LocalLifecycleOwner] by setting it as the - * [ViewTreeLifecycleOwner] on the view. - * - Propagate the current [OnBackPressedDispatcherOwner] from either - * [LocalOnBackPressedDispatcherOwner] or the [viewEnvironment], - * both on the [AndroidView] via [setViewTreeOnBackPressedDispatcherOwner], - * and in the [ViewEnvironment] for use by any nested [WorkflowViewStub] - * - * Like `WorkflowViewStub`, this function uses the [originalFactory] to create and memoize a - * [View] to display the [rendering], keeps it updated with the latest [rendering] and - * [viewEnvironment], and adds it to the composition. - */ - @Composable override fun Content( - rendering: ScreenT, - viewEnvironment: ViewEnvironment - ) { - val lifecycleOwner = LocalLifecycleOwner.current - - // Make sure any nested WorkflowViewStub will be able to propagate the - // OnBackPressedDispatcherOwner, if we found one. No need to fail fast here. - // It's only an issue if someone tries to use it, and the error message - // at those call sites should be clear enough. - val onBackOrNull = LocalOnBackPressedDispatcherOwner.current - ?: viewEnvironment.map[OnBackPressedDispatcherOwnerKey] as? OnBackPressedDispatcherOwner - - val envWithOnBack = onBackOrNull - ?.let { viewEnvironment + (OnBackPressedDispatcherOwnerKey to it) } - ?: viewEnvironment - - AndroidView( - factory = { context -> - - // We pass in a null container because the container isn't a View, it's a composable. The - // compose machinery will generate an intermediate view that it ends up adding this to but - // we don't have access to that. - originalFactory - .startShowing(rendering, envWithOnBack, context, container = null) - .let { viewHolder -> - // Put the viewHolder in a tag so that we can find it in the update lambda, below. - viewHolder.view.setTag(R.id.workflow_screen_view_holder, viewHolder) - - // Unfortunately AndroidView doesn't propagate these itself. - viewHolder.view.setViewTreeLifecycleOwner(lifecycleOwner) - onBackOrNull?.let { - viewHolder.view.setViewTreeOnBackPressedDispatcherOwner(it) - } - - // We don't propagate the (non-compose) SavedStateRegistryOwner, or the (compose) - // SaveableStateRegistry, because currently all our navigation is implemented as - // Android views, which ensures there is always an Android view between any state - // registry and any Android view shown as a child of it, even if there's a compose - // view in between. - viewHolder.view - } - }, - // This function will be invoked every time this composable is recomposed, which means that - // any time a new rendering or view environment are passed in we'll send them to the view. - update = { view -> - @Suppress("UNCHECKED_CAST") - val viewHolder = - view.getTag(R.id.workflow_screen_view_holder) as ScreenViewHolder - viewHolder.show(rendering, envWithOnBack) - } - ) - } - } diff --git a/workflow-ui/core-android/api/core-android.api b/workflow-ui/core-android/api/core-android.api index 110e3fd8a3..1d5e4df11c 100644 --- a/workflow-ui/core-android/api/core-android.api +++ b/workflow-ui/core-android/api/core-android.api @@ -14,6 +14,7 @@ public abstract interface class com/squareup/workflow1/ui/AndroidScreen : com/sq public final class com/squareup/workflow1/ui/LayoutScreenViewFactory : com/squareup/workflow1/ui/ScreenViewFactory { public fun (Lkotlin/reflect/KClass;ILkotlin/jvm/functions/Function1;)V public fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Lcom/squareup/workflow1/ui/ScreenViewHolder; + public fun getKey ()Lcom/squareup/workflow1/ui/ViewRegistry$Key; public fun getType ()Lkotlin/reflect/KClass; } @@ -45,6 +46,8 @@ public final class com/squareup/workflow1/ui/ParcelableTextController$CREATOR : public abstract interface class com/squareup/workflow1/ui/ScreenViewFactory : com/squareup/workflow1/ui/ViewRegistry$Entry { public static final field Companion Lcom/squareup/workflow1/ui/ScreenViewFactory$Companion; public abstract fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Lcom/squareup/workflow1/ui/ScreenViewHolder; + public abstract fun getKey ()Lcom/squareup/workflow1/ui/ViewRegistry$Key; + public abstract fun getType ()Lkotlin/reflect/KClass; } public final class com/squareup/workflow1/ui/ScreenViewFactory$Companion { @@ -52,6 +55,7 @@ public final class com/squareup/workflow1/ui/ScreenViewFactory$Companion { public final class com/squareup/workflow1/ui/ScreenViewFactory$DefaultImpls { public static synthetic fun buildView$default (Lcom/squareup/workflow1/ui/ScreenViewFactory;Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/ScreenViewHolder; + public static fun getKey (Lcom/squareup/workflow1/ui/ScreenViewFactory;)Lcom/squareup/workflow1/ui/ViewRegistry$Key; } public abstract interface class com/squareup/workflow1/ui/ScreenViewFactoryFinder { @@ -68,6 +72,10 @@ public final class com/squareup/workflow1/ui/ScreenViewFactoryFinder$DefaultImpl public static fun getViewFactoryForRendering (Lcom/squareup/workflow1/ui/ScreenViewFactoryFinder;Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/Screen;)Lcom/squareup/workflow1/ui/ScreenViewFactory; } +public final class com/squareup/workflow1/ui/ScreenViewFactoryFinderKt { + public static final fun requireViewFactoryForRendering (Lcom/squareup/workflow1/ui/ScreenViewFactoryFinder;Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/Screen;)Lcom/squareup/workflow1/ui/ScreenViewFactory; +} + public final class com/squareup/workflow1/ui/ScreenViewFactoryKt { public static final fun startShowing (Lcom/squareup/workflow1/ui/ScreenViewFactory;Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lcom/squareup/workflow1/ui/ViewStarter;)Lcom/squareup/workflow1/ui/ScreenViewHolder; public static final fun startShowing (Lcom/squareup/workflow1/ui/ScreenViewHolder;Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/ViewStarter;)V @@ -113,6 +121,7 @@ public final class com/squareup/workflow1/ui/ViewBackHandlerKt { public final class com/squareup/workflow1/ui/ViewBindingScreenViewFactory : com/squareup/workflow1/ui/ScreenViewFactory { public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;)V public fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Lcom/squareup/workflow1/ui/ScreenViewHolder; + public fun getKey ()Lcom/squareup/workflow1/ui/ViewRegistry$Key; public fun getType ()Lkotlin/reflect/KClass; } @@ -289,11 +298,17 @@ public final class com/squareup/workflow1/ui/container/LayeredDialogSessions$Sav public abstract interface class com/squareup/workflow1/ui/container/OverlayDialogFactory : com/squareup/workflow1/ui/ViewRegistry$Entry { public static final field Companion Lcom/squareup/workflow1/ui/container/OverlayDialogFactory$Companion; public abstract fun buildDialog (Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Lcom/squareup/workflow1/ui/container/OverlayDialogHolder; + public abstract fun getKey ()Lcom/squareup/workflow1/ui/ViewRegistry$Key; + public abstract fun getType ()Lkotlin/reflect/KClass; } public final class com/squareup/workflow1/ui/container/OverlayDialogFactory$Companion { } +public final class com/squareup/workflow1/ui/container/OverlayDialogFactory$DefaultImpls { + public static fun getKey (Lcom/squareup/workflow1/ui/container/OverlayDialogFactory;)Lcom/squareup/workflow1/ui/ViewRegistry$Key; +} + public abstract interface class com/squareup/workflow1/ui/container/OverlayDialogFactoryFinder { public static final field Companion Lcom/squareup/workflow1/ui/container/OverlayDialogFactoryFinder$Companion; public abstract fun getDialogFactoryForRendering (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/container/Overlay;)Lcom/squareup/workflow1/ui/container/OverlayDialogFactory; diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactory.kt index d0ad8159b4..27bad63e86 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactory.kt @@ -284,7 +284,7 @@ public interface ScreenViewFactory : ViewRegistry.Entry ScreenT.toViewFactory( environment: ViewEnvironment ): ScreenViewFactory { - return environment[ScreenViewFactoryFinder].getViewFactoryForRendering(environment, this) + return environment[ScreenViewFactoryFinder].requireViewFactoryForRendering(environment, this) } /** diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactoryFinder.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactoryFinder.kt index fac627c4c1..3fcd7b0915 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactoryFinder.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactoryFinder.kt @@ -54,12 +54,12 @@ public interface ScreenViewFactoryFinder { public fun getViewFactoryForRendering( environment: ViewEnvironment, rendering: ScreenT - ): ScreenViewFactory { - val entry = environment[ViewRegistry] - .getEntryFor(Key(rendering::class, ScreenViewFactory::class)) + ): ScreenViewFactory? { + val factoryOrNull: ScreenViewFactory? = + environment[ViewRegistry].getFactoryFor(rendering) @Suppress("UNCHECKED_CAST") - return (entry as? ScreenViewFactory) + return factoryOrNull ?: (rendering as? AndroidScreen<*>)?.viewFactory as? ScreenViewFactory ?: (rendering as? BackStackScreen<*>)?.let { BackStackScreenViewFactory as ScreenViewFactory @@ -77,10 +77,6 @@ public interface ScreenViewFactoryFinder { showContent(envScreen.content, environment + envScreen.environment) } as ScreenViewFactory } - ?: throw IllegalArgumentException( - "A ScreenViewFactory should have been registered to display $rendering, " + - "or that class should implement AndroidScreen. Instead found $entry." - ) } public companion object : ViewEnvironmentKey() { @@ -88,3 +84,19 @@ public interface ScreenViewFactoryFinder { get() = object : ScreenViewFactoryFinder {} } } + +@WorkflowUiExperimentalApi +public fun ScreenViewFactoryFinder.requireViewFactoryForRendering( + environment: ViewEnvironment, + rendering: ScreenT +): ScreenViewFactory { + return getViewFactoryForRendering(environment, rendering) + ?: throw IllegalArgumentException( + "A ScreenViewFactory should have been registered to display $rendering, " + + "or that class should implement AndroidScreen. Instead found " + + "${ + environment[ViewRegistry] + .getEntryFor(Key(rendering::class, ScreenViewFactory::class)) + }." + ) +} diff --git a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ScreenViewFactoryTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ScreenViewFactoryTest.kt index 4a888559b1..ea1f2a92d1 100644 --- a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ScreenViewFactoryTest.kt +++ b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ScreenViewFactoryTest.kt @@ -5,6 +5,7 @@ import android.view.ViewGroup import com.google.common.truth.Truth.assertThat import com.squareup.workflow1.ui.ScreenViewFactory.Companion.forWrapper import com.squareup.workflow1.ui.ViewRegistry.Entry +import com.squareup.workflow1.ui.ViewRegistry.Key import org.junit.Test import org.mockito.kotlin.mock import kotlin.reflect.KClass @@ -16,9 +17,10 @@ internal class ScreenViewFactoryTest { @Test fun missingBindingMessage_isUseful() { val emptyReg = object : ViewRegistry { - override val keys: Set> = emptySet() - override fun getEntryFor( - renderingType: KClass + + override val keys: Set> = emptySet() + override fun getEntryFor( + key: Key ): Entry? = null } val env = ViewEnvironment.EMPTY + emptyReg diff --git a/workflow-ui/core-common/api/core-common.api b/workflow-ui/core-common/api/core-common.api index 4fff428711..f1f88539be 100644 --- a/workflow-ui/core-common/api/core-common.api +++ b/workflow-ui/core-common/api/core-common.api @@ -75,7 +75,7 @@ public abstract class com/squareup/workflow1/ui/ViewEnvironmentKey { public abstract interface class com/squareup/workflow1/ui/ViewRegistry { public static final field Companion Lcom/squareup/workflow1/ui/ViewRegistry$Companion; - public abstract fun getEntryFor (Lkotlin/reflect/KClass;)Lcom/squareup/workflow1/ui/ViewRegistry$Entry; + public abstract fun getEntryFor (Lcom/squareup/workflow1/ui/ViewRegistry$Key;)Lcom/squareup/workflow1/ui/ViewRegistry$Entry; public abstract fun getKeys ()Ljava/util/Set; } @@ -87,7 +87,16 @@ public final class com/squareup/workflow1/ui/ViewRegistry$Companion : com/square } public abstract interface class com/squareup/workflow1/ui/ViewRegistry$Entry { - public abstract fun getType ()Lkotlin/reflect/KClass; + public abstract fun getKey ()Lcom/squareup/workflow1/ui/ViewRegistry$Key; +} + +public final class com/squareup/workflow1/ui/ViewRegistry$Key { + public fun (Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getFactoryType ()Lkotlin/reflect/KClass; + public final fun getRenderingType ()Lkotlin/reflect/KClass; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class com/squareup/workflow1/ui/ViewRegistryKt { @@ -233,6 +242,7 @@ public final class com/squareup/workflow1/ui/container/EnvironmentScreen : com/s public final class com/squareup/workflow1/ui/container/EnvironmentScreenKt { public static final fun withEnvironment (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/container/EnvironmentScreen; + public static final fun withEnvironment (Lcom/squareup/workflow1/ui/Screen;Lkotlin/Pair;)Lcom/squareup/workflow1/ui/container/EnvironmentScreen; public static synthetic fun withEnvironment$default (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/container/EnvironmentScreen; public static final fun withRegistry (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewRegistry;)Lcom/squareup/workflow1/ui/container/EnvironmentScreen; } diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/TypedViewRegistry.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/TypedViewRegistry.kt index 4b72ca50db..5d3fb0ea50 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/TypedViewRegistry.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/TypedViewRegistry.kt @@ -14,7 +14,12 @@ internal class TypedViewRegistry private constructor( ) : ViewRegistry { constructor(vararg bindings: Entry<*>) : this( - bindings.associateBy { it.key } + bindings.associateBy { + require(it.key.factoryType.isInstance(it)) { + "Factory $it must be of the type declared in its key, ${it.key.factoryType.qualifiedName}" + } + it.key + } .apply { check(keys.size == bindings.size) { "${bindings.map { it.key }} must not have duplicate entries." diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/ViewRegistry.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/ViewRegistry.kt index 8f767a4355..c7354e368e 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/ViewRegistry.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/ViewRegistry.kt @@ -3,6 +3,7 @@ package com.squareup.workflow1.ui import com.squareup.workflow1.ui.ViewRegistry.Entry import com.squareup.workflow1.ui.ViewRegistry.Key import kotlin.reflect.KClass +import kotlin.reflect.safeCast /** * The [ViewEnvironment] service that can be used to display the stream of renderings @@ -62,6 +63,12 @@ import kotlin.reflect.KClass */ @WorkflowUiExperimentalApi public interface ViewRegistry { + /** + * Identifies a UI factory [Entry] in a [ViewRegistry]. + * + * @param renderingType the type of view model for which [factoryType] instances can build UI + * @param factoryType the type of the UI factory that can build UI for [renderingType] + */ public class Key( public val renderingType: KClass, public val factoryType: KClass @@ -87,13 +94,18 @@ public interface ViewRegistry { } } + /** + * Implemented by a factory that can build some kind of UI for view models + * of type [RenderingT], and which can be listed in a [ViewRegistry]. The + * [Key.factoryType] field of [key] must be the type of this [Entry]. + */ public interface Entry { public val key: Key } /** * The set of unique keys which this registry can derive from the renderings passed to - * [getEntryFor] and for which it knows how to create views. + * [getEntryFor] and for which it knows how to create UI. * * Used to ensure that duplicate bindings are never registered. */ @@ -116,10 +128,25 @@ public interface ViewRegistry { } } +@WorkflowUiExperimentalApi +public inline fun ViewRegistry.getFactoryFor( + rendering: RenderingT +): FactoryT? { + return FactoryT::class.safeCast(getEntryFor(Key(rendering::class, FactoryT::class))) +} + +@WorkflowUiExperimentalApi +public inline fun < + reified RenderingT : Any, + reified FactoryT : Any + > ViewRegistry.getFactoryFor(): FactoryT? { + return FactoryT::class.safeCast(getEntryFor(Key(RenderingT::class, FactoryT::class))) +} + @WorkflowUiExperimentalApi public inline operator fun ViewRegistry.get( key: Key -): Entry? = getEntryFor(key) +): FactoryT? = FactoryT::class.safeCast(getEntryFor(key)) @WorkflowUiExperimentalApi public fun ViewRegistry(vararg bindings: Entry<*>): ViewRegistry = diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/EnvironmentScreen.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/EnvironmentScreen.kt index ffca533440..778b12de86 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/EnvironmentScreen.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/EnvironmentScreen.kt @@ -2,6 +2,7 @@ package com.squareup.workflow1.ui.container import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.ViewEnvironmentKey import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.Wrapper @@ -60,3 +61,12 @@ public fun Screen.withEnvironment( else -> EnvironmentScreen(this, environment) } } + +/** + * Returns an [EnvironmentScreen] derived from the receiver, + * whose [EnvironmentScreen.environment] includes the given entry. + */ +@WorkflowUiExperimentalApi +public fun Screen.withEnvironment( + entry: Pair, T> +): EnvironmentScreen<*> = withEnvironment(ViewEnvironment.EMPTY + entry) diff --git a/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/container/EnvironmentScreenTest.kt b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/container/EnvironmentScreenTest.kt index 3336da87b2..c0ae4147dc 100644 --- a/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/container/EnvironmentScreenTest.kt +++ b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/container/EnvironmentScreenTest.kt @@ -8,7 +8,7 @@ import com.squareup.workflow1.ui.ViewEnvironmentKey import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.ViewRegistry.Key import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.get +import com.squareup.workflow1.ui.getFactoryFor import com.squareup.workflow1.ui.plus import org.junit.Test import kotlin.reflect.KClass @@ -32,19 +32,20 @@ internal class EnvironmentScreenTest { } private object FooScreen : Screen - private val fooKey = Key(FooScreen::class, TestFactory::class) private object BarScreen : Screen - private val barKey = Key(BarScreen::class, TestFactory::class) @Test fun `Screen withRegistry works`() { val fooFactory = TestFactory(FooScreen::class) val viewRegistry = ViewRegistry(fooFactory) val envScreen = FooScreen.withRegistry(viewRegistry) - assertThat(envScreen.environment[ViewRegistry][fooKey]) - .isSameInstanceAs(fooFactory) + assertThat( + envScreen.environment[ViewRegistry].getFactoryFor>(FooScreen) + ).isSameInstanceAs(fooFactory) - assertThat(envScreen.environment[ViewRegistry][barKey]).isNull() + assertThat( + envScreen.environment[ViewRegistry].getFactoryFor>(BarScreen) + ).isNull() } @Test fun `Screen withEnvironment works`() { @@ -54,10 +55,12 @@ internal class EnvironmentScreenTest { EMPTY + viewRegistry + TestValue("foo") ) - assertThat(envScreen.environment[ViewRegistry][fooKey]) - .isSameInstanceAs(fooFactory) - assertThat(envScreen.environment[ViewRegistry][barKey]) - .isNull() + assertThat( + envScreen.environment[ViewRegistry].getFactoryFor>(FooScreen) + ).isSameInstanceAs(fooFactory) + assertThat( + envScreen.environment[ViewRegistry].getFactoryFor>(BarScreen) + ).isNull() assertThat(envScreen.environment[TestValue]) .isEqualTo(TestValue("foo")) } @@ -70,11 +73,13 @@ internal class EnvironmentScreenTest { val left = FooScreen.withRegistry(ViewRegistry(fooFactory1, barFactory)) val union = left.withRegistry(ViewRegistry(fooFactory2)) - assertThat(union.environment[ViewRegistry][fooKey]) - .isSameInstanceAs(fooFactory2) + assertThat( + union.environment[ViewRegistry].getFactoryFor>(FooScreen) + ).isSameInstanceAs(fooFactory2) - assertThat(union.environment[ViewRegistry][barKey]) - .isSameInstanceAs(barFactory) + assertThat( + union.environment[ViewRegistry].getFactoryFor>(BarScreen) + ).isSameInstanceAs(barFactory) } @Test fun `EnvironmentScreen withEnvironment merges`() { @@ -90,10 +95,12 @@ internal class EnvironmentScreenTest { EMPTY + ViewRegistry(fooFactory2) + TestValue("right") ) - assertThat(union.environment[ViewRegistry][fooKey]) - .isSameInstanceAs(fooFactory2) - assertThat(union.environment[ViewRegistry][barKey]) - .isSameInstanceAs(barFactory) + assertThat( + union.environment[ViewRegistry].getFactoryFor>(FooScreen) + ).isSameInstanceAs(fooFactory2) + assertThat( + union.environment[ViewRegistry].getFactoryFor>(BarScreen) + ).isSameInstanceAs(barFactory) assertThat(union.environment[TestValue]) .isEqualTo(TestValue("right")) }