From 0cde3f9c0d5c72a02689a244465027e73fe05876 Mon Sep 17 00:00:00 2001 From: Ray Ryan Date: Tue, 15 Mar 2022 16:35:47 -0700 Subject: [PATCH] Introduces ScreenViewFactory.updateView, drastically reduces cleverness. The legacy workflow-ui world is built around a set of extension methods on `View`, backed by a `WorkflowViewState` object hanging off of a tag. This is overly clever, needlessly throws away a good bit of compile time safety, and makes it much harder than it should be to implement wrapper renderings. In particular, every `ViewFactory` is required to call `View.bindShowRendering` to put all this machinery in place. When a screen rendering wraps another, with the intention of delegating to its registered `ViewFactory`, that requires creating a wrapper `ViewFactory` that makes a new `bindShowRendering` call replacing and wrapping the `WorkflowViewState` put in place by the delegate. That's so tricky it required the introduction of `DecorativeViewFactory`, which is still pretty tricky to use. To solve all this, we do two things: - deprecate all the existing `View` extension methods and abandon `WorkflowViewState`, replacing it with `ScreenViewHolder` - add an `updateView(View)` method to `ScreenViewFactory` `ScreenViewHolder` is inverted from `WorkflowViewState` -- instead of belonging to the `View`, nestled in a tag, the `View` belongs to it. Most of the old `View` extension methods are replaced by methods on `ScreenViewHolder`. It is very similar to `DialogHolder`, but is part of the public API. The change to `ScreenViewFactory`, giving it an `update` method, makes it much more like `OverlayDialogFactory`. I toyed a bit with the idea of trying to unify `ScreenViewFactory` / `OverlayDialogFactory` and `ScreenViewHolder` / `DialogHolder`, but it didn't seem worth the added complexity, especially around parameter types. Reviewers should also note that the **`buildView` method no longer has access to the rendering**, a choice I'm still second guessing. So far I haven't hit a use case that required it, beyond a couple of changes to expected strings in `BackStackContainerPersistenceTest`. Perhaps that's reason enough to bring it back? One of the deprecated `View` extensions did not move to `ScreenViewHolder`: `View.Start` is replaced by `ScreenViewFactory.start`. This extension method is what calls `ScreenViewFactory.buildView`. It creates and returns a `ScreenViewHolder` that wraps the new view and accept updates for it, which it does by calling `ScreenViewFactory.update`, as you'd expect. `ScreenViewFactory.start` is also the new home for the `ViewStarter` mechanism that is used by `WorkflowViewStub` and other containers to call `WorkflowLifecycleOwner.installOn`. We are not completely out of the `View.setTag` business. `ScreenViewFactory` implementations must be stateless -- it is common for separate instances to field calls to `buildView` and `updateView`, or for a single factory instance to build and update multiple views. `View.setTag` is the most practical way for `buildView` to set up state that `updateView` can rely on. This technique is used as an implementation detail of several standard `ScreenViewFactory` implementations, but is never a public concern. In particular, the factories built by the various `ScreenViewRunner.bind` methods rely on this technique to keep a `View` paired with its runner. And `ScreenViewRunner` is still very much needed under the new scheme, so that we don't force every implementation of `ScreenViewFactory` to solve this problem of maintaining per-view state themselves. But `ScreenViewRunner` is now a convenience, rather than a fundamental mechanism. By making `ScreenViewFactory` responsible for both building and updating, it becomes trivial for one factory to delegate to another -- the update method no longer lives in a fragile `View` tag, and so no longer needs to be replaced. A wrapper factory can just delegate to another one. This means there is no longer a problem to be solved by `DecorativeScreenViewFactory`, so that class is deleted. `ManualScreenViewFactory` is also gone, replaced by a `bindBuiltView()` function that returns `Pair`. The `ViewStarter` mechanism is also much simpler now, also no longer relying on any state stored in `View` tags. The implementation of `ScreenViewFactory.start()` remains a bit complex, but only because of the work it has to do to keep the legacy `View.start()` method working, in partnership with `AsScreenViewFactory()`. Once we delete the deprecated View code, most of the code in both of those can be deleted. Fixes #413, fixes #598, fixes #626, fixes #551, fixes #443. --- .../sample/container/ScrimContainer.kt | 27 +- .../com/squareup/sample/dungeon/BoardView.kt | 11 +- .../stubvisibility/ClickyTextRendering.kt | 24 +- .../squareup/sample/TicTacToeEspressoTest.kt | 56 ----- .../squareup/workflow1/BaseRenderContext.kt | 3 - workflow-ui/compose/api/compose.api | 3 +- .../compose/ComposeViewTreeIntegrationTest.kt | 31 +-- .../compose/NoTransitionBackStackContainer.kt | 27 +- .../ui/compose/WorkflowRenderingTest.kt | 73 ++---- .../ui/compose/ComposeScreenViewFactory.kt | 30 +-- .../workflow1/ui/compose/CompositionRoot.kt | 4 +- .../workflow1/ui/compose/WorkflowRendering.kt | 54 ++-- .../compose/src/main/res/values/ids.xml | 5 + .../ModalViewContainerLifecycleActivity.kt | 46 ++-- .../workflow1/ui/modal/ModalViewContainer.kt | 20 +- workflow-ui/core-android/api/core-android.api | 91 ++++--- .../ui/DecorativeScreenViewFactoryTest.kt | 230 ------------------ .../ui/WorkflowViewStubLifecycleActivity.kt | 21 +- .../ui/WorkflowViewStubLifecycleTest.kt | 107 ++++---- .../BackStackContainerPersistenceTest.kt | 8 +- .../ui/container/BackStackContainerTest.kt | 26 +- .../ui/container/ViewStateCacheTest.kt | 66 ++--- .../BackStackContainerLifecycleActivity.kt | 76 +++--- .../NoTransitionBackStackContainer.kt | 24 +- .../workflow1/ui/AndroidViewRegistry.kt | 2 +- .../workflow1/ui/AsScreenViewFactory.kt | 53 ++-- .../workflow1/ui/BuilderViewFactory.kt | 2 +- .../ui/DecorativeScreenViewFactory.kt | 179 -------------- .../workflow1/ui/DecorativeViewFactory.kt | 2 +- .../workflow1/ui/LayoutScreenViewFactory.kt | 20 +- .../workflow1/ui/ManualScreenViewFactory.kt | 43 ---- .../workflow1/ui/NamedScreenViewFactory.kt | 15 +- .../workflow1/ui/ScreenViewFactory.kt | 187 +++++++++----- .../workflow1/ui/ScreenViewFactoryFinder.kt | 11 +- .../squareup/workflow1/ui/ScreenViewHolder.kt | 55 +++++ .../squareup/workflow1/ui/ScreenViewRunner.kt | 50 +++- .../ui/ViewBindingScreenViewFactory.kt | 25 +- .../workflow1/ui/ViewSetViewRunner.kt | 31 +++ .../workflow1/ui/ViewShowRendering.kt | 12 + .../workflow1/ui/WorkflowViewState.kt | 5 +- .../squareup/workflow1/ui/WorkflowViewStub.kt | 66 +++-- .../ui/androidx/WorkflowLifecycleOwner.kt | 2 +- .../ui/container/AndroidDialogBounds.kt | 8 - .../ui/container/BackButtonScreen.kt | 22 +- .../ui/container/BackStackContainer.kt | 78 +++--- .../container/BackStackScreenViewFactory.kt | 11 +- .../ui/container/BodyAndModalsContainer.kt | 28 ++- .../container/EnvironmentScreenViewFactory.kt | 28 ++- .../ModalScreenOverlayDialogFactory.kt | 33 ++- .../ui/container/OverlayDialogFactory.kt | 4 +- .../workflow1/ui/container/ViewStateCache.kt | 28 +-- .../core-android/src/main/res/values/ids.xml | 13 +- .../ui/LegacyAndroidViewRegistryTest.kt | 2 +- .../workflow1/ui/ScreenViewFactoryTest.kt | 41 ++-- workflow-ui/core-common/api/core-common.api | 6 + .../com/squareup/workflow1/ui/Compatible.kt | 4 +- .../java/com/squareup/workflow1/ui/Screen.kt | 15 +- .../api/internal-testing-android.api | 4 +- .../test/AbstractLifecycleTestActivity.kt | 35 ++- .../internal/test/WorkflowUiTestActivity.kt | 3 +- 60 files changed, 943 insertions(+), 1243 deletions(-) create mode 100644 workflow-ui/compose/src/main/res/values/ids.xml delete mode 100644 workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeScreenViewFactoryTest.kt delete mode 100644 workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeScreenViewFactory.kt delete mode 100644 workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ManualScreenViewFactory.kt create mode 100644 workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewHolder.kt create mode 100644 workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewSetViewRunner.kt diff --git a/samples/containers/android/src/main/java/com/squareup/sample/container/ScrimContainer.kt b/samples/containers/android/src/main/java/com/squareup/sample/container/ScrimContainer.kt index a0748d8de4..1f8dbe2dc7 100644 --- a/samples/containers/android/src/main/java/com/squareup/sample/container/ScrimContainer.kt +++ b/samples/containers/android/src/main/java/com/squareup/sample/container/ScrimContainer.kt @@ -7,11 +7,11 @@ import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat import com.squareup.sample.container.panel.ScrimScreen -import com.squareup.workflow1.ui.ManualScreenViewFactory import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bindBuiltView import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub -import com.squareup.workflow1.ui.bindShowRendering /** * A view that renders only its first child, behind a smoke scrim if @@ -91,23 +91,20 @@ internal class ScrimContainer @JvmOverloads constructor( } @OptIn(WorkflowUiExperimentalApi::class) - companion object : ScreenViewFactory> by ManualScreenViewFactory( - type = ScrimScreen::class, - viewConstructor = { initialRendering, initialViewEnvironment, contextForNewView, _ -> - val stub = WorkflowViewStub(contextForNewView) - - ScrimContainer(contextForNewView) + companion object : ScreenViewFactory> by bindBuiltView( + buildViewAndRunner = { _, context, _ -> + val stub = WorkflowViewStub(context) + val scrimContainer = ScrimContainer(context) .also { view -> view.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) view.addView(stub) - - view.bindShowRendering( - initialRendering, initialViewEnvironment - ) { rendering, environment -> - stub.show(rendering.content, environment) - view.isDimmed = rendering.dimmed - } } + val runner = ScreenViewRunner> { rendering, viewEnvironment -> + stub.show(rendering.content, viewEnvironment) + scrimContainer.isDimmed = rendering.dimmed + } + + Pair(scrimContainer, runner) } ) } diff --git a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardView.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardView.kt index 28651a4fab..f050c46847 100644 --- a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardView.kt +++ b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardView.kt @@ -8,10 +8,8 @@ import android.graphics.Rect import android.view.View import androidx.core.content.ContextCompat import com.squareup.sample.dungeon.board.Board -import com.squareup.workflow1.ui.ManualScreenViewFactory import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.bindShowRendering import kotlin.math.abs import kotlin.math.min @@ -83,11 +81,8 @@ class BoardView(context: Context) : View(context) { } @OptIn(WorkflowUiExperimentalApi::class) - companion object : ScreenViewFactory by ManualScreenViewFactory( - type = Board::class, - viewConstructor = { initialRendering, initialEnv, contextForNewView, _ -> - BoardView(contextForNewView) - .apply { bindShowRendering(initialRendering, initialEnv) { r, _ -> update(r) } } - } + companion object : ScreenViewFactory by ScreenViewFactory( + buildView = { _, context, _ -> BoardView(context) }, + updateView = { view, rendering, _ -> (view as BoardView).update(rendering) } ) } diff --git a/samples/stub-visibility/src/main/java/com/squareup/sample/stubvisibility/ClickyTextRendering.kt b/samples/stub-visibility/src/main/java/com/squareup/sample/stubvisibility/ClickyTextRendering.kt index 2a68a552a5..f8279b0305 100644 --- a/samples/stub-visibility/src/main/java/com/squareup/sample/stubvisibility/ClickyTextRendering.kt +++ b/samples/stub-visibility/src/main/java/com/squareup/sample/stubvisibility/ClickyTextRendering.kt @@ -9,9 +9,8 @@ import android.view.ViewGroup.LayoutParams import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.TextView import com.squareup.workflow1.ui.AndroidScreen -import com.squareup.workflow1.ui.ManualScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.bindShowRendering @OptIn(WorkflowUiExperimentalApi::class) data class ClickyTextRendering( @@ -19,21 +18,20 @@ data class ClickyTextRendering( val visible: Boolean = true, val onClick: (() -> Unit)? = null ) : AndroidScreen { - override val viewFactory = ManualScreenViewFactory( - type = ClickyTextRendering::class, - viewConstructor = { initialRendering, initialEnv, context, _ -> + override val viewFactory = ScreenViewFactory( + buildView = { _, context, _ -> TextView(context).also { textView -> textView.layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT) textView.gravity = CENTER - - textView.bindShowRendering(initialRendering, initialEnv) { clickyText, _ -> - textView.text = clickyText.message - textView.isVisible = clickyText.visible - textView.setOnClickListener( - clickyText.onClick?.let { oc -> OnClickListener { oc() } } - ) - } } + }, + updateView = { view, rendering, _ -> + val textView = view as TextView + textView.text = rendering.message + textView.isVisible = rendering.visible + textView.setOnClickListener( + rendering.onClick?.let { oc -> OnClickListener { oc() } } + ) } ) } diff --git a/samples/tictactoe/app/src/androidTest/java/com/squareup/sample/TicTacToeEspressoTest.kt b/samples/tictactoe/app/src/androidTest/java/com/squareup/sample/TicTacToeEspressoTest.kt index d369dffe61..b3364cc098 100644 --- a/samples/tictactoe/app/src/androidTest/java/com/squareup/sample/TicTacToeEspressoTest.kt +++ b/samples/tictactoe/app/src/androidTest/java/com/squareup/sample/TicTacToeEspressoTest.kt @@ -2,14 +2,12 @@ package com.squareup.sample import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT -import android.view.View import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.ViewInteraction import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.closeSoftKeyboard import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withClassName import androidx.test.espresso.matcher.ViewMatchers.withId @@ -18,16 +16,9 @@ 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.google.common.truth.Truth.assertThat -import com.squareup.sample.gameworkflow.GamePlayScreen -import com.squareup.sample.gameworkflow.Player -import com.squareup.sample.gameworkflow.symbol import com.squareup.sample.mainactivity.TicTacToeActivity import com.squareup.sample.tictactoe.R -import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.environment -import com.squareup.workflow1.ui.getRendering import com.squareup.workflow1.ui.internal.test.DetectLeaksAfterTestSuccess import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule import com.squareup.workflow1.ui.internal.test.actuallyPressBack @@ -40,7 +31,6 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain import org.junit.runner.RunWith -import java.util.concurrent.atomic.AtomicReference /** * This app is our most complex sample, which makes it a great candidate for @@ -74,52 +64,6 @@ class TicTacToeEspressoTest { } } - @Test fun showRenderingTagStaysFresh() { - // Start a game so that there's something interesting in the Activity window. - // (Prior screens are all in a dialog window.) - - inAnyView(withId(R.id.login_email)).type("foo@bar") - inAnyView(withId(R.id.login_password)).type("password") - inAnyView(withId(R.id.login_button)).perform(click()) - - inAnyView(withId(R.id.start_game)).perform(click()) - - val environment = AtomicReference() - - // Why should I learn how to write a matcher when I can just grab the activity - // and work with it directly? - scenario.onActivity { activity -> - val button = activity.findViewById(R.id.game_play_board) - val parent = button.parent as View - val rendering = parent.getRendering()!! - assertThat(rendering.gameState.playing).isSameInstanceAs(Player.X) - val firstEnv = parent.environment - assertThat(firstEnv).isNotNull() - environment.set(firstEnv) - - // Make a move. - rendering.onClick(0, 0) - } - - // I'm not an animal, though. Pop back out to the test to check that the update - // has happened, to make sure the idle check is allowed to do its thing. (Didn't - // actually seem to be necessary, originally did everything synchronously in the - // lambda above and it all worked just fine. But that seems like a land mine.) - - inAnyView(withId(R.id.game_play_toolbar)) - .check(matches(hasDescendant(withText("O, place your ${Player.O.symbol}")))) - - // Now that we're confident the views have updated, back to the activity - // to mess with what should be the updated rendering. - scenario.onActivity { activity -> - val button = activity.findViewById(R.id.game_play_board) - val parent = button.parent as View - val rendering = parent.getRendering()!! - assertThat(rendering.gameState.playing).isSameInstanceAs(Player.O) - assertThat(parent.environment).isEqualTo(environment.get()) - } - } - @Test fun configChangeReflectsWorkflowState() { inAnyView(withId(R.id.login_email)).type("bad email") inAnyView(withId(R.id.login_button)).perform(click()) diff --git a/workflow-core/src/main/java/com/squareup/workflow1/BaseRenderContext.kt b/workflow-core/src/main/java/com/squareup/workflow1/BaseRenderContext.kt index c3f669bd94..1f1d906ba7 100644 --- a/workflow-core/src/main/java/com/squareup/workflow1/BaseRenderContext.kt +++ b/workflow-core/src/main/java/com/squareup/workflow1/BaseRenderContext.kt @@ -101,9 +101,6 @@ public interface BaseRenderContext { sideEffect: suspend CoroutineScope.() -> Unit ) - // TODO(218): We'd prefer the eventHandler methods to be extensions, but the - // compiler disagrees. https://youtrack.jetbrains.com/issue/KT-42741 - /** * Creates a function which builds a [WorkflowAction] from the * given [update] function, and immediately passes it to [actionSink]. Handy for diff --git a/workflow-ui/compose/api/compose.api b/workflow-ui/compose/api/compose.api index 47af2e7c4f..2aedb7639a 100644 --- a/workflow-ui/compose/api/compose.api +++ b/workflow-ui/compose/api/compose.api @@ -30,7 +30,8 @@ public abstract class com/squareup/workflow1/ui/compose/ComposeScreenViewFactory public static final field $stable I public fun ()V public abstract fun Content (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroidx/compose/runtime/Composer;I)V - public final fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; + public final fun buildView (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; + public final fun updateView (Landroid/view/View;Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V } public final class com/squareup/workflow1/ui/compose/ComposeScreenViewFactoryKt { 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 2654e0e21e..488e476924 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 @@ -32,7 +32,6 @@ import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.bindShowRendering import com.squareup.workflow1.ui.container.AndroidOverlay import com.squareup.workflow1.ui.container.BackStackScreen import com.squareup.workflow1.ui.container.BodyAndModalsScreen @@ -583,24 +582,26 @@ internal class ComposeViewTreeIntegrationTest { override val viewFactory: ScreenViewFactory get() = this override fun buildView( - initialRendering: TestComposeRendering, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, + environment: ViewEnvironment, + context: Context, container: ViewGroup? ): View { - var lastCompositionStrategy = initialRendering.disposeStrategy - - return ComposeView(contextForNewView).apply { - lastCompositionStrategy?.let(::setViewCompositionStrategy) - - bindShowRendering(initialRendering, initialViewEnvironment) { rendering, _ -> - if (rendering.disposeStrategy != lastCompositionStrategy) { - lastCompositionStrategy = rendering.disposeStrategy - lastCompositionStrategy?.let(::setViewCompositionStrategy) - } + return ComposeView(context) + } - setContent(rendering.content) + override fun updateView( + view: View, + rendering: TestComposeRendering, + environment: ViewEnvironment + ) { + (view as ComposeView).let { composeView -> + val lastCompositionStrategy = composeView.tag as? ViewCompositionStrategy + composeView.tag = rendering.disposeStrategy + if (rendering.disposeStrategy != lastCompositionStrategy) { + lastCompositionStrategy?.let { composeView.setViewCompositionStrategy(it) } } + + composeView.setContent(rendering.content) } } } diff --git a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/NoTransitionBackStackContainer.kt b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/NoTransitionBackStackContainer.kt index 5b8ebba7cc..e293008bbb 100644 --- a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/NoTransitionBackStackContainer.kt +++ b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/NoTransitionBackStackContainer.kt @@ -1,12 +1,11 @@ package com.squareup.workflow1.ui.compose import android.content.Context -import android.view.View import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import com.squareup.workflow1.ui.ManualScreenViewFactory +import com.squareup.workflow1.ui.NamedScreen import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewHolder import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.bindShowRendering import com.squareup.workflow1.ui.container.BackStackContainer import com.squareup.workflow1.ui.container.BackStackScreen import com.squareup.workflow1.ui.container.R @@ -21,28 +20,24 @@ import com.squareup.workflow1.ui.container.R internal class NoTransitionBackStackContainer(context: Context) : BackStackContainer(context) { override fun performTransition( - oldViewMaybe: View?, - newView: View, + oldHolderMaybe: ScreenViewHolder>?, + newHolder: ScreenViewHolder>, popped: Boolean ) { - oldViewMaybe?.let(::removeView) - addView(newView) + oldHolderMaybe?.view?.let(::removeView) + addView(newHolder.view) } - companion object : ScreenViewFactory> - by ManualScreenViewFactory( - type = BackStackScreen::class, - viewConstructor = { initialRendering, initialEnv, context, _ -> + companion object : ScreenViewFactory> by ScreenViewFactory( + buildView = { _, context, _ -> NoTransitionBackStackContainer(context) .apply { id = R.id.workflow_back_stack_container layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT) - bindShowRendering( - initialRendering, - initialEnv, - { newRendering, newViewEnvironment -> update(newRendering, newViewEnvironment) } - ) } + }, + updateView = { view, rendering, environment -> + (view as NoTransitionBackStackContainer).update(rendering, environment) } ) } 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 7ca7062e43..3bcd153f83 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 @@ -2,9 +2,7 @@ package com.squareup.workflow1.ui.compose -import android.content.Context import android.view.View -import android.view.ViewGroup import android.widget.TextView import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -60,14 +58,12 @@ 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.ManualScreenViewFactory import com.squareup.workflow1.ui.NamedScreen import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.ViewEnvironment 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.DetectLeaksAfterTestSuccess import com.squareup.workflow1.ui.internal.test.IdleAfterTestRule import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule @@ -233,27 +229,22 @@ internal class WorkflowRenderingTest { val lifecycleEvents = mutableListOf() class LifecycleRecorder : AndroidScreen { - override val viewFactory: ScreenViewFactory = ManualScreenViewFactory( - LifecycleRecorder::class - ) { initialRendering, initialViewEnvironment, contextForNewView, _ -> - object : View(contextForNewView) { - init { - bindShowRendering(initialRendering, initialViewEnvironment) { _, _ -> } - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - val lifecycle = ViewTreeLifecycleOwner.get(this)!!.lifecycle - lifecycle.addObserver( - LifecycleEventObserver { _, event -> - lifecycleEvents += event - } - ) - // Yes, we're leaking the observer. That's intentional: we need to make sure we see - // any lifecycle events that happen even after the composable is destroyed. + override val viewFactory = ScreenViewFactory( + buildView = { _, context, _ -> + object : View(context) { + override fun onAttachedToWindow() { + super.onAttachedToWindow() + val lifecycle = ViewTreeLifecycleOwner.get(this)!!.lifecycle + lifecycle.addObserver( + LifecycleEventObserver { _, event -> lifecycleEvents += event } + ) + // Yes, we're leaking the observer. That's intentional: we need to make sure we see + // any lifecycle events that happen even after the composable is destroyed. + } } - } - } + }, + updateView = { _, _, _ -> /* Noop */ } + ) } class EmptyRendering : ComposableRendering { @@ -386,17 +377,10 @@ internal class WorkflowRenderingTest { val viewId = View.generateViewId() class LegacyRendering(private val viewId: Int) : AndroidScreen { - override val viewFactory: ScreenViewFactory = ManualScreenViewFactory( - LegacyRendering::class - ) { initialRendering, initialViewEnvironment, contextForNewView, _ -> - object : View(contextForNewView) { - init { - bindShowRendering(initialRendering, initialViewEnvironment) { r, _ -> - id = r.viewId - } - } - } - } + override val viewFactory = ScreenViewFactory( + buildView = { _, context, _ -> View(context) }, + updateView = { view, rendering, _ -> view.id = rendering.viewId } + ) } composeRule.setContent { @@ -566,20 +550,9 @@ internal class WorkflowRenderingTest { private data class LegacyViewRendering( val text: String ) : AndroidScreen { - override val viewFactory: ScreenViewFactory = - object : ScreenViewFactory { - override val type = LegacyViewRendering::class - - override fun buildView( - initialRendering: LegacyViewRendering, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, - container: ViewGroup? - ): View = TextView(contextForNewView).apply { - bindShowRendering(initialRendering, initialViewEnvironment) { rendering, _ -> - text = rendering.text - } - } - } + override val viewFactory = ScreenViewFactory( + buildView = { _, context, _ -> TextView(context) }, + updateView = { view, rendering, _ -> (view as TextView).text = rendering.text } + ) } } diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeScreenViewFactory.kt b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeScreenViewFactory.kt index 3388da096b..fcd858c7e2 100644 --- a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeScreenViewFactory.kt +++ b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeScreenViewFactory.kt @@ -12,7 +12,6 @@ import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.bindShowRendering import kotlin.reflect.KClass /** @@ -115,8 +114,8 @@ public abstract class ComposeScreenViewFactory : ScreenViewFactory { /** - * The composable content of this [ScreenViewFactory]. This method will be called any time [rendering] - * or [viewEnvironment] change. It is the Compose-based analogue of + * The composable content of this [ScreenViewFactory]. This method will be called + * any time [rendering] or [viewEnvironment] change. It is the Compose-based analogue of * [ScreenViewRunner.showRendering][com.squareup.workflow1.ui.ScreenViewRunner.showRendering]. */ @Composable public abstract fun Content( @@ -125,21 +124,18 @@ public abstract class ComposeScreenViewFactory : ) final override fun buildView( - initialRendering: RenderingT, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, + environment: ViewEnvironment, + context: Context, container: ViewGroup? - ): View = ComposeView(contextForNewView).also { composeView -> + ): View = ComposeView(context) + + final override fun updateView( + view: View, + rendering: RenderingT, + environment: ViewEnvironment + ) { // Update the state whenever a new rendering is emitted. - // This lambda will be executed synchronously before bindShowRendering returns. - composeView.bindShowRendering( - initialRendering, - initialViewEnvironment - ) { rendering, environment -> - // Entry point to the world of Compose. - composeView.setContent { - Content(rendering, environment) - } - } + // This lambda will be executed synchronously before updateView returns. + (view as ComposeView).setContent { Content(rendering, environment) } } } diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/CompositionRoot.kt b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/CompositionRoot.kt index d110272eec..d6f101b65f 100644 --- a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/CompositionRoot.kt +++ b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/CompositionRoot.kt @@ -35,8 +35,8 @@ private val LocalHasViewFactoryRootBeenApplied = staticCompositionLocalOf { fals public typealias CompositionRoot = @Composable (content: @Composable () -> Unit) -> Unit /** - * Convenience function for applying a [CompositionRoot] to this [ViewEnvironment]'s [ViewRegistry]. - * See [ViewRegistry.withCompositionRoot]. + * Convenience function for applying a [CompositionRoot] to this [ViewEnvironment]'s + * [ScreenViewFactoryFinder]. See [ScreenViewFactoryFinder.withCompositionRoot]. */ @WorkflowUiExperimentalApi public fun ViewEnvironment.withCompositionRoot(root: CompositionRoot): ViewEnvironment { diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt index 9bc0c50c65..0c8ce90f60 100644 --- a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt +++ b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt @@ -1,4 +1,5 @@ @file:Suppress("DEPRECATION") + package com.squareup.workflow1.ui.compose import android.view.View @@ -22,24 +23,25 @@ import com.squareup.workflow1.ui.Compatible import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.ScreenViewFactoryFinder +import com.squareup.workflow1.ui.ScreenViewHolder import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.ViewFactory import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub import com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner -import com.squareup.workflow1.ui.getShowRendering -import com.squareup.workflow1.ui.showRendering import com.squareup.workflow1.ui.start +import com.squareup.workflow1.ui.toViewFactory import kotlin.reflect.KClass /** - * Renders [rendering] into the composition using this [ViewEnvironment]'s [ViewRegistry] to - * generate the view. + * Renders [rendering] into the composition using this [ViewEnvironment]'s + * [ScreenViewFactoryFinder] to generate the view. * - * This function fulfills a similar role as [WorkflowViewStub], but is much more convenient to use - * from Composable functions. Note, however, that just like [WorkflowViewStub], it doesn't matter - * whether the factory registered for the rendering is using classic Android views or Compose. + * This function fulfills a similar role as [ScreenViewHolder] and [WorkflowViewStub], + * but is much more convenient to use from Composable functions. Note that, + * just as with [ScreenViewHolder] and [WorkflowViewStub], it doesn't matter whether + * the factory registered for the rendering is using classic Android views or Compose. * * ## Example * @@ -86,9 +88,7 @@ import kotlin.reflect.KClass // intentionally don't ask it for a new instance every time to match the behavior of // WorkflowViewStub and other containers, which only ask for a new factory when the rendering is // incompatible. - viewEnvironment[ScreenViewFactoryFinder] - .getViewFactoryForRendering(viewEnvironment, rendering) - .asComposeViewFactory() + rendering.toViewFactory(viewEnvironment).asComposeViewFactory() } // Just like WorkflowViewStub, we need to manage a Lifecycle for the child view. We just provide @@ -102,7 +102,9 @@ import kotlin.reflect.KClass // into this function is to directly control the layout of the child view – which means // minimum constraints are likely to be significant. Box(modifier, propagateMinConstraints = true) { - viewFactory.Content(rendering, viewEnvironment) + // Note that we add rendering to the viewEnvironment, to honor the contract + // documented on Screen. + viewFactory.Content(rendering, viewEnvironment + (Screen to rendering)) } } } @@ -151,11 +153,11 @@ import kotlin.reflect.KClass * otherwise it wraps the factory in one that manages a classic Android view. */ @OptIn(WorkflowUiExperimentalApi::class) -private fun ScreenViewFactory.asComposeViewFactory() = - (this as? ComposeScreenViewFactory) ?: object : ComposeScreenViewFactory() { +private fun ScreenViewFactory.asComposeViewFactory() = + (this as? ComposeScreenViewFactory) ?: object : ComposeScreenViewFactory() { private val originalFactory = this@asComposeViewFactory - override val type: KClass get() = originalFactory.type + override val type: KClass get() = originalFactory.type /** * This is effectively the logic of [WorkflowViewStub], but translated into Compose idioms. @@ -174,7 +176,7 @@ private fun ScreenViewFactory.asComposeViewFactory() = * [viewEnvironment], and adds it to the composition. */ @Composable override fun Content( - rendering: R, + rendering: ScreenT, viewEnvironment: ViewEnvironment ) { val lifecycleOwner = LocalLifecycleOwner.current @@ -184,29 +186,27 @@ private fun ScreenViewFactory.asComposeViewFactory() = // We pass in a null container because the container isn't a View, it's a composable. The // compose machinery will generate an intermediate view that it ends up adding this to but // we don't have access to that. - originalFactory.buildView(rendering, viewEnvironment, context, container = null) - .also { view -> - view.start() - - // Mirrors the check done in ViewRegistry.buildView. - checkNotNull(view.getShowRendering()) { - "View.bindShowRendering should have been called for $view, typically by the " + - "ScreenViewFactory that created it." - } - + originalFactory.start(rendering, viewEnvironment, context, container = null) + .let { viewHolder -> + // Put the viewHolder in a tag so that we can find it in the update lambda, below. + viewHolder.view.setTag(R.id.workflow_screen_view_holder, viewHolder) // Unfortunately AndroidView doesn't propagate this itself. - ViewTreeLifecycleOwner.set(view, lifecycleOwner) + ViewTreeLifecycleOwner.set(viewHolder.view, lifecycleOwner) // We don't propagate the (non-compose) SavedStateRegistryOwner, or the (compose) // SaveableStateRegistry, because currently all our navigation is implemented as // Android views, which ensures there is always an Android view between any state // registry and any Android view shown as a child of it, even if there's a compose // view in between. + viewHolder.view } }, // This function will be invoked every time this composable is recomposed, which means that // any time a new rendering or view environment are passed in we'll send them to the view. update = { view -> - view.showRendering(rendering, viewEnvironment) + @Suppress("UNCHECKED_CAST") + val viewHolder = + view.getTag(R.id.workflow_screen_view_holder) as ScreenViewHolder + viewHolder.show(rendering, viewEnvironment) } ) } diff --git a/workflow-ui/compose/src/main/res/values/ids.xml b/workflow-ui/compose/src/main/res/values/ids.xml new file mode 100644 index 0000000000..39544e2f03 --- /dev/null +++ b/workflow-ui/compose/src/main/res/values/ids.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/modal/test/ModalViewContainerLifecycleActivity.kt b/workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/modal/test/ModalViewContainerLifecycleActivity.kt index 9a3ce908b7..0364b71064 100644 --- a/workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/modal/test/ModalViewContainerLifecycleActivity.kt +++ b/workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/modal/test/ModalViewContainerLifecycleActivity.kt @@ -2,41 +2,32 @@ package com.squareup.workflow1.ui.modal.test -import android.content.Context import android.view.View -import android.view.ViewGroup import android.widget.FrameLayout import com.squareup.workflow1.ui.Compatible -import com.squareup.workflow1.ui.ManualScreenViewFactory import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bindBuiltView import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub import com.squareup.workflow1.ui.asScreen -import com.squareup.workflow1.ui.bindShowRendering import com.squareup.workflow1.ui.internal.test.AbstractLifecycleTestActivity import com.squareup.workflow1.ui.modal.HasModals import com.squareup.workflow1.ui.modal.ModalViewContainer import com.squareup.workflow1.ui.modal.test.ModalViewContainerLifecycleActivity.TestRendering.LeafRendering import com.squareup.workflow1.ui.modal.test.ModalViewContainerLifecycleActivity.TestRendering.RecurseRendering -import kotlin.reflect.KClass @OptIn(WorkflowUiExperimentalApi::class) internal class ModalViewContainerLifecycleActivity : AbstractLifecycleTestActivity() { - object BaseRendering : Screen, ScreenViewFactory { - override val type: KClass = BaseRendering::class - override fun buildView( - initialRendering: BaseRendering, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, - container: ViewGroup? - ): View = View(contextForNewView).apply { - bindShowRendering(initialRendering, initialViewEnvironment) { _, _ -> /* Noop */ } - } - } + object BaseRendering : + Screen, + ScreenViewFactory by ScreenViewFactory( + buildView = { _, context, _ -> View(context) }, + updateView = { _, _, _ -> /* Noop */ } + ) data class TestModals( override val modals: List @@ -56,20 +47,17 @@ internal class ModalViewContainerLifecycleActivity : AbstractLifecycleTestActivi ModalViewContainer.binding(), BaseRendering, leafViewBinding(LeafRendering::class, lifecycleLoggingViewObserver { it.name }), - ManualScreenViewFactory(RecurseRendering::class) { initialRendering, - initialViewEnvironment, - contextForNewView, _ -> - FrameLayout(contextForNewView).also { container -> - val stub = WorkflowViewStub(contextForNewView) + bindBuiltView { _, context, _ -> + val stub = WorkflowViewStub(context) + val frame = FrameLayout(context).also { container -> container.addView(stub) - container.bindShowRendering( - initialRendering, - initialViewEnvironment - ) { rendering, env -> - stub.show(asScreen(TestModals(listOf(rendering.wrapped))), env) - } } - }, + val runner = ScreenViewRunner { rendering, viewEnvironment -> + stub.show(asScreen(TestModals(listOf(rendering.wrapped))), viewEnvironment) + } + + Pair(frame, runner) + } ) fun update(vararg modals: TestRendering) = diff --git a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalViewContainer.kt b/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalViewContainer.kt index eab62a01be..87f75474c0 100644 --- a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalViewContainer.kt +++ b/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalViewContainer.kt @@ -13,17 +13,17 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import androidx.annotation.IdRes import com.squareup.workflow1.ui.BuilderViewFactory +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.ScreenViewHolder import com.squareup.workflow1.ui.ViewEnvironment 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.buildView import com.squareup.workflow1.ui.container.BackButtonScreen import com.squareup.workflow1.ui.modal.ModalViewContainer.Companion.binding import com.squareup.workflow1.ui.onBackPressedDispatcherOwnerOrNull -import com.squareup.workflow1.ui.showRendering -import com.squareup.workflow1.ui.start +import com.squareup.workflow1.ui.toView import kotlin.reflect.KClass /** @@ -71,14 +71,13 @@ public open class ModalViewContainer @JvmOverloads constructor( // that should be blocked by this modal session. val wrappedRendering = BackButtonScreen(asScreen(initialModalRendering)) { } - val view = wrappedRendering.buildView( - viewEnvironment = initialViewEnvironment, + val viewHolder = wrappedRendering.toView( + initialViewEnvironment = initialViewEnvironment, contextForNewView = this.context, container = this ) - view.start() - return buildDialogForView(view) + return buildDialogForView(viewHolder.view) .apply { // Dialogs are modal windows and so they block events, including back button presses // -- that's their job! But we *want* the Activity's onBackPressedDispatcher to fire @@ -90,7 +89,7 @@ public open class ModalViewContainer @JvmOverloads constructor( setOnKeyListener { _, keyCode, keyEvent -> if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.action == ACTION_UP) { - view.context.onBackPressedDispatcherOwnerOrNull() + viewHolder.view.context.onBackPressedDispatcherOwnerOrNull() ?.onBackPressedDispatcher ?.let { if (it.hasEnabledCallbacks()) it.onBackPressed() @@ -102,7 +101,7 @@ public open class ModalViewContainer @JvmOverloads constructor( } } .run { - DialogRef(initialModalRendering, initialViewEnvironment, this, view) + DialogRef(initialModalRendering, initialViewEnvironment, this, viewHolder) } } @@ -113,7 +112,8 @@ public open class ModalViewContainer @JvmOverloads constructor( // able to do compatibility checks against it when deciding whether // or not to update the existing dialog.) val wrappedRendering = BackButtonScreen(asScreen(modalRendering)) { } - (extra as View).showRendering(wrappedRendering, viewEnvironment) + @Suppress("UNCHECKED_CAST") + (extra as ScreenViewHolder).show(wrappedRendering, viewEnvironment) } } diff --git a/workflow-ui/core-android/api/core-android.api b/workflow-ui/core-android/api/core-android.api index 64b1b38ad6..7f86fe3370 100644 --- a/workflow-ui/core-android/api/core-android.api +++ b/workflow-ui/core-android/api/core-android.api @@ -33,11 +33,8 @@ public final class com/squareup/workflow1/ui/AsScreenLegacyViewFactory : com/squ public fun getType ()Lkotlin/reflect/KClass; } -public final class com/squareup/workflow1/ui/AsScreenViewFactory : com/squareup/workflow1/ui/ScreenViewFactory { - public static final field INSTANCE Lcom/squareup/workflow1/ui/AsScreenViewFactory; - public fun buildView (Lcom/squareup/workflow1/ui/AsScreen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; - public synthetic fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; - public fun getType ()Lkotlin/reflect/KClass; +public final class com/squareup/workflow1/ui/AsScreenViewFactoryKt { + public static final fun AsScreenViewFactory (Lcom/squareup/workflow1/ui/AsScreen;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/ScreenViewFactory; } public final class com/squareup/workflow1/ui/BackButtonScreen : com/squareup/workflow1/ui/AndroidViewRendering { @@ -68,15 +65,6 @@ public final class com/squareup/workflow1/ui/BuilderViewFactory : com/squareup/w public fun getType ()Lkotlin/reflect/KClass; } -public final class com/squareup/workflow1/ui/DecorativeScreenViewFactory : com/squareup/workflow1/ui/ScreenViewFactory { - public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/ui/ViewStarter;Lkotlin/jvm/functions/Function4;)V - public synthetic fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/ui/ViewStarter;Lkotlin/jvm/functions/Function4;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function2;Lcom/squareup/workflow1/ui/ViewStarter;Lkotlin/jvm/functions/Function4;)V - public synthetic fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function2;Lcom/squareup/workflow1/ui/ViewStarter;Lkotlin/jvm/functions/Function4;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; - public fun getType ()Lkotlin/reflect/KClass; -} - public final class com/squareup/workflow1/ui/DecorativeViewFactory : com/squareup/workflow1/ui/ViewFactory { public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/ui/ViewStarter;Lkotlin/jvm/functions/Function4;)V public synthetic fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/ui/ViewStarter;Lkotlin/jvm/functions/Function4;ILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -111,21 +99,13 @@ public final class com/squareup/workflow1/ui/LayoutRunnerViewFactory : com/squar public final class com/squareup/workflow1/ui/LayoutScreenViewFactory : com/squareup/workflow1/ui/ScreenViewFactory { public fun (Lkotlin/reflect/KClass;ILkotlin/jvm/functions/Function1;)V - public fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; + public fun buildView (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; public fun getType ()Lkotlin/reflect/KClass; + public fun updateView (Landroid/view/View;Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V } -public final class com/squareup/workflow1/ui/ManualScreenViewFactory : com/squareup/workflow1/ui/ScreenViewFactory { - public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function4;)V - public fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; - public fun getType ()Lkotlin/reflect/KClass; -} - -public final class com/squareup/workflow1/ui/NamedScreenViewFactory : com/squareup/workflow1/ui/ScreenViewFactory { - public static final field INSTANCE Lcom/squareup/workflow1/ui/NamedScreenViewFactory; - public fun buildView (Lcom/squareup/workflow1/ui/NamedScreen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; - public synthetic fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; - public fun getType ()Lkotlin/reflect/KClass; +public final class com/squareup/workflow1/ui/NamedScreenViewFactoryKt { + public static final fun NamedScreenViewFactory (Lcom/squareup/workflow1/ui/NamedScreen;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/ScreenViewFactory; } public final class com/squareup/workflow1/ui/NamedViewFactory : com/squareup/workflow1/ui/ViewFactory { @@ -170,11 +150,12 @@ public final class com/squareup/workflow1/ui/PickledTreesnapshot$CREATOR : andro } public abstract interface class com/squareup/workflow1/ui/ScreenViewFactory : com/squareup/workflow1/ui/ViewRegistry$Entry { - public abstract fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; + public abstract fun buildView (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; + public abstract fun updateView (Landroid/view/View;Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V } public final class com/squareup/workflow1/ui/ScreenViewFactory$DefaultImpls { - public static synthetic fun buildView$default (Lcom/squareup/workflow1/ui/ScreenViewFactory;Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;ILjava/lang/Object;)Landroid/view/View; + public static synthetic fun buildView$default (Lcom/squareup/workflow1/ui/ScreenViewFactory;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;ILjava/lang/Object;)Landroid/view/View; } public abstract interface class com/squareup/workflow1/ui/ScreenViewFactoryFinder { @@ -192,8 +173,21 @@ public final class com/squareup/workflow1/ui/ScreenViewFactoryFinder$DefaultImpl } public final class com/squareup/workflow1/ui/ScreenViewFactoryKt { - public static final fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lcom/squareup/workflow1/ui/ViewStarter;)Landroid/view/View; - public static synthetic fun buildView$default (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lcom/squareup/workflow1/ui/ViewStarter;ILjava/lang/Object;)Landroid/view/View; + public static final synthetic fun ScreenViewFactory (Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;)Lcom/squareup/workflow1/ui/ScreenViewFactory; + public static final fun start (Lcom/squareup/workflow1/ui/ScreenViewFactory;Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lcom/squareup/workflow1/ui/ViewStarter;)Lcom/squareup/workflow1/ui/ScreenViewHolder; + public static synthetic fun start$default (Lcom/squareup/workflow1/ui/ScreenViewFactory;Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lcom/squareup/workflow1/ui/ViewStarter;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/ScreenViewHolder; + public static final fun toView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lcom/squareup/workflow1/ui/ViewStarter;)Lcom/squareup/workflow1/ui/ScreenViewHolder; + public static synthetic fun toView$default (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lcom/squareup/workflow1/ui/ViewStarter;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/ScreenViewHolder; + public static final fun toViewFactory (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/ScreenViewFactory; +} + +public final class com/squareup/workflow1/ui/ScreenViewHolder { + public fun (Lcom/squareup/workflow1/ui/ScreenViewFactory;Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/Screen;Landroid/view/View;)V + public final fun canShow (Lcom/squareup/workflow1/ui/Screen;)Z + public final fun getEnvironment ()Lcom/squareup/workflow1/ui/ViewEnvironment; + public final fun getScreen ()Lcom/squareup/workflow1/ui/Screen; + public final fun getView ()Landroid/view/View; + public final fun show (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V } public abstract interface class com/squareup/workflow1/ui/ScreenViewRunner { @@ -205,6 +199,7 @@ public final class com/squareup/workflow1/ui/ScreenViewRunner$Companion { public final synthetic fun bind (ILkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/ScreenViewFactory; public final synthetic fun bind (Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/ScreenViewFactory; public final synthetic fun bind (Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;)Lcom/squareup/workflow1/ui/ScreenViewFactory; + public final synthetic fun bindBuiltView (Lkotlin/jvm/functions/Function3;)Lcom/squareup/workflow1/ui/ScreenViewFactory; public final synthetic fun bindNoRunner (I)Lcom/squareup/workflow1/ui/ScreenViewFactory; } @@ -239,8 +234,9 @@ public abstract interface class com/squareup/workflow1/ui/TreeSnapshotSaver$HasT public final class com/squareup/workflow1/ui/ViewBindingScreenViewFactory : com/squareup/workflow1/ui/ScreenViewFactory { public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;)V - public fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; + public fun buildView (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; public fun getType ()Lkotlin/reflect/KClass; + public fun updateView (Landroid/view/View;Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V } public final class com/squareup/workflow1/ui/ViewBindingViewFactory : com/squareup/workflow1/ui/ViewFactory { @@ -262,6 +258,11 @@ public final class com/squareup/workflow1/ui/ViewLaunchWhenAttachedKt { public static synthetic fun launchWhenAttached$default (Landroid/view/View;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/Job; } +public final class com/squareup/workflow1/ui/ViewSetViewRunnerKt { + public static final fun getViewRunner (Landroid/view/View;)Lcom/squareup/workflow1/ui/ScreenViewRunner; + public static final fun setViewRunner (Landroid/view/View;Lcom/squareup/workflow1/ui/ScreenViewRunner;)V +} + public final class com/squareup/workflow1/ui/ViewShowRenderingKt { public static final fun bindShowRendering (Landroid/view/View;Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function2;)V public static final fun canShowRendering (Landroid/view/View;Ljava/lang/Object;)Z @@ -269,6 +270,7 @@ public final class com/squareup/workflow1/ui/ViewShowRenderingKt { public static final synthetic fun getRendering (Landroid/view/View;)Ljava/lang/Object; public static final fun getShowRendering (Landroid/view/View;)Lkotlin/jvm/functions/Function2; public static final fun getStarter (Landroid/view/View;)Lkotlin/jvm/functions/Function1; + public static final fun getStarterOrNull (Landroid/view/View;)Lkotlin/jvm/functions/Function1; public static final fun setStarter (Landroid/view/View;Lkotlin/jvm/functions/Function1;)V public static final fun showRendering (Landroid/view/View;Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;)V public static final fun start (Landroid/view/View;)V @@ -358,7 +360,7 @@ public final class com/squareup/workflow1/ui/WorkflowViewStub : android/view/Vie public final fun setReplaceOldViewInParent (Lkotlin/jvm/functions/Function2;)V public final fun setUpdatesVisibility (Z)V public fun setVisibility (I)V - public final fun show (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)Landroid/view/View; + public final fun show (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V public final fun update (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;)Landroid/view/View; } @@ -425,7 +427,6 @@ public final class com/squareup/workflow1/ui/container/AlertOverlayDialogFactory } public final class com/squareup/workflow1/ui/container/AndroidDialogBoundsKt { - public static final fun maintainBounds (Landroid/app/Dialog;Landroid/view/View;Lkotlin/jvm/functions/Function2;)V public static final fun maintainBounds (Landroid/app/Dialog;Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function2;)V public static final fun maintainBounds (Landroid/app/Dialog;Lkotlinx/coroutines/flow/StateFlow;Lkotlin/jvm/functions/Function2;)V public static final fun setBounds (Landroid/app/Dialog;Landroid/graphics/Rect;)V @@ -472,15 +473,16 @@ public class com/squareup/workflow1/ui/container/BackStackContainer : android/wi protected fun onDetachedFromWindow ()V protected fun onRestoreInstanceState (Landroid/os/Parcelable;)V protected fun onSaveInstanceState ()Landroid/os/Parcelable; - protected fun performTransition (Landroid/view/View;Landroid/view/View;Z)V + protected fun performTransition (Lcom/squareup/workflow1/ui/ScreenViewHolder;Lcom/squareup/workflow1/ui/ScreenViewHolder;Z)V public final fun update (Lcom/squareup/workflow1/ui/container/BackStackScreen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V } public final class com/squareup/workflow1/ui/container/BackStackScreenViewFactory : com/squareup/workflow1/ui/ScreenViewFactory { public static final field INSTANCE Lcom/squareup/workflow1/ui/container/BackStackScreenViewFactory; - public synthetic fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; - public fun buildView (Lcom/squareup/workflow1/ui/container/BackStackScreen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; + public fun buildView (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; public fun getType ()Lkotlin/reflect/KClass; + public synthetic fun updateView (Landroid/view/View;Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V + public fun updateView (Landroid/view/View;Lcom/squareup/workflow1/ui/container/BackStackScreen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V } public final class com/squareup/workflow1/ui/container/BodyAndModalsContainer : android/widget/FrameLayout { @@ -496,9 +498,10 @@ public final class com/squareup/workflow1/ui/container/BodyAndModalsContainer : } public final class com/squareup/workflow1/ui/container/BodyAndModalsContainer$Companion : com/squareup/workflow1/ui/ScreenViewFactory { - public synthetic fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; - public fun buildView (Lcom/squareup/workflow1/ui/container/BodyAndModalsScreen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; + public fun buildView (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; public fun getType ()Lkotlin/reflect/KClass; + public synthetic fun updateView (Landroid/view/View;Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V + public fun updateView (Landroid/view/View;Lcom/squareup/workflow1/ui/container/BodyAndModalsScreen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V } public final class com/squareup/workflow1/ui/container/CoveredByModal : com/squareup/workflow1/ui/ViewEnvironmentKey { @@ -547,11 +550,8 @@ public final class com/squareup/workflow1/ui/container/DispatchCancelEventKt { public static final fun dispatchCancelEvent (Lkotlin/jvm/functions/Function1;)V } -public final class com/squareup/workflow1/ui/container/EnvironmentScreenViewFactory : com/squareup/workflow1/ui/ScreenViewFactory { - public static final field INSTANCE Lcom/squareup/workflow1/ui/container/EnvironmentScreenViewFactory; - public synthetic fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; - public fun buildView (Lcom/squareup/workflow1/ui/container/EnvironmentScreen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; - public fun getType ()Lkotlin/reflect/KClass; +public final class com/squareup/workflow1/ui/container/EnvironmentScreenViewFactoryKt { + public static final fun EnvironmentScreenViewFactory (Lcom/squareup/workflow1/ui/container/EnvironmentScreen;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/ScreenViewFactory; } public final class com/squareup/workflow1/ui/container/LayeredDialogs { @@ -654,7 +654,7 @@ public final class com/squareup/workflow1/ui/container/ViewStateCache : android/ public final fun getViewStates$wf1_core_android ()Ljava/util/Map; public final fun prune (Ljava/util/Collection;)V public final fun restore (Lcom/squareup/workflow1/ui/container/ViewStateCache;)V - public final fun update (Ljava/util/Collection;Landroid/view/View;Landroid/view/View;)V + public final fun update (Ljava/util/Collection;Lcom/squareup/workflow1/ui/ScreenViewHolder;Lcom/squareup/workflow1/ui/ScreenViewHolder;)V public fun writeToParcel (Landroid/os/Parcel;I)V } @@ -680,9 +680,6 @@ public final class com/squareup/workflow1/ui/container/ViewStateCache$SavedState public synthetic fun newArray (I)[Ljava/lang/Object; } -public final class com/squareup/workflow1/ui/container/ViewStateCacheKt { -} - public final class com/squareup/workflow1/ui/container/ViewStateFrame : android/os/Parcelable { public static final field CREATOR Lcom/squareup/workflow1/ui/container/ViewStateFrame$CREATOR; public fun (Ljava/lang/String;Landroid/util/SparseArray;)V 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 deleted file mode 100644 index b679979801..0000000000 --- a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeScreenViewFactoryTest.kt +++ /dev/null @@ -1,230 +0,0 @@ -package com.squareup.workflow1.ui - -import android.content.Context -import android.view.View -import android.view.ViewGroup -import androidx.test.platform.app.InstrumentationRegistry -import com.google.common.truth.Truth.assertThat -import org.junit.Test - -@OptIn(WorkflowUiExperimentalApi::class) -internal class DecorativeScreenViewFactoryTest { - private val instrumentation = InstrumentationRegistry.getInstrumentation() - - @Test fun viewStarter_is_only_call_to_showRendering() { - val events = mutableListOf() - - val innerViewFactory = object : ScreenViewFactory { - override val type = InnerRendering::class - override fun buildView( - initialRendering: InnerRendering, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, - container: ViewGroup? - ): View = InnerView(contextForNewView).apply { - bindShowRendering(initialRendering, initialViewEnvironment) { rendering, _ -> - events += "inner showRendering $rendering" - } - } - } - - val envString = object : ViewEnvironmentKey(String::class) { - override val default: String get() = "Not set" - } - - val outerViewFactory = DecorativeScreenViewFactory( - type = OuterRendering::class, - unwrap = { outer, env -> - val enhancedEnv = env + (envString to "Updated environment") - Pair(outer.wrapped, enhancedEnv) - }, - viewStarter = { view, doStart -> - events += "viewStarter ${view.getRendering()} ${view.environment!![envString]}" - doStart() - events += "exit viewStarter" - } - ) - val viewEnvironment = ViewEnvironment.EMPTY + ViewRegistry(innerViewFactory, outerViewFactory) - - OuterRendering("outer", InnerRendering("inner")).buildView( - viewEnvironment, - instrumentation.context - ).start() - - assertThat(events).containsExactly( - "viewStarter OuterRendering(outerData=outer, wrapped=InnerRendering(innerData=inner)) " + - "Updated environment", - "inner showRendering InnerRendering(innerData=inner)", - "exit viewStarter" - ) - } - - @Test fun initial_doShowRendering_calls_wrapped_showRendering() { - val events = mutableListOf() - - val innerViewFactory = object : ScreenViewFactory { - override val type = InnerRendering::class - override fun buildView( - initialRendering: InnerRendering, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, - container: ViewGroup? - ): View = InnerView(contextForNewView).apply { - bindShowRendering(initialRendering, initialViewEnvironment) { rendering, _ -> - events += "inner showRendering $rendering" - } - } - } - val outerViewFactory = DecorativeScreenViewFactory( - type = OuterRendering::class, - unwrap = { outer -> outer.wrapped }, - doShowRendering = { _, innerShowRendering, outerRendering, env -> - events += "doShowRendering $outerRendering" - innerShowRendering(outerRendering.wrapped, env) - } - ) - val viewEnvironment = ViewEnvironment.EMPTY + ViewRegistry(innerViewFactory, outerViewFactory) - - OuterRendering("outer", InnerRendering("inner")).buildView( - viewEnvironment, - instrumentation.context - ).start() - - assertThat(events).containsExactly( - "doShowRendering OuterRendering(outerData=outer, wrapped=InnerRendering(innerData=inner))", - "inner showRendering InnerRendering(innerData=inner)" - ) - } - - // https://github.com/square/workflow-kotlin/issues/597 - @Test fun double_wrapping_only_calls_showRendering_once() { - val events = mutableListOf() - - val innerViewFactory = object : ScreenViewFactory { - override val type = InnerRendering::class - override fun buildView( - initialRendering: InnerRendering, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, - container: ViewGroup? - ): View = InnerView(contextForNewView).apply { - bindShowRendering(initialRendering, initialViewEnvironment) { rendering, _ -> - events += "inner showRendering $rendering" - } - } - } - - val envString = object : ViewEnvironmentKey(String::class) { - override val default: String get() = "Not set" - } - - val outerViewFactory = DecorativeScreenViewFactory( - type = OuterRendering::class, - unwrap = { outer, env -> - val enhancedEnv = env + ( - envString to "Outer Updated environment SHOULD NOT SEE THIS! " + - "It will be clobbered by WayOutRendering" - ) - Pair(outer.wrapped, enhancedEnv) - }, - viewStarter = { view, doStart -> - events += "outer viewStarter ${view.getRendering()} ${view.environment!![envString]}" - doStart() - events += "exit outer viewStarter" - } - ) - - val wayOutViewFactory = DecorativeScreenViewFactory( - type = WayOutRendering::class, - unwrap = { wayOut, env -> - val enhancedEnv = env + (envString to "Way Out Updated environment triumphs over all") - Pair(wayOut.wrapped, enhancedEnv) - }, - viewStarter = { view, doStart -> - events += "way out viewStarter ${view.getRendering()} ${view.environment!![envString]}" - doStart() - events += "exit way out viewStarter" - } - ) - val viewEnvironment = - ViewEnvironment.EMPTY + ViewRegistry(innerViewFactory, outerViewFactory, wayOutViewFactory) - - WayOutRendering("way out", OuterRendering("outer", InnerRendering("inner"))).buildView( - viewEnvironment, - instrumentation.context - ).start() - - assertThat(events).containsExactly( - "way out viewStarter " + - "WayOutRendering(wayOutData=way out, wrapped=" + - "OuterRendering(outerData=outer, wrapped=" + - "InnerRendering(innerData=inner))) " + - "Way Out Updated environment triumphs over all", - "outer viewStarter " + - // Notice that both the initial rendering and the ViewEnvironment are stomped by - // the outermost wrapper before inners are invoked. Could try to give - // the inner wrapper access to the rendering it expected, but there are no - // use cases and it trashes the API. - "WayOutRendering(wayOutData=way out, wrapped=" + - "OuterRendering(outerData=outer, wrapped=" + - "InnerRendering(innerData=inner))) " + - "Way Out Updated environment triumphs over all", - "inner showRendering InnerRendering(innerData=inner)", - "exit outer viewStarter", - "exit way out viewStarter" - ) - } - - @Test fun subsequent_showRendering_calls_wrapped_showRendering() { - val events = mutableListOf() - - val innerViewFactory = object : ScreenViewFactory { - override val type = InnerRendering::class - override fun buildView( - initialRendering: InnerRendering, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, - container: ViewGroup? - ): View = InnerView(contextForNewView).apply { - bindShowRendering(initialRendering, initialViewEnvironment) { rendering, _ -> - events += "inner showRendering $rendering" - } - } - } - val outerViewFactory = DecorativeScreenViewFactory( - type = OuterRendering::class, - unwrap = { outer -> outer.wrapped }, - doShowRendering = { _, innerShowRendering, outerRendering, env -> - events += "doShowRendering $outerRendering" - innerShowRendering(outerRendering.wrapped, env) - } - ) - val viewEnvironment = ViewEnvironment.EMPTY + ViewRegistry(innerViewFactory, outerViewFactory) - - val view = OuterRendering("out1", InnerRendering("in1")).buildView( - viewEnvironment, - instrumentation.context - ).apply { start() } - events.clear() - - view.showRendering(OuterRendering("out2", InnerRendering("in2")), viewEnvironment) - - assertThat(events).containsExactly( - "doShowRendering OuterRendering(outerData=out2, wrapped=InnerRendering(innerData=in2))", - "inner showRendering InnerRendering(innerData=in2)" - ) - } - - private data class InnerRendering(val innerData: String) : Screen - private data class OuterRendering( - val outerData: String, - val wrapped: InnerRendering - ) : Screen - - private data class WayOutRendering( - val wayOutData: String, - val wrapped: OuterRendering - ) : Screen - - private class InnerView(context: Context) : View(context) -} diff --git a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/WorkflowViewStubLifecycleActivity.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/WorkflowViewStubLifecycleActivity.kt index 9a1754bd2e..bb64ece6b4 100644 --- a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/WorkflowViewStubLifecycleActivity.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/WorkflowViewStubLifecycleActivity.kt @@ -1,6 +1,7 @@ package com.squareup.workflow1.ui import android.widget.FrameLayout +import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bindBuiltView import com.squareup.workflow1.ui.WorkflowViewStubLifecycleActivity.TestRendering.LeafRendering import com.squareup.workflow1.ui.WorkflowViewStubLifecycleActivity.TestRendering.RecurseRendering import com.squareup.workflow1.ui.internal.test.AbstractLifecycleTestActivity @@ -20,20 +21,16 @@ internal class WorkflowViewStubLifecycleActivity : AbstractLifecycleTestActivity override val viewRegistry: ViewRegistry = ViewRegistry( leafViewBinding(LeafRendering::class, lifecycleLoggingViewObserver { it.name }), - ManualScreenViewFactory(RecurseRendering::class) { initialRendering, - initialViewEnvironment, - contextForNewView, _ -> - FrameLayout(contextForNewView).also { container -> - val stub = WorkflowViewStub(contextForNewView) + bindBuiltView { _, context, _ -> + val stub = WorkflowViewStub(context) + val frame = FrameLayout(context).also { container -> container.addView(stub) - container.bindShowRendering( - initialRendering, - initialViewEnvironment - ) { rendering, env -> - stub.show(rendering.wrapped, env) - } } - }, + val runner = ScreenViewRunner { rendering, viewEnvironment -> + stub.show(rendering.wrapped, viewEnvironment) + } + Pair(frame, runner) + } ) fun update(rendering: TestRendering) = super.setRendering(rendering) diff --git a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/WorkflowViewStubLifecycleTest.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/WorkflowViewStubLifecycleTest.kt index d16932b938..a540946c8e 100644 --- a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/WorkflowViewStubLifecycleTest.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/WorkflowViewStubLifecycleTest.kt @@ -25,6 +25,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withTagValue import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule import com.google.common.truth.Truth.assertThat +import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bindBuiltView import com.squareup.workflow1.ui.WorkflowViewStubLifecycleActivity.TestRendering import com.squareup.workflow1.ui.WorkflowViewStubLifecycleActivity.TestRendering.LeafRendering import com.squareup.workflow1.ui.WorkflowViewStubLifecycleActivity.TestRendering.RecurseRendering @@ -261,19 +262,14 @@ internal class WorkflowViewStubLifecycleTest { } data class RegistrySetter(val wrapped: TestRendering) : ViewRendering() { - override val viewFactory: ScreenViewFactory = ManualScreenViewFactory( - RegistrySetter::class - ) { initialRendering, initialViewEnvironment, context, _ -> + override val viewFactory = bindBuiltView { _, context, _ -> val stub = WorkflowViewStub(context) ViewTreeSavedStateRegistryOwner.set(stub, expectedRegistryOwner) - - FrameLayout(context).apply { - addView(stub) - - bindShowRendering(initialRendering, initialViewEnvironment) { r, e -> - stub.show(r.wrapped, e) - } + val frame = FrameLayout(context).apply { addView(stub) } + val runner = ScreenViewRunner { rendering, viewEnvironment -> + stub.show(rendering.wrapped, viewEnvironment) } + Pair(frame, runner) } } @@ -312,61 +308,58 @@ internal class WorkflowViewStubLifecycleTest { const val Tag = "counter" } - override val viewFactory: ScreenViewFactory = ManualScreenViewFactory( - CounterRendering::class - ) { initialRendering, initialViewEnvironment, context, _ -> - var counter = 0 - Button(context).apply button@{ - tag = Tag + override val viewFactory = ScreenViewFactory( + buildView = { _, context, _ -> + var counter = 0 + Button(context).apply button@{ + tag = Tag - fun updateText() { - text = "Counter: $counter" - } + fun updateText() { + text = "Counter: $counter" + } - addOnAttachStateChangeListener(object : OnAttachStateChangeListener { - lateinit var registryOwner: SavedStateRegistryOwner - lateinit var lifecycleObserver: LifecycleObserver - - override fun onViewAttachedToWindow(v: View) { - onViewAttached(this@button) - registryOwner = ViewTreeSavedStateRegistryOwner.get(this@button)!! - lifecycleObserver = object : LifecycleEventObserver { - override fun onStateChanged( - source: LifecycleOwner, - event: Event - ) { - if (event == ON_CREATE) { - source.lifecycle.removeObserver(this) - registryOwner.savedStateRegistry.consumeRestoredStateForKey("counter") - ?.let { restoredState -> - counter = restoredState.getInt("value") - updateText() - } + addOnAttachStateChangeListener(object : OnAttachStateChangeListener { + lateinit var registryOwner: SavedStateRegistryOwner + lateinit var lifecycleObserver: LifecycleObserver + + override fun onViewAttachedToWindow(v: View) { + onViewAttached(this@button) + registryOwner = ViewTreeSavedStateRegistryOwner.get(this@button)!! + lifecycleObserver = object : LifecycleEventObserver { + override fun onStateChanged( + source: LifecycleOwner, + event: Event + ) { + if (event == ON_CREATE) { + source.lifecycle.removeObserver(this) + registryOwner.savedStateRegistry.consumeRestoredStateForKey("counter") + ?.let { restoredState -> + counter = restoredState.getInt("value") + updateText() + } + } } } + registryOwner.lifecycle.addObserver(lifecycleObserver) + registryOwner.savedStateRegistry.registerSavedStateProvider("counter") { + Bundle().apply { putInt("value", counter) } + } } - registryOwner.lifecycle.addObserver(lifecycleObserver) - registryOwner.savedStateRegistry.registerSavedStateProvider("counter") { - Bundle().apply { putInt("value", counter) } - } - } - override fun onViewDetachedFromWindow(v: View) { - registryOwner.lifecycle.removeObserver(lifecycleObserver) - registryOwner.savedStateRegistry.unregisterSavedStateProvider("counter") - } - }) + override fun onViewDetachedFromWindow(v: View) { + registryOwner.lifecycle.removeObserver(lifecycleObserver) + registryOwner.savedStateRegistry.unregisterSavedStateProvider("counter") + } + }) - updateText() - setOnClickListener { - counter++ updateText() + setOnClickListener { + counter++ + updateText() + } } - - bindShowRendering(initialRendering, initialViewEnvironment) { _, _ -> - // Noop - } - } - } + }, + updateView = { _, _, _ -> /* Noop */ } + ) } } diff --git a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/BackStackContainerPersistenceTest.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/BackStackContainerPersistenceTest.kt index 2cb4da562f..a833e2a7d5 100644 --- a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/BackStackContainerPersistenceTest.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/BackStackContainerPersistenceTest.kt @@ -93,7 +93,7 @@ internal class BackStackContainerPersistenceTest { scenario.onActivity { assertThat(it.consumeLifecycleEvents()) .containsExactly( - "nested onViewCreated viewState=", + "onViewCreated viewState=", "nested onShowRendering viewState=", "nested onAttach viewState=", "LeafView nested ON_CREATE", @@ -194,7 +194,7 @@ internal class BackStackContainerPersistenceTest { it.currentTestView.viewState = "hello" assertThat(it.consumeLifecycleEvents()).containsAtLeast( - "first onViewCreated viewState=", + "onViewCreated viewState=", "first onShowRendering viewState=", "first onAttach viewState=" ).inOrder() @@ -207,7 +207,7 @@ internal class BackStackContainerPersistenceTest { waitForScreen(secondRendering.name) scenario.onActivity { assertThat(it.consumeLifecycleEvents()).containsAtLeast( - "second onViewCreated viewState=", + "onViewCreated viewState=", "second onShowRendering viewState=", "first onSave viewState=hello", "first onDetach viewState=hello", @@ -222,7 +222,7 @@ internal class BackStackContainerPersistenceTest { waitForScreen(firstRendering.name) scenario.onActivity { assertThat(it.consumeLifecycleEvents()).containsAtLeast( - "first onViewCreated viewState=", + "onViewCreated viewState=", "first onShowRendering viewState=", "first onRestore viewState=hello", "second onDetach viewState=", 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 fa55ce8038..a206abf234 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 @@ -5,15 +5,15 @@ import android.view.View import androidx.activity.ComponentActivity import androidx.test.ext.junit.rules.ActivityScenarioRule import com.google.common.truth.Truth +import com.google.common.truth.Truth.assertThat import com.squareup.workflow1.ui.AndroidScreen import com.squareup.workflow1.ui.Compatible -import com.squareup.workflow1.ui.ManualScreenViewFactory import com.squareup.workflow1.ui.NamedScreen +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewHolder import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.bindShowRendering -import com.squareup.workflow1.ui.getRendering import org.junit.Rule import org.junit.Test @@ -25,9 +25,10 @@ internal class BackStackContainerTest { private data class Rendering(val name: String) : Compatible, AndroidScreen { override val compatibilityKey = name override val viewFactory: ScreenViewFactory - get() = ManualScreenViewFactory(Rendering::class) { r, e, ctx, _ -> - View(ctx).also { it.bindShowRendering(r, e) { _, _ -> /* Noop */ } } - } + get() = ScreenViewFactory( + buildView = { _, context, _ -> View(context) }, + updateView = { _, _, _ -> /* Noop */ } + ) } @Test fun firstScreenIsRendered() { @@ -82,19 +83,22 @@ internal class BackStackContainerTest { private class VisibleBackStackContainer(context: Context) : BackStackContainer(context) { var transitionCount = 0 - val visibleRendering: Any? get() = getChildAt(0)?.getRendering>()?.wrapped + @Suppress("UNCHECKED_CAST") val visibleRendering: Screen? + get() = (getChildAt(0)?.tag as NamedScreen<*>).wrapped fun show(rendering: BackStackScreen<*>) { - update(rendering, ViewEnvironment.EMPTY) + update(rendering, ViewEnvironment.EMPTY + (Screen to rendering)) } override fun performTransition( - oldViewMaybe: View?, - newView: View, + oldHolderMaybe: ScreenViewHolder>?, + newHolder: ScreenViewHolder>, popped: Boolean ) { transitionCount++ - super.performTransition(oldViewMaybe, newView, popped) + assertThat(newHolder.view.tag).isNull() + newHolder.view.tag = newHolder.screen + super.performTransition(oldHolderMaybe, newHolder, popped) } } } 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 d782a0e7df..094261ea1d 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 @@ -3,16 +3,16 @@ package com.squareup.workflow1.ui.container import android.os.Parcel import android.os.Parcelable import android.util.SparseArray -import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat import com.squareup.workflow1.ui.NamedScreen import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewHolder import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner -import com.squareup.workflow1.ui.bindShowRendering import com.squareup.workflow1.ui.container.fixtures.ViewStateTestView import org.junit.Assert.fail import org.junit.Test @@ -67,11 +67,13 @@ internal class ViewStateCacheTest { firstView.viewState = "hello world" // Show the first screen. - cache.update(retainedRenderings = emptyList(), oldViewMaybe = null, newView = firstView) + cache.update(retainedRenderings = emptyList(), oldHolderMaybe = null, newHolder = firstView) // "Navigate" to the second screen, saving the first screen. cache.update( - retainedRenderings = listOf(firstRendering), oldViewMaybe = firstView, newView = secondView + retainedRenderings = listOf(firstRendering), + oldHolderMaybe = firstView, + newHolder = secondView ) // Nothing should read this value again, but clear it to make sure. @@ -79,7 +81,7 @@ internal class ViewStateCacheTest { // "Navigate" back to the first screen, restoring state. val firstViewRestored = createTestView(firstRendering, id = 1) - cache.update(listOf(), oldViewMaybe = secondView, newView = firstViewRestored) + cache.update(listOf(), oldHolderMaybe = secondView, newHolder = firstViewRestored) // Check that the state was restored. assertThat(firstViewRestored.viewState).isEqualTo("hello world") @@ -94,14 +96,16 @@ internal class ViewStateCacheTest { val secondView = createTestView(secondRendering) // Show the first screen. - cache.update(retainedRenderings = emptyList(), oldViewMaybe = null, newView = firstView) + cache.update(retainedRenderings = emptyList(), oldHolderMaybe = null, newHolder = firstView) // Set some state on the first view that will be saved. firstView.viewState = "hello world" // "Navigate" to the second screen, saving the first screen. cache.update( - retainedRenderings = listOf(firstRendering), oldViewMaybe = firstView, newView = secondView + retainedRenderings = listOf(firstRendering), + oldHolderMaybe = firstView, + newHolder = secondView ) // Nothing should read this value again, but clear it to make sure. @@ -111,9 +115,13 @@ internal class ViewStateCacheTest { val firstViewRestored = ViewStateTestView(instrumentation.context).apply { id = 2 WorkflowLifecycleOwner.installOn(this) - bindShowRendering(firstRendering, viewEnvironment) { _, _ -> /* Noop */ } } - cache.update(listOf(firstRendering), oldViewMaybe = secondView, newView = firstViewRestored) + val firstHolderRestored = ScreenViewHolder>( + fakeFactory, ViewEnvironment.EMPTY, firstRendering, firstViewRestored + ) + cache.update( + listOf(firstRendering), oldHolderMaybe = secondView, newHolder = firstHolderRestored + ) // Check that the state was restored. assertThat(firstViewRestored.viewState).isEqualTo("") @@ -130,35 +138,22 @@ internal class ViewStateCacheTest { firstView.viewState = "hello world" // Show the first screen. - cache.update(retainedRenderings = emptyList(), oldViewMaybe = null, newView = firstView) + cache.update(retainedRenderings = emptyList(), oldHolderMaybe = null, newHolder = firstView) // "Navigate" to the second screen, saving the first screen. - cache.update(listOf(firstRendering), oldViewMaybe = firstView, newView = secondView) + cache.update(listOf(firstRendering), oldHolderMaybe = firstView, newHolder = secondView) // Nothing should read this value again, but clear it to make sure. firstView.viewState = "ignored" // "Navigate" back to the first screen, restoring state. val firstViewRestored = createTestView(firstRendering) - cache.update(listOf(firstRendering), oldViewMaybe = secondView, newView = firstViewRestored) + cache.update(listOf(firstRendering), oldHolderMaybe = secondView, newHolder = firstViewRestored) // Check that the state was NOT restored. assertThat(firstViewRestored.viewState).isEqualTo("") } - @Test fun throws_when_view_not_bound() { - val cache = ViewStateCache() - val rendering = NamedScreen(wrapped = AScreen, name = "duplicate") - val view = View(instrumentation.context) - - try { - cache.update(listOf(rendering, rendering), null, view) - fail("Expected exception.") - } catch (e: IllegalStateException) { - assertThat(e.message).contains("to be showing a NamedScreen<*> rendering, found null") - } - } - @Test fun throws_on_duplicate_renderings() { val cache = ViewStateCache() val rendering = NamedScreen(wrapped = AScreen, name = "duplicate") @@ -172,13 +167,26 @@ internal class ViewStateCacheTest { } } + private val fakeFactory = ScreenViewFactory>( + { _, _, _ -> error("not to be called") }, { _, _, _ -> } + ) + + private val ScreenViewHolder<*>.testView get() = (view as ViewStateTestView) + private var ScreenViewHolder<*>.viewState: String + get() = testView.viewState + set(value) { + testView.viewState = value + } + private fun createTestView( firstRendering: NamedScreen<*>, id: Int? = null - ) = ViewStateTestView(instrumentation.context).also { view -> - id?.let { view.id = id } - WorkflowLifecycleOwner.installOn(view) - view.bindShowRendering(firstRendering, viewEnvironment) { _, _ -> /* Noop */ } + ): ScreenViewHolder> { + val view = ViewStateTestView(instrumentation.context).also { view -> + id?.let { view.id = id } + WorkflowLifecycleOwner.installOn(view) + } + return ScreenViewHolder(fakeFactory, ViewEnvironment.EMPTY, firstRendering, view) } private fun ViewStateCache.equalsForTest(other: ViewStateCache): Boolean { diff --git a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/fixtures/BackStackContainerLifecycleActivity.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/fixtures/BackStackContainerLifecycleActivity.kt index f6d608f6b5..18f4add167 100644 --- a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/fixtures/BackStackContainerLifecycleActivity.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/fixtures/BackStackContainerLifecycleActivity.kt @@ -1,6 +1,5 @@ package com.squareup.workflow1.ui.container.fixtures -import android.content.Context import android.view.View import android.view.ViewGroup import android.widget.FrameLayout @@ -9,14 +8,13 @@ import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed import androidx.test.espresso.matcher.ViewMatchers.withTagValue import com.squareup.workflow1.ui.Compatible -import com.squareup.workflow1.ui.ManualScreenViewFactory import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bindBuiltView 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.container.BackStackScreen import com.squareup.workflow1.ui.container.fixtures.BackStackContainerLifecycleActivity.TestRendering.LeafRendering import com.squareup.workflow1.ui.container.fixtures.BackStackContainerLifecycleActivity.TestRendering.OuterRendering @@ -33,16 +31,13 @@ internal class BackStackContainerLifecycleActivity : AbstractLifecycleTestActivi /** * Default rendering always shown in the backstack to simplify test configuration. */ - object BaseRendering : Screen, ScreenViewFactory { + object BaseRendering : + Screen, + ScreenViewFactory by ScreenViewFactory( + buildView = { _, context, _ -> View(context) }, + updateView = { _, _, _ -> /* Noop */ } + ) { override val type: KClass = BaseRendering::class - override fun buildView( - initialRendering: BaseRendering, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, - container: ViewGroup? - ): View = View(contextForNewView).apply { - bindShowRendering(initialRendering, initialViewEnvironment) { _, _ -> /* Noop */ } - } } sealed class TestRendering : Screen { @@ -61,21 +56,22 @@ internal class BackStackContainerLifecycleActivity : AbstractLifecycleTestActivi private val viewObserver = object : ViewObserver by lifecycleLoggingViewObserver({ it.name }) { override fun onViewCreated( - view: View, - rendering: LeafRendering + view: View ) { - view.tag = rendering.name - - // Need to set the view to enable view persistence. - view.id = rendering.name.hashCode() - - logEvent("${rendering.name} onViewCreated viewState=${view.viewState}") + logEvent("onViewCreated viewState=${view.viewState}") } override fun onShowRendering( view: View, rendering: LeafRendering ) { + if (view.tag == null) { + view.tag = rendering.name + + // Need to set the view to enable view persistence. + view.id = rendering.name.hashCode() + } + check(view.tag == rendering.name) logEvent("${rendering.name} onShowRendering viewState=${view.viewState}") } @@ -115,34 +111,26 @@ internal class BackStackContainerLifecycleActivity : AbstractLifecycleTestActivi NoTransitionBackStackContainer, BaseRendering, leafViewBinding(LeafRendering::class, viewObserver, viewConstructor = ::ViewStateTestView), - ManualScreenViewFactory(RecurseRendering::class) { initialRendering, - initialViewEnvironment, - contextForNewView, _ -> - FrameLayout(contextForNewView).also { container -> - val stub = WorkflowViewStub(contextForNewView) + bindBuiltView { _, context, _ -> + val stub = WorkflowViewStub(context) + val frame = FrameLayout(context).also { container -> container.addView(stub) - container.bindShowRendering( - initialRendering, - initialViewEnvironment - ) { rendering, env -> - stub.show(rendering.wrappedBackstack.toBackstackWithBase(), env) - } } + val runner = ScreenViewRunner { rendering, viewEnvironment -> + stub.show(rendering.wrappedBackstack.toBackstackWithBase(), viewEnvironment) + } + Pair(frame, runner) }, - ManualScreenViewFactory(OuterRendering::class) { initialRendering, - initialViewEnvironment, - contextForNewView, _ -> - FrameLayout(contextForNewView).also { container -> - - val stub = WorkflowViewStub(contextForNewView) + bindBuiltView { _, context, _ -> + val stub = WorkflowViewStub(context) + val frame = FrameLayout(context).also { container -> container.addView(stub) - container.bindShowRendering( - initialRendering, initialViewEnvironment - ) { rendering, env -> - stub.show(rendering.backStack, env) - } } - }, + val runner = ScreenViewRunner { rendering, viewEnvironment -> + stub.show(rendering.backStack, viewEnvironment) + } + Pair(frame, runner) + } ) /** Returns the view that is the current screen. */ diff --git a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/fixtures/NoTransitionBackStackContainer.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/fixtures/NoTransitionBackStackContainer.kt index c053fc16e1..4895936bc6 100644 --- a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/fixtures/NoTransitionBackStackContainer.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/fixtures/NoTransitionBackStackContainer.kt @@ -1,13 +1,12 @@ package com.squareup.workflow1.ui.container.fixtures import android.content.Context -import android.view.View import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import com.squareup.workflow1.ui.ManualScreenViewFactory +import com.squareup.workflow1.ui.NamedScreen import com.squareup.workflow1.ui.R import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewHolder import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.bindShowRendering import com.squareup.workflow1.ui.container.BackStackContainer import com.squareup.workflow1.ui.container.BackStackScreen @@ -17,26 +16,25 @@ import com.squareup.workflow1.ui.container.BackStackScreen */ @OptIn(WorkflowUiExperimentalApi::class) internal class NoTransitionBackStackContainer(context: Context) : BackStackContainer(context) { - override fun performTransition( - oldViewMaybe: View?, - newView: View, + oldHolderMaybe: ScreenViewHolder>?, + newHolder: ScreenViewHolder>, popped: Boolean ) { - oldViewMaybe?.let(::removeView) - addView(newView) + oldHolderMaybe?.view?.let(::removeView) + addView(newHolder.view) } - companion object : ScreenViewFactory> - by ManualScreenViewFactory( - type = BackStackScreen::class, - viewConstructor = { initialRendering, initialEnv, context, _ -> + companion object : ScreenViewFactory> by ScreenViewFactory( + buildView = { _, context, _ -> NoTransitionBackStackContainer(context) .apply { id = R.id.workflow_back_stack_container layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT) - bindShowRendering(initialRendering, initialEnv, ::update) } + }, + updateView = { view, rendering, environment -> + (view as NoTransitionBackStackContainer).update(rendering, environment) } ) } 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 fb21b07c2d..657c1fb7eb 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 @@ -38,7 +38,7 @@ public fun ViewRegistry.getFactoryFor( return getEntryFor(renderingType) as? ViewFactory } -@Deprecated("Use Screen.buildView") +@Deprecated("Use Screen.toView") @WorkflowUiExperimentalApi public fun ViewRegistry.buildView( initialRendering: RenderingT, diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AsScreenViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AsScreenViewFactory.kt index 3b3e40f8d1..204ce894c6 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AsScreenViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AsScreenViewFactory.kt @@ -1,24 +1,41 @@ package com.squareup.workflow1.ui -@WorkflowUiExperimentalApi @Suppress("DEPRECATION") -internal object AsScreenViewFactory : ScreenViewFactory> -by ManualScreenViewFactory( - type = AsScreen::class, - viewConstructor = { initialRendering, initialViewEnvironment, context, container -> - initialViewEnvironment[ViewRegistry] - .buildView( - initialRendering.rendering, - initialViewEnvironment, - context, - container - ).also { view -> +@WorkflowUiExperimentalApi +internal fun AsScreenViewFactory( + initialRendering: AsScreen<*>, + initialViewEnvironment: ViewEnvironment +): ScreenViewFactory> { + val wrapped = initialRendering.rendering + val registry = initialViewEnvironment[ViewRegistry] + + return ScreenViewFactory>( + buildView = { environment, context, container -> + registry.buildView(wrapped, environment, context, container).also { view -> + view.getTag(R.id.workflow_legacy_show_rendering)?.let { + error("AsScreen does not support recursion, found existing ViewShowRendering: $it") + } + + // Capture the legacy showRendering function so that we can call it from our own + // updateView. val legacyShowRendering = view.getShowRendering()!! + view.setTag(R.id.workflow_legacy_show_rendering, legacyShowRendering) - view.bindShowRendering( - initialRendering, - initialViewEnvironment - ) { rendering, env -> legacyShowRendering(rendering.rendering, env) } + // Like any decorator, we need to call bindShowRendering again to + // ensure that the wrapper initialRendering is in place for View.getRendering() calls. + // Note that we're careful to preserve the ViewEnvironment put in place by the + // legacy ViewFactory + view.bindShowRendering(initialRendering, view.environment!!) { _, _ -> + // We leave a no-op (this lambda) in place for View.showRendering(), + // but ScreenViewFactory.start() will soon put something else in its place. + } } - } -) + }, + updateView = { view, asScreen, environment -> + @Suppress("UNCHECKED_CAST") + val legacyShowRendering = + view.getTag(R.id.workflow_legacy_show_rendering) as ViewShowRendering + legacyShowRendering(asScreen.rendering, environment) + } + ) +} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/BuilderViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/BuilderViewFactory.kt index e18aa37185..50b9a74f0b 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/BuilderViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/BuilderViewFactory.kt @@ -6,7 +6,7 @@ import android.view.ViewGroup import kotlin.reflect.KClass @Suppress("DEPRECATION") -@Deprecated("Use ManualScreenViewFactory") +@Deprecated("Use ScreenViewRunner.forBuiltView") @WorkflowUiExperimentalApi public class BuilderViewFactory( override val type: KClass, diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeScreenViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeScreenViewFactory.kt deleted file mode 100644 index 1603162934..0000000000 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeScreenViewFactory.kt +++ /dev/null @@ -1,179 +0,0 @@ -package com.squareup.workflow1.ui - -import android.content.Context -import android.view.View -import android.view.ViewGroup -import kotlin.reflect.KClass - -/** - * A [ScreenViewFactory] for [WrapperT] that delegates view construction responsibilities - * to the factory registered for [WrappedT]. Allows [WrapperT] to wrap instances of [WrappedT] - * to add information or behavior, without requiring wasteful wrapping in the view system. - * - * One general note: when creating a wrapper rendering, you're very likely to want it - * to implement [Compatible], to ensure that checks made to update or replace a view - * are based on the wrapped item. Each wrapper example below illustrates this. - * - * ## Examples - * - * To make one rendering type an "alias" for another -- that is, to use the same [ScreenViewFactory] - * to display it -- provide nothing but a single-arg unwrap function: - * - * class RealRendering(val data: String) : AndroidScreen { - * ... - * } - * class AliasRendering(val similarData: String) - * - * object DecorativeScreenViewFactory : ScreenViewFactory - * by DecorativeScreenViewFactory( - * type = AliasRendering::class, unwrap = { alias -> - * RealRendering(alias.similarData) - * } - * ) - * - * To make a wrapper that adds information to the [ViewEnvironment]: - * - * class NeutronFlowPolarity(val reversed: Boolean) { - * companion object : ViewEnvironmentKey( - * NeutronFlowPolarity::class - * ) { - * override val default: NeutronFlowPolarity = - * NeutronFlowPolarity(reversed = false) - * } - * } - * - * class NeutronFlowPolarityOverride( - * val wrapped: W, - * val polarity: NeutronFlowPolarity - * ) : Screen, Compatible { - * override val compatibilityKey: String = Compatible.keyFor(wrapped) - * } - * - * object NeutronFlowPolarityViewFactory : - * ScreenViewFactory> - * by DecorativeScreenViewFactory( - * type = NeutronFlowPolarityOverride::class, - * unwrap = { override, env -> - * Pair(override.wrapped, env + (NeutronFlowPolarity to override.polarity)) - * } - * ) - * - * To make a wrapper that customizes [View] initialization: - * - * class WithTutorialTips(val wrapped: W) : Screen, Compatible { - * override val compatibilityKey: String = Compatible.keyFor(wrapped) - * } - * - * object WithTutorialTipsViewFactory : ScreenViewFactory> - * by DecorativeScreenViewFactory( - * type = WithTutorialTips::class, - * unwrap = { withTips -> withTips.wrapped }, - * viewStarter = { view, doStart -> - * TutorialTipRunner.run(this) - * doStart() - * } - * ) - * - * To make a wrapper that adds pre- or post-processing to [View] updates: - * - * class BackButtonScreen( - * val wrapped: W, - * val override: Boolean = false, - * val onBackPressed: (() -> Unit)? = null - * ) : Screen, Compatible { - * override val compatibilityKey: String = Compatible.keyFor(wrapped) - * } - * - * object BackButtonViewFactory : ScreenViewFactory> - * by DecorativeScreenViewFactory( - * type = BackButtonScreen::class, - * unwrap = { wrapper -> wrapper.wrapped }, - * doShowRendering = { view, wrappedShowRendering, wrapper, viewEnvironment -> - * if (!wrapper.override) { - * // Place our handler before invoking wrappedShowRendering, so that - * // its later calls to view.backPressedHandler will take precedence - * // over ours. - * view.backPressedHandler = wrapper.onBackPressed - * } - * - * wrappedShowRendering.invoke(wrapper.wrapped, viewEnvironment) - * - * if (wrapper.override) { - * // Place our handler after invoking wrappedShowRendering, so that ours wins. - * view.backPressedHandler = wrapper.onBackPressed - * } - * } - * ) - * - * @param unwrap called to convert instances of [WrapperT] to [WrappedT], and to - * allow [ViewEnvironment] to be transformed. - * - * @param viewStarter An optional wrapper for the function invoked when [View.start] - * is called, allowing for last second initialization of a newly built [View]. - * See [ViewStarter] for details. - * - * @param doShowRendering called to apply the [ViewShowRendering] function for - * [WrappedT], allowing pre- and post-processing. Default implementation simply - * uses [unwrap] to extract the [WrappedT] instance from [WrapperT] and makes the function call. - */ -@WorkflowUiExperimentalApi -public class DecorativeScreenViewFactory( - override val type: KClass, - private val unwrap: (WrapperT, ViewEnvironment) -> Pair, - private val viewStarter: ViewStarter? = null, - private val doShowRendering: ( - view: View, - wrappedShowRendering: ViewShowRendering, - wrapper: WrapperT, - env: ViewEnvironment - ) -> Unit = { _, wrappedShowRendering, wrapper, viewEnvironment -> - val (unwrapped, processedEnv) = unwrap(wrapper, viewEnvironment) - wrappedShowRendering(unwrapped, processedEnv) - } -) : ScreenViewFactory { - - /** - * Convenience constructor for cases requiring no changes to the [ViewEnvironment]. - */ - public constructor( - type: KClass, - unwrap: (WrapperT) -> WrappedT, - viewStarter: ViewStarter? = null, - doShowRendering: ( - view: View, - wrappedShowRendering: ViewShowRendering, - wrapper: WrapperT, - env: ViewEnvironment - ) -> Unit = { _, wrappedShowRendering, wrapper, viewEnvironment -> - wrappedShowRendering(unwrap(wrapper), viewEnvironment) - } - ) : this( - type, - unwrap = { wrapper, viewEnvironment -> Pair(unwrap(wrapper), viewEnvironment) }, - viewStarter = viewStarter, - doShowRendering = doShowRendering - ) - - override fun buildView( - initialRendering: WrapperT, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, - container: ViewGroup? - ): View { - val (unwrapped, processedEnv) = unwrap(initialRendering, initialViewEnvironment) - - return unwrapped.buildView( - processedEnv, - contextForNewView, - container, - viewStarter - ).also { view -> - val wrappedShowRendering: ViewShowRendering = view.getShowRendering()!! - - view.bindShowRendering( - initialRendering, - processedEnv - ) { rendering, env -> doShowRendering(view, wrappedShowRendering, rendering, env) } - } - } -} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeViewFactory.kt index a165c18155..c6434d7484 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeViewFactory.kt @@ -6,7 +6,7 @@ import android.view.ViewGroup import kotlin.reflect.KClass @Suppress("DEPRECATION") -@Deprecated("Use DecorativeScreenViewFactory") +@Deprecated("Use the ScreenViewFactory function, wrapping is much simpler now.") @WorkflowUiExperimentalApi public class DecorativeViewFactory( override val type: KClass, diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutScreenViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutScreenViewFactory.kt index 49a117406b..897bbf62d6 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutScreenViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutScreenViewFactory.kt @@ -19,18 +19,22 @@ internal class LayoutScreenViewFactory( private val runnerConstructor: (View) -> ScreenViewRunner ) : ScreenViewFactory { override fun buildView( - initialRendering: RenderingT, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, + environment: ViewEnvironment, + context: Context, container: ViewGroup? ): View { - return contextForNewView.viewBindingLayoutInflater(container) + return context.viewBindingLayoutInflater(container) .inflate(layoutId, container, false) .also { view -> - val runner = runnerConstructor(view) - view.bindShowRendering(initialRendering, initialViewEnvironment) { rendering, environment -> - runner.showRendering(rendering, environment) - } + view.setViewRunner(runnerConstructor(view)) } } + + override fun updateView( + view: View, + rendering: RenderingT, + environment: ViewEnvironment + ) { + view.getViewRunner().showRendering(rendering, environment) + } } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ManualScreenViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ManualScreenViewFactory.kt deleted file mode 100644 index 97147b970e..0000000000 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ManualScreenViewFactory.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.squareup.workflow1.ui - -import android.content.Context -import android.view.View -import android.view.ViewGroup -import kotlin.reflect.KClass - -/** - * A [ScreenViewFactory] that creates [View]s that need to be generated from code. - * (Use [ScreenViewRunner] to work with XML layout resources.) - * - * data class MyScreen(): AndroidScreen { - * val viewFactory = ManualScreenViewFactory( - * type = MyScreen::class, - * viewConstructor = { initialRendering, _, context, _ -> - * MyFrame(context).apply { - * layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) - * bindShowRendering(initialRendering, ::update) - * } - * ) - * } - * - * private class MyFrame(context: Context) : FrameLayout(context, attributeSet) { - * private fun update(rendering: MyScreen) { ... } - * } - */ -@WorkflowUiExperimentalApi -public class ManualScreenViewFactory( - override val type: KClass, - private val viewConstructor: ( - initialRendering: RenderingT, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, - container: ViewGroup? - ) -> View -) : ScreenViewFactory { - override fun buildView( - initialRendering: RenderingT, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, - container: ViewGroup? - ): View = viewConstructor(initialRendering, initialViewEnvironment, contextForNewView, container) -} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/NamedScreenViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/NamedScreenViewFactory.kt index ddff8051a3..1cec1331a0 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/NamedScreenViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/NamedScreenViewFactory.kt @@ -5,5 +5,16 @@ package com.squareup.workflow1.ui * to the factory for [NamedScreen.wrapped]. */ @WorkflowUiExperimentalApi -internal object NamedScreenViewFactory : ScreenViewFactory> -by DecorativeScreenViewFactory(NamedScreen::class, { named -> named.wrapped }) +internal fun NamedScreenViewFactory( + initialRendering: NamedScreen<*>, + initialViewEnvironment: ViewEnvironment +): ScreenViewFactory> { + val wrappedFactory = initialRendering.wrapped.toViewFactory(initialViewEnvironment) + + return ScreenViewFactory( + buildView = wrappedFactory::buildView, + updateView = { view, rendering, environment -> + wrappedFactory.updateView(view, rendering.wrapped, environment) + } + ) +} 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 17d570b76d..b799934464 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 @@ -5,85 +5,162 @@ import android.view.View import android.view.ViewGroup /** - * Factory for [View] instances that can show renderings of type [RenderingT] : [Screen]. + * A [ViewRegistry.Entry] that can build and update Android [View] instances + * to display [Screen] renderings of a particular [type]. * - * Two concrete [ScreenViewFactory] implementations are provided: + * It is more common to create instances via [ScreenViewRunner.bind] et al. * - * - The various [bind][ScreenViewRunner.bind] methods on [ScreenViewRunner] allow easy use of - * Android XML layout resources and [AndroidX ViewBinding][androidx.viewbinding.ViewBinding]. - * - * - [ManualScreenViewFactory] allows views to be built from code. - * - * It's simplest to have your rendering classes implement [AndroidScreen] to associate - * them with appropriate an appropriate [ScreenViewFactory]. For more flexibility, and to - * avoid coupling your workflow directly to the Android runtime, see [ViewRegistry]. + * TODO: move [ScreenViewRunner.bind] et al here along with their docs; give them better names. */ @WorkflowUiExperimentalApi -public interface ScreenViewFactory : ViewRegistry.Entry { - /** - * Returns a View ready to display [initialRendering] (and any succeeding values) - * via [View.showRendering]. - */ +public interface ScreenViewFactory : ViewRegistry.Entry { public fun buildView( - initialRendering: RenderingT, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, + environment: ViewEnvironment, + context: Context, container: ViewGroup? = null ): View + + public fun updateView( + view: View, + rendering: ScreenT, + environment: ViewEnvironment, + ) +} + +@WorkflowUiExperimentalApi +public inline fun ScreenViewFactory( + crossinline buildView: (ViewEnvironment, Context, ViewGroup?) -> View, + crossinline updateView: (View, ScreenT, ViewEnvironment) -> Unit +): ScreenViewFactory { + return object : ScreenViewFactory { + override val type = ScreenT::class + + override fun buildView( + environment: ViewEnvironment, + context: Context, + container: ViewGroup? + ) = buildView(environment, context, container) + + override fun updateView( + view: View, + rendering: ScreenT, + environment: ViewEnvironment + ) = updateView(view, rendering, environment) + } } /** - * It is usually more convenient to use [WorkflowViewStub] or [DecorativeScreenViewFactory] - * than to call this method directly. - * - * Finds a [ScreenViewFactory] to create a [View] to display the receiving [Screen]. - * The caller is responsible for calling [View.start] on the new [View]. After that, - * [View.showRendering] can be used to update it with new renderings that - * are [compatible] with this [Screen]. [WorkflowViewStub] takes care of this chore itself. + * Use the [ScreenViewFactoryFinder] in [environment] to return the [ScreenViewFactory] + * bound to the type of the receiving [Screen]. * - * @param viewStarter An optional wrapper for the function invoked when [View.start] - * is called, allowing for last second initialization of a newly built [View]. - * See [ViewStarter] for details. - * - * @throws IllegalArgumentException if no builder can be found for type [ScreenT] + * - It is more common to use [WorkflowViewStub.show] than to call this method directly + * - Call [ScreenViewFactory.start] to create and initialize a new [View] + * - If you don't particularly need to mess with the [ScreenViewFactory] before creating + * a view, use [Screen.toView] instead of this method. + */ +@WorkflowUiExperimentalApi +public fun ScreenT.toViewFactory( + environment: ViewEnvironment +): ScreenViewFactory { + return environment[ScreenViewFactoryFinder].getViewFactoryForRendering(environment, this) +} + +/** + * Creates a [ScreenViewHolder] wrapping a [View] able to display [initialRendering], + * and initializes the view. * - * @throws IllegalStateException if the matching [ScreenViewFactory] fails to call - * [View.bindShowRendering] when constructing the view + * By default "initialize" means making the first call to [ScreenViewHolder.show]. + * To add more initialization behavior (typically a call to [WorkflowLifecycleOwner.installOn]), + * provide a [viewStarter]. */ +@Suppress("DEPRECATION") @WorkflowUiExperimentalApi -public fun ScreenT.buildView( - viewEnvironment: ViewEnvironment, +public fun ScreenViewFactory.start( + initialRendering: ScreenT, + initialViewEnvironment: ViewEnvironment, contextForNewView: Context, container: ViewGroup? = null, - viewStarter: ViewStarter? = null, -): View { - val viewFactory = viewEnvironment[ScreenViewFactoryFinder].getViewFactoryForRendering( - viewEnvironment, this - ) + viewStarter: ViewStarter? = null +): ScreenViewHolder { + return ScreenViewHolder( + this, + initialViewEnvironment, + initialRendering, + buildView(initialViewEnvironment, contextForNewView, container), + ).also { holder -> + val resolvedStarter = viewStarter ?: ViewStarter { _, doStart -> doStart() } - return viewFactory.buildView(this, viewEnvironment, contextForNewView, container).also { view -> - checkNotNull(view.workflowViewStateOrNull) { - "View.bindShowRendering should have been called for $view, typically by the " + - "ScreenViewFactory that created it." - } - viewStarter?.let { givenStarter -> - val doStart = view.starter - view.starter = { newView -> - givenStarter.startView(newView) { doStart.invoke(newView) } + val legacyStarter: ((View) -> Unit)? = holder.view.starterOrNull + + if (legacyStarter != null) { + var shown = false + // This View was built by a legacy ViewFactory, and so it needs to be + // started in just the right way. + // + // The tricky bit is the old starter's default value, a function that calls + // View.showRendering(). Odds are it's wrapped and wrapped again deep inside + // legacyStarter. To ensure it gets called at the right time, and that we don't + // update the view redundantly, we use bindShowRendering to replace View.showRendering() + // with a call to our own holder.show(). (No need to call the original showRendering(), + // AsScreenViewFactory blanked it.) + // + // This same call to bindShowRendering will also update View.getRendering() and + // View.environment() to return what was passed in here, as expected. + holder.view.bindShowRendering( + initialRendering, initialViewEnvironment + ) { rendering, environment -> + holder.show(rendering, environment) + shown = true + } + holder.view.starter = { startingView -> + resolvedStarter.startView(startingView) { legacyStarter(startingView) } + } + // We have to call View.start() to fire this off rather than calling the starter directly, + // to keep the rest of the legacy machinery happy. + holder.view.start() + check(shown) { + "A ViewStarter provided to ViewRegistry.buildView or a DecorativeViewFactory " + + "neglected to call the given doStart() function" + } + } else { + var shown = false + resolvedStarter.startView(holder.view) { + holder.show(initialRendering, initialViewEnvironment) + shown = true + } + check(shown) { + "A ViewStarter provided to Screen.toView or ScreenViewFactory.start " + + "neglected to call the given doStart() function" } } } } /** - * A wrapper for the function invoked when [View.start] is called, allowing for - * last second initialization of a newly built [View]. Provided via [Screen.buildView] - * or [DecorativeScreenViewFactory.viewStarter]. + * Creates a [View] able to display [initialRendering], and initializes it. By + * default "initialize" means making the first call to [ScreenViewFactory.updateView]. + * To add more initialization behavior (typically a call to [WorkflowLifecycleOwner.installOn]), + * provide a [viewStarter]. * - * While [View.getRendering] may be called from [startView], it is not safe to - * assume that the type of the rendering retrieved matches the type the view was - * originally built to display. [ScreenViewFactory] instances can be wrapped, and - * renderings can be mapped to other types. + * This method is purely shorthand for calling [Screen.toViewFactory] and then + * [ScreenViewFactory.start]. You might wish to make those calls separately if you + * need to treat the [ScreenViewFactory] before using it. + */ +@WorkflowUiExperimentalApi +public fun ScreenT.toView( + initialViewEnvironment: ViewEnvironment, + contextForNewView: Context, + container: ViewGroup? = null, + viewStarter: ViewStarter? = null +): ScreenViewHolder { + return toViewFactory(initialViewEnvironment) + .start(this, initialViewEnvironment, contextForNewView, container, viewStarter) +} + +/** + * A wrapper for the function invoked when [ScreenViewFactory.start] or + * [Screen.toView] is called, allowing for custom initialization of + * a newly built [View] before or after the first call to [ScreenViewFactory.updateView]. */ @WorkflowUiExperimentalApi public fun interface ViewStarter { 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 9f5f5312f2..4d4336c3fd 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 @@ -7,6 +7,7 @@ import com.squareup.workflow1.ui.container.BodyAndModalsScreen import com.squareup.workflow1.ui.container.EnvironmentScreen import com.squareup.workflow1.ui.container.EnvironmentScreenViewFactory +// TODO: stale /** * [ViewEnvironment] service object used by [Screen.buildView] to find the right * [ScreenViewFactory]. The default implementation makes [AndroidScreen] work @@ -57,7 +58,9 @@ public interface ScreenViewFactoryFinder { @Suppress("UNCHECKED_CAST") return (entry as? ScreenViewFactory) ?: (rendering as? AndroidScreen<*>)?.viewFactory as? ScreenViewFactory - ?: (rendering as? AsScreen<*>)?.let { AsScreenViewFactory as ScreenViewFactory } + ?: (rendering as? AsScreen<*>)?.let { + AsScreenViewFactory(it, environment) as ScreenViewFactory + } ?: (rendering as? BackStackScreen<*>)?.let { BackStackScreenViewFactory as ScreenViewFactory } @@ -65,10 +68,10 @@ public interface ScreenViewFactoryFinder { BodyAndModalsContainer as ScreenViewFactory } ?: (rendering as? NamedScreen<*>)?.let { - NamedScreenViewFactory as ScreenViewFactory + NamedScreenViewFactory(it, environment) as ScreenViewFactory } - ?: (rendering as? EnvironmentScreen<*>)?.let { - EnvironmentScreenViewFactory as ScreenViewFactory + ?: (rendering as? EnvironmentScreen<*>)?.let { environmentScreen -> + EnvironmentScreenViewFactory(environmentScreen, 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/ScreenViewHolder.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewHolder.kt new file mode 100644 index 0000000000..dba6215118 --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewHolder.kt @@ -0,0 +1,55 @@ +package com.squareup.workflow1.ui + +import android.view.View + +/** + * Created by [ScreenViewFactory.start], a [ScreenViewHolder] holds a live + * Android [View] driven by a workflow [ScreenT] rendering. It is rare + * to use this class directly, [WorkflowViewStub] drives it and is more convenient. + */ +@WorkflowUiExperimentalApi +public class ScreenViewHolder( + private val factory: ScreenViewFactory, + initialEnvironment: ViewEnvironment, + initialRendering: ScreenT, + public val view: View +) { + public var screen: ScreenT = initialRendering + private set + + public var environment: ViewEnvironment = initialEnvironment + private set + + /** + * Returns true if the [screen] is [compatible] with [rendering], implying that it is safe + * to use [rendering] to update [view] via a call to [show]. + */ + public fun canShow(rendering: Screen): Boolean { + return compatible(screen, rendering) + } + + /** + * Uses [factory] to update [view] to show [rendering]. Adds [rendering] to + * [environment] as the value for [Screen] before calling [ScreenViewFactory.updateView]. + * + * This is done to ensure that view code has access to any wrapper renderings (e.g. + * [NamedScreen], [EnvironmentScreen][com.squareup.workflow1.ui.container.EnvironmentScreen]) + * around the one whose [ScreenViewFactory] is actually driving the [View]. For example, + * the standard containers rely on this mechanism to provide sufficiently unique keys + * to [WorkflowSavedStateRegistryAggregator][com.squareup.workflow1.ui.androidx.WorkflowSavedStateRegistryAggregator]. + */ + public fun show( + rendering: ScreenT, + environment: ViewEnvironment + ) { + check(compatible(screen, rendering)) { + "Expected $this to be able to show rendering $rendering, but that did not match " + + "previous rendering $screen. " + + "Consider using WorkflowViewStub to display arbitrary types." + } + + screen = rendering + this.environment = environment + (Screen to rendering) + factory.updateView(view, screen, this.environment) + } +} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewRunner.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewRunner.kt index 018315fd7b..b551a1018d 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewRunner.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewRunner.kt @@ -6,17 +6,28 @@ import android.view.View import android.view.ViewGroup import androidx.annotation.LayoutRes import androidx.viewbinding.ViewBinding +import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bind +import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bindBuiltView @WorkflowUiExperimentalApi public typealias ViewBindingInflater = (LayoutInflater, ViewGroup?, Boolean) -> BindingT +@WorkflowUiExperimentalApi +public typealias ViewAndRunnerBuilder = + (ViewEnvironment, Context, ViewGroup?) -> Pair> + /** - * A delegate that implements a [showRendering] method to be called when a workflow - * rendering of type [RenderingT] : [Screen] is ready to be displayed in a view created - * by a [ScreenViewFactory]. + * An object that manages a [View] instance built by a [ScreenViewFactory.buildView], providing + * continuity between calls to [ScreenViewFactory.updateView]. A [ScreenViewRunner] + * is instantiated when its [View] is built -- there is a 1:1 relationship between a [View] + * and the [ScreenViewRunner] that drives it. * - * If you're using [AndroidX ViewBinding][ViewBinding] you likely won't need to - * implement this interface at all. For details, see the three overloads of [ScreenViewRunner.bind]. + * Note that use of [ScreenViewRunner] is not required by [ScreenViewFactory]. [ScreenViewRunner] + * is just a convenient bit of glue for working with [AndroidX ViewBinding][ViewBinding], XML + * layout resources, etc. + * + * Use a [bind] function to tie a [ScreenViewRunner] implementation to a [ScreenViewFactory] + * derived from an Android [ViewBinding], XML layout resource, or [factory function][bindBuiltView]. */ @WorkflowUiExperimentalApi public fun interface ScreenViewRunner { @@ -51,7 +62,7 @@ public fun interface ScreenViewRunner { /** * Creates a [ScreenViewFactory] that [inflates][bindingInflater] a [ViewBinding] ([BindingT]) - * to show renderings of type [RenderingT] : [Screen], using a [ScreenViewRunner] + * to show renderings of type [RenderingT], using a [ScreenViewRunner] * created by [constructor]. Handy if you need to perform some set up before * [showRendering] is called. * @@ -72,6 +83,8 @@ public fun interface ScreenViewRunner { * If the view doesn't need to be initialized before [showRendering] is called, * use the variant above which just takes a lambda. */ + // TODO: all these bind names are terrible, and these functions should move + // to ScreenViewFactory.Companion public inline fun bind( noinline bindingInflater: ViewBindingInflater, noinline constructor: (BindingT) -> ScreenViewRunner @@ -80,7 +93,7 @@ public fun interface ScreenViewRunner { /** * Creates a [ScreenViewFactory] that inflates [layoutId] to show renderings of - * type [RenderingT] : [Screen], using a [ScreenViewRunner] created by [constructor]. + * type [RenderingT], using a [ScreenViewRunner] created by [constructor]. * Avoids any use of [AndroidX ViewBinding][ViewBinding]. */ public inline fun bind( @@ -90,13 +103,32 @@ public fun interface ScreenViewRunner { LayoutScreenViewFactory(RenderingT::class, layoutId, constructor) /** - * Creates a [ScreenViewFactory] that inflates [layoutId] to "show" renderings of type [RenderingT], - * with a no-op [ScreenViewRunner]. Handy for showing static views, e.g. when prototyping. + * Creates a [ScreenViewFactory] that inflates [layoutId] to "show" renderings of type + * [RenderingT], but never updates the created view. Handy for showing static displays, + * e.g. when prototyping. */ @Suppress("unused") public inline fun bindNoRunner( @LayoutRes layoutId: Int ): ScreenViewFactory = bind(layoutId) { ScreenViewRunner { _, _ -> } } + + /** + * Creates a [ScreenViewFactory] that uses [buildViewAndRunner] to create both a + * [View] and a mated [ScreenViewRunner] to handle calls to [ScreenViewFactory.updateView]. + */ + public inline fun bindBuiltView( + crossinline buildViewAndRunner: ViewAndRunnerBuilder, + ): ScreenViewFactory = ScreenViewFactory( + buildView = { environment, context, container -> + val (view: View, runner: ScreenViewRunner) = + buildViewAndRunner(environment, context, container) + view.setViewRunner(runner) + view + }, + updateView = { view, rendering, environment -> + view.getViewRunner().showRendering(rendering, environment) + } + ) } } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewBindingScreenViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewBindingScreenViewFactory.kt index ab8d0b072a..57aed55162 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewBindingScreenViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewBindingScreenViewFactory.kt @@ -14,20 +14,19 @@ internal class ViewBindingScreenViewFactory ScreenViewRunner ) : ScreenViewFactory { override fun buildView( - initialRendering: RenderingT, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, + environment: ViewEnvironment, + context: Context, container: ViewGroup? ): View = - bindingInflater(contextForNewView.viewBindingLayoutInflater(container), container, false) - .also { binding -> - val runner = runnerConstructor(binding) - binding.root.bindShowRendering( - initialRendering, - initialViewEnvironment - ) { rendering, environment -> - runner.showRendering(rendering, environment) - } - } + bindingInflater(context.viewBindingLayoutInflater(container), container, false) + .also { binding -> binding.root.setViewRunner(runnerConstructor(binding)) } .root + + override fun updateView( + view: View, + rendering: RenderingT, + environment: ViewEnvironment + ) { + view.getViewRunner().showRendering(rendering, environment) + } } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewSetViewRunner.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewSetViewRunner.kt new file mode 100644 index 0000000000..69dc0f2407 --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewSetViewRunner.kt @@ -0,0 +1,31 @@ +package com.squareup.workflow1.ui + +import android.view.View + +/** + * Puts the given [ScreenViewRunner] in a tag on the receiving view. Expected to be + * called immediately after the view is built, and never again. + * + * @throws IllegalStateException if the runner was already set. + */ +@WorkflowUiExperimentalApi +@PublishedApi +internal fun View.setViewRunner(runner: ScreenViewRunner) { + getTag(R.id.workflow_view_runner)?.let { + error( + "Updating a view's ScreenViewRunner is not supported, " + + "but found existing runner $it on view $this" + ) + } + setTag(R.id.workflow_view_runner, runner) +} + +@WorkflowUiExperimentalApi +@PublishedApi +internal fun View.getViewRunner(): ScreenViewRunner { + @Suppress("UNCHECKED_CAST") + return getTag(R.id.workflow_view_runner) as? ScreenViewRunner + ?: error( + "Expected a ScreenViewRunner on $this, instead found ${getTag(R.id.workflow_view_runner)}" + ) +} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewShowRendering.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewShowRendering.kt index 7400ec2fb1..893963d235 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewShowRendering.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewShowRendering.kt @@ -29,6 +29,7 @@ public typealias ViewShowRendering = * @see DecorativeViewFactory */ @WorkflowUiExperimentalApi +@Deprecated("Replaced by ScreenViewFactory.updateView") public fun View.bindShowRendering( initialRendering: RenderingT, initialViewEnvironment: ViewEnvironment, @@ -58,6 +59,7 @@ public fun View.bindShowRendering( * - It is an error to call [View.showRendering] without having called this method first. */ @WorkflowUiExperimentalApi +@Deprecated("Use ScreenViewFactory.start to create a ScreenViewHolder") public fun View.start() { val current = workflowViewStateAsNew workflowViewState = Started(current.showing, current.environment, current.showRendering) @@ -74,7 +76,9 @@ public fun View.start() { * [View.getRendering] and the new one. */ @WorkflowUiExperimentalApi +@Deprecated("Replaced by ScreenViewHolder.canShow") public fun View.canShowRendering(rendering: Any): Boolean { + @Suppress("DEPRECATION") return getRendering()?.let { compatible(it, rendering) } == true } @@ -88,6 +92,7 @@ public fun View.canShowRendering(rendering: Any): Boolean { * @throws IllegalStateException if [bindShowRendering] has not been called. */ @WorkflowUiExperimentalApi +@Deprecated("Replaced by ScreenViewHolder.show") public fun View.showRendering( rendering: RenderingT, viewEnvironment: ViewEnvironment @@ -113,6 +118,7 @@ public fun View.showRendering( * @throws ClassCastException if the current rendering is not of type [RenderingT] */ @WorkflowUiExperimentalApi +@Deprecated("Replaced by ViewEnvironment[Screen]") public inline fun View.getRendering(): RenderingT? { // Can't use a val because of the parameter type. return when (val showing = workflowViewStateOrNull?.showing) { @@ -126,6 +132,7 @@ public inline fun View.getRendering(): RenderingT? { * has never been called. */ @WorkflowUiExperimentalApi +@Deprecated("Replaced by ScreenViewHolder.environment") public val View.environment: ViewEnvironment? get() = workflowViewStateOrNull?.environment @@ -134,6 +141,7 @@ public val View.environment: ViewEnvironment? * if that method has never been called. */ @WorkflowUiExperimentalApi +@Deprecated("Replaced by ScreenViewHolder") public fun View.getShowRendering(): ViewShowRendering? { return workflowViewStateOrNull?.showRendering } @@ -144,3 +152,7 @@ internal var View.starter: (View) -> Unit set(value) { workflowViewState = workflowViewStateAsNew.copy(starter = value) } + +@WorkflowUiExperimentalApi +internal val View.starterOrNull: ((View) -> Unit)? + get() = (workflowViewStateOrNull as? New<*>)?.starter diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowViewState.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowViewState.kt index a0d42a28c7..7616898e7c 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowViewState.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowViewState.kt @@ -22,6 +22,7 @@ internal sealed class WorkflowViewState { override val showRendering: ViewShowRendering, val starter: (View) -> Unit = { view -> + @Suppress("DEPRECATION") view.showRendering(view.getRendering()!!, view.environment!!) } ) : WorkflowViewState() @@ -37,7 +38,7 @@ internal sealed class WorkflowViewState { @WorkflowUiExperimentalApi @PublishedApi internal val View.workflowViewStateOrNull: WorkflowViewState<*>? - get() = getTag(R.id.workflow_ui_view_state) as? WorkflowViewState<*> + get() = getTag(R.id.legacy_workflow_view_state) as? WorkflowViewState<*> @WorkflowUiExperimentalApi internal var View.workflowViewState: WorkflowViewState<*> @@ -45,7 +46,7 @@ internal var View.workflowViewState: WorkflowViewState<*> "Expected $this to have been built by a ViewFactory. " + "Perhaps the factory did not call View.bindShowRendering." ) - set(value) = setTag(R.id.workflow_ui_view_state, value) + set(value) = setTag(R.id.legacy_workflow_view_state, value) @WorkflowUiExperimentalApi internal val View.workflowViewStateAsNew: New<*> get() = workflowViewState as? New<*> ?: error( diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowViewStub.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowViewStub.kt index 46557974a0..6f6fb66f16 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowViewStub.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowViewStub.kt @@ -69,12 +69,13 @@ public class WorkflowViewStub @JvmOverloads constructor( defStyle: Int = 0, defStyleRes: Int = 0 ) : View(context, attributeSet, defStyle, defStyleRes) { + private var holder: ScreenViewHolder? = null + /** * On-demand access to the view created by the last call to [update], * or this [WorkflowViewStub] instance if none has yet been made. */ - public var actual: View = this - private set + public val actual: View get() = holder?.view ?: this /** * If true, the visibility of views created by [update] will be copied @@ -175,8 +176,12 @@ public class WorkflowViewStub @JvmOverloads constructor( rendering: Any, viewEnvironment: ViewEnvironment ): View { - @Suppress("DEPRECATION") - return show(asScreen(rendering), viewEnvironment) + show( + @Suppress("DEPRECATION") + asScreen(rendering), + viewEnvironment + ) + return holder!!.view } /** @@ -206,49 +211,36 @@ public class WorkflowViewStub @JvmOverloads constructor( public fun show( rendering: Screen, viewEnvironment: ViewEnvironment - ): View { - actual.takeIf { it.canShowRendering(rendering) } + ) { + holder?.takeIf { it.canShow(rendering) } ?.let { - it.showRendering(rendering, viewEnvironment) - return it + it.show(rendering, viewEnvironment) + return } val parent = actual.parent as? ViewGroup ?: throw IllegalStateException("WorkflowViewStub must have a non-null ViewGroup parent") - // If we have a delegate view (i.e. this !== actual), then the old delegate is going to - // eventually be detached by replaceOldViewInParent. When that happens, it's not just a regular - // detach, it's a navigation event that effectively says that view will never come back. Thus, - // we want its Lifecycle to move to permanently destroyed, even though the parent lifecycle is - // still probably alive. - // - // If actual === this, then this stub hasn't been initialized with a real delegate view yet. If - // we're a child of another container which set a WorkflowLifecycleOwner on this view, this - // get() call will return the WLO owned by that parent. We noop in that case since destroying - // that lifecycle is our parent's responsibility in that case, not ours. - if (actual !== this) { + holder?.view?.let { + // The old view is about to be detached by replaceOldViewInParent. When that happens, + // it's not just a regular detach, it's a navigation event that effectively says that view + // will never come back. Thus, we want its Lifecycle to move to permanently destroyed, even + // though the parent lifecycle is still probably alive. WorkflowLifecycleOwner.get(actual)?.destroyOnDetach() } - return rendering.buildView( - viewEnvironment, - parent.context, - parent, - viewStarter = { view, doStart -> - WorkflowLifecycleOwner.installOn(view) - doStart() - } - ) - .also { newView -> - newView.start() + holder = rendering.toView(viewEnvironment, parent.context, parent) { view, doStart -> + WorkflowLifecycleOwner.installOn(view) + doStart() + }.also { + val newView = it.view - if (inflatedId != NO_ID) newView.id = inflatedId - if (updatesVisibility) newView.visibility = visibility - background?.let { newView.background = it } - propagateSavedStateRegistryOwner(newView) - replaceOldViewInParent(parent, newView) - actual = newView - } + if (inflatedId != NO_ID) newView.id = inflatedId + if (updatesVisibility) newView.visibility = visibility + background?.let { newView.background = it } + propagateSavedStateRegistryOwner(newView) + replaceOldViewInParent(parent, newView) + } } /** diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/androidx/WorkflowLifecycleOwner.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/androidx/WorkflowLifecycleOwner.kt index 3899a522c7..283fa072a0 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/androidx/WorkflowLifecycleOwner.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/androidx/WorkflowLifecycleOwner.kt @@ -74,7 +74,7 @@ public interface WorkflowLifecycleOwner : LifecycleOwner { */ public fun installOn( view: View, - findParentLifecycle: (View) -> Lifecycle = { v -> findParentViewTreeLifecycle(v) } + findParentLifecycle: (View) -> Lifecycle = this::findParentViewTreeLifecycle ) { RealWorkflowLifecycleOwner(findParentLifecycle).also { ViewTreeLifecycleOwner.set(view, it) diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AndroidDialogBounds.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AndroidDialogBounds.kt index 301e929ba0..6013d72900 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AndroidDialogBounds.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AndroidDialogBounds.kt @@ -35,14 +35,6 @@ public fun Dialog.setBounds(bounds: Rect) { } } -@WorkflowUiExperimentalApi -internal fun D.maintainBounds( - view: View, - onBoundsChange: (D, Rect) -> Unit -) { - maintainBounds(view.environment!!, onBoundsChange) -} - @WorkflowUiExperimentalApi internal fun D.maintainBounds( environment: ViewEnvironment, 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 85d3107db9..0215b69d65 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 @@ -1,11 +1,11 @@ package com.squareup.workflow1.ui.container import com.squareup.workflow1.ui.AndroidScreen -import com.squareup.workflow1.ui.DecorativeScreenViewFactory import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.backPressedHandler +import com.squareup.workflow1.ui.toViewFactory /** * Adds optional back button handling to a [wrapped] rendering, possibly overriding that @@ -28,22 +28,24 @@ public class BackButtonScreen( public val shadow: Boolean = false, public val onBackPressed: (() -> Unit)? = null ) : AndroidScreen> { - override val viewFactory: ScreenViewFactory> = DecorativeScreenViewFactory( - type = BackButtonScreen::class, - unwrap = { outer -> outer.wrapped }, - doShowRendering = { view, innerShowRendering, outerRendering, viewEnvironment -> - if (!outerRendering.shadow) { + + override val viewFactory: ScreenViewFactory> = ScreenViewFactory( + buildView = { environment, context, container -> + wrapped.toViewFactory(environment).buildView(environment, context, container) + }, + updateView = { view, rendering, environment -> + if (!rendering.shadow) { // Place our handler before invoking innerShowRendering, so that // its later calls to view.backPressedHandler will take precedence // over ours. - view.backPressedHandler = outerRendering.onBackPressed + view.backPressedHandler = rendering.onBackPressed } - innerShowRendering.invoke(outerRendering.wrapped, viewEnvironment) + rendering.wrapped.toViewFactory(environment).updateView(view, rendering.wrapped, environment) - if (outerRendering.shadow) { + if (rendering.shadow) { // Place our handler after invoking innerShowRendering, so that ours wins. - view.backPressedHandler = outerRendering.onBackPressed + view.backPressedHandler = rendering.onBackPressed } } ) 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 5353d5a9cc..5c1afc884b 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 @@ -14,21 +14,21 @@ import androidx.transition.Slide import androidx.transition.TransitionManager import androidx.transition.TransitionSet import com.squareup.workflow1.ui.Compatible +import com.squareup.workflow1.ui.Compatible.Companion.keyFor import com.squareup.workflow1.ui.NamedScreen import com.squareup.workflow1.ui.R +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.ScreenViewHolder import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.androidx.WorkflowAndroidXSupport.stateRegistryOwnerFromViewTreeOrContext import com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner -import com.squareup.workflow1.ui.buildView -import com.squareup.workflow1.ui.canShowRendering import com.squareup.workflow1.ui.compatible import com.squareup.workflow1.ui.container.BackStackConfig.First import com.squareup.workflow1.ui.container.BackStackConfig.Other import com.squareup.workflow1.ui.container.ViewStateCache.SavedState -import com.squareup.workflow1.ui.getRendering -import com.squareup.workflow1.ui.showRendering import com.squareup.workflow1.ui.start +import com.squareup.workflow1.ui.toViewFactory /** * A container view that can display a stream of [BackStackScreen] instances. @@ -48,13 +48,23 @@ public open class BackStackContainer @JvmOverloads constructor( private val viewStateCache = ViewStateCache() - private val currentView: View? get() = if (childCount > 0) getChildAt(0) else null + private var currentViewHolder: ScreenViewHolder>? = null private var currentRendering: BackStackScreen>? = null + /** + * Unique identifier for this view for SavedStateRegistry purposes. Based on the + * [Compatible.keyFor] the current rendering. Taking this approach allows + * feature developers to take control over naming, e.g. by wrapping renderings + * with [NamedScreen][com.squareup.workflow1.ui.NamedScreen]. + */ + private lateinit var savedStateParentKey: String + public fun update( newRendering: BackStackScreen<*>, newViewEnvironment: ViewEnvironment ) { + savedStateParentKey = keyFor(newViewEnvironment[Screen]) + val config = if (newRendering.backStack.isEmpty()) First else Other val environment = newViewEnvironment + config @@ -63,19 +73,20 @@ public open class BackStackContainer @JvmOverloads constructor( // It's fine if client code is already using Named for its own purposes, recursion works. .map { NamedScreen(it, "backstack") } - val oldViewMaybe = currentView + val oldViewHolderMaybe = currentViewHolder // If existing view is compatible, just update it. - oldViewMaybe - ?.takeIf { it.canShowRendering(named.top) } + oldViewHolderMaybe + ?.takeIf { it.canShow(named.top) } ?.let { viewStateCache.prune(named.frames) - it.showRendering(named.top, environment) + it.show(named.top, environment) return } - val newView = named.top.buildView( - viewEnvironment = environment, + val newViewHolder = named.top.toViewFactory(environment).start( + initialRendering = named.top, + initialViewEnvironment = environment, contextForNewView = this.context, container = this, viewStarter = { view, doStart -> @@ -83,39 +94,41 @@ public open class BackStackContainer @JvmOverloads constructor( doStart() } ) - newView.start() - viewStateCache.update(named.backStack, oldViewMaybe, newView) + viewStateCache.update(named.backStack, oldViewHolderMaybe, newViewHolder) val popped = currentRendering?.backStack?.any { compatible(it, named.top) } == true - performTransition(oldViewMaybe, newView, popped) + performTransition(oldViewHolderMaybe, newViewHolder, popped) // Notify the view we're about to replace that it's going away. - oldViewMaybe?.let(WorkflowLifecycleOwner::get)?.destroyOnDetach() + oldViewHolderMaybe?.view?.let(WorkflowLifecycleOwner::get)?.destroyOnDetach() + currentViewHolder = newViewHolder currentRendering = named } /** - * Called from [View.showRendering] to swap between views. - * Subclasses can override to customize visual effects. There is no need to call super. - * Note that views are showing renderings of type [NamedScreen]`>`. + * Called from + * [ScreenViewFactory.updateView][com.squareup.workflow1.ui.ScreenViewFactory.updateView] + * to swap between views. Subclasses can override to customize visual effects. + * There is no need to call super. Note that views are showing renderings + * of type [NamedScreen]`<*>`. * - * @param oldViewMaybe the outgoing view, or null if this is the initial rendering. - * @param newView the view that should replace [oldViewMaybe] (if it exists), and become + * @param oldHolderMaybe the outgoing view, or null if this is the initial rendering. + * @param newHolder the view that should replace [oldHolderMaybe] (if it exists), and become * this view's only child * @param popped true if we should give the appearance of popping "back" to a previous rendering, - * false if a new rendering is being "pushed". Should be ignored if [oldViewMaybe] is null. + * false if a new rendering is being "pushed". Should be ignored if [oldHolderMaybe] is null. */ protected open fun performTransition( - oldViewMaybe: View?, - newView: View, + oldHolderMaybe: ScreenViewHolder>?, + newHolder: ScreenViewHolder>, popped: Boolean ) { // Showing something already, transition with push or pop effect. - oldViewMaybe - ?.let { oldView -> - val oldBody: View? = oldView.findViewById(R.id.back_stack_body) - val newBody: View? = newView.findViewById(R.id.back_stack_body) + oldHolderMaybe + ?.let { oldHolder -> + val oldBody: View? = oldHolder.view.findViewById(R.id.back_stack_body) + val newBody: View? = newHolder.view.findViewById(R.id.back_stack_body) val oldTarget: View val newTarget: View @@ -123,8 +136,8 @@ public open class BackStackContainer @JvmOverloads constructor( oldTarget = oldBody newTarget = newBody } else { - oldTarget = oldView - newTarget = newView + oldTarget = oldHolder.view + newTarget = newHolder.view } val (outEdge, inEdge) = when (popped) { @@ -138,12 +151,12 @@ public open class BackStackContainer @JvmOverloads constructor( .setInterpolator(AccelerateDecelerateInterpolator()) TransitionManager.endTransitions(this) - TransitionManager.go(Scene(this, newView), transition) + TransitionManager.go(Scene(this, newHolder.view), transition) return } // This is the first view, just show it. - addView(newView) + addView(newHolder.view) } override fun onSaveInstanceState(): Parcelable { @@ -166,8 +179,7 @@ public open class BackStackContainer @JvmOverloads constructor( // Wire up our viewStateCache to our parent SavedStateRegistry. val parentRegistryOwner = stateRegistryOwnerFromViewTreeOrContext(this) - val key = Compatible.keyFor(this.getRendering()!!) - viewStateCache.attachToParentRegistryOwner(key, parentRegistryOwner) + viewStateCache.attachToParentRegistryOwner(savedStateParentKey, parentRegistryOwner) } override fun onDetachedFromWindow() { diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackScreenViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackScreenViewFactory.kt index 6708071272..1c7042a039 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackScreenViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackScreenViewFactory.kt @@ -2,22 +2,21 @@ package com.squareup.workflow1.ui.container import android.view.ViewGroup.LayoutParams import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import com.squareup.workflow1.ui.ManualScreenViewFactory import com.squareup.workflow1.ui.R import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.bindShowRendering @WorkflowUiExperimentalApi internal object BackStackScreenViewFactory : ScreenViewFactory> -by ManualScreenViewFactory( - type = BackStackScreen::class, - viewConstructor = { initialRendering, initialEnv, context, _ -> +by ScreenViewFactory( + buildView = { _, context, _ -> BackStackContainer(context) .apply { id = R.id.workflow_back_stack_container layoutParams = (LayoutParams(MATCH_PARENT, MATCH_PARENT)) - bindShowRendering(initialRendering, initialEnv, ::update) } + }, + updateView = { view, rendering, environment -> + (view as BackStackContainer).update(rendering, environment) } ) 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 9a24d4359e..0c6a3c6181 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 @@ -13,15 +13,14 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewTreeObserver.OnGlobalLayoutListener import android.widget.FrameLayout import com.squareup.workflow1.ui.Compatible -import com.squareup.workflow1.ui.ManualScreenViewFactory +import com.squareup.workflow1.ui.Compatible.Companion.keyFor import com.squareup.workflow1.ui.R +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub import com.squareup.workflow1.ui.androidx.WorkflowAndroidXSupport -import com.squareup.workflow1.ui.bindShowRendering -import com.squareup.workflow1.ui.getRendering import kotlinx.coroutines.flow.MutableStateFlow @WorkflowUiExperimentalApi @@ -31,6 +30,14 @@ internal class BodyAndModalsContainer @JvmOverloads constructor( defStyle: Int = 0, defStyleRes: Int = 0 ) : FrameLayout(context, attributeSet, defStyle, defStyleRes) { + /** + * Unique identifier for this view for SavedStateRegistry purposes. Based on the + * [Compatible.keyFor] the current rendering. Taking this approach allows + * feature developers to take control over naming, e.g. by wrapping renderings + * with [NamedScreen][com.squareup.workflow1.ui.NamedScreen]. + */ + private lateinit var savedStateParentKey: String + private val baseViewStub: WorkflowViewStub = WorkflowViewStub(context).also { addView(it, ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)) } @@ -75,6 +82,8 @@ internal class BodyAndModalsContainer @JvmOverloads constructor( newScreen: BodyAndModalsScreen<*, *>, viewEnvironment: ViewEnvironment ) { + savedStateParentKey = keyFor(viewEnvironment[Screen]) + val showingModals = newScreen.modals.isNotEmpty() // There is a long wait from when we show a dialog until it starts blocking @@ -97,8 +106,7 @@ internal class BodyAndModalsContainer @JvmOverloads constructor( // Wire up dialogs to our parent SavedStateRegistry. val parentRegistryOwner = WorkflowAndroidXSupport.stateRegistryOwnerFromViewTreeOrContext(this) - val key = Compatible.keyFor(this.getRendering()!!) - dialogs.attachToParentRegistryOwner(key, parentRegistryOwner) + dialogs.attachToParentRegistryOwner(savedStateParentKey, parentRegistryOwner) } override fun onDetachedFromWindow() { @@ -170,16 +178,16 @@ internal class BodyAndModalsContainer @JvmOverloads constructor( } } - companion object : ScreenViewFactory> - by ManualScreenViewFactory( - type = BodyAndModalsScreen::class, - viewConstructor = { initialRendering, initialEnv, context, _ -> + companion object : ScreenViewFactory> by ScreenViewFactory( + buildView = { _, context, _ -> BodyAndModalsContainer(context) .apply { id = R.id.workflow_body_and_modals_container layoutParams = (LayoutParams(MATCH_PARENT, MATCH_PARENT)) - bindShowRendering(initialRendering, initialEnv, ::update) } + }, + updateView = { view, rendering, environment -> + (view as BodyAndModalsContainer).update(rendering, environment) } ) } 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 afd5a57a2b..db897b62ac 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 @@ -1,18 +1,24 @@ package com.squareup.workflow1.ui.container -import com.squareup.workflow1.ui.DecorativeScreenViewFactory import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.merge +import com.squareup.workflow1.ui.toViewFactory @WorkflowUiExperimentalApi -internal object EnvironmentScreenViewFactory : ScreenViewFactory> -by DecorativeScreenViewFactory( - type = EnvironmentScreen::class, - unwrap = { withEnvironment, inheritedEnvironment -> - Pair( - withEnvironment.screen, - inheritedEnvironment merge withEnvironment.viewEnvironment - ) - } -) +internal fun EnvironmentScreenViewFactory( + initialRendering: EnvironmentScreen<*>, + initialViewEnvironment: ViewEnvironment +): ScreenViewFactory> { + val realFactory = initialRendering.screen.toViewFactory( + initialViewEnvironment merge initialRendering.viewEnvironment + ) + + return ScreenViewFactory( + buildView = realFactory::buildView, + updateView = { view, rendering, environment -> + realFactory.updateView(view, rendering.screen, environment) + } + ) +} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactory.kt index 843488cbb0..b35c5593b9 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactory.kt @@ -11,13 +11,12 @@ import android.view.View import android.view.Window import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL import com.squareup.workflow1.ui.R +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.ScreenViewHolder import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.backPressedHandler -import com.squareup.workflow1.ui.buildView -import com.squareup.workflow1.ui.environment -import com.squareup.workflow1.ui.showRendering -import com.squareup.workflow1.ui.start +import com.squareup.workflow1.ui.toView import kotlin.reflect.KClass /** @@ -72,21 +71,18 @@ public abstract class ModalScreenOverlayDialogFactory>( // that should be blocked by this modal session. val wrappedContentRendering = BackButtonScreen(initialRendering.content) { } - val contentView = wrappedContentRendering.buildView(initialEnvironment, context).apply { - start() + val contentViewHolder = wrappedContentRendering.toView(initialEnvironment, context).apply { // If the content view has no backPressedHandler, add a no-op one to // ensure that the `onBackPressed` call below will not leak up to handlers // that should be blocked by this modal session. - if (backPressedHandler == null) backPressedHandler = { } + if (view.backPressedHandler == null) view.backPressedHandler = { } } - return buildDialogWithContentView(contentView).also { dialog -> + return buildDialogWithContentView(contentViewHolder.view).also { dialog -> val window = requireNotNull(dialog.window) { "Dialog must be attached to a window." } - // There is no Dialog.getContentView method, and no reliable way to reverse - // engineer one (no, android.R.id.content doesn't work). So we stick the - // contentView in a tag here, where updateDialog can find it later. - window.peekDecorView()?.setTag(R.id.workflow_modal_dialog_content, contentView) + // Stick the contentViewHolder in a tag, where updateDialog can find it later. + window.peekDecorView()?.setTag(R.id.workflow_modal_dialog_content, contentViewHolder) ?: throw IllegalStateException("Expected decorView to have been built.") val realWindowCallback = window.callback @@ -96,15 +92,15 @@ public abstract class ModalScreenOverlayDialogFactory>( event.action == ACTION_UP return when { - isBackPress -> contentView.environment?.get(ModalScreenOverlayOnBackPressed) - ?.onBackPressed(contentView) == true + isBackPress -> contentViewHolder.environment.get(ModalScreenOverlayOnBackPressed) + .onBackPressed(contentViewHolder.view) == true else -> realWindowCallback.dispatchKeyEvent(event) } } } window.setFlags(FLAG_NOT_TOUCH_MODAL, FLAG_NOT_TOUCH_MODAL) - dialog.maintainBounds(contentView) { d, b -> updateBounds(d, Rect(b)) } + dialog.maintainBounds(contentViewHolder.environment) { d, b -> updateBounds(d, Rect(b)) } } } @@ -115,8 +111,11 @@ public abstract class ModalScreenOverlayDialogFactory>( ) { dialog.window?.peekDecorView() - ?.let { it.getTag(R.id.workflow_modal_dialog_content) as? View } - ?.showRendering( + ?.let { + @Suppress("UNCHECKED_CAST") + it.getTag(R.id.workflow_modal_dialog_content) as? ScreenViewHolder + } + ?.show( // Have to preserve the wrapping done in buildDialog. BackButtonScreen(rendering.content) { }, environment diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayDialogFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayDialogFactory.kt index 4bd302f048..aaf1c8cb60 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayDialogFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayDialogFactory.kt @@ -36,6 +36,6 @@ public interface OverlayDialogFactory : ViewRegistry.Entry @WorkflowUiExperimentalApi public fun T.toDialogFactory( - viewEnvironment: ViewEnvironment + environment: ViewEnvironment ): OverlayDialogFactory = - viewEnvironment[OverlayDialogFactoryFinder].getDialogFactoryForRendering(viewEnvironment, this) + environment[OverlayDialogFactoryFinder].getDialogFactoryForRendering(environment, this) diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ViewStateCache.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ViewStateCache.kt index 58cc8b73da..9ab3f9663f 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ViewStateCache.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ViewStateCache.kt @@ -11,10 +11,10 @@ import androidx.annotation.VisibleForTesting.PRIVATE import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.ViewTreeSavedStateRegistryOwner import com.squareup.workflow1.ui.NamedScreen +import com.squareup.workflow1.ui.ScreenViewHolder import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.androidx.WorkflowSavedStateRegistryAggregator import com.squareup.workflow1.ui.container.ViewStateCache.SavedState -import com.squareup.workflow1.ui.getRendering /** * Handles persistence chores for container views that manage a set of [NamedScreen] renderings, @@ -74,10 +74,10 @@ internal constructor( */ public fun update( retainedRenderings: Collection>, - oldViewMaybe: View?, - newView: View + oldHolderMaybe: ScreenViewHolder>?, + newHolder: ScreenViewHolder> ) { - val newKey = newView.namedKey + val newKey = newHolder.screen.compatibilityKey val hiddenKeys = retainedRenderings.asSequence() .map { it.compatibilityKey } .toSet() @@ -88,19 +88,19 @@ internal constructor( } // Put the [ViewTreeSavedStateRegistryOwner] in place. - stateRegistryAggregator.installChildRegistryOwnerOn(newView, newKey) + stateRegistryAggregator.installChildRegistryOwnerOn(newHolder.view, newKey) viewStates.remove(newKey) - ?.let { newView.restoreHierarchyState(it.viewState) } + ?.let { newHolder.view.restoreHierarchyState(it.viewState) } // Save both the view state and state registry of the view that's going away, as long as it's // still in the backstack. - if (oldViewMaybe != null) { - oldViewMaybe.namedKey.takeIf { hiddenKeys.contains(it) } + if (oldHolderMaybe != null) { + oldHolderMaybe.screen.compatibilityKey.takeIf { hiddenKeys.contains(it) } ?.let { savedKey -> // View state val saved = SparseArray().apply { - oldViewMaybe.saveHierarchyState(this) + oldHolderMaybe.view.saveHierarchyState(this) } viewStates += savedKey to ViewStateFrame(savedKey, saved) @@ -206,13 +206,3 @@ internal constructor( // endregion } - -@WorkflowUiExperimentalApi -private val View.namedKey: String - get() { - val rendering = getRendering>() - return checkNotNull(rendering?.compatibilityKey) { - "Expected $this to be showing a ${NamedScreen::class.java.simpleName}<*> rendering, " + - "found $rendering" - } - } diff --git a/workflow-ui/core-android/src/main/res/values/ids.xml b/workflow-ui/core-android/src/main/res/values/ids.xml index 7bb8e9e4ef..1432d6a765 100644 --- a/workflow-ui/core-android/src/main/res/values/ids.xml +++ b/workflow-ui/core-android/src/main/res/values/ids.xml @@ -5,8 +5,8 @@ Otherwise animates its entire body. --> - - + + @@ -23,9 +23,10 @@ - + + + + + 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 2ec419bb4d..983f413658 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 @@ -169,7 +169,7 @@ internal class LegacyAndroidViewRegistryTest { called = true return mock { on { - getTag(eq(com.squareup.workflow1.ui.R.id.workflow_ui_view_state)) + getTag(eq(com.squareup.workflow1.ui.R.id.legacy_workflow_view_state)) } doReturn (WorkflowViewState.New(initialRendering, initialViewEnvironment, { _, _ -> })) } } 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 4702e004a5..111027c346 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 @@ -8,8 +8,6 @@ import android.view.ViewGroup import com.google.common.truth.Truth.assertThat import com.squareup.workflow1.ui.ViewRegistry.Entry import org.junit.Test -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.eq import org.mockito.kotlin.mock import kotlin.reflect.KClass import kotlin.test.assertFailsWith @@ -31,7 +29,7 @@ internal class ScreenViewFactoryTest { } val error = assertFailsWith { - fooScreen.buildView(env, mock()) + fooScreen.toView(env, mock()) } assertThat(error.message).isEqualTo( "A ScreenViewFactory should have been registered to display " + @@ -44,37 +42,44 @@ internal class ScreenViewFactoryTest { val env = ViewEnvironment.EMPTY + ViewRegistry() val screen = MyAndroidScreen() - screen.buildView(env, mock()) - assertThat(screen.viewFactory.called).isTrue() + screen.toView(env, mock()) + assertThat(screen.viewFactory.built).isTrue() + assertThat(screen.viewFactory.updated).isTrue() } @Test fun `buildView prefers registry entries to AndroidViewRendering`() { val env = ViewEnvironment.EMPTY + ViewRegistry(overrideViewRenderingFactory) val screen = MyAndroidScreen() - screen.buildView(env, mock()) - assertThat(screen.viewFactory.called).isFalse() - assertThat(overrideViewRenderingFactory.called).isTrue() + screen.toView(env, mock()) + assertThat(screen.viewFactory.built).isFalse() + assertThat(screen.viewFactory.updated).isFalse() + assertThat(overrideViewRenderingFactory.built).isTrue() + assertThat(overrideViewRenderingFactory.updated).isTrue() } private class TestViewFactory( override val type: KClass ) : ScreenViewFactory { - var called = false + var built = false + var updated = false override fun buildView( - initialRendering: T, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, + environment: ViewEnvironment, + context: Context, container: ViewGroup? ): View { - called = true + built = true - return mock { - on { - getTag(eq(R.id.workflow_ui_view_state)) - } doReturn (WorkflowViewState.New(initialRendering, initialViewEnvironment, { _, _ -> })) - } + return mock() + } + + override fun updateView( + view: View, + rendering: T, + environment: ViewEnvironment + ) { + updated = true } } diff --git a/workflow-ui/core-common/api/core-common.api b/workflow-ui/core-common/api/core-common.api index 8b97adc3b9..df06d8c83f 100644 --- a/workflow-ui/core-common/api/core-common.api +++ b/workflow-ui/core-common/api/core-common.api @@ -62,6 +62,12 @@ public final class com/squareup/workflow1/ui/NamedScreen : com/squareup/workflow } public abstract interface class com/squareup/workflow1/ui/Screen { + public static final field Companion Lcom/squareup/workflow1/ui/Screen$Companion; +} + +public final class com/squareup/workflow1/ui/Screen$Companion : com/squareup/workflow1/ui/ViewEnvironmentKey { + public fun getDefault ()Lcom/squareup/workflow1/ui/Screen; + public synthetic fun getDefault ()Ljava/lang/Object; } public abstract interface class com/squareup/workflow1/ui/TextController { diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Compatible.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Compatible.kt index 16476a807a..bd653a688a 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Compatible.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Compatible.kt @@ -6,7 +6,7 @@ package com.squareup.workflow1.ui * have the same [Compatible.compatibilityKey]. * * A convenient way to take control over the matching behavior of objects that - * don't implement [Compatible] is to wrap them with [Named]. + * don't implement [Compatible] is to wrap them with [NamedScreen]. */ @WorkflowUiExperimentalApi public fun compatible( @@ -25,7 +25,7 @@ public fun compatible( * than just being of the same type. * * Renderings that don't implement this interface directly can be distinguished - * by wrapping them with [Named]. + * by wrapping them with [NamedScreen]. */ @WorkflowUiExperimentalApi public interface Compatible { diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Screen.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Screen.kt index 37b586e8aa..93c94d2fb3 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Screen.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Screen.kt @@ -2,6 +2,19 @@ package com.squareup.workflow1.ui /** * Marker interface implemented by renderings that map to a UI system's 2d view class. + * + * It is expected that view systems will make the rendering driving a view component + * available in the [ViewEnvironment], as the value for the [Screen] key. In the + * case of wrapper renderings like [NamedScreen], the [ViewEnvironment] entry should + * include all wrappers. */ @WorkflowUiExperimentalApi -public interface Screen +public interface Screen { + public companion object : ViewEnvironmentKey(Screen::class) { + override val default: Screen + get() = error( + "Expected the rendering driving this section of the UI to have been " + + "provided by the appropriate builder." + ) + } +} diff --git a/workflow-ui/internal-testing-android/api/internal-testing-android.api b/workflow-ui/internal-testing-android/api/internal-testing-android.api index 82beba49fb..1358c0dcc4 100644 --- a/workflow-ui/internal-testing-android/api/internal-testing-android.api +++ b/workflow-ui/internal-testing-android/api/internal-testing-android.api @@ -33,7 +33,7 @@ public abstract interface class com/squareup/workflow1/ui/internal/test/Abstract public abstract fun onRestoreInstanceState (Landroid/view/View;Ljava/lang/Object;)V public abstract fun onSaveInstanceState (Landroid/view/View;Ljava/lang/Object;)V public abstract fun onShowRendering (Landroid/view/View;Ljava/lang/Object;)V - public abstract fun onViewCreated (Landroid/view/View;Ljava/lang/Object;)V + public abstract fun onViewCreated (Landroid/view/View;)V public abstract fun onViewTreeLifecycleStateChanged (Ljava/lang/Object;Landroidx/lifecycle/Lifecycle$Event;)V } @@ -43,7 +43,7 @@ public final class com/squareup/workflow1/ui/internal/test/AbstractLifecycleTest public static fun onRestoreInstanceState (Lcom/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity$ViewObserver;Landroid/view/View;Ljava/lang/Object;)V public static fun onSaveInstanceState (Lcom/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity$ViewObserver;Landroid/view/View;Ljava/lang/Object;)V public static fun onShowRendering (Lcom/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity$ViewObserver;Landroid/view/View;Ljava/lang/Object;)V - public static fun onViewCreated (Lcom/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity$ViewObserver;Landroid/view/View;Ljava/lang/Object;)V + public static fun onViewCreated (Lcom/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity$ViewObserver;Landroid/view/View;)V public static fun onViewTreeLifecycleStateChanged (Lcom/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity$ViewObserver;Ljava/lang/Object;Landroidx/lifecycle/Lifecycle$Event;)V } 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 881abc8ad2..27cf9aef85 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 @@ -4,19 +4,18 @@ import android.content.Context import android.os.Bundle import android.os.Parcelable import android.view.View +import android.view.ViewGroup import android.widget.FrameLayout import androidx.lifecycle.Lifecycle.Event import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewTreeLifecycleOwner -import com.squareup.workflow1.ui.ManualScreenViewFactory import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.ViewEnvironment 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 @@ -92,19 +91,30 @@ public abstract class AbstractLifecycleTestActivity : WorkflowUiTestActivity() { type: KClass, viewObserver: ViewObserver, viewConstructor: (Context) -> LeafView = ::LeafView - ): ScreenViewFactory = - ManualScreenViewFactory(type) { initialRendering, initialViewEnvironment, context, _ -> - viewConstructor(context).apply { + ): ScreenViewFactory = object : ScreenViewFactory { + override val type = type + override fun buildView( + environment: ViewEnvironment, + context: Context, + container: ViewGroup? + ): View { + return viewConstructor(context).apply { this.viewObserver = viewObserver - viewObserver.onViewCreated(this, initialRendering) - - bindShowRendering(initialRendering, initialViewEnvironment) { rendering, _ -> - this.rendering = rendering - viewObserver.onShowRendering(this, rendering) - } + viewObserver.onViewCreated(this) } } + override fun updateView( + view: View, + rendering: R, + environment: ViewEnvironment + ) { + @Suppress("UNCHECKED_CAST") + (view as LeafView).rendering = rendering + viewObserver.onShowRendering(view, rendering) + } + } + protected fun lifecycleLoggingViewObserver( describeRendering: (R) -> String ): ViewObserver = object : ViewObserver { @@ -132,8 +142,7 @@ public abstract class AbstractLifecycleTestActivity : WorkflowUiTestActivity() { public interface ViewObserver { public fun onViewCreated( - view: View, - rendering: R + view: View ) { } 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 70676137ae..acdf72d60a 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 @@ -104,7 +104,8 @@ public open class WorkflowUiTestActivity : AppCompatActivity() { wrapped = rendering, name = renderingCounter.toString() ) - return rootStub.show(named, viewEnvironment) + rootStub.show(named, viewEnvironment) + return rootStub.actual } private class NonConfigurationData(