diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocompose/App.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocompose/App.kt index 1c682ab2f4..78d8b7faec 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocompose/App.kt +++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocompose/App.kt @@ -16,10 +16,9 @@ import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.compose.WorkflowRendering import com.squareup.workflow1.ui.compose.renderAsState +import com.squareup.workflow1.ui.plus -private val viewRegistry = ViewRegistry(HelloBinding) - -private val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) +private val viewEnvironment = ViewEnvironment.EMPTY + ViewRegistry(HelloBinding) @Composable fun App() { MaterialTheme { 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 4e99b10bcf..9b85adfcde 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 @@ -15,15 +15,13 @@ import com.squareup.workflow1.ui.WorkflowLayout import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.compose.composeViewFactory import com.squareup.workflow1.ui.compose.withCompositionRoot +import com.squareup.workflow1.ui.plus import com.squareup.workflow1.ui.renderWorkflowIn import kotlinx.coroutines.flow.StateFlow -@OptIn(WorkflowUiExperimentalApi::class) -private val viewRegistry = ViewRegistry(HelloBinding) - @OptIn(WorkflowUiExperimentalApi::class) private val viewEnvironment = - ViewEnvironment(mapOf(ViewRegistry to viewRegistry)).withCompositionRoot { content -> + (ViewEnvironment.EMPTY + ViewRegistry(HelloBinding)).withCompositionRoot { content -> MaterialTheme(content = content) } @@ -37,12 +35,12 @@ class HelloBindingActivity : AppCompatActivity() { val model: HelloBindingModel by viewModels() setContentView( - WorkflowLayout(this).apply { - start( - renderings = model.renderings, - environment = viewEnvironment - ) - } + WorkflowLayout(this).apply { + start( + renderings = model.renderings, + environment = viewEnvironment + ) + } ) } @@ -50,9 +48,9 @@ class HelloBindingActivity : AppCompatActivity() { @OptIn(WorkflowUiExperimentalApi::class) val renderings: StateFlow by lazy { renderWorkflowIn( - workflow = HelloWorkflow, - scope = viewModelScope, - savedStateHandle = savedState + workflow = HelloWorkflow, + scope = viewModelScope, + savedStateHandle = savedState ) } } diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflow.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflow.kt index e83c8d7908..74d1a16e93 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflow.kt +++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflow.kt @@ -48,5 +48,5 @@ object HelloComposeWorkflow : ComposeWorkflow() { @Preview(showBackground = true) @Composable fun HelloComposeWorkflowPreview() { val rendering by HelloComposeWorkflow.renderAsState(props = "hello", onOutput = {}) - WorkflowRendering(rendering, ViewEnvironment()) + WorkflowRendering(rendering, ViewEnvironment.EMPTY) } 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 727da411ab..f4ae44a141 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 @@ -55,7 +55,7 @@ object InlineRenderingWorkflow : StatefulWorkflow + (ViewEnvironment.EMPTY + viewRegistry).withCompositionRoot { content -> CompositionLocalProvider(LocalBackgroundColor provides Color.Green) { content() } diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/textinput/App.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/textinput/App.kt index 6cc7371016..7a3be9e907 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/textinput/App.kt +++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/textinput/App.kt @@ -1,4 +1,5 @@ @file:OptIn(WorkflowUiExperimentalApi::class) +@file:Suppress("FunctionName") package com.squareup.sample.compose.textinput @@ -11,10 +12,9 @@ import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.compose.WorkflowRendering import com.squareup.workflow1.ui.compose.renderAsState +import com.squareup.workflow1.ui.plus -private val viewRegistry = ViewRegistry(TextInputViewFactory) - -private val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) +private val viewEnvironment = ViewEnvironment.EMPTY + ViewRegistry(TextInputViewFactory) @Composable fun TextInputApp() { MaterialTheme { diff --git a/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailConfig.kt b/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailConfig.kt index 25c22aa873..4b83a5ee23 100644 --- a/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailConfig.kt +++ b/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailConfig.kt @@ -1,7 +1,8 @@ package com.squareup.sample.container.overviewdetail -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.ViewEnvironmentKey +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi /** * [com.squareup.workflow1.ui.ViewEnvironment] value that informs views @@ -34,3 +35,7 @@ enum class OverviewDetailConfig { override val default = None } } + +@WorkflowUiExperimentalApi +operator fun ViewEnvironment.plus(config: OverviewDetailConfig): ViewEnvironment = + this + (OverviewDetailConfig to config) diff --git a/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailContainer.kt b/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailContainer.kt index 47b8b2c17c..6fa6219ec7 100644 --- a/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailContainer.kt +++ b/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailContainer.kt @@ -56,14 +56,14 @@ class OverviewDetailContainer(view: View) : ScreenViewRunner detailStub!!.actual.visibility = VISIBLE detailStub.show( detail, - viewEnvironment + (OverviewDetailConfig to Detail) + viewEnvironment + Detail ) } ?: run { @@ -81,7 +81,7 @@ class OverviewDetailContainer(view: View) : ScreenViewRunner by ScreenViewRunner.bind( 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 40b98e51b8..f7a6edfd86 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 @@ -11,6 +11,7 @@ import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.ViewFactory import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.plus import kotlin.reflect.KClass /** @@ -30,7 +31,7 @@ import kotlin.reflect.KClass PreviewViewRegistry(mainFactory, placeholderViewFactory(placeholderModifier)) } return remember(viewRegistry, viewEnvironmentUpdater) { - ViewEnvironment(mapOf(ViewRegistry to viewRegistry)).let { environment -> + (ViewEnvironment.EMPTY + viewRegistry).let { environment -> // Give the preview a chance to add its own elements to the ViewEnvironment. viewEnvironmentUpdater?.let { it(environment) } ?: environment } diff --git a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewFactoryTest.kt b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewFactoryTest.kt index 82d7e56eb2..efffb71943 100644 --- a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewFactoryTest.kt +++ b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewFactoryTest.kt @@ -24,6 +24,7 @@ import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub import com.squareup.workflow1.ui.internal.test.IdleAfterTestRule +import com.squareup.workflow1.ui.plus import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -39,8 +40,7 @@ internal class ComposeViewFactoryTest { val viewFactory = composeViewFactory { _, _ -> BasicText("Hello, world!") } - val viewRegistry = ViewRegistry(viewFactory) - val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) + val viewEnvironment = ViewEnvironment.EMPTY + ViewRegistry(viewFactory) composeRule.setContent { AndroidView(::RootView) { @@ -55,8 +55,7 @@ internal class ComposeViewFactoryTest { val viewFactory = composeViewFactory { rendering, _ -> BasicText(rendering, Modifier.testTag("text")) } - val viewRegistry = ViewRegistry(viewFactory) - val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) + val viewEnvironment = ViewEnvironment.EMPTY + ViewRegistry(viewFactory) var rendering by mutableStateOf("hello") composeRule.setContent { @@ -82,12 +81,7 @@ internal class ComposeViewFactoryTest { } val viewRegistry = ViewRegistry(viewFactory) var viewEnvironment by mutableStateOf( - ViewEnvironment( - mapOf( - ViewRegistry to viewRegistry, - testEnvironmentKey to "hello" - ) - ) + ViewEnvironment.EMPTY + viewRegistry + (testEnvironmentKey to "hello") ) composeRule.setContent { @@ -104,7 +98,7 @@ internal class ComposeViewFactoryTest { @Test fun wrapsFactoryWithRoot() { val wrapperText = mutableStateOf("one") - val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to ViewRegistry(TestFactory))) + val viewEnvironment = ViewEnvironment.EMPTY + ViewRegistry(TestFactory) .withCompositionRoot { content -> Column { BasicText(wrapperText.value) diff --git a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt index e88e443f66..9d3de8b70a 100644 --- a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt +++ b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt @@ -27,19 +27,20 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import com.google.common.truth.Truth.assertThat import com.squareup.workflow1.ui.AndroidViewRendering -import com.squareup.workflow1.ui.asScreen import com.squareup.workflow1.ui.Compatible import com.squareup.workflow1.ui.NamedScreen import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.ViewFactory import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.asScreen import com.squareup.workflow1.ui.bindShowRendering import com.squareup.workflow1.ui.container.BackStackScreen import com.squareup.workflow1.ui.internal.test.IdleAfterTestRule import com.squareup.workflow1.ui.internal.test.WorkflowUiTestActivity import com.squareup.workflow1.ui.modal.HasModals import com.squareup.workflow1.ui.modal.ModalViewContainer +import com.squareup.workflow1.ui.plus import org.junit.Before import org.junit.Rule import org.junit.Test @@ -55,14 +56,8 @@ internal class ComposeViewTreeIntegrationTest { @Before fun setUp() { scenario.onActivity { - it.viewEnvironment = ViewEnvironment( - mapOf( - ViewRegistry to ViewRegistry( - ModalViewContainer.binding(), - NoTransitionBackStackContainer, - ) - ) - ) + it.viewEnvironment = ViewEnvironment.EMPTY + + ViewRegistry(ModalViewContainer.binding(), NoTransitionBackStackContainer) } } 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 8573e35998..2fbab065f1 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 @@ -68,6 +68,7 @@ import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.bindShowRendering import com.squareup.workflow1.ui.internal.test.IdleAfterTestRule +import com.squareup.workflow1.ui.plus import org.hamcrest.Description import org.hamcrest.TypeSafeMatcher import org.junit.Rule @@ -92,7 +93,7 @@ internal class WorkflowRenderingTest { val registry = mutableStateOf(registry1) composeRule.setContent { - WorkflowRendering("hello", ViewEnvironment(mapOf(ViewRegistry to registry.value))) + WorkflowRendering("hello", ViewEnvironment.EMPTY + registry.value) } composeRule.onNodeWithText("hello").assertIsDisplayed() @@ -116,7 +117,7 @@ internal class WorkflowRenderingTest { var rendering by mutableStateOf(ShiftyRendering(true)) composeRule.setContent { - WorkflowRendering(rendering, ViewEnvironment()) + WorkflowRendering(rendering, ViewEnvironment.EMPTY) } composeRule.onNodeWithText("one").assertIsDisplayed() @@ -130,7 +131,7 @@ internal class WorkflowRenderingTest { val testFactory = composeViewFactory { rendering, _ -> BasicText(rendering.text) } - val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to ViewRegistry(testFactory))) + val viewEnvironment = ViewEnvironment.EMPTY + ViewRegistry(testFactory) .withCompositionRoot { content -> Column { BasicText("one") @@ -150,7 +151,7 @@ internal class WorkflowRenderingTest { val wrapperText = mutableStateOf("two") composeRule.setContent { - WorkflowRendering(LegacyViewRendering(wrapperText.value), ViewEnvironment()) + WorkflowRendering(LegacyViewRendering(wrapperText.value), ViewEnvironment.EMPTY) } onView(withText("two")).check(matches(isDisplayed())) @@ -164,7 +165,7 @@ internal class WorkflowRenderingTest { composeRule.setContent { val rendering = Named(LegacyViewRendering(wrapperText.value), "fnord") - WorkflowRendering(rendering, ViewEnvironment()) + WorkflowRendering(rendering, ViewEnvironment.EMPTY) } onView(withText("two")).check(matches(isDisplayed())) @@ -196,7 +197,7 @@ internal class WorkflowRenderingTest { var rendering: Any by mutableStateOf(LifecycleRecorder()) composeRule.setContent { - WorkflowRendering(rendering, ViewEnvironment()) + WorkflowRendering(rendering, ViewEnvironment.EMPTY) } composeRule.runOnIdle { @@ -242,7 +243,7 @@ internal class WorkflowRenderingTest { var rendering: Any by mutableStateOf(LifecycleRecorder()) composeRule.setContent { - WorkflowRendering(rendering, ViewEnvironment()) + WorkflowRendering(rendering, ViewEnvironment.EMPTY) } composeRule.runOnIdle { @@ -266,7 +267,7 @@ internal class WorkflowRenderingTest { composeRule.setContent { CompositionLocalProvider(LocalLifecycleOwner provides parentOwner) { - WorkflowRendering(LifecycleRecorder(states), ViewEnvironment()) + WorkflowRendering(LifecycleRecorder(states), ViewEnvironment.EMPTY) } } @@ -311,7 +312,7 @@ internal class WorkflowRenderingTest { composeRule.setContent { CompositionLocalProvider(LocalLifecycleOwner provides parentOwner) { - WorkflowRendering(LifecycleRecorder(states), ViewEnvironment()) + WorkflowRendering(LifecycleRecorder(states), ViewEnvironment.EMPTY) } } @@ -333,7 +334,7 @@ internal class WorkflowRenderingTest { composeRule.setContent { WorkflowRendering( - Rendering(), ViewEnvironment(), + Rendering(), ViewEnvironment.EMPTY, Modifier.size(width = 42.dp, height = 43.dp) ) } @@ -352,7 +353,7 @@ internal class WorkflowRenderingTest { composeRule.setContent { WorkflowRendering( - Rendering(), ViewEnvironment(), + Rendering(), ViewEnvironment.EMPTY, Modifier.sizeIn(minWidth = 42.dp, minHeight = 43.dp) ) } @@ -382,7 +383,7 @@ internal class WorkflowRenderingTest { composeRule.setContent { with(LocalDensity.current) { WorkflowRendering( - LegacyRendering(viewId), ViewEnvironment(), + LegacyRendering(viewId), ViewEnvironment.EMPTY, Modifier.size(42.toDp(), 43.toDp()) ) } @@ -393,6 +394,7 @@ internal class WorkflowRenderingTest { @Test fun skipsPreviousContentWhenIncompatible() { var disposeCount = 0 + class Rendering( override val compatibilityKey: String ) : ComposableRendering, Compatible { @@ -416,7 +418,7 @@ internal class WorkflowRenderingTest { var key by mutableStateOf("one") composeRule.setContent { - WorkflowRendering(Rendering(key), ViewEnvironment()) + WorkflowRendering(Rendering(key), ViewEnvironment.EMPTY) } composeRule.onNodeWithTag("tag") @@ -466,7 +468,7 @@ internal class WorkflowRenderingTest { var text by mutableStateOf("one") composeRule.setContent { - WorkflowRendering(Rendering(text), ViewEnvironment()) + WorkflowRendering(Rendering(text), ViewEnvironment.EMPTY) } composeRule.onNodeWithTag("tag") @@ -542,7 +544,7 @@ internal class WorkflowRenderingTest { private data class LegacyViewRendering( val text: String - ) : AndroidViewRendering { + ) : AndroidViewRendering { override val viewFactory: ViewFactory = object : ViewFactory { override val type = LegacyViewRendering::class diff --git a/workflow-ui/core-android/api/core-android.api b/workflow-ui/core-android/api/core-android.api index c8f2e368d6..e8fad850d9 100644 --- a/workflow-ui/core-android/api/core-android.api +++ b/workflow-ui/core-android/api/core-android.api @@ -274,6 +274,10 @@ public final class com/squareup/workflow1/ui/container/BackStackConfig$Companion public synthetic fun getDefault ()Ljava/lang/Object; } +public final class com/squareup/workflow1/ui/container/BackStackConfigKt { + public static final fun plus (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/container/BackStackConfig;)Lcom/squareup/workflow1/ui/ViewEnvironment; +} + public class com/squareup/workflow1/ui/container/BackStackContainer : android/widget/FrameLayout { public fun (Landroid/content/Context;)V public fun (Landroid/content/Context;Landroid/util/AttributeSet;)V @@ -326,14 +330,18 @@ public abstract class com/squareup/workflow1/ui/container/ModalScreenOverlayDial public final fun updateDialog (Landroid/app/Dialog;Lcom/squareup/workflow1/ui/container/ScreenOverlay;Lcom/squareup/workflow1/ui/ViewEnvironment;)V } -public final class com/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed : com/squareup/workflow1/ui/ViewEnvironmentKey { - public static final field INSTANCE Lcom/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed; - public fun getDefault ()Lcom/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed$Handler; +public abstract interface class com/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed { + public static final field Companion Lcom/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed$Companion; + public abstract fun onBackPressed (Landroid/view/View;)Z +} + +public final class com/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed$Companion : com/squareup/workflow1/ui/ViewEnvironmentKey { + public fun getDefault ()Lcom/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed; public synthetic fun getDefault ()Ljava/lang/Object; } -public abstract interface class com/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed$Handler { - public abstract fun onBackPressed (Landroid/view/View;)Z +public final class com/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressedKt { + public static final fun plus (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed;)Lcom/squareup/workflow1/ui/ViewEnvironment; } public abstract interface class com/squareup/workflow1/ui/container/OverlayDialogFactory : com/squareup/workflow1/ui/ViewRegistry$Entry { diff --git a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeScreenViewFactoryTest.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeScreenViewFactoryTest.kt index b14c273762..1315ce5b46 100644 --- a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeScreenViewFactoryTest.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeScreenViewFactoryTest.kt @@ -44,8 +44,7 @@ internal class DecorativeScreenViewFactoryTest { events += "exit viewStarter" } ) - val viewRegistry = ViewRegistry(innerViewFactory, outerViewFactory) - val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) + val viewEnvironment = ViewEnvironment.EMPTY + ViewRegistry(innerViewFactory, outerViewFactory) OuterRendering("outer", InnerRendering("inner")).buildView( viewEnvironment, @@ -84,8 +83,7 @@ internal class DecorativeScreenViewFactoryTest { innerShowRendering(outerRendering.wrapped, env) } ) - val viewRegistry = ViewRegistry(innerViewFactory, outerViewFactory) - val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) + val viewEnvironment = ViewEnvironment.EMPTY + ViewRegistry(innerViewFactory, outerViewFactory) OuterRendering("outer", InnerRendering("inner")).buildView( viewEnvironment, @@ -146,8 +144,8 @@ internal class DecorativeScreenViewFactoryTest { events += "exit way out viewStarter" } ) - val viewRegistry = ViewRegistry(innerViewFactory, outerViewFactory, wayOutViewFactory) - val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) + val viewEnvironment = + ViewEnvironment.EMPTY + ViewRegistry(innerViewFactory, outerViewFactory, wayOutViewFactory) WayOutRendering("way out", OuterRendering("outer", InnerRendering("inner"))).buildView( viewEnvironment, @@ -199,8 +197,7 @@ internal class DecorativeScreenViewFactoryTest { innerShowRendering(outerRendering.wrapped, env) } ) - val viewRegistry = ViewRegistry(innerViewFactory, outerViewFactory) - val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) + val viewEnvironment = ViewEnvironment.EMPTY + ViewRegistry(innerViewFactory, outerViewFactory) val view = OuterRendering("out1", InnerRendering("in1")).buildView( viewEnvironment, diff --git a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeViewFactoryTest.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeViewFactoryTest.kt index ea88b4134c..3e6623f845 100644 --- a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeViewFactoryTest.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeViewFactoryTest.kt @@ -47,7 +47,7 @@ internal class DecorativeViewFactoryTest { } ) val viewRegistry = ViewRegistry(innerViewFactory, outerViewFactory) - val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) + val viewEnvironment = ViewEnvironment.EMPTY + viewRegistry viewRegistry.buildView( OuterRendering("outer", InnerRendering("inner")), @@ -88,7 +88,7 @@ internal class DecorativeViewFactoryTest { } ) val viewRegistry = ViewRegistry(innerViewFactory, outerViewFactory) - val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) + val viewEnvironment = ViewEnvironment.EMPTY + viewRegistry viewRegistry.buildView( OuterRendering("outer", InnerRendering("inner")), @@ -151,7 +151,7 @@ internal class DecorativeViewFactoryTest { } ) val viewRegistry = ViewRegistry(innerViewFactory, outerViewFactory, wayOutViewFactory) - val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) + val viewEnvironment = ViewEnvironment.EMPTY + viewRegistry viewRegistry.buildView( WayOutRendering("way out", OuterRendering("outer", InnerRendering("inner"))), @@ -205,7 +205,7 @@ internal class DecorativeViewFactoryTest { } ) val viewRegistry = ViewRegistry(innerViewFactory, outerViewFactory) - val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) + val viewEnvironment = ViewEnvironment.EMPTY + viewRegistry val view = viewRegistry.buildView( OuterRendering("out1", InnerRendering("in1")), diff --git a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/ViewStateCacheTest.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/ViewStateCacheTest.kt index 090fe6f59f..b23d168863 100644 --- a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/ViewStateCacheTest.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/ViewStateCacheTest.kt @@ -28,7 +28,7 @@ import org.junit.runner.RunWith internal class ViewStateCacheTest { private val instrumentation = InstrumentationRegistry.getInstrumentation() - private val viewEnvironment = ViewEnvironment() + private val viewEnvironment = ViewEnvironment.EMPTY private object AScreen : Screen diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewRegistry.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewRegistry.kt index 8c53d418c9..6c06930851 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewRegistry.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewRegistry.kt @@ -26,7 +26,10 @@ public fun ) } -@Deprecated("Use getEntryFor()") +@Deprecated( + "Use getEntryFor()", + ReplaceWith("getEntryFor(renderingType)") +) @WorkflowUiExperimentalApi public fun ViewRegistry.getFactoryFor( renderingType: KClass @@ -34,8 +37,7 @@ public fun ViewRegistry.getFactoryFor( return getEntryFor(renderingType) as? ViewFactory } -@Suppress("DEPRECATION") -@Deprecated("Use Screen.buildview") +@Deprecated("Use Screen.buildView") @WorkflowUiExperimentalApi public fun ViewRegistry.buildView( initialRendering: RenderingT, 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 41d53a28f3..cc3f0f8a7f 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 @@ -36,7 +36,7 @@ import com.squareup.workflow1.ui.container.EnvironmentScreenViewFactory * * class MyViewModel(savedState: SavedStateHandle) : ViewModel() { * val renderings: StateFlow by lazy { - * val customized = ViewEnvironment() + (ScreenViewFactoryFinder to MyFinder) + * val customized = ViewEnvironment.EMPTY + (ScreenViewFactoryFinder to MyFinder) * renderWorkflowIn( * workflow = MyRootWorkflow.withEnvironment(customized), * scope = viewModelScope, diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowLayout.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowLayout.kt index 1cb6b2bcc7..b4f9075812 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowLayout.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowLayout.kt @@ -78,7 +78,7 @@ public class WorkflowLayout( registry: ViewRegistry ) { @Suppress("DEPRECATION") - start(renderings, ViewEnvironment(mapOf(ViewRegistry to registry))) + start(renderings, ViewEnvironment.EMPTY + registry) } @Deprecated( @@ -94,7 +94,7 @@ public class WorkflowLayout( ) public fun start( renderings: Flow, - environment: ViewEnvironment = ViewEnvironment() + environment: ViewEnvironment = ViewEnvironment.EMPTY ) { takeWhileAttached(renderings) { @Suppress("DEPRECATION") diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackConfig.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackConfig.kt index 53aa5f5652..7fb5c2949f 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackConfig.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackConfig.kt @@ -1,7 +1,8 @@ package com.squareup.workflow1.ui.container -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.ViewEnvironmentKey +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.container.BackStackConfig.First import com.squareup.workflow1.ui.container.BackStackConfig.Other @@ -32,3 +33,7 @@ public enum class BackStackConfig { override val default: BackStackConfig = None } } + +@WorkflowUiExperimentalApi +public operator fun ViewEnvironment.plus(config: BackStackConfig): ViewEnvironment = + this + (BackStackConfig to config) diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackContainer.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackContainer.kt index be81a2590f..7300faecfb 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackContainer.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackContainer.kt @@ -77,7 +77,7 @@ public open class BackStackContainer @JvmOverloads constructor( updateStateRegistryKey(newViewEnvironment) val config = if (newRendering.backStack.isEmpty()) First else Other - val environment = newViewEnvironment + (BackStackConfig to config) + val environment = newViewEnvironment + config val named: BackStackScreen> = newRendering // ViewStateCache requires that everything be Named. diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndModalsContainer.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndModalsContainer.kt index 8b37b6f6fe..97e80f8f50 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndModalsContainer.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndModalsContainer.kt @@ -36,6 +36,7 @@ internal class BodyAndModalsContainer @JvmOverloads constructor( } private val dialogs = LayeredDialogs(view = this, modal = true) + // The bounds of this view in global (display) coordinates, as reported // by getGlobalVisibleRect. // @@ -86,7 +87,7 @@ internal class BodyAndModalsContainer @JvmOverloads constructor( // Allow modal dialogs to restrict themselves to cover only this view. val dialogsEnv = - if (showingModals) viewEnvironment + (ModalArea to ModalArea(bounds)) else viewEnvironment + if (showingModals) viewEnvironment + ModalArea(bounds) else viewEnvironment dialogs.update(newScreen.modals, dialogsEnv) } @@ -103,7 +104,7 @@ internal class BodyAndModalsContainer @JvmOverloads constructor( override fun onDetachedFromWindow() { // Don't leak the dialogs if we're suddenly yanked out of view. // https://github.com/square/workflow-kotlin/issues/314 - dialogs.update(emptyList(), ViewEnvironment()) + dialogs.update(emptyList(), ViewEnvironment.EMPTY) viewTreeObserver.removeOnGlobalLayoutListener(boundsListener) bounds.value = Rect() super.onDetachedFromWindow() diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/EnvironmentScreenViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/EnvironmentScreenViewFactory.kt index c07a482b06..281c834190 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/EnvironmentScreenViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/EnvironmentScreenViewFactory.kt @@ -3,7 +3,7 @@ package com.squareup.workflow1.ui.container import com.squareup.workflow1.ui.DecorativeScreenViewFactory import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.updateFrom +import com.squareup.workflow1.ui.merge @WorkflowUiExperimentalApi internal object EnvironmentScreenViewFactory : ScreenViewFactory> @@ -12,7 +12,7 @@ by DecorativeScreenViewFactory( map = { withEnvironment, inheritedEnvironment -> Pair( withEnvironment.screen, - inheritedEnvironment.updateFrom(withEnvironment.viewEnvironment) + inheritedEnvironment merge withEnvironment.viewEnvironment ) } ) diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalArea.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalArea.kt index 5472e9912a..fefa29336f 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalArea.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalArea.kt @@ -1,6 +1,7 @@ package com.squareup.workflow1.ui.container import android.graphics.Rect +import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.ViewEnvironmentKey import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import kotlinx.coroutines.flow.MutableStateFlow @@ -18,3 +19,7 @@ internal class ModalArea( override val default: ModalArea = ModalArea(MutableStateFlow(Rect())) } } + +@WorkflowUiExperimentalApi +internal operator fun ViewEnvironment.plus(modalArea: ModalArea): ViewEnvironment = + this + (ModalArea to modalArea) diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed.kt index 3641e1f44e..711f5a0e2f 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed.kt @@ -1,9 +1,9 @@ package com.squareup.workflow1.ui.container import android.view.View +import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.ViewEnvironmentKey import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.container.ModalScreenOverlayOnBackPressed.Handler import com.squareup.workflow1.ui.onBackPressedDispatcherOwnerOrNull /** @@ -16,25 +16,31 @@ import com.squareup.workflow1.ui.onBackPressedDispatcherOwnerOrNull * that predates `OnBackPressedDispatcher`. */ @WorkflowUiExperimentalApi -public object ModalScreenOverlayOnBackPressed : ViewEnvironmentKey( - type = Handler::class -) { - public fun interface Handler { - /** - * Called when the device back button is pressed and a dialog built by a - * [ModalScreenOverlayDialogFactory] has window focus. - * - * @return true if the back press event was consumed - */ - public fun onBackPressed(contentView: View): Boolean - } +public fun interface ModalScreenOverlayOnBackPressed { + /** + * Called when the device back button is pressed and a dialog built by a + * [ModalScreenOverlayDialogFactory] has window focus. + * + * @return true if the back press event was consumed + */ + public fun onBackPressed(contentView: View): Boolean - override val default: Handler = Handler { view -> - view.context.onBackPressedDispatcherOwnerOrNull() - ?.onBackPressedDispatcher - ?.let { - if (it.hasEnabledCallbacks()) it.onBackPressed() + public companion object : ViewEnvironmentKey( + type = ModalScreenOverlayOnBackPressed::class + ) { + override val default: ModalScreenOverlayOnBackPressed = + ModalScreenOverlayOnBackPressed { view -> + view.context.onBackPressedDispatcherOwnerOrNull() + ?.onBackPressedDispatcher + ?.let { + if (it.hasEnabledCallbacks()) it.onBackPressed() + } + true } - true } } + +@WorkflowUiExperimentalApi +public operator fun ViewEnvironment.plus( + onBackPressed: ModalScreenOverlayOnBackPressed +): ViewEnvironment = this + (ModalScreenOverlayOnBackPressed to onBackPressed) diff --git a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/LegacyAndroidViewRegistryTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/LegacyAndroidViewRegistryTest.kt index 2fcabed911..2ec419bb4d 100644 --- a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/LegacyAndroidViewRegistryTest.kt +++ b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/LegacyAndroidViewRegistryTest.kt @@ -57,9 +57,10 @@ internal class LegacyAndroidViewRegistryTest { .isSameInstanceAs(bazFactory) } - @Test fun `getFactoryFor returns null on missing registry`() { + @Test fun `getFactoryFor returns null on missing registry in composite`() { val fooRegistry = TestRegistry(setOf(FooRendering::class)) - val registry = ViewRegistry() + fooRegistry + val bazRegistry = TestRegistry(setOf(BazRendering::class)) + val registry = bazRegistry + fooRegistry assertThat(registry.getEntryFor(BarRendering::class)).isNull() } @@ -153,7 +154,7 @@ internal class LegacyAndroidViewRegistryTest { @OptIn(WorkflowUiExperimentalApi::class) private fun ViewRegistry.buildView(rendering: R): View = - buildView(rendering, ViewEnvironment(mapOf(ViewRegistry to this)), mock()) + buildView(rendering, ViewEnvironment.EMPTY + this, mock()) @OptIn(WorkflowUiExperimentalApi::class) private class TestViewFactory(override val type: KClass) : ViewFactory { 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 0cbb9c622d..4702e004a5 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 @@ -24,7 +24,7 @@ internal class ScreenViewFactoryTest { renderingType: KClass ): Entry? = null } - val env = ViewEnvironment(mapOf(ViewRegistry to emptyReg)) + val env = ViewEnvironment.EMPTY + emptyReg val fooScreen = object : Screen { override fun toString() = "FooScreen" @@ -41,8 +41,7 @@ internal class ScreenViewFactoryTest { } @Test fun `buildView honors AndroidScreen`() { - val registry = ViewRegistry() - val env = ViewEnvironment(mapOf(ViewRegistry to registry)) + val env = ViewEnvironment.EMPTY + ViewRegistry() val screen = MyAndroidScreen() screen.buildView(env, mock()) @@ -50,8 +49,7 @@ internal class ScreenViewFactoryTest { } @Test fun `buildView prefers registry entries to AndroidViewRendering`() { - val registry = ViewRegistry(overrideViewRenderingFactory) - val env = ViewEnvironment(mapOf(ViewRegistry to registry)) + val env = ViewEnvironment.EMPTY + ViewRegistry(overrideViewRenderingFactory) val screen = MyAndroidScreen() screen.buildView(env, mock()) diff --git a/workflow-ui/core-common/api/core-common.api b/workflow-ui/core-common/api/core-common.api index 95266c518e..291482c641 100644 --- a/workflow-ui/core-common/api/core-common.api +++ b/workflow-ui/core-common/api/core-common.api @@ -63,6 +63,7 @@ public final class com/squareup/workflow1/ui/TextController { } public final class com/squareup/workflow1/ui/ViewEnvironment { + public static final field Companion Lcom/squareup/workflow1/ui/ViewEnvironment$Companion; public fun ()V public fun (Ljava/util/Map;)V public synthetic fun (Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -75,6 +76,10 @@ public final class com/squareup/workflow1/ui/ViewEnvironment { public fun toString ()Ljava/lang/String; } +public final class com/squareup/workflow1/ui/ViewEnvironment$Companion { + public final fun getEMPTY ()Lcom/squareup/workflow1/ui/ViewEnvironment; +} + public abstract class com/squareup/workflow1/ui/ViewEnvironmentKey { public fun (Lkotlin/reflect/KClass;)V public final fun equals (Ljava/lang/Object;)Z @@ -83,10 +88,6 @@ public abstract class com/squareup/workflow1/ui/ViewEnvironmentKey { public fun toString ()Ljava/lang/String; } -public final class com/squareup/workflow1/ui/ViewEnvironmentKt { - public static final fun updateFrom (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/ViewEnvironment; -} - 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; @@ -105,6 +106,10 @@ public abstract interface class com/squareup/workflow1/ui/ViewRegistry$Entry { public final class com/squareup/workflow1/ui/ViewRegistryKt { public static final fun ViewRegistry ()Lcom/squareup/workflow1/ui/ViewRegistry; public static final fun ViewRegistry ([Lcom/squareup/workflow1/ui/ViewRegistry$Entry;)Lcom/squareup/workflow1/ui/ViewRegistry; + public static final fun merge (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/ViewEnvironment; + public static final fun merge (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/ViewRegistry;)Lcom/squareup/workflow1/ui/ViewEnvironment; + public static final fun merge (Lcom/squareup/workflow1/ui/ViewRegistry;Lcom/squareup/workflow1/ui/ViewRegistry;)Lcom/squareup/workflow1/ui/ViewRegistry; + public static final fun plus (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/ViewRegistry;)Lcom/squareup/workflow1/ui/ViewEnvironment; public static final fun plus (Lcom/squareup/workflow1/ui/ViewRegistry;Lcom/squareup/workflow1/ui/ViewRegistry$Entry;)Lcom/squareup/workflow1/ui/ViewRegistry; public static final fun plus (Lcom/squareup/workflow1/ui/ViewRegistry;Lcom/squareup/workflow1/ui/ViewRegistry;)Lcom/squareup/workflow1/ui/ViewRegistry; } @@ -195,7 +200,6 @@ public final class com/squareup/workflow1/ui/container/EnvironmentScreen : com/s } public final class com/squareup/workflow1/ui/container/EnvironmentScreenKt { - public static final fun plus (Lcom/squareup/workflow1/ui/container/EnvironmentScreen;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/container/EnvironmentScreen; public static final fun withEnvironment (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)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/CompositeViewRegistry.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/CompositeViewRegistry.kt index 1a387cf51b..17d08e228c 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/CompositeViewRegistry.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/CompositeViewRegistry.kt @@ -37,7 +37,8 @@ internal class CompositeViewRegistry private constructor( fun putAllUnique(other: Map, ViewRegistry>) { val duplicateKeys = registriesByKey.keys.intersect(other.keys) - check(duplicateKeys.isEmpty()) { "Must not have duplicate entries: $duplicateKeys" } + require(duplicateKeys.isEmpty()) { "Must not have duplicate entries: $duplicateKeys. " + + "Use merge to replace existing entries." } registriesByKey.putAll(other) } 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 3bddf1a244..500a0aa779 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 @@ -30,4 +30,8 @@ internal class TypedViewRegistry private constructor( @Suppress("UNCHECKED_CAST") return bindings[renderingType] as? Entry } + + override fun toString(): String { + return "TypedViewRegistry(bindings=$bindings)" + } } diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/ViewEnvironment.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/ViewEnvironment.kt index 77315afa5a..65f759367e 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/ViewEnvironment.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/ViewEnvironment.kt @@ -3,25 +3,35 @@ package com.squareup.workflow1.ui import kotlin.reflect.KClass /** - * Immutable, append-only map of values that a parent view can pass down to + * Immutable map of values that a parent view can pass down to * its children. Allows containers to give descendants information about * the context in which they're drawing. * * Calling [Screen.withEnvironment][com.squareup.workflow1.ui.container.withEnvironment] - * is the easiest way to customize its environment. + * on a [Screen] is the easiest way to customize its environment before rendering it. */ @WorkflowUiExperimentalApi -public class ViewEnvironment( +public class ViewEnvironment +@Deprecated( + "To eliminate runtime errors this constructor will become private. " + + "Use ViewEnvironment.EMPTY and ViewEnvironment.plus" +) +constructor( public val map: Map, Any> = emptyMap() ) { @Suppress("UNCHECKED_CAST") public operator fun get(key: ViewEnvironmentKey): T = map[key] as? T ?: key.default + @Suppress("DEPRECATION") public operator fun plus(pair: Pair, T>): ViewEnvironment = ViewEnvironment(map + pair) - public operator fun plus(other: ViewEnvironment): ViewEnvironment = - ViewEnvironment(map + other.map) + @Suppress("DEPRECATION") + public operator fun plus(other: ViewEnvironment): ViewEnvironment { + if (other.map.isEmpty()) return this + if (this.map.isEmpty()) return other + return ViewEnvironment(map + other.map) + } override fun toString(): String = "ViewEnvironment($map)" @@ -29,6 +39,11 @@ public class ViewEnvironment( (other as? ViewEnvironment)?.let { it.map == map } ?: false override fun hashCode(): Int = map.hashCode() + + public companion object { + @Suppress("DEPRECATION") + public val EMPTY: ViewEnvironment = ViewEnvironment() + } } /** @@ -53,24 +68,3 @@ public abstract class ViewEnvironmentKey( return "ViewEnvironmentKey($type)-${super.toString()}" } } - -/** - * Combines the receiving [ViewEnvironment] with [other], taking care to merge - * their [ViewRegistry] entries. Duplicate values in [other] replace those - * in the receiver. - */ -@WorkflowUiExperimentalApi -public fun ViewEnvironment.updateFrom(other: ViewEnvironment): ViewEnvironment { - if (other.map.isEmpty()) return this - - val myReg = this[ViewRegistry] - val yourReg = other[ViewRegistry] - - val union = (myReg.keys + yourReg.keys).asSequence() - .map { yourReg.getEntryFor(it) ?: myReg.getEntryFor(it)!! } - .toList() - .toTypedArray() - - val unionRegistry = ViewRegistry(*union) - return this + other + (ViewRegistry to unionRegistry) -} 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 cf74263c50..be0461c93b 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 @@ -76,9 +76,7 @@ public interface ViewRegistry { public val keys: Set> /** - * This method is not for general use, use [WorkflowViewStub] instead. - * - * Returns the [ViewFactory] that was registered for the given [renderingType], or null + * Returns the [Entry] that was registered for the given [renderingType], or null * if none was found. */ public fun getEntryFor( @@ -90,6 +88,10 @@ public interface ViewRegistry { } } +@WorkflowUiExperimentalApi public inline operator fun ViewRegistry.get( + renderingType: KClass +): Entry? = getEntryFor(renderingType) + @WorkflowUiExperimentalApi public fun ViewRegistry(vararg bindings: Entry<*>): ViewRegistry = TypedViewRegistry(*bindings) @@ -102,10 +104,70 @@ public fun ViewRegistry(vararg bindings: Entry<*>): ViewRegistry = @WorkflowUiExperimentalApi public fun ViewRegistry(): ViewRegistry = TypedViewRegistry() +/** + * @throws IllegalArgumentException if the receiver already has a matching [entry]. + */ +@WorkflowUiExperimentalApi +public operator fun ViewRegistry.plus(entry: Entry<*>): ViewRegistry = + this + ViewRegistry(entry) + +/** @throws IllegalArgumentException if other has redundant entries. */ +@WorkflowUiExperimentalApi +public operator fun ViewRegistry.plus(other: ViewRegistry): ViewRegistry { + if (other.keys.isEmpty()) return this + if (this.keys.isEmpty()) return other + return CompositeViewRegistry(this, other) +} + +/** + * Replaces the existing [ViewRegistry] of the receiver with [registry]. Use + * [ViewEnvironment.merge] to combine them instead. + */ +@WorkflowUiExperimentalApi +public operator fun ViewEnvironment.plus(registry: ViewRegistry): ViewEnvironment { + if (registry.keys.isEmpty()) return this + return this + (ViewRegistry to registry) +} + +/** + * Combines the receiver with [other]. If there are conflicting entries, + * those in [other] are preferred. + */ +@WorkflowUiExperimentalApi +public infix fun ViewRegistry.merge(other: ViewRegistry): ViewRegistry { + if (other.keys.isEmpty()) return this + if (this.keys.isEmpty()) return other + + return (keys + other.keys).asSequence() + .map { other.getEntryFor(it) ?: getEntryFor(it)!! } + .toList() + .toTypedArray() + .let { ViewRegistry(*it) } +} + +/** + * Merges the [ViewRegistry] of the receiver with [registry]. If there are conflicting entries, + * those in [registry] are preferred. + */ @WorkflowUiExperimentalApi -public operator fun ViewRegistry.plus(binding: Entry<*>): ViewRegistry = - this + ViewRegistry(binding) +public infix fun ViewEnvironment.merge(registry: ViewRegistry): ViewEnvironment { + if (registry.keys.isEmpty()) return this + + val merged = this[ViewRegistry] merge registry + return this + merged +} +/** + * Combines the receiving [ViewEnvironment] with [other], taking care to merge + * their [ViewRegistry] entries. Any other conflicting values in [other] replace those + * in the receiver. + */ @WorkflowUiExperimentalApi -public operator fun ViewRegistry.plus(other: ViewRegistry): ViewRegistry = - CompositeViewRegistry(this, other) +public infix fun ViewEnvironment.merge(other: ViewEnvironment): ViewEnvironment { + if (other.map.isEmpty()) return this + if (this.map.isEmpty()) return other + + val oldReg = this[ViewRegistry] + val newReg = other[ViewRegistry] + return this + other + (ViewRegistry to oldReg.merge(newReg)) +} 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 1cd93bc9de..bac66531c4 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 @@ -5,7 +5,7 @@ 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 com.squareup.workflow1.ui.updateFrom +import com.squareup.workflow1.ui.merge /** * Pairs a [screen] rendering with a [viewEnvironment] to support its display. @@ -18,7 +18,7 @@ import com.squareup.workflow1.ui.updateFrom @WorkflowUiExperimentalApi public class EnvironmentScreen internal constructor( public val screen: V, - public val viewEnvironment: ViewEnvironment = ViewEnvironment() + public val viewEnvironment: ViewEnvironment = ViewEnvironment.EMPTY ) : Compatible, Screen { /** * Ensures that we make the decision to update or replace the root view based on @@ -27,35 +27,34 @@ public class EnvironmentScreen internal constructor( override val compatibilityKey: String = Compatible.keyFor(screen, "EnvironmentScreen") } -@WorkflowUiExperimentalApi -public operator fun EnvironmentScreen.plus( - environment: ViewEnvironment -): EnvironmentScreen { - return when { - environment.map.isEmpty() -> this - else -> EnvironmentScreen(screen, viewEnvironment.updateFrom(environment)) - } -} - /** - * Returns a [EnvironmentScreen] derived from the receiver, whose [ViewEnvironment] - * includes a [ViewRegistry] updated from the given [viewRegistry]. + * Returns an [EnvironmentScreen] derived from the receiver, whose + * [EnvironmentScreen.viewEnvironment] includes [viewRegistry]. + * + * If the receiver is an [EnvironmentScreen], uses [ViewRegistry.merge] + * to preserve the [ViewRegistry] entries of both. */ @WorkflowUiExperimentalApi public fun Screen.withRegistry(viewRegistry: ViewRegistry): EnvironmentScreen<*> { - return withEnvironment(ViewEnvironment(mapOf(ViewRegistry to viewRegistry))) + return withEnvironment(ViewEnvironment.EMPTY merge viewRegistry) } /** - * Returns a [EnvironmentScreen] derived from the receiver, whose [ViewEnvironment] - * is [updated][updateFrom] the given [viewEnvironment]. + * Returns an [EnvironmentScreen] derived from the receiver, + * whose [EnvironmentScreen.viewEnvironment] includes the values in the given [environment]. + * + * If the receiver is an [EnvironmentScreen], uses [ViewEnvironment.merge] + * to preserve the [ViewRegistry] entries of both. */ @WorkflowUiExperimentalApi public fun Screen.withEnvironment( - viewEnvironment: ViewEnvironment = ViewEnvironment() + environment: ViewEnvironment = ViewEnvironment.EMPTY ): EnvironmentScreen<*> { return when (this) { - is EnvironmentScreen<*> -> this + viewEnvironment - else -> EnvironmentScreen(this, viewEnvironment) + is EnvironmentScreen<*> -> { + if (environment.map.isEmpty()) this + else EnvironmentScreen(screen, this.viewEnvironment merge environment) + } + else -> EnvironmentScreen(this, environment) } } diff --git a/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/CompositeViewRegistryTest.kt b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/CompositeViewRegistryTest.kt index 026b4245d7..4761de6eba 100644 --- a/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/CompositeViewRegistryTest.kt +++ b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/CompositeViewRegistryTest.kt @@ -13,7 +13,7 @@ internal class CompositeViewRegistryTest { val fooBarRegistry = TestRegistry(setOf(FooRendering::class, BarRendering::class)) val barBazRegistry = TestRegistry(setOf(BarRendering::class, BazRendering::class)) - val error = assertFailsWith { + val error = assertFailsWith { fooBarRegistry + barBazRegistry } assertThat(error).hasMessageThat() @@ -45,7 +45,7 @@ internal class CompositeViewRegistryTest { @Test fun `getFactoryFor returns null on missing registry`() { val fooRegistry = TestRegistry(setOf(FooRendering::class)) - val registry = ViewRegistry() + fooRegistry + val registry = CompositeViewRegistry(ViewRegistry(), fooRegistry) assertThat(registry.getEntryFor(BarRendering::class)).isNull() } @@ -53,7 +53,7 @@ internal class CompositeViewRegistryTest { @Test fun `keys includes all composite registries' keys`() { val fooBarRegistry = TestRegistry(setOf(FooRendering::class, BarRendering::class)) val bazRegistry = TestRegistry(setOf(BazRendering::class)) - val registry = fooBarRegistry + bazRegistry + val registry = CompositeViewRegistry(fooBarRegistry, bazRegistry) assertThat(registry.keys).containsExactly( FooRendering::class, diff --git a/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/TypedViewRegistryTest.kt b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/TypedViewRegistryTest.kt deleted file mode 100644 index 1c23bbf077..0000000000 --- a/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/TypedViewRegistryTest.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.squareup.workflow1.ui - -import com.google.common.truth.Truth.assertThat -import com.squareup.workflow1.ui.ViewRegistry.Entry -import org.junit.Test -import kotlin.reflect.KClass -import kotlin.test.assertFailsWith -import kotlin.test.assertTrue - -@OptIn(WorkflowUiExperimentalApi::class) -internal class TypedViewRegistryTest { - - @Test fun `keys from bindings`() { - val factory1 = TestEntry(FooRendering::class) - val factory2 = TestEntry(BarRendering::class) - val registry = ViewRegistry(factory1, factory2) - - assertThat(registry.keys).containsExactly(factory1.type, factory2.type) - } - - @Test fun `constructor throws on duplicates`() { - val factory1 = TestEntry(FooRendering::class) - val factory2 = TestEntry(FooRendering::class) - - val error = assertFailsWith { - ViewRegistry(factory1, factory2) - } - assertThat(error).hasMessageThat() - .endsWith("must not have duplicate entries.") - assertThat(error).hasMessageThat() - .contains(FooRendering::class.java.name) - } - - @Test fun `getFactoryFor works`() { - val fooFactory = TestEntry(FooRendering::class) - val registry = ViewRegistry(fooFactory) - - val factory = registry.getEntryFor(FooRendering::class) - assertThat(factory).isSameInstanceAs(fooFactory) - } - - @Test fun `getFactoryFor returns null on missing binding`() { - val fooFactory = TestEntry(FooRendering::class) - val registry = ViewRegistry(fooFactory) - - assertThat(registry.getEntryFor(BarRendering::class)).isNull() - } - - @Test fun `ViewRegistry with no arguments infers type`() { - val registry = ViewRegistry() - assertTrue(registry.keys.isEmpty()) - } - - private class TestEntry( - override val type: KClass - ) : Entry - - private object FooRendering - private object BarRendering -} diff --git a/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/ViewEnvironmentTest.kt b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/ViewEnvironmentTest.kt index 540931e8fe..2928f605bb 100644 --- a/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/ViewEnvironmentTest.kt +++ b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/ViewEnvironmentTest.kt @@ -1,6 +1,7 @@ package com.squareup.workflow1.ui import com.google.common.truth.Truth.assertThat +import com.squareup.workflow1.ui.ViewEnvironment.Companion.EMPTY import org.junit.Test @OptIn(WorkflowUiExperimentalApi::class) @@ -22,41 +23,39 @@ internal class ViewEnvironmentTest { } } - private val emptyEnv = ViewEnvironment() - @Test fun defaults() { - assertThat(emptyEnv[DataHint]).isEqualTo(DataHint()) + assertThat(EMPTY[DataHint]).isEqualTo(DataHint()) } @Test fun put() { - val environment = emptyEnv + - (StringHint to "fnord") + - (DataHint to DataHint(42, "foo")) + val environment = EMPTY + + (StringHint to "fnord") + + (DataHint to DataHint(42, "foo")) assertThat(environment[StringHint]).isEqualTo("fnord") assertThat(environment[DataHint]).isEqualTo(DataHint(42, "foo")) } @Test fun `map equality`() { - val env1 = emptyEnv + - (StringHint to "fnord") + - (DataHint to DataHint(42, "foo")) + val env1 = EMPTY + + (StringHint to "fnord") + + (DataHint to DataHint(42, "foo")) - val env2 = emptyEnv + - (StringHint to "fnord") + - (DataHint to DataHint(42, "foo")) + val env2 = EMPTY + + (StringHint to "fnord") + + (DataHint to DataHint(42, "foo")) assertThat(env1).isEqualTo(env2) } @Test fun `map inequality`() { - val env1 = emptyEnv + - (StringHint to "fnord") + - (DataHint to DataHint(42, "foo")) + val env1 = EMPTY + + (StringHint to "fnord") + + (DataHint to DataHint(42, "foo")) - val env2 = emptyEnv + - (StringHint to "fnord") + - (DataHint to DataHint(43, "foo")) + val env2 = EMPTY + + (StringHint to "fnord") + + (DataHint to DataHint(43, "foo")) assertThat(env1).isNotEqualTo(env2) } @@ -70,19 +69,29 @@ internal class ViewEnvironmentTest { } @Test fun override() { - val environment = emptyEnv + - (StringHint to "able") + - (StringHint to "baker") + val environment = EMPTY + + (StringHint to "able") + + (StringHint to "baker") assertThat(environment[StringHint]).isEqualTo("baker") } @Test fun `keys of the same type`() { - val environment = emptyEnv + - (StringHint to "able") + - (OtherStringHint to "baker") + val environment = EMPTY + + (StringHint to "able") + + (OtherStringHint to "baker") assertThat(environment[StringHint]).isEqualTo("able") assertThat(environment[OtherStringHint]).isEqualTo("baker") } + + @Test fun `preserve this when merging empty`() { + val environment = EMPTY + (StringHint to "able") + assertThat(environment + EMPTY).isSameInstanceAs(environment) + } + + @Test fun `preserve other when merging to empty`() { + val environment = EMPTY + (StringHint to "able") + assertThat(EMPTY + environment).isSameInstanceAs(environment) + } } diff --git a/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/ViewRegistryTest.kt b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/ViewRegistryTest.kt new file mode 100644 index 0000000000..6cfa841a93 --- /dev/null +++ b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/ViewRegistryTest.kt @@ -0,0 +1,127 @@ +package com.squareup.workflow1.ui + +import com.google.common.truth.Truth.assertThat +import com.squareup.workflow1.ui.ViewEnvironment.Companion.EMPTY +import com.squareup.workflow1.ui.ViewRegistry.Entry +import org.junit.Test +import kotlin.reflect.KClass +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +@OptIn(WorkflowUiExperimentalApi::class) +internal class ViewRegistryTest { + + @Test fun `keys from bindings`() { + val factory1 = TestEntry(FooRendering::class) + val factory2 = TestEntry(BarRendering::class) + val registry = ViewRegistry(factory1, factory2) + + assertThat(registry.keys).containsExactly(factory1.type, factory2.type) + } + + @Test fun `constructor throws on duplicates`() { + val factory1 = TestEntry(FooRendering::class) + val factory2 = TestEntry(FooRendering::class) + + val error = assertFailsWith { + ViewRegistry(factory1, factory2) + } + assertThat(error).hasMessageThat() + .endsWith("must not have duplicate entries.") + assertThat(error).hasMessageThat() + .contains(FooRendering::class.java.name) + } + + @Test fun `getFactoryFor works`() { + val fooFactory = TestEntry(FooRendering::class) + val registry = ViewRegistry(fooFactory) + + val factory = registry[FooRendering::class] + assertThat(factory).isSameInstanceAs(fooFactory) + } + + @Test fun `getFactoryFor returns null on missing binding`() { + val fooFactory = TestEntry(FooRendering::class) + val registry = ViewRegistry(fooFactory) + + assertThat(registry[BarRendering::class]).isNull() + } + + @Test fun `ViewRegistry with no arguments infers type`() { + val registry = ViewRegistry() + assertTrue(registry.keys.isEmpty()) + } + + @Test fun `merge prefers right side`() { + val factory1 = TestEntry(FooRendering::class) + val factory2 = TestEntry(FooRendering::class) + val merged = ViewRegistry(factory1) merge ViewRegistry(factory2) + + assertThat(merged[FooRendering::class]).isSameInstanceAs(factory2) + } + + @Test fun `merge into ViewEnvironment prefers right side`() { + val factory1 = TestEntry(FooRendering::class) + val factory2 = TestEntry(FooRendering::class) + val merged = (EMPTY + ViewRegistry(factory1)) merge ViewRegistry(factory2) + + assertThat(merged[ViewRegistry][FooRendering::class]).isSameInstanceAs(factory2) + } + + @Test fun `merge of ViewEnvironments prefers right side`() { + val factory1 = TestEntry(FooRendering::class) + val factory2 = TestEntry(FooRendering::class) + val e1 = EMPTY + ViewRegistry(factory1) + val e2 = EMPTY + ViewRegistry(factory2) + val merged = e1 + e2 + + assertThat(merged[ViewRegistry][FooRendering::class]).isSameInstanceAs(factory2) + } + + @Test fun `plus of empty returns this`() { + val reg = ViewRegistry(TestEntry(FooRendering::class)) + assertThat(reg + ViewRegistry()).isSameInstanceAs(reg) + } + + @Test fun `plus to empty returns other`() { + val reg = ViewRegistry(TestEntry(FooRendering::class)) + assertThat(ViewRegistry() + reg).isSameInstanceAs(reg) + } + + @Test fun `merge of empty reg returns this`() { + val reg = ViewRegistry(TestEntry(FooRendering::class)) + assertThat(reg merge ViewRegistry()).isSameInstanceAs(reg) + } + + @Test fun `merge to empty reg returns other`() { + val reg = ViewRegistry(TestEntry(FooRendering::class)) + assertThat(ViewRegistry() merge reg).isSameInstanceAs(reg) + } + + @Test fun `env merge of empty reg returns this env`() { + val env = EMPTY + ViewRegistry(TestEntry(FooRendering::class)) + assertThat(env merge ViewRegistry()).isSameInstanceAs(env) + } + + @Test fun `env merge of empty env returns other env`() { + val env = EMPTY + ViewRegistry(TestEntry(FooRendering::class)) + assertThat(env merge EMPTY).isSameInstanceAs(env) + } + + @Test fun `env merge to empty env returns other env`() { + val env = EMPTY + ViewRegistry(TestEntry(FooRendering::class)) + assertThat(EMPTY merge env).isSameInstanceAs(env) + } + + @Test fun `env plus empty reg returns env`() { + val env = EMPTY + ViewRegistry(TestEntry(FooRendering::class)) + assertThat(env + ViewRegistry()).isSameInstanceAs(env) + } + + private class TestEntry( + override val type: KClass + ) : Entry + + private object FooRendering + private object BarRendering +} 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 new file mode 100644 index 0000000000..b90565bd3e --- /dev/null +++ b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/container/EnvironmentScreenTest.kt @@ -0,0 +1,101 @@ +package com.squareup.workflow1.ui.container + +import com.google.common.truth.Truth.assertThat +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.ViewEnvironment.Companion.EMPTY +import com.squareup.workflow1.ui.ViewEnvironmentKey +import com.squareup.workflow1.ui.ViewRegistry +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.get +import com.squareup.workflow1.ui.plus +import org.junit.Test +import kotlin.reflect.KClass + +@OptIn(WorkflowUiExperimentalApi::class) +internal class EnvironmentScreenTest { + private class TestFactory( + override val type: KClass + ) : ViewRegistry.Entry + + private data class TestValue(val value: String) { + companion object : ViewEnvironmentKey(TestValue::class) { + override val default: TestValue get() = error("Set a default") + } + } + + private operator fun ViewEnvironment.plus(other: TestValue): ViewEnvironment { + return this + (TestValue to other) + } + + private object FooScreen : Screen + private object BarScreen : Screen + + @Test fun `Screen withRegistry works`() { + val fooFactory = TestFactory(FooScreen::class) + val viewRegistry = ViewRegistry(fooFactory) + val envScreen = FooScreen.withRegistry(viewRegistry) + + assertThat(envScreen.viewEnvironment[ViewRegistry][FooScreen::class]) + .isSameInstanceAs(fooFactory) + + assertThat(envScreen.viewEnvironment[ViewRegistry][BarScreen::class]) + .isNull() + } + + @Test fun `Screen withEnvironment works`() { + val fooFactory = TestFactory(FooScreen::class) + val viewRegistry = ViewRegistry(fooFactory) + val envScreen = FooScreen.withEnvironment( + EMPTY + viewRegistry + TestValue("foo") + ) + + assertThat(envScreen.viewEnvironment[ViewRegistry][FooScreen::class]) + .isSameInstanceAs(fooFactory) + assertThat(envScreen.viewEnvironment[ViewRegistry][BarScreen::class]) + .isNull() + assertThat(envScreen.viewEnvironment[TestValue]) + .isEqualTo(TestValue("foo")) + } + + @Test fun `EnvironmentScreen withRegistry merges`() { + val fooFactory1 = TestFactory(FooScreen::class) + val fooFactory2 = TestFactory(FooScreen::class) + val barFactory = TestFactory(BarScreen::class) + + val left = FooScreen.withRegistry(ViewRegistry(fooFactory1, barFactory)) + val union = left.withRegistry(ViewRegistry(fooFactory2)) + + assertThat(union.viewEnvironment[ViewRegistry][FooScreen::class]) + .isSameInstanceAs(fooFactory2) + + assertThat(union.viewEnvironment[ViewRegistry][BarScreen::class]) + .isSameInstanceAs(barFactory) + } + + @Test fun `EnvironmentScreen withEnvironment merges`() { + val fooFactory1 = TestFactory(FooScreen::class) + val fooFactory2 = TestFactory(FooScreen::class) + val barFactory = TestFactory(BarScreen::class) + + val left = FooScreen.withEnvironment( + EMPTY + ViewRegistry(fooFactory1, barFactory) + TestValue("left") + ) + + val union = left.withEnvironment( + EMPTY + ViewRegistry(fooFactory2) + TestValue("right") + ) + + assertThat(union.viewEnvironment[ViewRegistry][FooScreen::class]) + .isSameInstanceAs(fooFactory2) + assertThat(union.viewEnvironment[ViewRegistry][BarScreen::class]) + .isSameInstanceAs(barFactory) + assertThat(union.viewEnvironment[TestValue]) + .isEqualTo(TestValue("right")) + } + + @Test fun `keep existing instance on vacuous merge`() { + val left = FooScreen.withEnvironment(EMPTY + TestValue("whatever")) + assertThat(left.withEnvironment()).isSameInstanceAs(left) + } +} diff --git a/workflow-ui/internal-testing-android/src/main/java/com/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity.kt b/workflow-ui/internal-testing-android/src/main/java/com/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity.kt index 5e7115d128..54791b6db8 100644 --- a/workflow-ui/internal-testing-android/src/main/java/com/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity.kt +++ b/workflow-ui/internal-testing-android/src/main/java/com/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity.kt @@ -19,6 +19,7 @@ import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub import com.squareup.workflow1.ui.bindShowRendering +import com.squareup.workflow1.ui.plus import kotlin.reflect.KClass /** @@ -57,7 +58,7 @@ public abstract class AbstractLifecycleTestActivity : WorkflowUiTestActivity() { // This will override WorkflowUiTestActivity's retention of the environment across config // changes. This is intentional, since our ViewRegistry probably contains a leafBinding which // captures the events list. - viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) + viewEnvironment = ViewEnvironment.EMPTY + viewRegistry } override fun onStart() {