From 0d1785375d17633e80808565b77dd81272aaf4a4 Mon Sep 17 00:00:00 2001 From: Ray Ryan Date: Tue, 7 Feb 2023 19:02:31 -0800 Subject: [PATCH 1/6] `Container` and `Wrapper` interfaces, improved `forWrapper()` Introduces the `Container` and `Wrapper` types, giving Workflow UI its first general notion of structure. Their integration with `Compatibile` reduces the likelihood of making the most common mistake with wrapper types (namely, forgetting to do that). And they standardize the `map()` function that gets implemented by wrappers more often than not. Also updates `forWrapper` and `toUnwrappingViewFactory` to be defined in terms of `Wrapper`, allowing us to simplify their APIs by relying on `Wrapper.content` in default lambda implementations. And while we're in the neighborhood, adds long needed `prepEnvironment` and `prepContext` function arguments that simplify transforming a `ScreenViewFactory` to pre-process its `ViewEnvironment` and `Context`. We use this new capability to simplify the default `ScreenViewFactory` implementation for `EnvironmentScreen`. Closes #916 --- .../sample/container/panel/PanelOverlay.kt | 9 +- .../sample/gameworkflow/RunGameWorkflow.kt | 28 ++- .../sample/mainworkflow/TicTacToeWorkflow.kt | 3 +- .../compose/ComposeViewTreeIntegrationTest.kt | 2 + workflow-ui/core-android/api/core-android.api | 9 +- .../ui/container/BackStackContainerTest.kt | 2 +- .../ui/container/DialogIntegrationTest.kt | 6 +- .../ui/container/ViewStateCacheTest.kt | 16 +- .../workflow1/ui/ScreenViewFactory.kt | 175 +++++++----------- .../workflow1/ui/ScreenViewFactoryFinder.kt | 10 +- .../ui/container/BackButtonScreen.kt | 23 ++- .../EnvironmentScreenLegacyViewFactory.kt | 2 +- .../container/EnvironmentScreenViewFactory.kt | 25 --- .../workflow1/ui/ScreenViewFactoryTest.kt | 24 +-- ...EnvironmentScreenAndroidIntegrationTest.kt | 3 +- workflow-ui/core-common/api/core-common.api | 57 +++++- .../com/squareup/workflow1/ui/AsScreen.kt | 17 +- .../com/squareup/workflow1/ui/Container.kt | 74 ++++++++ .../com/squareup/workflow1/ui/NamedScreen.kt | 16 +- .../workflow1/ui/container/BackStackScreen.kt | 12 +- .../ui/container/EnvironmentScreen.kt | 22 +-- .../ui/container/FullScreenOverlay.kt | 9 +- .../workflow1/ui/container/ScreenOverlay.kt | 10 +- .../squareup/workflow1/ui/NamedScreenTest.kt | 1 - .../internal/test/WorkflowUiTestActivity.kt | 2 +- .../ui/radiography/WorkflowViewRenderer.kt | 2 +- 26 files changed, 323 insertions(+), 236 deletions(-) delete mode 100644 workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/EnvironmentScreenViewFactory.kt create mode 100644 workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Container.kt diff --git a/samples/containers/common/src/main/java/com/squareup/sample/container/panel/PanelOverlay.kt b/samples/containers/common/src/main/java/com/squareup/sample/container/panel/PanelOverlay.kt index b9368f559..f4de422e8 100644 --- a/samples/containers/common/src/main/java/com/squareup/sample/container/panel/PanelOverlay.kt +++ b/samples/containers/common/src/main/java/com/squareup/sample/container/panel/PanelOverlay.kt @@ -6,6 +6,9 @@ import com.squareup.workflow1.ui.container.ModalOverlay import com.squareup.workflow1.ui.container.ScreenOverlay @OptIn(WorkflowUiExperimentalApi::class) -class PanelOverlay( - override val content: T -) : ScreenOverlay, ModalOverlay +class PanelOverlay( + override val content: C +) : ScreenOverlay, ModalOverlay { + override fun map(transform: (C) -> D): PanelOverlay = + PanelOverlay(transform(content)) +} diff --git a/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt index 845c2a970..5d65ef66e 100644 --- a/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt +++ b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt @@ -29,7 +29,6 @@ import com.squareup.workflow1.ui.container.AlertOverlay.Button.NEUTRAL import com.squareup.workflow1.ui.container.AlertOverlay.Button.POSITIVE import com.squareup.workflow1.ui.container.AlertOverlay.Event.ButtonClicked import com.squareup.workflow1.ui.container.AlertOverlay.Event.Canceled -import com.squareup.workflow1.ui.container.ScreenOverlay enum class RunGameResult { CanceledStart, @@ -37,14 +36,15 @@ enum class RunGameResult { } /** - * This workflow renders up to three layers. There is always a [gameScreen], which - * may be covered by a [namePrompt] and [alerts]. By declaring our rendering shape - * this explicitly, we give parent workflows just enough information to recompose, - * without leaking details about every single type of screen we render. + * This workflow renders in up to three parts, whose display a parent is responsible for + * managing. There is always a [gameScreen], which may be augmented by a [namePrompt] + * and [alerts]. By declaring our rendering shape this explicitly, we give parent workflows + * just enough information to recompose, without leaking details about every single type + * of screen we render. */ data class RunGameRendering( val gameScreen: Screen, - val namePrompt: ScreenOverlay<*>? = null, + val namePrompt: Screen? = null, val alerts: List = emptyList() ) @@ -52,7 +52,6 @@ data class RunGameRendering( * We define this otherwise redundant typealias to keep composite workflows * that build on [RunGameWorkflow] decoupled from it, for ease of testing. */ -@OptIn(WorkflowUiExperimentalApi::class) typealias RunGameWorkflow = Workflow @@ -86,14 +85,12 @@ class RealRunGameWorkflow( RunGameRendering( gameScreen = emptyGameScreen, - namePrompt = object : ScreenOverlay { - override val content = NewGameScreen( - renderState.defaultXName, - renderState.defaultOName, - onCancel = context.eventHandler { setOutput(CanceledStart) }, - onStartGame = context.eventHandler { x, o -> state = Playing(PlayerInfo(x, o)) } - ) - } + namePrompt = NewGameScreen( + renderState.defaultXName, + renderState.defaultOName, + onCancel = context.eventHandler { setOutput(CanceledStart) }, + onStartGame = context.eventHandler { x, o -> state = Playing(PlayerInfo(x, o)) } + ) ) } @@ -233,6 +230,7 @@ class RealRunGameWorkflow( NEGATIVE -> continuePlaying() NEUTRAL -> throw IllegalArgumentException() } + Canceled -> continuePlaying() } } diff --git a/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/TicTacToeWorkflow.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/TicTacToeWorkflow.kt index 4db026a4c..265c3843b 100644 --- a/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/TicTacToeWorkflow.kt +++ b/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/TicTacToeWorkflow.kt @@ -91,8 +91,7 @@ class TicTacToeWorkflow( // Authenticating state was allowed to die, so that this one will start fresh // in its logged out state. val stubAuthBackStack = context.renderChild(authWorkflow, "fake") { noAction() } - val fullBackStack = stubAuthBackStack + - BackStackScreen(gameRendering.namePrompt.content) + val fullBackStack = stubAuthBackStack + BackStackScreen(gameRendering.namePrompt) val allModals = listOf(PanelOverlay(fullBackStack)) + gameRendering.alerts BodyAndOverlaysScreen(gameRendering.gameScreen, allModals) 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 5f7711963..2898e8ea3 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 @@ -570,6 +570,8 @@ internal class ComposeViewTreeIntegrationTest { data class TestModal( override val content: Screen ) : ScreenOverlay, AndroidOverlay { + override fun map(transform: (Screen) -> ContentU) = error("Not implemented") + override val dialogFactory = object : ScreenOverlayDialogFactory( TestModal::class ) { diff --git a/workflow-ui/core-android/api/core-android.api b/workflow-ui/core-android/api/core-android.api index 60fecdaef..c361f1237 100644 --- a/workflow-ui/core-android/api/core-android.api +++ b/workflow-ui/core-android/api/core-android.api @@ -313,13 +313,20 @@ public abstract interface class com/squareup/workflow1/ui/container/AndroidOverl public abstract fun getDialogFactory ()Lcom/squareup/workflow1/ui/container/OverlayDialogFactory; } -public final class com/squareup/workflow1/ui/container/BackButtonScreen : com/squareup/workflow1/ui/AndroidScreen { +public final class com/squareup/workflow1/ui/container/BackButtonScreen : com/squareup/workflow1/ui/AndroidScreen, com/squareup/workflow1/ui/Wrapper { public fun (Lcom/squareup/workflow1/ui/Screen;ZLkotlin/jvm/functions/Function0;)V public synthetic fun (Lcom/squareup/workflow1/ui/Screen;ZLkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun asSequence ()Lkotlin/sequences/Sequence; + public fun getCompatibilityKey ()Ljava/lang/String; + public fun getContent ()Lcom/squareup/workflow1/ui/Screen; + public synthetic fun getContent ()Ljava/lang/Object; public final fun getOnBackPressed ()Lkotlin/jvm/functions/Function0; public final fun getShadow ()Z public fun getViewFactory ()Lcom/squareup/workflow1/ui/ScreenViewFactory; public final fun getWrapped ()Lcom/squareup/workflow1/ui/Screen; + public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container; + public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Wrapper; + public fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/container/BackButtonScreen; } public final class com/squareup/workflow1/ui/container/BackStackConfig : java/lang/Enum { diff --git a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/BackStackContainerTest.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/BackStackContainerTest.kt index ee0192873..56633e306 100644 --- a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/BackStackContainerTest.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/BackStackContainerTest.kt @@ -167,7 +167,7 @@ internal class BackStackContainerTest { var transitionCount = 0 val visibleRendering: Screen - get() = (getChildAt(0)?.tag as NamedScreen<*>).wrapped + get() = (getChildAt(0)?.tag as NamedScreen<*>).content override fun performTransition( oldHolderMaybe: ScreenViewHolder>?, diff --git a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/DialogIntegrationTest.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/DialogIntegrationTest.kt index d23a304b4..fcc20fe9a 100644 --- a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/DialogIntegrationTest.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/DialogIntegrationTest.kt @@ -10,6 +10,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.squareup.workflow1.ui.AndroidScreen import com.squareup.workflow1.ui.Compatible +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.ScreenViewHolder import com.squareup.workflow1.ui.ViewEnvironment @@ -47,7 +48,10 @@ internal class DialogIntegrationTest { name: String, override val content: ContentRendering ) : - Compatible, AndroidOverlay, ScreenOverlay { + AndroidOverlay, ScreenOverlay { + override fun map(transform: (ContentRendering) -> ContentU) = + error("Not implemented") + override val compatibilityKey = name override val dialogFactory = object : ScreenOverlayDialogFactory( 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 f224b35f4..d1c041d5b 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 @@ -35,7 +35,7 @@ internal class ViewStateCacheTest { private object AScreen : Screen @Test fun saves_and_restores_self() { - val rendering = NamedScreen(wrapped = AScreen, name = "rendering") + val rendering = NamedScreen(content = AScreen, name = "rendering") val childState = SparseArray().apply { put(0, TestChildState("hello world")) } @@ -68,8 +68,8 @@ internal class ViewStateCacheTest { @Test fun saves_and_restores_child_states_on_navigation() { val cache = ViewStateCache() - val firstRendering = NamedScreen(wrapped = AScreen, name = "first") - val secondRendering = NamedScreen(wrapped = AScreen, name = "second") + val firstRendering = NamedScreen(content = AScreen, name = "first") + val secondRendering = NamedScreen(content = AScreen, name = "second") // Android requires ID to be set for view hierarchy to be saved or restored. val firstView = createTestView(firstRendering, id = 1) val secondView = createTestView(secondRendering) @@ -100,8 +100,8 @@ internal class ViewStateCacheTest { @Test fun doesnt_restore_state_when_restored_view_id_is_different() { val cache = ViewStateCache() - val firstRendering = NamedScreen(wrapped = AScreen, name = "first") - val secondRendering = NamedScreen(wrapped = AScreen, name = "second") + val firstRendering = NamedScreen(content = AScreen, name = "first") + val secondRendering = NamedScreen(content = AScreen, name = "second") // Android requires ID to be set for view hierarchy to be saved or restored. val firstView = createTestView(firstRendering, id = 1) val secondView = createTestView(secondRendering) @@ -143,8 +143,8 @@ internal class ViewStateCacheTest { @Test fun doesnt_restore_state_when_view_id_not_set() { val cache = ViewStateCache() - val firstRendering = NamedScreen(wrapped = AScreen, name = "first") - val secondRendering = NamedScreen(wrapped = AScreen, name = "second") + val firstRendering = NamedScreen(content = AScreen, name = "first") + val secondRendering = NamedScreen(content = AScreen, name = "second") val firstView = createTestView(firstRendering) val secondView = createTestView(secondRendering) @@ -170,7 +170,7 @@ internal class ViewStateCacheTest { @Test fun throws_on_duplicate_renderings() { val cache = ViewStateCache() - val rendering = NamedScreen(wrapped = AScreen, name = "duplicate") + val rendering = NamedScreen(content = AScreen, name = "duplicate") val view = createTestView(rendering) try { 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 06f4a7a98..9fa05f2e6 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 @@ -139,107 +139,83 @@ public interface ScreenViewFactory : ViewRegistry.Entry { override val viewFactory = - * fromLayout(...) } - * - * class AliasScreen(val similarData: String) : AndroidScreen { - * override val viewFactory = forWrapper { aliasScreen -> - * RealScreen(aliasScreen.similarData) } } - * * To make one [Screen] type a wrapper for others: * - * class Wrapper(val wrapped: W: Screen) : AndroidScreen>, Compatible { - * override val compatibilityKey = Compatible.keyFor(wrapped) override val viewFactory = - * ScreenViewFactory.forWrapper, W> { it.wrapped } } - * - * To make a wrapper that adds information to the [ViewEnvironment]: + * class MyWrapper( + * override val content: W + * ) : AndroidScreen>, Wrapper { + * override val viewFactory = forWrapper, W>() * - * class ReverseNeutronFlowPolarity : ViewEnvironmentKey(Boolean::class) { override val - * default = false } + * override fun map(transform: (W) -> U) = + * MyWrapper(transform(content)) + * } * - * class ReversePolarityScreen( val wrapped: W ) : - * AndroidScreen>, Compatible { override val compatibilityKey: String - * = Compatible.keyFor(wrapped) override val viewFactory = forWrapper, - * Screen> { it.wrapped.withEnvironment( Environment.EMPTY + (ReverseNeutronFlowPolarity to - * true) ) } } + * To make a wrapper that customizes [View] initialization: * - * @param unwrap a function to extract [WrappedT] instances from [WrapperT]s. - */ - @WorkflowUiExperimentalApi - public inline fun < - reified WrapperT : Screen, - WrappedT : Screen - > forWrapper( - crossinline unwrap: (wrapperScreen: WrapperT) -> WrappedT, - ): ScreenViewFactory = forWrapper( - unwrap = unwrap, - beforeShowing = {} - ) { _, wrapper, e, showWrapper -> - showWrapper(unwrap(wrapper), e) - } - - /** - * Creates a [ScreenViewFactory] for [WrapperT] that finds and delegates to the one for - * [WrappedT]. Allows [WrapperT] to wrap instances of [WrappedT] to add information or behavior, - * without requiring wasteful wrapping in the view system. + * class WithTutorialTips( + * override val content: W + * ) : AndroidScreen>, Wrapper { + * override val viewFactory = forWrapper, W>( + * beforeShowing = { TutorialTipRunner.initialize(it.view) } + * ) * - * This fully featured variant of the function is able to initialize the freshly created - * [ScreenViewHolder], and transform the wrapped [ScreenViewHolder.runner]. + * override fun map(transform: (W) -> U) = + * WithTutorialTips(transform(content)) + * } * - * To make a wrapper that customizes [View] initialization: + * @param prepEnvironment a function to process the initial [ViewEnvironment] + * before the [ScreenViewFactory] is fetched. Note that this function is not + * applied on updates. Add a [showWrapperScreen] function if you need that. * - * class WithTutorialTips( val wrapped: W ) : AndroidScreen>, - * Compatible { override val compatibilityKey = Compatible.keyFor(wrapped) override - * val viewFactory = forWrapper, W>( unwrap = { it.wrapped }, - * beforeShowing = { TutorialTipRunner.initialize(it.view) }, showWrapperScreen = { _, - * wrapper, environment, showWrapper -> showWrapper(unwrap(wrapper), environment) } ) } + * @param prepContext a function to process the [Context] used to create each [View]. + * it is passed the product of [prepEnvironment] * * @param unwrap a function to extract [WrappedT] instances from [WrapperT]s. + * * @param beforeShowing a function to be invoked immediately after a new [View] is built. - * @param showWrapperScreen a function to be invoked when an instance of [WrapperT] needs to be - * shown in a [View] built to display instances of [WrappedT]. Allows pre- and - * post-processing of the [View]. + * + * @param showWrapperScreen a function to be invoked when an instance of [WrapperT] needs + * to be shown in a [View] built to display instances of [WrappedT]. Allows pre- + * and post-processing of the [View]. */ @WorkflowUiExperimentalApi - public inline fun < - reified WrapperT : Screen, - WrappedT : Screen - > forWrapper( - crossinline unwrap: (wrapperScreen: WrapperT) -> WrappedT, + public inline fun forWrapper( + crossinline prepEnvironment: (environment: ViewEnvironment) -> ViewEnvironment = { it }, + crossinline prepContext: ( + environment: ViewEnvironment, + context: Context + ) -> Context = { _, c -> c }, + crossinline unwrap: (wrapperScreen: WrapperT) -> WrappedT = { it.content }, crossinline beforeShowing: (viewHolder: ScreenViewHolder) -> Unit = {}, crossinline showWrapperScreen: ( view: View, wrapperScreen: WrapperT, environment: ViewEnvironment, showUnwrappedScreen: (WrappedT, ViewEnvironment) -> Unit - ) -> Unit, - ): ScreenViewFactory = - fromCode { initialRendering, initialEnvironment, context, container -> - val wrappedFactory = unwrap(initialRendering).toViewFactory(initialEnvironment) - val wrapperFactory = wrappedFactory.toUnwrappingViewFactory(unwrap, showWrapperScreen) - wrapperFactory.buildView( - initialRendering, - initialEnvironment, - context, - container - ).also { beforeShowing(it) } + ) -> Unit = { _, wrapper, e, showWrapper -> showWrapper(wrapper.content, e) }, + ): ScreenViewFactory where WrapperT : Screen, WrapperT : Wrapper { + return fromCode { initialRendering, initialEnvironment, context, container -> + val preppedEnvironment = prepEnvironment(initialEnvironment) + val wrappedFactory = unwrap(initialRendering).toViewFactory(preppedEnvironment) + val wrapperFactory = wrappedFactory.toUnwrappingViewFactory( + prepEnvironment, + prepContext, + unwrap, + showWrapperScreen + ) + + // Note that we give the factory the original initialEnvironment. + // It applies prepEnvironment itself. + wrapperFactory.buildView(initialRendering, initialEnvironment, context, container) + .also { beforeShowing(it) } } + } } } @@ -353,43 +329,34 @@ public fun interface ViewStarter { * @see [ScreenViewFactory.forWrapper]. */ @WorkflowUiExperimentalApi -public inline fun < - reified WrapperT : Screen, - WrappedT : Screen - > ScreenViewFactory.toUnwrappingViewFactory( - crossinline unwrap: (wrapperScreen: WrapperT) -> WrappedT -): ScreenViewFactory { - return toUnwrappingViewFactory(unwrap) { _, wrapperScreen, environment, showUnwrappedScreen -> - showUnwrappedScreen(unwrap(wrapperScreen), environment) - } -} - -/** - * Transforms a [ScreenViewFactory] of [WrappedT] into one that can handle instances of [WrapperT]. - * - * @see [ScreenViewFactory.forWrapper]. - */ -@WorkflowUiExperimentalApi -public inline fun < - reified WrapperT : Screen, - WrappedT : Screen - > ScreenViewFactory.toUnwrappingViewFactory( - crossinline unwrap: (wrapperScreen: WrapperT) -> WrappedT, +public inline fun ScreenViewFactory.toUnwrappingViewFactory( + crossinline prepEnvironment: (environment: ViewEnvironment) -> ViewEnvironment = { e -> e }, + crossinline prepContext: ( + environment: ViewEnvironment, + context: Context + ) -> Context = { _, c -> c }, + crossinline unwrap: (wrapperScreen: WrapperT) -> WrappedT = { it.content }, crossinline showWrapperScreen: ( view: View, wrapperScreen: WrapperT, environment: ViewEnvironment, showUnwrappedScreen: (WrappedT, ViewEnvironment) -> Unit - ) -> Unit -): ScreenViewFactory { + ) -> Unit = { _, wrapperScreen, environment, showUnwrappedScreen -> + showUnwrappedScreen(wrapperScreen.content, environment) + } +): ScreenViewFactory + where WrapperT : Screen, WrapperT : Wrapper, WrappedT : Screen { val wrappedFactory = this return object : ScreenViewFactory by fromCode( buildView = { initialRendering, initialEnvironment, context, container -> + val preppedInitialEnvironment = prepEnvironment(initialEnvironment) + val preppedContext = prepContext(preppedInitialEnvironment, context) + val wrappedHolder = wrappedFactory.buildView( unwrap(initialRendering), - initialEnvironment, - context, + preppedInitialEnvironment, + preppedContext, container ) 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 3b8e4cf97..2e16a92b7 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 @@ -6,7 +6,6 @@ import com.squareup.workflow1.ui.container.BackStackScreenViewFactory import com.squareup.workflow1.ui.container.BodyAndOverlaysContainer import com.squareup.workflow1.ui.container.BodyAndOverlaysScreen import com.squareup.workflow1.ui.container.EnvironmentScreen -import com.squareup.workflow1.ui.container.EnvironmentScreenViewFactory /** * [ViewEnvironment] service object used by [Screen.toViewFactory] to find the right @@ -71,10 +70,15 @@ public interface ScreenViewFactoryFinder { BodyAndOverlaysContainer as ScreenViewFactory } ?: (rendering as? NamedScreen<*>)?.let { - forWrapper, ScreenT> { it.wrapped } as ScreenViewFactory + forWrapper, ScreenT>() as ScreenViewFactory } ?: (rendering as? EnvironmentScreen<*>)?.let { - EnvironmentScreenViewFactory() as ScreenViewFactory + forWrapper, ScreenT>( + prepEnvironment = { e -> e + rendering.environment }, + showWrapperScreen = { _, envScreen, environment, showUnwrapped -> + showUnwrapped(envScreen.content, environment + envScreen.environment) + } + ) as ScreenViewFactory } ?: throw IllegalArgumentException( "A ScreenViewFactory should have been registered to display $rendering, " + diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackButtonScreen.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackButtonScreen.kt index 13c6e7c86..808bf2dd0 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackButtonScreen.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackButtonScreen.kt @@ -4,15 +4,16 @@ import com.squareup.workflow1.ui.AndroidScreen import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.Wrapper import com.squareup.workflow1.ui.backPressedHandler /** - * Adds optional back button handling to a [wrapped] rendering, possibly overriding that + * Adds optional back button handling to a [content] rendering, possibly overriding that * the wrapped rendering's own back button handler. * * @param shadow If `true`, [onBackPressed] is set as the * [backPressedHandler][android.view.View.backPressedHandler] after - * the [wrapped] rendering's view is built / updated, effectively overriding it. + * the [content] rendering's view is built / updated, effectively overriding it. * If false (the default), [onBackPressed] is set afterward, to allow the wrapped rendering to * take precedence if it sets a `backPressedHandler` of its own -- the handler provided * here serves as a default. @@ -22,15 +23,16 @@ import com.squareup.workflow1.ui.backPressedHandler * Defaults to `null`. */ @WorkflowUiExperimentalApi -public class BackButtonScreen( - public val wrapped: W, +public class BackButtonScreen( + public override val content: C, public val shadow: Boolean = false, public val onBackPressed: (() -> Unit)? = null -) : AndroidScreen> { +) : Wrapper, AndroidScreen> { + override fun map(transform: (C) -> D): BackButtonScreen = + BackButtonScreen(transform(content), shadow, onBackPressed) - override val viewFactory: ScreenViewFactory> = + override val viewFactory: ScreenViewFactory> = ScreenViewFactory.forWrapper( - unwrap = { it.wrapped }, showWrapperScreen = { view, backButtonScreen, env, showUnwrapped -> if (!backButtonScreen.shadow) { // Place our handler before invoking innerShowRendering, so that @@ -39,8 +41,8 @@ public class BackButtonScreen( view.backPressedHandler = backButtonScreen.onBackPressed } - // Show the wrapped Screen. - showUnwrapped(backButtonScreen.wrapped, env) + // Show the content Screen. + showUnwrapped(backButtonScreen.content, env) if (backButtonScreen.shadow) { // Place our handler after invoking innerShowRendering, so that ours wins. @@ -48,4 +50,7 @@ public class BackButtonScreen( } } ) + + @Deprecated("Use content", ReplaceWith("content")) + public val wrapped: C = content } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/EnvironmentScreenLegacyViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/EnvironmentScreenLegacyViewFactory.kt index e568c047b..580faca27 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/EnvironmentScreenLegacyViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/EnvironmentScreenLegacyViewFactory.kt @@ -12,6 +12,6 @@ internal object EnvironmentScreenLegacyViewFactory : ViewFactory - Pair(environmentScreen.wrapped, environmentScreen.environment + inheritedEnvironment) + Pair(environmentScreen.content, environmentScreen.environment + inheritedEnvironment) } ) 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 deleted file mode 100644 index b13945164..000000000 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/EnvironmentScreenViewFactory.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.squareup.workflow1.ui.container - -import com.squareup.workflow1.ui.Screen -import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromCode -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.toUnwrappingViewFactory -import com.squareup.workflow1.ui.toViewFactory - -@WorkflowUiExperimentalApi -internal fun EnvironmentScreenViewFactory(): - ScreenViewFactory> { - return fromCode { initialEnvScreen, initialEnvironment, context, container -> - val mergedInitialEnvironment = initialEnvironment + initialEnvScreen.environment - - initialEnvScreen.wrapped.toViewFactory(mergedInitialEnvironment) - .toUnwrappingViewFactory, WrappedT>( - unwrap = { it.wrapped }, - showWrapperScreen = { _, envScreen, environment, showUnwrapped -> - showUnwrapped(envScreen.wrapped, environment + envScreen.environment) - } - ) - .buildView(initialEnvScreen, mergedInitialEnvironment, context, container) - } -} 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 ea6cabb3c..4a888559b 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 @@ -1,20 +1,18 @@ -@file:OptIn(WorkflowUiExperimentalApi::class) - package com.squareup.workflow1.ui import android.content.Context import android.view.ViewGroup import com.google.common.truth.Truth.assertThat -import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromCode +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.forWrapper import com.squareup.workflow1.ui.ViewRegistry.Entry import org.junit.Test import org.mockito.kotlin.mock import kotlin.reflect.KClass import kotlin.test.assertFailsWith +@OptIn(WorkflowUiExperimentalApi::class) internal class ScreenViewFactoryTest { - @OptIn(WorkflowUiExperimentalApi::class) @Test fun missingBindingMessage_isUseful() { val emptyReg = object : ViewRegistry { @@ -67,20 +65,16 @@ internal class ScreenViewFactoryTest { val screen = MyWrapper(MyAndroidScreen()) screen.toViewFactory(env).startShowing(screen, env, mock()) - assertThat(screen.wrapped.viewFactory.built).isTrue() - assertThat(screen.wrapped.viewFactory.updated).isTrue() + assertThat(screen.content.viewFactory.built).isTrue() + assertThat(screen.content.viewFactory.updated).isTrue() } @OptIn(WorkflowUiExperimentalApi::class) - private class MyWrapper( - val wrapped: MyAndroidScreen - ) : AndroidScreen { - override val viewFactory = - fromCode { initialScreen, initialEnvironment, _, _ -> - wrapped.viewFactory.toUnwrappingViewFactory( - unwrap = { wrapped } - ).startShowing(initialScreen, initialEnvironment, mock()) - } + private class MyWrapper( + override val content: C + ) : Wrapper, AndroidScreen> { + override fun map(transform: (C) -> D) = MyWrapper(transform(content)) + override val viewFactory = forWrapper, C>() } private class TestViewFactory( diff --git a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/container/EnvironmentScreenAndroidIntegrationTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/container/EnvironmentScreenAndroidIntegrationTest.kt index fd110a050..6030e7a44 100644 --- a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/container/EnvironmentScreenAndroidIntegrationTest.kt +++ b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/container/EnvironmentScreenAndroidIntegrationTest.kt @@ -1,5 +1,3 @@ -@file:OptIn(WorkflowUiExperimentalApi::class) - package com.squareup.workflow1.ui.container import com.google.common.truth.Truth.assertThat @@ -13,6 +11,7 @@ import com.squareup.workflow1.ui.toViewFactory import org.junit.Test import org.mockito.kotlin.mock +@OptIn(WorkflowUiExperimentalApi::class) internal class EnvironmentScreenAndroidIntegrationTest { @Test fun mergingWorksForBuild() { val altFactory = WrappedFactory() diff --git a/workflow-ui/core-common/api/core-common.api b/workflow-ui/core-common/api/core-common.api index 9668794d2..dcf71ed3b 100644 --- a/workflow-ui/core-common/api/core-common.api +++ b/workflow-ui/core-common/api/core-common.api @@ -1,7 +1,12 @@ -public final class com/squareup/workflow1/ui/AsScreen : com/squareup/workflow1/ui/Compatible, com/squareup/workflow1/ui/Screen { +public final class com/squareup/workflow1/ui/AsScreen : com/squareup/workflow1/ui/Screen, com/squareup/workflow1/ui/Wrapper { public fun (Ljava/lang/Object;)V + public fun asSequence ()Lkotlin/sequences/Sequence; public fun getCompatibilityKey ()Ljava/lang/String; + public fun getContent ()Ljava/lang/Object; public final fun getRendering ()Ljava/lang/Object; + public fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/AsScreen; + public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container; + public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Wrapper; } public final class com/squareup/workflow1/ui/AsScreenKt { @@ -22,6 +27,11 @@ public final class com/squareup/workflow1/ui/CompatibleKt { public static final fun compatible (Ljava/lang/Object;Ljava/lang/Object;)Z } +public abstract interface class com/squareup/workflow1/ui/Container { + public abstract fun asSequence ()Lkotlin/sequences/Sequence; + public abstract fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container; +} + public final class com/squareup/workflow1/ui/Named : com/squareup/workflow1/ui/Compatible { public fun (Ljava/lang/Object;Ljava/lang/String;)V public final fun component1 ()Ljava/lang/Object; @@ -36,17 +46,23 @@ public final class com/squareup/workflow1/ui/Named : com/squareup/workflow1/ui/C public fun toString ()Ljava/lang/String; } -public final class com/squareup/workflow1/ui/NamedScreen : com/squareup/workflow1/ui/Compatible, com/squareup/workflow1/ui/Screen { +public final class com/squareup/workflow1/ui/NamedScreen : com/squareup/workflow1/ui/Screen, com/squareup/workflow1/ui/Wrapper { public fun (Lcom/squareup/workflow1/ui/Screen;Ljava/lang/String;)V + public fun asSequence ()Lkotlin/sequences/Sequence; public final fun component1 ()Lcom/squareup/workflow1/ui/Screen; public final fun component2 ()Ljava/lang/String; public final fun copy (Lcom/squareup/workflow1/ui/Screen;Ljava/lang/String;)Lcom/squareup/workflow1/ui/NamedScreen; public static synthetic fun copy$default (Lcom/squareup/workflow1/ui/NamedScreen;Lcom/squareup/workflow1/ui/Screen;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/NamedScreen; public fun equals (Ljava/lang/Object;)Z public fun getCompatibilityKey ()Ljava/lang/String; + public fun getContent ()Lcom/squareup/workflow1/ui/Screen; + public synthetic fun getContent ()Ljava/lang/Object; public final fun getName ()Ljava/lang/String; public final fun getWrapped ()Lcom/squareup/workflow1/ui/Screen; public fun hashCode ()I + public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container; + public fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/NamedScreen; + public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Wrapper; public fun toString ()Ljava/lang/String; } @@ -120,6 +136,18 @@ public final class com/squareup/workflow1/ui/ViewRegistryKt { public abstract interface annotation class com/squareup/workflow1/ui/WorkflowUiExperimentalApi : java/lang/annotation/Annotation { } +public abstract interface class com/squareup/workflow1/ui/Wrapper : com/squareup/workflow1/ui/Compatible, com/squareup/workflow1/ui/Container { + public abstract fun asSequence ()Lkotlin/sequences/Sequence; + public abstract fun getCompatibilityKey ()Ljava/lang/String; + public abstract fun getContent ()Ljava/lang/Object; + public abstract fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Wrapper; +} + +public final class com/squareup/workflow1/ui/Wrapper$DefaultImpls { + public static fun asSequence (Lcom/squareup/workflow1/ui/Wrapper;)Lkotlin/sequences/Sequence; + public static fun getCompatibilityKey (Lcom/squareup/workflow1/ui/Wrapper;)Ljava/lang/String; +} + public final class com/squareup/workflow1/ui/container/AlertOverlay : com/squareup/workflow1/ui/container/ModalOverlay { public fun (Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)V public synthetic fun (Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -166,16 +194,18 @@ public final class com/squareup/workflow1/ui/container/AlertOverlay$Event$Cancel public static final field INSTANCE Lcom/squareup/workflow1/ui/container/AlertOverlay$Event$Canceled; } -public final class com/squareup/workflow1/ui/container/BackStackScreen : com/squareup/workflow1/ui/Screen { +public final class com/squareup/workflow1/ui/container/BackStackScreen : com/squareup/workflow1/ui/Container, com/squareup/workflow1/ui/Screen { public fun (Lcom/squareup/workflow1/ui/Screen;Ljava/util/List;)V public fun (Lcom/squareup/workflow1/ui/Screen;[Lcom/squareup/workflow1/ui/Screen;)V + public fun asSequence ()Lkotlin/sequences/Sequence; public fun equals (Ljava/lang/Object;)Z public final fun get (I)Lcom/squareup/workflow1/ui/Screen; public final fun getBackStack ()Ljava/util/List; public final fun getFrames ()Ljava/util/List; public final fun getTop ()Lcom/squareup/workflow1/ui/Screen; public fun hashCode ()I - public final fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/container/BackStackScreen; + public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container; + public fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/container/BackStackScreen; public final fun mapIndexed (Lkotlin/jvm/functions/Function2;)Lcom/squareup/workflow1/ui/container/BackStackScreen; public final fun plus (Lcom/squareup/workflow1/ui/container/BackStackScreen;)Lcom/squareup/workflow1/ui/container/BackStackScreen; public fun toString ()Ljava/lang/String; @@ -196,12 +226,18 @@ public final class com/squareup/workflow1/ui/container/BodyAndOverlaysScreen : c public final fun mapModals (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/container/BodyAndOverlaysScreen; } -public final class com/squareup/workflow1/ui/container/EnvironmentScreen : com/squareup/workflow1/ui/Compatible, com/squareup/workflow1/ui/Screen { +public final class com/squareup/workflow1/ui/container/EnvironmentScreen : com/squareup/workflow1/ui/Screen, com/squareup/workflow1/ui/Wrapper { public fun (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V public synthetic fun (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun asSequence ()Lkotlin/sequences/Sequence; public fun getCompatibilityKey ()Ljava/lang/String; + public fun getContent ()Lcom/squareup/workflow1/ui/Screen; + public synthetic fun getContent ()Ljava/lang/Object; public final fun getEnvironment ()Lcom/squareup/workflow1/ui/ViewEnvironment; public final fun getWrapped ()Lcom/squareup/workflow1/ui/Screen; + public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container; + public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Wrapper; + public fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/container/EnvironmentScreen; } public final class com/squareup/workflow1/ui/container/EnvironmentScreenKt { @@ -212,8 +248,14 @@ public final class com/squareup/workflow1/ui/container/EnvironmentScreenKt { public final class com/squareup/workflow1/ui/container/FullScreenOverlay : com/squareup/workflow1/ui/container/ScreenOverlay { public fun (Lcom/squareup/workflow1/ui/Screen;)V + public fun asSequence ()Lkotlin/sequences/Sequence; public fun getCompatibilityKey ()Ljava/lang/String; public fun getContent ()Lcom/squareup/workflow1/ui/Screen; + public synthetic fun getContent ()Ljava/lang/Object; + public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container; + public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Wrapper; + public fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/container/FullScreenOverlay; + public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/container/ScreenOverlay; } public abstract interface class com/squareup/workflow1/ui/container/ModalOverlay : com/squareup/workflow1/ui/container/Overlay { @@ -222,12 +264,13 @@ public abstract interface class com/squareup/workflow1/ui/container/ModalOverlay public abstract interface class com/squareup/workflow1/ui/container/Overlay { } -public abstract interface class com/squareup/workflow1/ui/container/ScreenOverlay : com/squareup/workflow1/ui/Compatible, com/squareup/workflow1/ui/container/Overlay { - public abstract fun getCompatibilityKey ()Ljava/lang/String; +public abstract interface class com/squareup/workflow1/ui/container/ScreenOverlay : com/squareup/workflow1/ui/Wrapper, com/squareup/workflow1/ui/container/Overlay { public abstract fun getContent ()Lcom/squareup/workflow1/ui/Screen; + public abstract fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/container/ScreenOverlay; } public final class com/squareup/workflow1/ui/container/ScreenOverlay$DefaultImpls { + public static fun asSequence (Lcom/squareup/workflow1/ui/container/ScreenOverlay;)Lkotlin/sequences/Sequence; public static fun getCompatibilityKey (Lcom/squareup/workflow1/ui/container/ScreenOverlay;)Ljava/lang/String; } diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/AsScreen.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/AsScreen.kt index 6a7f8cc1f..72926206b 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/AsScreen.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/AsScreen.kt @@ -6,17 +6,20 @@ package com.squareup.workflow1.ui * `Overlay` interfaces, and will be deprecated and deleted sooner or later. */ @WorkflowUiExperimentalApi -public class AsScreen( - public val rendering: W -) : Screen, Compatible { +public class AsScreen( + override val content: C +) : Screen, Wrapper { init { - check(rendering !is Screen) { - "AsScreen is for converting non-Screen renderings, it should not wrap Screen $rendering." + check(content !is Screen) { + "AsScreen is for converting non-Screen renderings, it should not wrap Screen $content." } } - override val compatibilityKey: String - get() = Compatible.keyFor(rendering, "AsScreen") + @Deprecated("Use content", ReplaceWith("content")) + public val rendering: C = content + + override fun map(transform: (C) -> D): AsScreen = + AsScreen(transform(content)) } /** diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Container.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Container.kt new file mode 100644 index 000000000..dea97c927 --- /dev/null +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Container.kt @@ -0,0 +1,74 @@ +package com.squareup.workflow1.ui + +import com.squareup.workflow1.ui.Compatible.Companion.keyFor + +/** + * A rendering type comprised of a set of other renderings. + * + * Why two parameter types? The separate [BaseT] type allows implementations + * and sub-interfaces to constrain the types that [map] is allowed to + * transform [ContentT] to. E.g., it allows `FooWrapper` to declare + * that [map] is only able to transform `S` to other types of `Screen`. + * + * @param BaseT the invariant base type of the contents of such a container, + * usually [Screen] or [Overlay][com.squareup.workflow1.ui.container.Overlay]. + * It is common for the [Container] itself to implement [BaseT], but that is + * not a requirement. E.g., [ScreenOverlay][com.squareup.workflow1.ui.container.ScreenOverlay] + * is an [Overlay][com.squareup.workflow1.ui.container.Overlay], but it + * wraps a [Screen]. + * + * @param ContentT the specific subtype of [BaseT] collected by this [Container]. + */ +@WorkflowUiExperimentalApi +public interface Container { + public fun asSequence(): Sequence + + /** + * Returns a [Container] with the [transform]ed contents of the receiver. + * It is expected that an implementation will take advantage of covariance + * to declare its own type as the return type, rather than plain old [Container]. + * This requirement is not enforced because recursive generics are a fussy nuisance. + * + * For example, suppose we want to create `LoggingScreen`, one that wraps any + * other screen to add some logging calls. Its implementation of this method + * would be expected to have a return type of `LoggingScreen` rather than `Container`: + * + * override fun map(transform: (C) -> D): LoggingScreen = + * LoggingScreen(transform(content)) + * + * By requiring all [Container] types to implement [map], we ensure that their + * contents can be repackaged in interesting ways, e.g.: + * + * val childBackStackScreen = renderChild(childWorkflow) { ... } + * val loggingBackStackScreen = childBackStackScreen.map { LoggingScreen(it) } + */ + public fun map(transform: (ContentT) -> ContentU): Container +} + +/** + * A [Container] rendering that wraps exactly one other rendering, its [content]. These are + * typically used to "add value" to the [content], e.g. an + * [EnvironmentScreen][com.squareup.workflow1.ui.container.EnvironmentScreen] that allows + * changes to be made to the the [ViewEnvironment]. + * + * Usually a [Wrapper] is [Compatible] only with others of the same type with + * [Compatible] [content]. In aid of that, this interface extends [Compatible] and + * provides a convenient default implementation of [compatibilityKey]. + */ +@WorkflowUiExperimentalApi +public interface Wrapper : Container, Compatible { + public val content: ContentT + + /** + * Default implementation makes this [Wrapper] compatible with others of the same type, + * and which wrap compatible [content]. + */ + public override val compatibilityKey: String + get() = keyFor(content, this::class.simpleName ?: "Wrapper") + + public override fun asSequence(): Sequence = sequenceOf(content) + + public override fun map( + transform: (ContentT) -> ContentU + ): Wrapper +} diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/NamedScreen.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/NamedScreen.kt index c35160977..519c87a8c 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/NamedScreen.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/NamedScreen.kt @@ -3,20 +3,26 @@ package com.squareup.workflow1.ui /** * Allows [Screen] renderings that do not implement [Compatible] themselves to be distinguished * by more than just their type. Instances are [compatible] if they have the same name - * and have [compatible] [wrapped] fields. + * and have [compatible] [content] fields. * * UI kits are expected to provide handling for this class by default. */ @WorkflowUiExperimentalApi -public data class NamedScreen( - val wrapped: W, +public data class NamedScreen( + override val content: C, val name: String -) : Screen, Compatible { +) : Screen, Wrapper { init { require(name.isNotBlank()) { "name must not be blank." } } - override val compatibilityKey: String = Compatible.keyFor(wrapped, "NamedScreen($name)") + override val compatibilityKey: String = Compatible.keyFor(content, "NamedScreen($name)") + + @Deprecated("Use content", ReplaceWith("content")) + public val wrapped: C = content + + override fun map(transform: (C) -> D): NamedScreen = + NamedScreen(transform(content), name) override fun toString(): String { return "${super.toString()}: $compatibilityKey" diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/BackStackScreen.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/BackStackScreen.kt index aa236752d..bb8815eee 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/BackStackScreen.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/BackStackScreen.kt @@ -1,5 +1,6 @@ package com.squareup.workflow1.ui.container +import com.squareup.workflow1.ui.Container import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi @@ -19,7 +20,7 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi public class BackStackScreen( bottom: StackedT, rest: List -) : Screen { +) : Screen, Container { /** * Creates a screen with elements listed from the [bottom] to the top. */ @@ -28,6 +29,8 @@ public class BackStackScreen( vararg rest: StackedT ) : this(bottom, rest.toList()) + override fun asSequence(): Sequence = frames.asSequence() + public val frames: List = listOf(bottom) + rest /** @@ -50,9 +53,10 @@ public class BackStackScreen( } } - public fun map(transform: (StackedT) -> R): BackStackScreen { - return frames.map(transform) - .toBackStackScreen() + public override fun map( + transform: (StackedT) -> StackedU + ): BackStackScreen { + return frames.map(transform).toBackStackScreen() } public fun mapIndexed(transform: (index: Int, StackedT) -> R): BackStackScreen { 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 89aef75fa..6b05343e2 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 @@ -1,14 +1,14 @@ package com.squareup.workflow1.ui.container -import com.squareup.workflow1.ui.Compatible 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.Wrapper import com.squareup.workflow1.ui.plus /** - * Pairs a [wrapped] rendering with a [environment] to support its display. + * Pairs a [content] rendering with a [environment] to support its display. * Typically the rendering type (`RenderingT`) of the root of a UI workflow, * but can be used at any point to modify the [ViewEnvironment] received from * a parent view. @@ -16,15 +16,15 @@ import com.squareup.workflow1.ui.plus * UI kits are expected to provide handling for this class by default. */ @WorkflowUiExperimentalApi -public class EnvironmentScreen( - public val wrapped: V, +public class EnvironmentScreen( + public override val content: C, public val environment: ViewEnvironment = ViewEnvironment.EMPTY -) : Compatible, Screen { - /** - * Ensures that we make the decision to update or replace the root view based on - * the wrapped [wrapped]. - */ - override val compatibilityKey: String = Compatible.keyFor(wrapped, "EnvironmentScreen") +) : Wrapper, Screen { + override fun map(transform: (C) -> D): EnvironmentScreen = + EnvironmentScreen(transform(content), environment) + + @Deprecated("Use content", ReplaceWith("content")) + public val wrapped: C = content } /** @@ -55,7 +55,7 @@ public fun Screen.withEnvironment( if (environment.map.isEmpty()) { this } else { - EnvironmentScreen(wrapped, this.environment + environment) + EnvironmentScreen(content, this.environment + environment) } } else -> EnvironmentScreen(this, environment) diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/FullScreenOverlay.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/FullScreenOverlay.kt index e0130762a..a6b8f41bf 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/FullScreenOverlay.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/FullScreenOverlay.kt @@ -9,6 +9,9 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi * UI kits are expected to provide handling for this class by default. */ @WorkflowUiExperimentalApi -public class FullScreenOverlay( - public override val content: ContentT -) : ScreenOverlay +public class FullScreenOverlay( + public override val content: C +) : ScreenOverlay { + override fun map(transform: (C) -> D): FullScreenOverlay = + FullScreenOverlay(transform(content)) +} diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/ScreenOverlay.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/ScreenOverlay.kt index 23a459ae2..7d7814d72 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/ScreenOverlay.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/ScreenOverlay.kt @@ -1,17 +1,15 @@ package com.squareup.workflow1.ui.container -import com.squareup.workflow1.ui.Compatible -import com.squareup.workflow1.ui.Compatible.Companion.keyFor import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.Wrapper /** * An [Overlay] built around a root [content] [Screen]. */ @WorkflowUiExperimentalApi -public interface ScreenOverlay : Overlay, Compatible { - public val content: ContentT +public interface ScreenOverlay : Overlay, Wrapper { + public override val content: ContentT - override val compatibilityKey: String - get() = keyFor(content, this::class.simpleName ?: ScreenOverlay::class.simpleName!!) + override fun map(transform: (ContentT) -> ContentU): ScreenOverlay } diff --git a/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/NamedScreenTest.kt b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/NamedScreenTest.kt index 7b1ebac56..602f56edf 100644 --- a/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/NamedScreenTest.kt +++ b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/NamedScreenTest.kt @@ -5,7 +5,6 @@ import org.junit.Test // If you try to replace isTrue() with isTrue compilation fails. @OptIn(WorkflowUiExperimentalApi::class) -@Suppress("UsePropertyAccessSyntax") internal class NamedScreenTest { object Whut : Screen object Hey : Screen diff --git a/workflow-ui/internal-testing-android/src/main/java/com/squareup/workflow1/ui/internal/test/WorkflowUiTestActivity.kt b/workflow-ui/internal-testing-android/src/main/java/com/squareup/workflow1/ui/internal/test/WorkflowUiTestActivity.kt index acdf72d60..aacf46fa7 100644 --- a/workflow-ui/internal-testing-android/src/main/java/com/squareup/workflow1/ui/internal/test/WorkflowUiTestActivity.kt +++ b/workflow-ui/internal-testing-android/src/main/java/com/squareup/workflow1/ui/internal/test/WorkflowUiTestActivity.kt @@ -101,7 +101,7 @@ public open class WorkflowUiTestActivity : AppCompatActivity() { public fun setRendering(rendering: Screen): View { lastRendering = rendering val named = NamedScreen( - wrapped = rendering, + content = rendering, name = renderingCounter.toString() ) rootStub.show(named, viewEnvironment) diff --git a/workflow-ui/radiography/src/main/java/com/squareup/workflow1/ui/radiography/WorkflowViewRenderer.kt b/workflow-ui/radiography/src/main/java/com/squareup/workflow1/ui/radiography/WorkflowViewRenderer.kt index b3643a301..43c0e4987 100644 --- a/workflow-ui/radiography/src/main/java/com/squareup/workflow1/ui/radiography/WorkflowViewRenderer.kt +++ b/workflow-ui/radiography/src/main/java/com/squareup/workflow1/ui/radiography/WorkflowViewRenderer.kt @@ -37,7 +37,7 @@ private object WorkflowViewRendererImpl : ViewStateRenderer { private fun AttributeAppendable.renderRendering(rendering: Any) { val actualRendering = (rendering as? Named<*>)?.wrapped - ?: (rendering as? NamedScreen<*>)?.wrapped + ?: (rendering as? NamedScreen<*>)?.content ?: rendering append("workflow-rendering-type:${actualRendering::class.java.name}") From 51d59367cdcd716c2d293f36e5049a66de6d6a74 Mon Sep 17 00:00:00 2001 From: Ray Ryan Date: Wed, 1 Mar 2023 09:33:12 -0800 Subject: [PATCH 2/6] s/the the /the /g --- build.gradle.kts | 2 +- .../main/java/com/squareup/workflow1/testing/WorkerTester.kt | 5 +++-- .../java/com/squareup/workflow1/ui/modal/ModalContainer.kt | 2 +- .../java/com/squareup/workflow1/ui/container/OverlayArea.kt | 2 +- .../src/main/java/com/squareup/workflow1/ui/Container.kt | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 136075093..aa928b9f1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -73,7 +73,7 @@ subprojects { if (file("src/${dokkaSourceSet.name}").exists()) { val readmeFile = file("$projectDir/README.md") - // If the module has a README, add it to the the module's index + // If the module has a README, add it to the module's index if (readmeFile.exists()) { includes.from(readmeFile) } diff --git a/workflow-testing/src/main/java/com/squareup/workflow1/testing/WorkerTester.kt b/workflow-testing/src/main/java/com/squareup/workflow1/testing/WorkerTester.kt index 9a85510a4..3ba94da61 100644 --- a/workflow-testing/src/main/java/com/squareup/workflow1/testing/WorkerTester.kt +++ b/workflow-testing/src/main/java/com/squareup/workflow1/testing/WorkerTester.kt @@ -19,8 +19,9 @@ public interface WorkerTester { * Access the [TestCoroutineScheduler] of the [kotlinx.coroutines.test.TestScope] running * the [Worker]'s [test]. * - * This can be used to advance virtual time for the [CoroutineDispatcher] that the the Worker's - * flow is flowing on. + * This can be used to advance virtual time for the + * [CoroutineDispatcher][kotlinx.coroutines.CoroutineDispatcher] + * that the Worker's flow is flowing on. */ public val testCoroutineScheduler: TestCoroutineScheduler diff --git a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalContainer.kt b/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalContainer.kt index a28b2c572..edb8b086d 100644 --- a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalContainer.kt +++ b/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalContainer.kt @@ -47,7 +47,7 @@ public abstract class ModalContainer @JvmOverloads constr /** * Stores the result of looking for the nearest [LifecycleOwner] that should be the parent of all - * this container's modals. Only valid after the the view has been attached. + * this container's modals. Only valid after the view has been attached. */ private val parentLifecycleOwner by lazy(mode = LazyThreadSafetyMode.NONE) { WorkflowLifecycleOwner.get(this) ?: error( diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayArea.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayArea.kt index 249fbed9a..cbc95a013 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayArea.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayArea.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow /** - * Reports the the area of the screen whose events should be blocked by any modal [Overlay], + * Reports the area of the screen whose events should be blocked by any modal [Overlay], * in the style reported by [View.getGlobalVisibleRect][android.view.View.getGlobalVisibleRect]. * Expected to be supplied by containers that support [BodyAndOverlaysScreen]. */ diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Container.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Container.kt index dea97c927..92100388b 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Container.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Container.kt @@ -49,7 +49,7 @@ public interface Container { * A [Container] rendering that wraps exactly one other rendering, its [content]. These are * typically used to "add value" to the [content], e.g. an * [EnvironmentScreen][com.squareup.workflow1.ui.container.EnvironmentScreen] that allows - * changes to be made to the the [ViewEnvironment]. + * changes to be made to the [ViewEnvironment]. * * Usually a [Wrapper] is [Compatible] only with others of the same type with * [Compatible] [content]. In aid of that, this interface extends [Compatible] and From cc29669cfb010969dead05fd2579c1641fd72848 Mon Sep 17 00:00:00 2001 From: Ray Ryan Date: Thu, 9 Mar 2023 11:16:52 -0800 Subject: [PATCH 3/6] cp samples/{hellow-workflow,nested-overlays} --- samples/nested-overlays/build.gradle.kts | 25 +++++++++++ .../nestedoverlays/NestedOverlaysAppTest.kt | 40 +++++++++++++++++ .../src/main/AndroidManifest.xml | 23 ++++++++++ .../sample/nestedoverlays/HelloRendering.kt | 19 ++++++++ .../nestedoverlays/NestedOverlaysActivity.kt | 43 +++++++++++++++++++ .../nestedoverlays/NestedOverlaysWorkflow.kt | 42 ++++++++++++++++++ .../main/res/layout/hello_goodbye_layout.xml | 14 ++++++ .../src/main/res/values/strings.xml | 3 ++ .../src/main/res/values/styles.xml | 8 ++++ settings.gradle.kts | 1 + 10 files changed, 218 insertions(+) create mode 100644 samples/nested-overlays/build.gradle.kts create mode 100644 samples/nested-overlays/src/androidTest/java/com/squareup/sample/nestedoverlays/NestedOverlaysAppTest.kt create mode 100644 samples/nested-overlays/src/main/AndroidManifest.xml create mode 100644 samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/HelloRendering.kt create mode 100644 samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysActivity.kt create mode 100644 samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysWorkflow.kt create mode 100644 samples/nested-overlays/src/main/res/layout/hello_goodbye_layout.xml create mode 100644 samples/nested-overlays/src/main/res/values/strings.xml create mode 100644 samples/nested-overlays/src/main/res/values/styles.xml diff --git a/samples/nested-overlays/build.gradle.kts b/samples/nested-overlays/build.gradle.kts new file mode 100644 index 000000000..7d31f7a46 --- /dev/null +++ b/samples/nested-overlays/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + id("com.android.application") + `kotlin-android` + `android-sample-app` + `android-ui-tests` +} + +android { + defaultConfig { + applicationId = "com.squareup.sample.nestedoverlays" + } + namespace = "com.squareup.sample.nestedoverlays" +} + +dependencies { + debugImplementation(libs.squareup.leakcanary.android) + + implementation(libs.androidx.activity.ktx) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.lifecycle.viewmodel.savedstate) + implementation(libs.androidx.viewbinding) + + implementation(project(":workflow-ui:core-android")) + implementation(project(":workflow-ui:core-common")) +} diff --git a/samples/nested-overlays/src/androidTest/java/com/squareup/sample/nestedoverlays/NestedOverlaysAppTest.kt b/samples/nested-overlays/src/androidTest/java/com/squareup/sample/nestedoverlays/NestedOverlaysAppTest.kt new file mode 100644 index 000000000..5d20722b2 --- /dev/null +++ b/samples/nested-overlays/src/androidTest/java/com/squareup/sample/nestedoverlays/NestedOverlaysAppTest.kt @@ -0,0 +1,40 @@ +package com.squareup.sample.nestedoverlays + +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule +import com.squareup.workflow1.ui.internal.test.inAnyView +import leakcanary.DetectLeaksAfterTestSuccess +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@OptIn(WorkflowUiExperimentalApi::class) +class NestedOverlaysAppTest { + + private val scenarioRule = ActivityScenarioRule(NestedOverlaysActivity::class.java) + + @get:Rule val rules = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) + .around(scenarioRule) + .around(IdlingDispatcherRule) + + @Test fun togglesHelloAndGoodbye() { + inAnyView(withText("Hello")) + .check(matches(isDisplayed())) + .perform(click()) + + inAnyView(withText("Goodbye")) + .check(matches(isDisplayed())) + .perform(click()) + + inAnyView(withText("Hello")) + .check(matches(isDisplayed())) + } +} diff --git a/samples/nested-overlays/src/main/AndroidManifest.xml b/samples/nested-overlays/src/main/AndroidManifest.xml new file mode 100644 index 000000000..594e1fde1 --- /dev/null +++ b/samples/nested-overlays/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/HelloRendering.kt b/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/HelloRendering.kt new file mode 100644 index 000000000..3087d9ded --- /dev/null +++ b/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/HelloRendering.kt @@ -0,0 +1,19 @@ +package com.squareup.sample.nestedoverlays + +import com.squareup.sample.nestedoverlays.databinding.HelloGoodbyeLayoutBinding +import com.squareup.workflow1.ui.AndroidScreen +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromViewBinding +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +@OptIn(WorkflowUiExperimentalApi::class) +data class HelloRendering( + val message: String, + val onClick: () -> Unit +) : AndroidScreen { + override val viewFactory: ScreenViewFactory = + fromViewBinding(HelloGoodbyeLayoutBinding::inflate) { r, _ -> + helloMessage.text = r.message + helloMessage.setOnClickListener { r.onClick() } + } +} diff --git a/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysActivity.kt b/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysActivity.kt new file mode 100644 index 000000000..44b5b2b2d --- /dev/null +++ b/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysActivity.kt @@ -0,0 +1,43 @@ +@file:OptIn(WorkflowExperimentalRuntime::class) + +package com.squareup.sample.nestedoverlays + +import android.os.Bundle +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.squareup.workflow1.WorkflowExperimentalRuntime +import com.squareup.workflow1.config.AndroidRuntimeConfigTools +import com.squareup.workflow1.ui.WorkflowLayout +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.renderWorkflowIn +import kotlinx.coroutines.flow.StateFlow + +@OptIn(WorkflowUiExperimentalApi::class) +class NestedOverlaysActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // This ViewModel will survive configuration changes. It's instantiated + // by the first call to viewModels(), and that original instance is returned by + // succeeding calls. + val model: HelloViewModel by viewModels() + setContentView( + WorkflowLayout(this).apply { take(lifecycle, model.renderings) } + ) + } +} + +class HelloViewModel(savedState: SavedStateHandle) : ViewModel() { + @OptIn(WorkflowUiExperimentalApi::class) + val renderings: StateFlow by lazy { + renderWorkflowIn( + workflow = NestedOverlaysWorkflow, + scope = viewModelScope, + savedStateHandle = savedState, + runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig() + ) + } +} diff --git a/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysWorkflow.kt b/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysWorkflow.kt new file mode 100644 index 000000000..9503cf705 --- /dev/null +++ b/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/NestedOverlaysWorkflow.kt @@ -0,0 +1,42 @@ +package com.squareup.sample.nestedoverlays + +import com.squareup.sample.nestedoverlays.NestedOverlaysWorkflow.State +import com.squareup.sample.nestedoverlays.NestedOverlaysWorkflow.State.Goodbye +import com.squareup.sample.nestedoverlays.NestedOverlaysWorkflow.State.Hello +import com.squareup.workflow1.Snapshot +import com.squareup.workflow1.StatefulWorkflow +import com.squareup.workflow1.action +import com.squareup.workflow1.parse + +object NestedOverlaysWorkflow : StatefulWorkflow() { + enum class State { + Hello, + Goodbye + } + + override fun initialState( + props: Unit, + snapshot: Snapshot? + ): State = snapshot?.bytes?.parse { source -> if (source.readInt() == 1) Hello else Goodbye } + ?: Hello + + override fun render( + renderProps: Unit, + renderState: State, + context: RenderContext + ): HelloRendering { + return HelloRendering( + message = renderState.name, + onClick = { context.actionSink.send(helloAction) } + ) + } + + override fun snapshotState(state: State): Snapshot = Snapshot.of(if (state == Hello) 1 else 0) + + private val helloAction = action { + state = when (state) { + Hello -> Goodbye + Goodbye -> Hello + } + } +} diff --git a/samples/nested-overlays/src/main/res/layout/hello_goodbye_layout.xml b/samples/nested-overlays/src/main/res/layout/hello_goodbye_layout.xml new file mode 100644 index 000000000..dcd6f7c0b --- /dev/null +++ b/samples/nested-overlays/src/main/res/layout/hello_goodbye_layout.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/samples/nested-overlays/src/main/res/values/strings.xml b/samples/nested-overlays/src/main/res/values/strings.xml new file mode 100644 index 000000000..eaa5276c9 --- /dev/null +++ b/samples/nested-overlays/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Nested Overlays + diff --git a/samples/nested-overlays/src/main/res/values/styles.xml b/samples/nested-overlays/src/main/res/values/styles.xml new file mode 100644 index 000000000..e2331afcc --- /dev/null +++ b/samples/nested-overlays/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/settings.gradle.kts b/settings.gradle.kts index 662672fed..c2a8ddcac 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -55,6 +55,7 @@ include( ":samples:hello-terminal:todo-terminal-app", ":samples:hello-workflow", ":samples:hello-workflow-fragment", + ":samples:nested-overlays", ":samples:stub-visibility", ":samples:tictactoe:app", ":samples:tictactoe:common", From fbfda508ef331ee039f7970f03b633c18d049162 Mon Sep 17 00:00:00 2001 From: Ray Ryan Date: Thu, 9 Mar 2023 14:44:59 -0800 Subject: [PATCH 4/6] Nested overlays sample uses overlays. And can demonstrate the bug where dialogs are shown out of order: - Click _Cover Everything_ - Click _Cover Body_ The red inner dialog is shown over the outer green dialog, but the green one should always be on top. (#966) Also introduces `name` parameter for `BodyAndOverlaysScreen`, because they're a nightmare to nest without it. See the kdoc for details. --- samples/nested-overlays/lint-baseline.xml | 4 + .../nestedoverlays/NestedOverlaysAppTest.kt | 85 +++++++++++--- .../sample/nestedoverlays/ButtonBar.kt | 52 +++++++++ .../sample/nestedoverlays/HelloRendering.kt | 19 ---- .../nestedoverlays/NestedOverlaysActivity.kt | 7 +- .../nestedoverlays/NestedOverlaysWorkflow.kt | 107 ++++++++++++++---- .../nestedoverlays/TopAndBottomBarsScreen.kt | 31 +++++ .../main/res/layout/hello_goodbye_layout.xml | 14 --- .../main/res/layout/top_and_bottom_bars.xml | 28 +++++ .../src/main/res/values/strings.xml | 10 ++ .../container/ScreenOverlayDialogFactory.kt | 3 +- .../ui/container/BodyAndOverlaysScreen.kt | 15 ++- 12 files changed, 300 insertions(+), 75 deletions(-) create mode 100644 samples/nested-overlays/lint-baseline.xml create mode 100644 samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/ButtonBar.kt delete mode 100644 samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/HelloRendering.kt create mode 100644 samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/TopAndBottomBarsScreen.kt delete mode 100644 samples/nested-overlays/src/main/res/layout/hello_goodbye_layout.xml create mode 100644 samples/nested-overlays/src/main/res/layout/top_and_bottom_bars.xml diff --git a/samples/nested-overlays/lint-baseline.xml b/samples/nested-overlays/lint-baseline.xml new file mode 100644 index 000000000..4aec7fcd5 --- /dev/null +++ b/samples/nested-overlays/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/samples/nested-overlays/src/androidTest/java/com/squareup/sample/nestedoverlays/NestedOverlaysAppTest.kt b/samples/nested-overlays/src/androidTest/java/com/squareup/sample/nestedoverlays/NestedOverlaysAppTest.kt index 5d20722b2..bebcd2ac7 100644 --- a/samples/nested-overlays/src/androidTest/java/com/squareup/sample/nestedoverlays/NestedOverlaysAppTest.kt +++ b/samples/nested-overlays/src/androidTest/java/com/squareup/sample/nestedoverlays/NestedOverlaysAppTest.kt @@ -1,40 +1,99 @@ package com.squareup.sample.nestedoverlays +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.ViewInteraction import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withParent +import androidx.test.espresso.matcher.ViewMatchers.withParentIndex import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule -import com.squareup.workflow1.ui.internal.test.inAnyView import leakcanary.DetectLeaksAfterTestSuccess +import org.hamcrest.core.AllOf.allOf +import org.hamcrest.core.IsNot.not import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -@OptIn(WorkflowUiExperimentalApi::class) class NestedOverlaysAppTest { private val scenarioRule = ActivityScenarioRule(NestedOverlaysActivity::class.java) - @get:Rule val rules = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) + @get:Rule val rules: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) .around(scenarioRule) .around(IdlingDispatcherRule) - @Test fun togglesHelloAndGoodbye() { - inAnyView(withText("Hello")) - .check(matches(isDisplayed())) - .perform(click()) + @Test fun basics() { + onTopCoverBody().assertDisplayed() + onTopCoverEverything().assertDisplayed() + onBottomCoverBody().assertDisplayed() + onBottomCoverEverything().assertDisplayed() - inAnyView(withText("Goodbye")) - .check(matches(isDisplayed())) - .perform(click()) + onTopCoverBody().perform(click()) + onView(withText("Close")).perform(click()) + onTopCoverEverything().perform(click()) + onView(withText("Close")).perform(click()) - inAnyView(withText("Hello")) - .check(matches(isDisplayed())) + onView(withText("Hide Top Bar")).perform(click()) + onTopCoverBody().assertNotDisplayed() + onTopCoverEverything().assertNotDisplayed() + onBottomCoverBody().assertDisplayed() + onBottomCoverEverything().assertDisplayed() + + onView(withText("Hide Bottom Bar")).perform(click()) + onTopCoverBody().assertNotDisplayed() + onTopCoverEverything().assertNotDisplayed() + onBottomCoverBody().assertNotDisplayed() + onBottomCoverEverything().assertNotDisplayed() + } + + // https://github.com/square/workflow-kotlin/issues/966 + @Test fun canInsertDialog() { + onTopCoverEverything().perform(click()) + onView(withText("Hide Top Bar")).check(doesNotExist()) + onView(withText("Cover Body")).perform(click()) + + // This line fails due to https://github.com/square/workflow-kotlin/issues/966 + // onView(withText("Hide Top Bar")).check(doesNotExist()) + + // Should continue to close the top sheet and assert that the inner sheet is visible. + } + + // So far can't express this in Espresso. Considering move to Maestro + // @Test fun canClickPastInnerWindow() { + // onView(allOf(withText("Cover Everything"), withParent(withParentIndex(0)))) + // .perform(click()) + // + // scenario.onActivity { activity -> + // onView(allOf(withText("Cover Everything"), withParent(withParentIndex(0)))) + // .inRoot(withDecorView(not(`is`(activity.window.decorView)))) + // .perform(click()) + // } + // } + + private fun ViewInteraction.assertNotDisplayed() { + check(matches(not(isDisplayed()))) } + + private fun ViewInteraction.assertDisplayed() { + check(matches(isDisplayed())) + } + + private fun onBottomCoverEverything() = + onView(allOf(withText("Cover Everything"), withParent(withParentIndex(2)))) + + private fun onBottomCoverBody() = + onView(allOf(withText("Cover Body"), withParent(withParentIndex(2)))) + + private fun onTopCoverBody() = + onView(allOf(withText("Cover Body"), withParent(withParentIndex(0)))) + + private fun onTopCoverEverything() = + onView(allOf(withText("Cover Everything"), withParent(withParentIndex(0)))) } diff --git a/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/ButtonBar.kt b/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/ButtonBar.kt new file mode 100644 index 000000000..ce85e72f3 --- /dev/null +++ b/samples/nested-overlays/src/main/java/com/squareup/sample/nestedoverlays/ButtonBar.kt @@ -0,0 +1,52 @@ +package com.squareup.sample.nestedoverlays + +import android.graphics.drawable.ColorDrawable +import android.view.Gravity +import android.widget.LinearLayout +import androidx.annotation.ColorRes +import androidx.annotation.StringRes +import androidx.core.view.get +import com.squareup.workflow1.ui.AndroidScreen +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewHolder +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import android.widget.Button as ButtonView + +data class Button( + @StringRes val name: Int, + val onClick: () -> Unit +) + +@OptIn(WorkflowUiExperimentalApi::class) +class ButtonBar( + vararg buttons: Button?, + @ColorRes val color: Int = -1, +) : AndroidScreen { + val buttons: List