diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/LegacyRunner.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/LegacyRunner.kt index 67da3ee1ef..63da7c5f55 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/LegacyRunner.kt +++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/LegacyRunner.kt @@ -9,8 +9,8 @@ import androidx.compose.ui.tooling.preview.Preview import com.squareup.sample.compose.databinding.LegacyViewBinding import com.squareup.sample.compose.nestedrenderings.RecursiveWorkflow.LegacyRendering import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromViewBinding import com.squareup.workflow1.ui.ScreenViewRunner -import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bind import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.compose.tooling.Preview @@ -28,7 +28,7 @@ class LegacyRunner(private val binding: LegacyViewBinding) : ScreenViewRunner by bind( + companion object : ScreenViewFactory by fromViewBinding( LegacyViewBinding::inflate, ::LegacyRunner ) } 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..cee8e215a5 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 @@ -5,13 +5,14 @@ import android.content.Context import android.util.AttributeSet import android.view.View import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT 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.ScreenViewFactory.Companion.fromCode +import com.squareup.workflow1.ui.ScreenViewHolder 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 +92,17 @@ internal class ScrimContainer @JvmOverloads constructor( } @OptIn(WorkflowUiExperimentalApi::class) - companion object : ScreenViewFactory> by ManualScreenViewFactory( - type = ScrimScreen::class, - viewConstructor = { initialRendering, initialViewEnvironment, contextForNewView, _ -> - val stub = WorkflowViewStub(contextForNewView) + companion object : ScreenViewFactory> by fromCode( + buildView = { _, initialEnvironment, context, _ -> + val stub = WorkflowViewStub(context) + val scrimContainer = ScrimContainer(context) + scrimContainer.layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT) + scrimContainer.addView(stub) - ScrimContainer(contextForNewView) - .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 - } - } + ScreenViewHolder(initialEnvironment, scrimContainer) { rendering, viewEnvironment -> + stub.show(rendering.content, viewEnvironment) + scrimContainer.isDimmed = rendering.dimmed + } } ) } diff --git a/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailContainer.kt b/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailContainer.kt index c2808ca7fd..e2489bd78e 100644 --- a/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailContainer.kt +++ b/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailContainer.kt @@ -89,7 +89,7 @@ class OverviewDetailContainer(view: View) : ScreenViewRunner by ScreenViewRunner.bind( + companion object : ScreenViewFactory by ScreenViewFactory.fromLayout( layoutId = R.layout.overview_detail, constructor = ::OverviewDetailContainer ) diff --git a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonLayoutRunner.kt b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonLayoutRunner.kt index 5250b429f2..284e3df1ff 100644 --- a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonLayoutRunner.kt +++ b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonLayoutRunner.kt @@ -15,7 +15,7 @@ data class HelloBackButtonScreen( val onClick: () -> Unit, val onBackPressed: (() -> Unit)? ) : AndroidScreen { - override val viewFactory: ScreenViewFactory = ScreenViewRunner.bind( + override val viewFactory = ScreenViewFactory.fromLayout( R.layout.hello_back_button_layout, ::HelloBackButtonLayoutRunner ) } diff --git a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoemListRendering.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoemListRendering.kt index f4db773ca9..87e5b359ec 100644 --- a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoemListRendering.kt +++ b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoemListRendering.kt @@ -12,6 +12,7 @@ import com.squareup.sample.container.overviewdetail.OverviewDetailConfig.Overvie import com.squareup.sample.container.poetry.R import com.squareup.sample.poetry.model.Poem import com.squareup.workflow1.ui.AndroidScreen +import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.ScreenViewRunner import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi @@ -22,9 +23,8 @@ data class PoemListScreen( val onPoemSelected: (Int) -> Unit, val selection: Int = -1 ) : AndroidScreen { - override val viewFactory = ScreenViewRunner.bind( - R.layout.list, - ::PoemListLayoutRunner + override val viewFactory = ScreenViewFactory.fromLayout( + R.layout.list, ::PoemListLayoutRunner ) companion object { diff --git a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListScreen.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListScreen.kt index ec5ebe70c6..7b52bf40d7 100644 --- a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListScreen.kt +++ b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListScreen.kt @@ -28,10 +28,8 @@ data class StanzaListScreen( val onExit: () -> Unit, val selection: Int = -1 ) : AndroidScreen { - override val viewFactory: ScreenViewFactory = ScreenViewRunner.bind( - R.layout.list, - ::StanzaListLayoutRunner - ) + override val viewFactory = + ScreenViewFactory.fromLayout(R.layout.list, ::StanzaListLayoutRunner) } @OptIn(WorkflowUiExperimentalApi::class) diff --git a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaScreen.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaScreen.kt index ed438007b3..b83eb7a745 100644 --- a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaScreen.kt +++ b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaScreen.kt @@ -32,10 +32,8 @@ data class StanzaScreen( ) : AndroidScreen, Compatible { override val compatibilityKey = "$title: $stanzaNumber" - override val viewFactory: ScreenViewFactory = ScreenViewRunner.bind( - R.layout.stanza_layout, - ::StanzaLayoutRunner - ) + override val viewFactory = + ScreenViewFactory.fromLayout(R.layout.stanza_layout, ::StanzaLayoutRunner) } @OptIn(WorkflowUiExperimentalApi::class) @@ -113,9 +111,4 @@ private class StanzaLayoutRunner(private val view: View) : ScreenViewRunner by ScreenViewRunner.bind( - R.layout.stanza_layout, - ::StanzaLayoutRunner - ) } 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..6894feb859 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,9 @@ 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.ScreenViewHolder import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.bindShowRendering import kotlin.math.abs import kotlin.math.min @@ -83,11 +82,13 @@ 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.fromCode( + buildView = { _, initialEnvironment, context, _ -> + val view = BoardView(context) + ScreenViewHolder(initialEnvironment, view) { screen, _ -> + view.update(screen) + } } ) } diff --git a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardsListLayoutRunner.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardsListLayoutRunner.kt index 080c8444ca..35cc36d201 100644 --- a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardsListLayoutRunner.kt +++ b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardsListLayoutRunner.kt @@ -11,8 +11,8 @@ import com.squareup.cycler.toDataSource import com.squareup.sample.dungeon.DungeonAppWorkflow.DisplayBoardsListScreen import com.squareup.sample.dungeon.board.Board import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromLayout import com.squareup.workflow1.ui.ScreenViewRunner -import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bind import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub @@ -101,7 +101,7 @@ class BoardsListLayoutRunner(rootView: View) : ScreenViewRunner by bind( + companion object : ScreenViewFactory by fromLayout( R.layout.boards_list_layout, ::BoardsListLayoutRunner ) } diff --git a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/Component.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/Component.kt index 79de358385..54574379f3 100644 --- a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/Component.kt +++ b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/Component.kt @@ -25,9 +25,9 @@ class Component(context: AppCompatActivity) { @OptIn(WorkflowUiExperimentalApi::class) val viewRegistry = ViewRegistry( ShakeableTimeMachineLayoutRunner, - LoadingBinding(R.string.loading_boards_list), + LoadingScreenViewFactory(R.string.loading_boards_list), BoardsListLayoutRunner, - LoadingBinding(R.string.loading_board), + LoadingScreenViewFactory(R.string.loading_board), GameLayoutRunner, BoardView, AlertContainer diff --git a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameLayoutRunner.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameLayoutRunner.kt index 36a5bc80a3..5fc013165a 100644 --- a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameLayoutRunner.kt +++ b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameLayoutRunner.kt @@ -11,8 +11,8 @@ import com.squareup.sample.dungeon.Direction.RIGHT import com.squareup.sample.dungeon.Direction.UP import com.squareup.sample.dungeon.GameWorkflow.GameRendering import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromLayout import com.squareup.workflow1.ui.ScreenViewRunner -import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bind import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub @@ -66,7 +66,7 @@ class GameLayoutRunner(view: View) : ScreenViewRunner { } } - companion object : ScreenViewFactory by bind( + companion object : ScreenViewFactory by fromLayout( R.layout.game_layout, ::GameLayoutRunner ) } diff --git a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/LoadingBinding.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/LoadingBinding.kt index 227e14b69d..d0277b40c5 100644 --- a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/LoadingBinding.kt +++ b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/LoadingBinding.kt @@ -7,8 +7,8 @@ import android.widget.TextView import androidx.annotation.StringRes import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromLayout import com.squareup.workflow1.ui.ScreenViewRunner -import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bind import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi @@ -19,10 +19,10 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi * The binding is parameterized on two things: the type of the rendering that this binding is * keyed off of, and the resource ID of the string to use for the label. */ -inline fun LoadingBinding( +inline fun LoadingScreenViewFactory( @StringRes loadingLabelRes: Int ): ScreenViewFactory = - bind(R.layout.loading_layout) { view -> LoadingLayoutRunner(loadingLabelRes, view) } + fromLayout(R.layout.loading_layout) { view -> LoadingLayoutRunner(loadingLabelRes, view) } @PublishedApi internal class LoadingLayoutRunner( diff --git a/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineLayoutRunner.kt b/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineLayoutRunner.kt index c40b23abbf..cf0d623d0f 100644 --- a/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineLayoutRunner.kt +++ b/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineLayoutRunner.kt @@ -8,8 +8,8 @@ import androidx.constraintlayout.widget.Group import androidx.transition.TransitionManager import com.squareup.sample.timemachine.shakeable.internal.GlassFrameLayout import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromLayout import com.squareup.workflow1.ui.ScreenViewRunner -import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bind import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub @@ -88,7 +88,7 @@ class ShakeableTimeMachineLayoutRunner( private fun Duration.toUiString(): String = toString() - companion object : ScreenViewFactory by bind( + companion object : ScreenViewFactory by fromLayout( R.layout.shakeable_time_machine_layout, ::ShakeableTimeMachineLayoutRunner ) } diff --git a/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloRendering.kt b/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloRendering.kt index 3757079052..f10966e066 100644 --- a/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloRendering.kt +++ b/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloRendering.kt @@ -3,7 +3,7 @@ package com.squareup.sample.helloworkflowfragment import com.squareup.sample.helloworkflowfragment.databinding.HelloGoodbyeLayoutBinding import com.squareup.workflow1.ui.AndroidScreen import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromViewBinding import com.squareup.workflow1.ui.WorkflowUiExperimentalApi @OptIn(WorkflowUiExperimentalApi::class) @@ -12,8 +12,8 @@ data class HelloRendering( val onClick: () -> Unit ) : AndroidScreen { override val viewFactory: ScreenViewFactory = - ScreenViewRunner.bind(HelloGoodbyeLayoutBinding::inflate) { r, _ -> - helloMessage.text = "${r.message} Fragment" - helloMessage.setOnClickListener { r.onClick() } + fromViewBinding(HelloGoodbyeLayoutBinding::inflate) { rendering, _ -> + helloMessage.text = "${rendering.message} Fragment" + helloMessage.setOnClickListener { rendering.onClick() } } } diff --git a/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloRendering.kt b/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloRendering.kt index ee34b15391..e1a9b8be79 100644 --- a/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloRendering.kt +++ b/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloRendering.kt @@ -3,7 +3,7 @@ package com.squareup.sample.helloworkflow import com.squareup.sample.helloworkflow.databinding.HelloGoodbyeLayoutBinding import com.squareup.workflow1.ui.AndroidScreen import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromViewBinding import com.squareup.workflow1.ui.WorkflowUiExperimentalApi @OptIn(WorkflowUiExperimentalApi::class) @@ -12,7 +12,7 @@ data class HelloRendering( val onClick: () -> Unit ) : AndroidScreen { override val viewFactory: ScreenViewFactory = - ScreenViewRunner.bind(HelloGoodbyeLayoutBinding::inflate) { r, _ -> + fromViewBinding(HelloGoodbyeLayoutBinding::inflate) { r, _ -> helloMessage.text = r.message helloMessage.setOnClickListener { r.onClick() } } 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..dd58631778 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,9 @@ 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.ScreenViewHolder import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.bindShowRendering @OptIn(WorkflowUiExperimentalApi::class) data class ClickyTextRendering( @@ -19,20 +19,19 @@ data class ClickyTextRendering( val visible: Boolean = true, val onClick: (() -> Unit)? = null ) : AndroidScreen { - override val viewFactory = ManualScreenViewFactory( - type = ClickyTextRendering::class, - viewConstructor = { initialRendering, initialEnv, context, _ -> - TextView(context).also { textView -> + override val viewFactory = ScreenViewFactory.fromCode( + buildView = { _, initialEnvironment, context, _ -> + val view = 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() } } - ) - } + } + ScreenViewHolder(initialEnvironment, view) { rendering, _ -> + val textView = view + textView.text = rendering.message + textView.isVisible = rendering.visible + textView.setOnClickListener( + rendering.onClick?.let { oc -> OnClickListener { oc() } } + ) } } ) diff --git a/samples/stub-visibility/src/main/java/com/squareup/sample/stubvisibility/OuterRendering.kt b/samples/stub-visibility/src/main/java/com/squareup/sample/stubvisibility/OuterRendering.kt index ac22b64f52..b50ae12553 100644 --- a/samples/stub-visibility/src/main/java/com/squareup/sample/stubvisibility/OuterRendering.kt +++ b/samples/stub-visibility/src/main/java/com/squareup/sample/stubvisibility/OuterRendering.kt @@ -3,7 +3,7 @@ package com.squareup.sample.stubvisibility import com.squareup.sample.stubvisibility.databinding.StubVisibilityLayoutBinding import com.squareup.workflow1.ui.AndroidScreen import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromViewBinding import com.squareup.workflow1.ui.WorkflowUiExperimentalApi @OptIn(WorkflowUiExperimentalApi::class) @@ -12,7 +12,7 @@ data class OuterRendering( val bottom: ClickyTextRendering ) : AndroidScreen { override val viewFactory: ScreenViewFactory = - ScreenViewRunner.bind(StubVisibilityLayoutBinding::inflate) { rendering, env -> + fromViewBinding(StubVisibilityLayoutBinding::inflate) { rendering, env -> shouldBeFilledStub.show(rendering.top, env) shouldBeWrappedStub.show(rendering.bottom, env) } 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/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/AuthorizingViewFactory.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/AuthorizingViewFactory.kt index a9f2411280..b3e12f94d8 100644 --- a/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/AuthorizingViewFactory.kt +++ b/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/AuthorizingViewFactory.kt @@ -2,11 +2,11 @@ package com.squareup.sample.authworkflow import com.squareup.sample.tictactoe.databinding.AuthorizingLayoutBinding import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromViewBinding import com.squareup.workflow1.ui.WorkflowUiExperimentalApi @OptIn(WorkflowUiExperimentalApi::class) internal val AuthorizingViewFactory: ScreenViewFactory = - ScreenViewRunner.bind(AuthorizingLayoutBinding::inflate) { rendering, _ -> + fromViewBinding(AuthorizingLayoutBinding::inflate) { rendering, _ -> authorizingMessage.text = rendering.message } diff --git a/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/LoginViewFactory.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/LoginViewFactory.kt index b16254b150..926e34a937 100644 --- a/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/LoginViewFactory.kt +++ b/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/LoginViewFactory.kt @@ -2,13 +2,13 @@ package com.squareup.sample.authworkflow import com.squareup.sample.tictactoe.databinding.LoginLayoutBinding import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromViewBinding import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.backPressedHandler @OptIn(WorkflowUiExperimentalApi::class) internal val LoginViewFactory: ScreenViewFactory = - ScreenViewRunner.bind(LoginLayoutBinding::inflate) { rendering, _ -> + fromViewBinding(LoginLayoutBinding::inflate) { rendering, _ -> loginErrorMessage.text = rendering.errorMessage loginButton.setOnClickListener { diff --git a/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/SecondFactorViewFactory.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/SecondFactorViewFactory.kt index 411a8db473..ced7617eeb 100644 --- a/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/SecondFactorViewFactory.kt +++ b/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/SecondFactorViewFactory.kt @@ -2,13 +2,13 @@ package com.squareup.sample.authworkflow import com.squareup.sample.tictactoe.databinding.SecondFactorLayoutBinding import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromViewBinding import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.backPressedHandler @OptIn(WorkflowUiExperimentalApi::class) internal val SecondFactorViewFactory: ScreenViewFactory = - ScreenViewRunner.bind(SecondFactorLayoutBinding::inflate) { rendering, _ -> + fromViewBinding(SecondFactorLayoutBinding::inflate) { rendering, _ -> root.backPressedHandler = { rendering.onCancel() } secondFactorToolbar.setNavigationOnClickListener { rendering.onCancel() } diff --git a/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GameOverLayoutRunner.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GameOverLayoutRunner.kt index ebfdd6002e..acc373817a 100644 --- a/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GameOverLayoutRunner.kt +++ b/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GameOverLayoutRunner.kt @@ -11,8 +11,8 @@ import com.squareup.sample.gameworkflow.SyncState.SAVING import com.squareup.sample.tictactoe.databinding.BoardBinding import com.squareup.sample.tictactoe.databinding.GamePlayLayoutBinding import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromViewBinding import com.squareup.workflow1.ui.ScreenViewRunner -import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bind import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.backPressedHandler @@ -102,7 +102,7 @@ internal class GameOverLayoutRunner( } /** Note how easily we're sharing this layout with [GamePlayViewFactory]. */ - companion object : ScreenViewFactory by bind( + companion object : ScreenViewFactory by fromViewBinding( GamePlayLayoutBinding::inflate, ::GameOverLayoutRunner ) } diff --git a/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GamePlayViewFactory.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GamePlayViewFactory.kt index 6d384590bd..9de1ed14bb 100644 --- a/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GamePlayViewFactory.kt +++ b/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GamePlayViewFactory.kt @@ -4,13 +4,13 @@ import android.view.View import android.view.ViewGroup import com.squareup.sample.tictactoe.databinding.GamePlayLayoutBinding import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromViewBinding import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.backPressedHandler @OptIn(WorkflowUiExperimentalApi::class) internal val GamePlayViewFactory: ScreenViewFactory = - ScreenViewRunner.bind(GamePlayLayoutBinding::inflate) { rendering, _ -> + fromViewBinding(GamePlayLayoutBinding::inflate) { rendering, _ -> renderBanner(rendering.gameState, rendering.playerInfo) rendering.gameState.board.render(gamePlayBoard.root) diff --git a/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/NewGameViewFactory.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/NewGameViewFactory.kt index 9669693d56..5279abcd54 100644 --- a/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/NewGameViewFactory.kt +++ b/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/NewGameViewFactory.kt @@ -2,13 +2,13 @@ package com.squareup.sample.gameworkflow import com.squareup.sample.tictactoe.databinding.NewGameLayoutBinding import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromViewBinding import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.backPressedHandler @OptIn(WorkflowUiExperimentalApi::class) internal val NewGameViewFactory: ScreenViewFactory = - ScreenViewRunner.bind(NewGameLayoutBinding::inflate) { rendering, _ -> + fromViewBinding(NewGameLayoutBinding::inflate) { rendering, _ -> if (playerX.text.isBlank()) playerX.setText(rendering.defaultNameX) if (playerO.text.isBlank()) playerO.setText(rendering.defaultNameO) diff --git a/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/TicTacToeViewBindings.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/TicTacToeViewFactories.kt similarity index 100% rename from samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/TicTacToeViewBindings.kt rename to samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/TicTacToeViewFactories.kt diff --git a/samples/todo-android/app/src/main/java/com/squareup/sample/todo/TodoEditorScreen.kt b/samples/todo-android/app/src/main/java/com/squareup/sample/todo/TodoEditorScreen.kt index 72ef00d114..74fda8ae77 100644 --- a/samples/todo-android/app/src/main/java/com/squareup/sample/todo/TodoEditorScreen.kt +++ b/samples/todo-android/app/src/main/java/com/squareup/sample/todo/TodoEditorScreen.kt @@ -1,4 +1,5 @@ @file:OptIn(WorkflowUiExperimentalApi::class) + package com.squareup.sample.todo import android.content.Context.INPUT_METHOD_SERVICE @@ -7,8 +8,8 @@ import android.view.inputmethod.InputMethodManager import com.squareup.sample.todo.databinding.TodoEditorLayoutBinding import com.squareup.workflow1.ui.AndroidScreen import com.squareup.workflow1.ui.Compatible +import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.ScreenViewRunner -import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bind import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.backPressedHandler @@ -25,7 +26,8 @@ data class TodoEditorScreen( ) : AndroidScreen, Compatible { override val compatibilityKey = Compatible.keyFor(this, "${session.id}") - override val viewFactory = bind(TodoEditorLayoutBinding::inflate, ::Runner) + override val viewFactory = + ScreenViewFactory.fromViewBinding(TodoEditorLayoutBinding::inflate, ::Runner) } @OptIn(WorkflowUiExperimentalApi::class) diff --git a/samples/todo-android/app/src/main/java/com/squareup/sample/todo/TodoListsScreen.kt b/samples/todo-android/app/src/main/java/com/squareup/sample/todo/TodoListsScreen.kt index 573dfd40c9..8ec861b018 100644 --- a/samples/todo-android/app/src/main/java/com/squareup/sample/todo/TodoListsScreen.kt +++ b/samples/todo-android/app/src/main/java/com/squareup/sample/todo/TodoListsScreen.kt @@ -7,7 +7,7 @@ import com.squareup.sample.container.overviewdetail.OverviewDetailConfig.Overvie import com.squareup.sample.todo.databinding.TodoListsLayoutBinding import com.squareup.workflow1.ui.AndroidScreen import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bind +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromViewBinding import com.squareup.workflow1.ui.WorkflowUiExperimentalApi /** @@ -26,7 +26,7 @@ data class TodoListsScreen( val selection: Int = -1 ) : AndroidScreen { override val viewFactory: ScreenViewFactory = - bind(TodoListsLayoutBinding::inflate) { rendering, viewEnvironment -> + fromViewBinding(TodoListsLayoutBinding::inflate) { rendering, viewEnvironment -> for ((index, list) in rendering.lists.withIndex()) { addRow( index, 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..869ec092c7 100644 --- a/workflow-ui/compose/api/compose.api +++ b/workflow-ui/compose/api/compose.api @@ -30,7 +30,7 @@ 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/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Lcom/squareup/workflow1/ui/ScreenViewHolder; } 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..fce8e424f8 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 @@ -29,10 +29,10 @@ import com.squareup.workflow1.ui.AndroidScreen import com.squareup.workflow1.ui.Compatible import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewHolder import com.squareup.workflow1.ui.ViewEnvironment 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 @@ -584,23 +584,19 @@ internal class ComposeViewTreeIntegrationTest { override fun buildView( initialRendering: TestComposeRendering, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, + initialEnvironment: 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) - } - - setContent(rendering.content) + ): ScreenViewHolder { + val view = ComposeView(context) + return ScreenViewHolder(initialEnvironment, view) { rendering, _ -> + val lastCompositionStrategy = view.tag as? ViewCompositionStrategy + view.tag = rendering.disposeStrategy + if (rendering.disposeStrategy != lastCompositionStrategy) { + lastCompositionStrategy?.let { view.setViewCompositionStrategy(it) } } + + view.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..19b3fa1502 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,25 @@ 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, _ -> - NoTransitionBackStackContainer(context) + by ScreenViewFactory.fromCode( + buildView = { _, initialEnvironment, context, _ -> + val view = NoTransitionBackStackContainer(context) .apply { id = R.id.workflow_back_stack_container layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT) - bindShowRendering( - initialRendering, - initialEnv, - { newRendering, newViewEnvironment -> update(newRendering, newViewEnvironment) } - ) } + ScreenViewHolder(initialEnvironment, view) { rendering, environment -> + view.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..63c2e50c7d 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,13 @@ 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.ScreenViewHolder 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 +230,21 @@ 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.fromCode { _, initialEnvironment, context, _ -> + val view = 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. + } } + ScreenViewHolder(initialEnvironment, view) { _, _ -> /* Noop */ } } - } } class EmptyRendering : ComposableRendering { @@ -386,17 +377,13 @@ 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.fromCode { _, initialEnvironment, context, _ -> + val view = View(context) + ScreenViewHolder(initialEnvironment, view) { rendering, _ -> + view.id = rendering.viewId } } - } } composeRule.setContent { @@ -563,22 +550,12 @@ internal class WorkflowRenderingTest { @Composable fun Content(viewEnvironment: ViewEnvironment) } - 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 - } + private data class LegacyViewRendering(val text: String) : AndroidScreen { + override val viewFactory = + ScreenViewFactory.fromCode { _, initialEnvironment, context, _ -> + val view = TextView(context) + ScreenViewHolder(initialEnvironment, view) { rendering, _ -> + view.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..1225d12b9c 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 @@ -4,15 +4,14 @@ package com.squareup.workflow1.ui.compose import android.content.Context -import android.view.View import android.view.ViewGroup import androidx.compose.runtime.Composable import androidx.compose.ui.platform.ComposeView import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewHolder import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.bindShowRendering import kotlin.reflect.KClass /** @@ -113,11 +112,10 @@ internal fun composeScreenViewFactory( @WorkflowUiExperimentalApi 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 - * [ScreenViewRunner.showRendering][com.squareup.workflow1.ui.ScreenViewRunner.showRendering]. + * 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.show]. */ @Composable public abstract fun Content( rendering: RenderingT, @@ -126,20 +124,15 @@ public abstract class ComposeScreenViewFactory : final override fun buildView( initialRendering: RenderingT, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, + initialEnvironment: ViewEnvironment, + context: Context, container: ViewGroup? - ): View = ComposeView(contextForNewView).also { composeView -> - // 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) - } + ): ScreenViewHolder { + val view = ComposeView(context) + return ScreenViewHolder(initialEnvironment, view) { rendering, environment -> + // Update the state whenever a new rendering is emitted. + // This lambda will be executed synchronously before ScreenViewHolder.show returns. + view.setContent { Content(rendering, environment) } } } } diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/CompositionRoot.kt b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/CompositionRoot.kt index 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..58d3587c5c 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,26 @@ 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.show +import com.squareup.workflow1.ui.startShowing +import com.squareup.workflow1.ui.toViewFactory import kotlin.reflect.KClass /** - * Renders [rendering] into the composition using this [ViewEnvironment]'s [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 +89,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 @@ -151,11 +152,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 +175,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 +185,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.startShowing(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..8425bab9e1 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,33 @@ 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.ScreenViewFactory.Companion.fromCode +import com.squareup.workflow1.ui.ScreenViewHolder 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.fromCode( + buildView = { _, environment, context, _ -> + ScreenViewHolder(environment, View(context)) { _, _ -> /* Noop */ } + } + ) data class TestModals( override val modals: List @@ -56,20 +48,15 @@ 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) + fromCode { _, environment, 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) - } } - }, + ScreenViewHolder(environment, frame) { rendering, viewEnvironment -> + stub.show(asScreen(TestModals(listOf(rendering.wrapped))), viewEnvironment) + } + } ) 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..23af029de7 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,19 @@ 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.show +import com.squareup.workflow1.ui.startShowing +import com.squareup.workflow1.ui.toViewFactory import kotlin.reflect.KClass /** @@ -71,14 +73,15 @@ 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, - contextForNewView = this.context, - container = this - ) - view.start() + val viewHolder = wrappedRendering.toViewFactory(initialViewEnvironment) + .startShowing( + initialRendering = wrappedRendering, + initialEnvironment = initialViewEnvironment, + contextForNewView = this.context, + container = this + ) - 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 +93,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 +105,7 @@ public open class ModalViewContainer @JvmOverloads constructor( } } .run { - DialogRef(initialModalRendering, initialViewEnvironment, this, view) + DialogRef(initialModalRendering, initialViewEnvironment, this, viewHolder) } } @@ -113,7 +116,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..58355eb257 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,12 @@ 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/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Lcom/squareup/workflow1/ui/ScreenViewHolder; public fun getType ()Lkotlin/reflect/KClass; } -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/ScreenViewFactory; } public final class com/squareup/workflow1/ui/NamedViewFactory : com/squareup/workflow1/ui/ViewFactory { @@ -169,12 +148,28 @@ public final class com/squareup/workflow1/ui/PickledTreesnapshot$CREATOR : andro public synthetic fun newArray (I)[Ljava/lang/Object; } +public final class com/squareup/workflow1/ui/RealScreenViewHolder : com/squareup/workflow1/ui/ScreenViewHolder { + public fun (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/view/View;Lcom/squareup/workflow1/ui/ScreenViewRunner;)V + public fun getEnvironment ()Lcom/squareup/workflow1/ui/ViewEnvironment; + public fun getRunner ()Lcom/squareup/workflow1/ui/ScreenViewRunner; + public fun getView ()Landroid/view/View; +} + 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 static final field Companion Lcom/squareup/workflow1/ui/ScreenViewFactory$Companion; + public abstract fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Lcom/squareup/workflow1/ui/ScreenViewHolder; +} + +public final class com/squareup/workflow1/ui/ScreenViewFactory$Companion { + public final synthetic fun fromCode (Lkotlin/jvm/functions/Function4;)Lcom/squareup/workflow1/ui/ScreenViewFactory; + public final synthetic fun fromLayout (ILkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/ScreenViewFactory; + public final synthetic fun fromStaticLayout (I)Lcom/squareup/workflow1/ui/ScreenViewFactory; + public final synthetic fun fromViewBinding (Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/ScreenViewFactory; + public final synthetic fun fromViewBinding (Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;)Lcom/squareup/workflow1/ui/ScreenViewFactory; } 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/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/ScreenViewHolder; } public abstract interface class com/squareup/workflow1/ui/ScreenViewFactoryFinder { @@ -192,24 +187,38 @@ 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 fun startShowing (Lcom/squareup/workflow1/ui/ScreenViewFactory;Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lcom/squareup/workflow1/ui/ViewStarter;)Lcom/squareup/workflow1/ui/ScreenViewHolder; + public static synthetic fun startShowing$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 synthetic fun toUnwrappingViewFactory (Lcom/squareup/workflow1/ui/ScreenViewFactory;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/ScreenViewFactory; + public static final synthetic fun toUnwrappingViewFactory (Lcom/squareup/workflow1/ui/ScreenViewFactory;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;)Lcom/squareup/workflow1/ui/ScreenViewFactory; + public static final fun toViewFactory (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/ScreenViewFactory; + public static final fun viewBindingLayoutInflater (Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/LayoutInflater; } -public abstract interface class com/squareup/workflow1/ui/ScreenViewRunner { - public static final field Companion Lcom/squareup/workflow1/ui/ScreenViewRunner$Companion; - public abstract fun showRendering (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V +public abstract interface class com/squareup/workflow1/ui/ScreenViewHolder { + public static final field Companion Lcom/squareup/workflow1/ui/ScreenViewHolder$Companion; + public abstract fun getEnvironment ()Lcom/squareup/workflow1/ui/ViewEnvironment; + public abstract fun getRunner ()Lcom/squareup/workflow1/ui/ScreenViewRunner; + public abstract fun getView ()Landroid/view/View; } -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 bindNoRunner (I)Lcom/squareup/workflow1/ui/ScreenViewFactory; +public final class com/squareup/workflow1/ui/ScreenViewHolder$Companion { + public final fun getShowing ()Lcom/squareup/workflow1/ui/ViewEnvironmentKey; } -public final class com/squareup/workflow1/ui/ScreenViewRunnerKt { - public static final fun viewBindingLayoutInflater (Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/LayoutInflater; +public final class com/squareup/workflow1/ui/ScreenViewHolder$Companion$ShowingNothing : com/squareup/workflow1/ui/Screen { + public static final field INSTANCE Lcom/squareup/workflow1/ui/ScreenViewHolder$Companion$ShowingNothing; +} + +public final class com/squareup/workflow1/ui/ScreenViewHolderKt { + public static final fun ScreenViewHolder (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/view/View;Lcom/squareup/workflow1/ui/ScreenViewRunner;)Lcom/squareup/workflow1/ui/ScreenViewHolder; + public static final fun canShow (Lcom/squareup/workflow1/ui/ScreenViewHolder;Lcom/squareup/workflow1/ui/Screen;)Z + public static final fun getShowing (Lcom/squareup/workflow1/ui/ScreenViewHolder;)Lcom/squareup/workflow1/ui/Screen; + public static final fun show (Lcom/squareup/workflow1/ui/ScreenViewHolder;Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V +} + +public abstract interface class com/squareup/workflow1/ui/ScreenViewRunner { + public abstract fun showRendering (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V } public final class com/squareup/workflow1/ui/SnapshotParcelsKt { @@ -239,7 +248,7 @@ 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/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Lcom/squareup/workflow1/ui/ScreenViewHolder; public fun getType ()Lkotlin/reflect/KClass; } @@ -269,6 +278,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 +368,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 +435,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,14 +481,14 @@ 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 synthetic fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Lcom/squareup/workflow1/ui/ScreenViewHolder; + public fun buildView (Lcom/squareup/workflow1/ui/container/BackStackScreen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Lcom/squareup/workflow1/ui/ScreenViewHolder; public fun getType ()Lkotlin/reflect/KClass; } @@ -496,8 +505,8 @@ 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 synthetic fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Lcom/squareup/workflow1/ui/ScreenViewHolder; + public fun buildView (Lcom/squareup/workflow1/ui/container/BodyAndModalsScreen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Lcom/squareup/workflow1/ui/ScreenViewHolder; public fun getType ()Lkotlin/reflect/KClass; } @@ -547,11 +556,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/ScreenViewFactory; } public final class com/squareup/workflow1/ui/container/LayeredDialogs { @@ -654,7 +660,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 +686,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..e4afc76adf 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.ScreenViewFactory.Companion.fromCode 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,14 @@ 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) - container.addView(stub) - container.bindShowRendering( - initialRendering, - initialViewEnvironment - ) { rendering, env -> - stub.show(rendering.wrapped, env) - } + fromCode { _, initialEnvironment, context, _ -> + val stub = WorkflowViewStub(context) + val frame = FrameLayout(context) + frame.addView(stub) + ScreenViewHolder(initialEnvironment, frame) { rendering, viewEnvironment -> + stub.show(rendering.wrapped, viewEnvironment) } - }, + } ) 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..25893fd8a3 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.ScreenViewFactory.Companion.fromCode import com.squareup.workflow1.ui.WorkflowViewStubLifecycleActivity.TestRendering import com.squareup.workflow1.ui.WorkflowViewStubLifecycleActivity.TestRendering.LeafRendering import com.squareup.workflow1.ui.WorkflowViewStubLifecycleActivity.TestRendering.RecurseRendering @@ -261,18 +262,13 @@ internal class WorkflowViewStubLifecycleTest { } data class RegistrySetter(val wrapped: TestRendering) : ViewRendering() { - override val viewFactory: ScreenViewFactory = ManualScreenViewFactory( - RegistrySetter::class - ) { initialRendering, initialViewEnvironment, context, _ -> + override val viewFactory = fromCode { _, initialEnvironment, context, _ -> val stub = WorkflowViewStub(context) ViewTreeSavedStateRegistryOwner.set(stub, expectedRegistryOwner) + val frame = FrameLayout(context).apply { addView(stub) } - FrameLayout(context).apply { - addView(stub) - - bindShowRendering(initialRendering, initialViewEnvironment) { r, e -> - stub.show(r.wrapped, e) - } + ScreenViewHolder(initialEnvironment, frame) { rendering, viewEnvironment -> + stub.show(rendering.wrapped, viewEnvironment) } } } @@ -312,61 +308,57 @@ 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.fromCode { _, initialEnvironment, context, _ -> + var counter = 0 + val view = 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 - } + ScreenViewHolder(initialEnvironment, view) { _, _ -> /* Noop */ } } - } } } 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..243470622a 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,18 @@ 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.ViewEnvironment.Companion.EMPTY import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.bindShowRendering -import com.squareup.workflow1.ui.getRendering +import com.squareup.workflow1.ui.show +import com.squareup.workflow1.ui.showing import org.junit.Rule import org.junit.Test @@ -25,40 +28,49 @@ 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.fromCode { _, initialRendering, context, _ -> + ScreenViewHolder(initialRendering, View(context)) { _, _ -> /* Noop */ } } } @Test fun firstScreenIsRendered() { scenario.onActivity { activity -> - val c = VisibleBackStackContainer(activity) + val view = VisibleBackStackContainer(activity) + val holder = ScreenViewHolder>(EMPTY, view) { r, e -> + view.update(r, e) + } - c.show(BackStackScreen(Rendering("able"))) - val showing = c.visibleRendering as Rendering + holder.show(BackStackScreen(Rendering("able")), EMPTY) + val showing = view.visibleRendering as Rendering Truth.assertThat(showing).isEqualTo(Rendering("able")) } } @Test fun secondScreenIsRendered() { scenario.onActivity { activity -> - val c = VisibleBackStackContainer(activity) + val view = VisibleBackStackContainer(activity) + val holder = ScreenViewHolder>(EMPTY, view) { r, e -> + view.update(r, e) + } - c.show(BackStackScreen(Rendering("able"))) - c.show(BackStackScreen(Rendering("baker"))) - val showing = c.visibleRendering as Rendering + holder.show(BackStackScreen(Rendering("able")), EMPTY) + holder.show(BackStackScreen(Rendering("baker")), EMPTY) + val showing = view.visibleRendering as Rendering Truth.assertThat(showing).isEqualTo(Rendering("baker")) } } @Test fun thirdScreenIsRendered() { scenario.onActivity { activity -> - val c = VisibleBackStackContainer(activity) + val view = VisibleBackStackContainer(activity) + val holder = ScreenViewHolder>(EMPTY, view) { r, e -> + view.update(r, e) + } - c.show(BackStackScreen(Rendering("able"))) - c.show(BackStackScreen(Rendering("baker"))) - c.show(BackStackScreen(Rendering("charlie"))) - val showing = c.visibleRendering as Rendering + holder.show(BackStackScreen(Rendering("able")), EMPTY) + holder.show(BackStackScreen(Rendering("baker")), EMPTY) + holder.show(BackStackScreen(Rendering("charlie")), EMPTY) + val showing = view.visibleRendering as Rendering Truth.assertThat(showing).isEqualTo(Rendering("charlie")) // This used to fail because of our naive use of TransitionManager. The @@ -69,32 +81,38 @@ internal class BackStackContainerTest { @Test fun isDebounced() { scenario.onActivity { activity -> - val c = VisibleBackStackContainer(activity) + val view = VisibleBackStackContainer(activity) + val holder = ScreenViewHolder>(EMPTY, view) { r, e -> + view.update(r, e) + } - c.show(BackStackScreen(Rendering("able"))) - c.show(BackStackScreen(Rendering("able"))) - c.show(BackStackScreen(Rendering("able"))) - c.show(BackStackScreen(Rendering("able"))) + holder.show(BackStackScreen(Rendering("able")), EMPTY) + holder.show(BackStackScreen(Rendering("able")), EMPTY) + holder.show(BackStackScreen(Rendering("able")), EMPTY) + holder.show(BackStackScreen(Rendering("able")), EMPTY) - Truth.assertThat(c.transitionCount).isEqualTo(1) + Truth.assertThat(view.transitionCount).isEqualTo(1) } } 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) } 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.showing + 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..9512c326f5 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,17 +3,17 @@ 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.ViewEnvironment +import com.squareup.workflow1.ui.ScreenViewHolder +import com.squareup.workflow1.ui.ViewEnvironment.Companion.EMPTY 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 com.squareup.workflow1.ui.show import org.junit.Assert.fail import org.junit.Test import org.junit.runner.RunWith @@ -28,7 +28,7 @@ import org.junit.runner.RunWith internal class ViewStateCacheTest { private val instrumentation = InstrumentationRegistry.getInstrumentation() - private val viewEnvironment = ViewEnvironment.EMPTY + private val viewEnvironment = EMPTY private object AScreen : Screen @@ -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,14 @@ 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>(EMPTY, firstViewRestored) { _, _ -> }.also { + it.show(firstRendering, viewEnvironment) + } + cache.update( + listOf(firstRendering), oldHolderMaybe = secondView, newHolder = firstHolderRestored + ) // Check that the state was restored. assertThat(firstViewRestored.viewState).isEqualTo("") @@ -130,35 +139,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 +168,24 @@ internal class ViewStateCacheTest { } } + 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>(EMPTY, view) { _, _ -> }.also { + it.show(firstRendering, viewEnvironment) + } } 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..65b3b1f8d7 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 @@ -9,14 +9,14 @@ 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.ScreenViewFactory.Companion.fromCode +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.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 @@ -25,7 +25,6 @@ import com.squareup.workflow1.ui.internal.test.AbstractLifecycleTestActivity import com.squareup.workflow1.ui.internal.test.inAnyView import org.hamcrest.Matcher import org.hamcrest.Matchers.equalTo -import kotlin.reflect.KClass @OptIn(WorkflowUiExperimentalApi::class) internal class BackStackContainerLifecycleActivity : AbstractLifecycleTestActivity() { @@ -34,14 +33,14 @@ internal class BackStackContainerLifecycleActivity : AbstractLifecycleTestActivi * Default rendering always shown in the backstack to simplify test configuration. */ object BaseRendering : Screen, ScreenViewFactory { - override val type: KClass = BaseRendering::class + override val type = BaseRendering::class override fun buildView( initialRendering: BaseRendering, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, + initialEnvironment: ViewEnvironment, + context: Context, container: ViewGroup? - ): View = View(contextForNewView).apply { - bindShowRendering(initialRendering, initialViewEnvironment) { _, _ -> /* Noop */ } + ): ScreenViewHolder = View(context).let { view -> + ScreenViewHolder(initialEnvironment, view) { _, _ -> /* Noop */ } } } @@ -115,34 +114,24 @@ 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) + fromCode { _, initialEnvironment, 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) - } + } + ScreenViewHolder(initialEnvironment, frame) { rendering, env -> + stub.show(rendering.wrappedBackstack.toBackstackWithBase(), env) } }, - ManualScreenViewFactory(OuterRendering::class) { initialRendering, - initialViewEnvironment, - contextForNewView, _ -> - FrameLayout(contextForNewView).also { container -> - - val stub = WorkflowViewStub(contextForNewView) + fromCode { _, initialEnvironment, 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) - } } - }, + ScreenViewHolder(initialEnvironment, frame) { rendering, env -> + stub.show(rendering.backStack, env) + } + } ) /** 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..1f2ccc85bb 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,26 @@ 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, _ -> - NoTransitionBackStackContainer(context) + by ScreenViewFactory.fromCode( + buildView = { _, initialEnvironment, context, _ -> + val view = NoTransitionBackStackContainer(context) .apply { id = R.id.workflow_back_stack_container layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT) - bindShowRendering(initialRendering, initialEnv, ::update) } + ScreenViewHolder(initialEnvironment, view) { rendering, environment -> + view.update(rendering, environment) + } } ) } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidScreen.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidScreen.kt index 2d99280ee4..8f90006c8f 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidScreen.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidScreen.kt @@ -4,18 +4,16 @@ package com.squareup.workflow1.ui * Interface implemented by a rendering class to allow it to drive an Android UI * via an appropriate [ScreenViewFactory] implementation. * - * You will rarely, if ever, write a [ScreenViewFactory] yourself. Instead - * use [ScreenViewRunner.bind] to work with XML layout resources, or - * [BuilderViewFactory] to create views from code. See [ScreenViewRunner] for more - * details. + * You will rarely, if ever, write a [ScreenViewFactory] yourself. Use one + * of its [companion methods][ScreenViewFactory.Companion] like [ScreenViewFactory.fromViewBinding] + * instead. * - * @OptIn(WorkflowUiExperimentalApi::class) * data class HelloScreen( * val message: String, * val onClick: () -> Unit * ) : AndroidScreen { - * override val viewFactory = - * ScreenViewRunner.bind(HelloGoodbyeLayoutBinding::inflate) { screen, _ -> + * override val viewFactory : ScreenViewFactory = + * forViewBinding(HelloGoodbyeLayoutBinding::inflate) { screen, _ -> * helloMessage.text = screen.message * helloMessage.setOnClickListener { screen.onClick() } * } @@ -25,8 +23,14 @@ package com.squareup.workflow1.ui * but using it requires your workflows code to reside in Android modules, instead * of pure Kotlin. If this is a problem, or you need more flexibility for any other * reason, you can use [ViewRegistry] to bind your renderings to [ScreenViewFactory] - * implementations at runtime. Also note that a [ViewRegistry] entry will override - * the [viewFactory] returned by an [AndroidScreen]. + * implementations at runtime. + * + * Also note that a [ViewRegistry] entry will override the [viewFactory] returned + * by an [AndroidScreen]. This means that an [AndroidScreen] implementation can provide + * a default UI that can be completely customized at runtime via [ViewRegistry] configuration. + * + * See also [ScreenViewFactoryFinder] to customize built in rendering types like + * [BackStackScreen][com.squareup.workflow1.ui.container.BackStackScreen]. * * @see com.squareup.workflow1.ui.container.AndroidOverlay */ 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..0740ddaed7 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 ScreenViewFactory.startShowing") @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..e9e07e7c72 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,32 @@ 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 -> - val legacyShowRendering = view.getShowRendering()!! +@WorkflowUiExperimentalApi +internal fun AsScreenViewFactory( + initialRendering: AsScreen<*>, + initialViewEnvironment: ViewEnvironment +): ScreenViewFactory> { + val wrapped = initialRendering.rendering + val registry = initialViewEnvironment[ViewRegistry] + + return ScreenViewFactory.fromCode { _, environment, context, container -> + registry.buildView(wrapped, environment, context, container).let { view -> + // Capture the legacy showRendering function so that we can call it from our own + // ScreenViewHolder. + val legacyShowRendering = view.getShowRendering()!! + + // Like any legacy 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. + } - view.bindShowRendering( - initialRendering, - initialViewEnvironment - ) { rendering, env -> legacyShowRendering(rendering.rendering, env) } + ScreenViewHolder(environment, view) { asScreen, newEnv -> + legacyShowRendering(asScreen.rendering, newEnv) } + } } -) +} 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..d224958204 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 ScreenViewFactory.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..2988adb308 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 ScreenViewFactory.unwrapping") @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..2b5a26c526 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 @@ -20,17 +20,12 @@ internal class LayoutScreenViewFactory( ) : ScreenViewFactory { override fun buildView( initialRendering: RenderingT, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, + initialEnvironment: ViewEnvironment, + context: Context, container: ViewGroup? - ): View { - return contextForNewView.viewBindingLayoutInflater(container) + ): ScreenViewHolder { + return context.viewBindingLayoutInflater(container) .inflate(layoutId, container, false) - .also { view -> - val runner = runnerConstructor(view) - view.bindShowRendering(initialRendering, initialViewEnvironment) { rendering, environment -> - runner.showRendering(rendering, environment) - } - } + .let { view -> ScreenViewHolder(initialEnvironment, view, runnerConstructor(view)) } } } 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..a5e68016b0 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 @@ -1,9 +1,15 @@ package com.squareup.workflow1.ui +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromCode + /** * [ScreenViewFactory] that allows views to display instances of [NamedScreen]. Delegates * to the factory for [NamedScreen.wrapped]. */ @WorkflowUiExperimentalApi -internal object NamedScreenViewFactory : ScreenViewFactory> -by DecorativeScreenViewFactory(NamedScreen::class, { named -> named.wrapped }) +internal fun NamedScreenViewFactory() = + fromCode> { namedScreen, environment, context, container -> + namedScreen.wrapped.toViewFactory(environment) + .toUnwrappingViewFactory, WrappedT> { it.wrapped } + .buildView(namedScreen, environment, context, container) + } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/RealScreenViewHolder.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/RealScreenViewHolder.kt new file mode 100644 index 0000000000..081023d294 --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/RealScreenViewHolder.kt @@ -0,0 +1,20 @@ +package com.squareup.workflow1.ui + +import android.view.View + +@WorkflowUiExperimentalApi +internal class RealScreenViewHolder( + initialEnvironment: ViewEnvironment, + override val view: View, + viewRunner: ScreenViewRunner +) : ScreenViewHolder { + + private var _environment: ViewEnvironment = initialEnvironment + override val environment: ViewEnvironment get() = _environment + + override val runner: ScreenViewRunner = + ScreenViewRunner { newScreen, newEnvironment -> + _environment = newEnvironment + viewRunner.showRendering(newScreen, newEnvironment) + } +} 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..a1f658ef96 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 @@ -1,89 +1,270 @@ package com.squareup.workflow1.ui import android.content.Context +import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.viewbinding.ViewBinding +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromCode +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromLayout +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromViewBinding + +@WorkflowUiExperimentalApi +public typealias ViewBindingInflater = (LayoutInflater, ViewGroup?, Boolean) -> BindingT /** - * Factory for [View] instances that can show renderings of type [RenderingT] : [Screen]. + * The function that updates a [View] instance built by a [ScreenViewFactory]. + * Each [ScreenViewRunner] instance is paired with the single [View] instance, + * its neighbor in a [ScreenViewHolder]. * - * Two concrete [ScreenViewFactory] implementations are provided: + * This is the interface you'll implement directly to update Android view code + * from your [Screen] renderings. A [ScreenViewRunner] serves as the strategy + * object of a [ScreenViewHolder] instantiated by a [ScreenViewFactory] -- the + * runner provides the implmenetation for the holder's [ScreenViewHolder.show] + * method. + */ +@WorkflowUiExperimentalApi +public fun interface ScreenViewRunner { + public fun showRendering( + rendering: ScreenT, + viewEnvironment: ViewEnvironment + ) +} + +/** + * A [ViewRegistry.Entry] that can build Android [View] instances, along with functions + * that can update them to display [Screen] renderings of a particular [type], bundled + * together in instances of [ScreenViewHolder]. * - * - The various [bind][ScreenViewRunner.bind] methods on [ScreenViewRunner] allow easy use of - * Android XML layout resources and [AndroidX ViewBinding][androidx.viewbinding.ViewBinding]. + * Use [fromLayout], [fromViewBinding], etc., to create a [ScreenViewFactory]. + * These helper methods take a layout resource, view binding, or view building + * function as arguments, along with a factory to create a [showRendering] + * [ScreenViewRunner.showRendering] function. * - * - [ManualScreenViewFactory] allows views to be built from code. + * It is rare to call [buildView] directly. Instead the most common path is to pass [Screen] + * instances to [WorkflowViewStub.show], which will apply the [ScreenViewFactory] machinery + * for you. * - * 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]. + * If you are building a custom container and [WorkflowViewStub] is too restrictive, + * use [ScreenViewFactory.startShowing]. */ @WorkflowUiExperimentalApi -public interface ScreenViewFactory : ViewRegistry.Entry { +public interface ScreenViewFactory : ViewRegistry.Entry { /** - * Returns a View ready to display [initialRendering] (and any succeeding values) - * via [View.showRendering]. + * It is rare to call this method directly. Instead the most common path is to pass [Screen] + * instances to [WorkflowViewStub.show], which will apply the [ScreenViewFactory] machinery + * for you. + * + * Called by [startShowing] to create a [ScreenViewHolder] wrapping a [View] able to + * display a stream of [ScreenT] renderings, starting with [initialRendering]. */ public fun buildView( - initialRendering: RenderingT, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, + initialRendering: ScreenT, + initialEnvironment: ViewEnvironment, + context: Context, container: ViewGroup? = null - ): View + ): ScreenViewHolder + + public companion object { + /** + * Creates a [ScreenViewFactory] that [inflates][bindingInflater] a [ViewBinding] ([BindingT]) + * to show renderings of type [ScreenT] : [Screen], using [a lambda][showRendering]. + * + * val HelloViewFactory: ScreenViewFactory = + * forViewBinding(HelloGoodbyeViewBinding::inflate) { rendering, _ -> + * helloMessage.text = rendering.message + * helloMessage.setOnClickListener { rendering.onClick(Unit) } + * } + * + * If you need to initialize your view before [showRendering] is called, + * implement [ScreenViewRunner] and create a binding using the `forViewBinding` variant + * that accepts a `(ViewBinding) -> ScreenViewRunner` function, below. + */ + public inline fun fromViewBinding( + noinline bindingInflater: ViewBindingInflater, + crossinline showRendering: BindingT.(ScreenT, ViewEnvironment) -> Unit + ): ScreenViewFactory = fromViewBinding(bindingInflater) { binding -> + ScreenViewRunner { rendering, viewEnvironment -> + binding.showRendering(rendering, viewEnvironment) + } + } + + /** + * Creates a [ScreenViewFactory] that [inflates][bindingInflater] a + * [ViewBinding] (of type [BindingT]) to show renderings of type [ScreenT], + * using a [ScreenViewRunner] created by [constructor]. Handy if you need + * to perform some set up before [ScreenViewRunner.showRendering] is called. + * + * class HelloScreenRunner( + * private val binding: HelloGoodbyeViewBinding + * ) : ScreenViewRunner { + * + * override fun showRendering(rendering: HelloScreen) { + * binding.messageView.text = rendering.message + * binding.messageView.setOnClickListener { rendering.onClick(Unit) } + * } + * + * companion object : ScreenViewFactory by forViewBinding( + * HelloGoodbyeViewBinding::inflate, ::HelloScreenRunner + * ) + * } + * + * If the view doesn't need to be initialized before [showRendering] is called, + * use the variant above which just takes a lambda. + */ + public inline fun fromViewBinding( + noinline bindingInflater: ViewBindingInflater, + noinline constructor: (BindingT) -> ScreenViewRunner + ): ScreenViewFactory = + ViewBindingScreenViewFactory(ScreenT::class, bindingInflater, constructor) + + /** + * Creates a [ScreenViewFactory] that inflates [layoutId] to show renderings of + * type [ScreenT], using a [ScreenViewRunner] created by [constructor] to update it. + * Avoids any use of [AndroidX ViewBinding][ViewBinding]. + */ + public inline fun fromLayout( + @LayoutRes layoutId: Int, + noinline constructor: (View) -> ScreenViewRunner + ): ScreenViewFactory = + LayoutScreenViewFactory(ScreenT::class, layoutId, constructor) + + /** + * Creates a [ScreenViewFactory] that inflates [layoutId] to "show" renderings of type + * [ScreenT], but never updates the created view. Handy for showing static displays, + * e.g. when prototyping. + */ + @Suppress("unused") + public inline fun fromStaticLayout( + @LayoutRes layoutId: Int + ): ScreenViewFactory = fromLayout(layoutId) { ScreenViewRunner { _, _ -> } } + + /** + * Creates a [ScreenViewFactory] that builds [View] instances entirely from code, + * using a [ScreenViewRunner] created by [constructor] to update it. + */ + @WorkflowUiExperimentalApi + public inline fun fromCode( + crossinline buildView: ( + initialRendering: ScreenT, + initialEnvironment: ViewEnvironment, + context: Context, + container: ViewGroup? + ) -> ScreenViewHolder, + ): ScreenViewFactory { + return object : ScreenViewFactory { + override val type = ScreenT::class + + override fun buildView( + initialRendering: ScreenT, + initialEnvironment: ViewEnvironment, + context: Context, + container: ViewGroup? + ): ScreenViewHolder = + buildView(initialRendering, initialEnvironment, context, container) + } + } + } } /** - * It is usually more convenient to use [WorkflowViewStub] or [DecorativeScreenViewFactory] - * than to call this method directly. + * It is rare to call this method directly. Instead the most common path is to pass [Screen] + * instances to [WorkflowViewStub.show], which will apply the [ScreenViewFactory] machinery + * for you. * - * 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. + * - Call [ScreenViewFactory.startShowing] to create and initialize a new [View] + * - If you don't particularly need to mess with the [ScreenViewFactory] before creating + * a view, use [Screen.startShowing] instead of this method. + */ +@WorkflowUiExperimentalApi +public fun ScreenT.toViewFactory( + environment: ViewEnvironment +): ScreenViewFactory { + return environment[ScreenViewFactoryFinder].getViewFactoryForRendering(environment, this) +} + +/** + * It is rare to call this method directly. Instead the most common path is to pass [Screen] + * instances to [WorkflowViewStub.show], which will apply the [ScreenViewFactory] machinery + * for you. * - * @throws IllegalArgumentException if no builder can be found for type [ScreenT] + * Creates a [ScreenViewHolder] wrapping a [View] able to display a stream + * of [ScreenT] renderings, starting with [initialRendering]. * - * @throws IllegalStateException if the matching [ScreenViewFactory] fails to call - * [View.bindShowRendering] when constructing the view + * 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.startShowing( + initialRendering: ScreenT, + initialEnvironment: ViewEnvironment, contextForNewView: Context, container: ViewGroup? = null, - viewStarter: ViewStarter? = null, -): View { - val viewFactory = viewEnvironment[ScreenViewFactoryFinder].getViewFactoryForRendering( - viewEnvironment, this - ) + viewStarter: ViewStarter? = null +): ScreenViewHolder { + return buildView( + initialRendering, + initialEnvironment, + 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, initialEnvironment + ) { 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, initialEnvironment) + shown = true + } + check(shown) { + "A ViewStarter provided to ScreenViewFactory.startShowing " + + "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]. - * - * 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. + * A wrapper for the function invoked when [ScreenViewFactory.startShowing] is called, + * allowing for custom initialization of a newly built [View] before or after the first + * call to [ScreenViewHolder.show]. */ @WorkflowUiExperimentalApi public fun interface ViewStarter { @@ -93,3 +274,173 @@ public fun interface ViewStarter { doStart: () -> Unit ) } + +/** + * Transforms a [ScreenViewFactory] of [WrappedT] into one that can handle + * instances of [WrapperT]. 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. + * + * This a simpler variant of the like named function that takes two arguments, for + * use when there is no need to customize the [view][ScreenViewHolder.view] or + * the [environment][ScreenViewHolder.environment]. + * + * ## Examples + * + * To make one rendering type an "alias" for another -- that is, to use the + * same [ScreenViewFactory] to display it: + * + * class RealScreen(val data: String): Screen + * object RealScreenViewFactory = ScreenViewFactory.fromLayout(...) + * + * class AliasScreen(val similarData: String) : Screen + * + * object AliasScreenViewFactory = + * RealScreenViewFactory.unwrapping { aliasScreen -> + * RealScreen(aliasScreen.similarData) + * } + * + * To make one rendering type a wrapper for others: + * + * class Wrapper(val wrapped: W: Screen) : Screen, Compatible { + * override val compatibilityKey = Compatible.keyFor(wrapped) + * } + * + * fun WrapperViewFactory() = + * ScreenViewFactory.forBuiltView> { wrapper, env, context, container -> + * // Get the view factory of the wrapped screen. + * wrapper.wrapped.toViewFactory(env) + * // Transform it to factory that accepts Wrapper + * .unwrapping, W> { it.wrapped } + * // Delegate to the transformed factory to build the view. + * .buildView(wrapper, env, context, container) + * } + * + * 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 OverrideNeutronFlow( + * val wrapped: W, + * val polarity: NeutronFlowPolarity + * ) : Screen, Compatible { + * override val compatibilityKey: String = Compatible.keyFor(wrapped) + * } + * + * fun OverrideNeutronFlowViewFactory() = + * ScreenViewFactory.forBuiltView> { wrapper, env, context, container -> + * // Get the view factory of the wrapped screen. + * wrapper.wrapped.toViewFactory(env) + * // Transform it to factory that accepts OverrideNeutronFlow, by + * // replacing the OverrideNeutronFlow with an EnvironmentScreen + * .unwrapping, EnvironmentScreen> { + * it.wrapped.withEnvironment( + * Environment.EMPTY + (NeutronFlowPolarity to it.polarity) + * ) + * } + * // Delegate to the transformed factory to build the view. + * .buildView(wrapper, env, context, container) + * } + * + * @param unwrap a function to extract [WrappedT] instances from [WrapperT]s. + */ +@WorkflowUiExperimentalApi +public inline fun < + reified WrapperT : Screen, + WrappedT : Screen + > ScreenViewFactory.toUnwrappingViewFactory( + crossinline unwrap: (wrapperScreen: WrapperT) -> WrappedT, +): ScreenViewFactory = + toUnwrappingViewFactory(unwrap) { _, ws, e, su -> su(unwrap(ws), e) } + +/** + * Transforms a [ScreenViewFactory] of [WrappedT] into one that can handle + * instances of [WrapperT]. + * + * 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. + * + * Also see the simpler variant of this function that takes only an [unwrap] argument. + * + * ## Example + * + * To make a wrapper that customizes [View] initialization: + * + * class WithTutorialTips(val wrapped: W) : Screen, Compatible { + * override val compatibilityKey = Compatible.keyFor(wrapped) + * } + * + * fun WithTutorialTipsFactory() = + * ScreenViewFactory.forBuiltView> = { + * initialRendering, initialEnv, context, container -> + * // Get the view factory of the wrapped screen. + * initialRendering.wrapped.toViewFactory(initialEnv) + * // Transform it to factory that accepts WithTutorialTips + * .unwrapping, W>( + * unwrap = { it.wrapped }, + * showWrapperScreen = { view, withTips, env, showUnwrapped -> + * TutorialTipRunner.run(view) + * showUnwrapped(withTips.wrapped, env) + * } + * // Delegate to the transformed factory to build the view. + * .buildView(initialRendering, initialEnv, context, container) + * } + * + * @param unwrap a function to extract [WrappedT] instances from [WrapperT]s. + * + * @param showWrapperScreen a function invoked when an instance of [WrapperT] needs + * to be shown in a [View] built to display instances of [WrappedT]. Allows + * pre- and post-processing of the [View]. + */ +@WorkflowUiExperimentalApi +public inline fun < + reified WrapperT : Screen, + WrappedT : Screen + > ScreenViewFactory.toUnwrappingViewFactory( + crossinline unwrap: (wrapperScreen: WrapperT) -> WrappedT, + crossinline showWrapperScreen: ( + view: View, + wrapperScreen: WrapperT, + environment: ViewEnvironment, + showUnwrappedScreen: (WrappedT, ViewEnvironment) -> Unit + ) -> Unit +): ScreenViewFactory { + val wrappedFactory = this + + return object : ScreenViewFactory + by fromCode( + buildView = { initialRendering, initialEnvironment, context, container -> + val wrappedHolder = wrappedFactory.buildView( + unwrap(initialRendering), initialEnvironment, context, container + ) + + object : ScreenViewHolder { + override val view = wrappedHolder.view + override val environment: ViewEnvironment get() = wrappedHolder.environment + + override val runner: ScreenViewRunner = + ScreenViewRunner { wrapperScreen, newEnvironment -> + showWrapperScreen(view, wrapperScreen, newEnvironment) { unwrappedScreen, env -> + wrappedHolder.runner.showRendering(unwrappedScreen, env) + } + } + } + } + ) { + } +} + +internal fun Context.viewBindingLayoutInflater(container: ViewGroup?) = + LayoutInflater.from(container?.context ?: this) + .cloneInContext(this) diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactoryFinder.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactoryFinder.kt index 9f5f5312f2..91db863ea7 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 @@ -8,22 +8,22 @@ import com.squareup.workflow1.ui.container.EnvironmentScreen import com.squareup.workflow1.ui.container.EnvironmentScreenViewFactory /** - * [ViewEnvironment] service object used by [Screen.buildView] to find the right - * [ScreenViewFactory]. The default implementation makes [AndroidScreen] work - * and provides default bindings for [NamedScreen], [EnvironmentScreen], [BackStackScreen], + * [ViewEnvironment] service object used by [Screen.toViewFactory] to find the right + * [ScreenViewFactory] to build and manage a [View][android.view.View] to display + * [Screen]s of the type of the receiver. The default implementation makes [AndroidScreen] + * work and provides default bindings for [NamedScreen], [EnvironmentScreen], [BackStackScreen], * etc. * * Here is how this hook could be used to provide a custom view to handle [BackStackScreen]: * * object MyViewFactory : ScreenViewFactory> - * by ManualScreenViewFactory( - * type = BackStackScreen::class, - * viewConstructor = { initialRendering, initialEnv, context, _ -> - * MyBackStackContainer(context) - * .apply { - * layoutParams = (LayoutParams(MATCH_PARENT, MATCH_PARENT)) - * bindShowRendering(initialRendering, initialEnv, ::update) - * } + * by ScreenViewFactory( + * buildView = { environment, context, _ -> + * val view = MyBackStackContainer(context) + * .apply { layoutParams = (LayoutParams(MATCH_PARENT, MATCH_PARENT)) } + * ScreenViewHolder(environment, view) { rendering, environment -> + * view.update(rendering, environment) + * } * } * ) * @@ -57,7 +57,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 +67,10 @@ public interface ScreenViewFactoryFinder { BodyAndModalsContainer as ScreenViewFactory } ?: (rendering as? NamedScreen<*>)?.let { - NamedScreenViewFactory as ScreenViewFactory + NamedScreenViewFactory() as ScreenViewFactory } ?: (rendering as? EnvironmentScreen<*>)?.let { - EnvironmentScreenViewFactory as ScreenViewFactory + EnvironmentScreenViewFactory() 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..b016f869f8 --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewHolder.kt @@ -0,0 +1,93 @@ +package com.squareup.workflow1.ui + +import android.view.View +import com.squareup.workflow1.ui.ScreenViewHolder.Companion.Showing +import com.squareup.workflow1.ui.ScreenViewHolder.Companion.ShowingNothing + +/** + * Associates a [view] with a function ([runner]) that can update it to display instances + * of [ScreenT]. Also holds a reference to the [ViewEnvironment][environment] that was + * most recently used to update the [view]. + * + * [environment] should always hold a reference to the [Screen] most recently shown + * in [view], with the key [Showing]. [ScreenViewHolder.showing] provides easy access + * to it. Note that the shown [Screen] may not be of type [ScreenT], if this + * [ScreenViewHolder] is wrapped by another one. (See [ScreenViewFactory.toUnwrappingViewFactory].) + * + * Do not call [runner] directly. Use [ScreenViewHolder.show] instead. Or most commonly, + * allow [WorkflowViewStub.show] to call it for you. + */ +@WorkflowUiExperimentalApi +public interface ScreenViewHolder { + /** The [View] managed by this holder, and updated via [runner] */ + public val view: View + + /** The [ViewEnvironment] that was provided the last time [view] was updated by [runner]. */ + public val environment: ViewEnvironment + + /** + * The function that is run by [show] to update [view] with a new [Screen] rendering and + * [ViewEnvironment]. + */ + public val runner: ScreenViewRunner + + public companion object { + /** + * Default value returned for the [Showing] [ViewEnvironmentKey], and therefore the + * default value returned by the [showing] method. Indicates that [show] has not yet + * been called, during the window between a [ScreenViewHolder] being instantiated, + * and the first call to [show]. + */ + public object ShowingNothing : Screen + + /** + * Provides access to the [Screen] instance most recently shown in a [ScreenViewHolder]'s + * [view] via [show]. Call [showing] for more convenient access. + */ + public val Showing: ViewEnvironmentKey = ViewEnvironmentKey { ShowingNothing } + } +} + +/** + * Returns true if [screen] is [compatible] with the [Screen] instance that + * was last [shown][show] by the [view] managed by the receiver. + */ +@WorkflowUiExperimentalApi +public fun ScreenViewHolder<*>.canShow(screen: Screen): Boolean { + // The ShowingNothing case covers bootstrapping, during the first call to show() + // from ScreenViewFactory.start(). + return showing.let { it is ShowingNothing || compatible(it, screen) } +} + +/** + * Updates the [view] managed by the receiver to display [screen], and + * updates the receiver's [environment] as well. The new [environment] + * will hold a reference to [screen] with key [Showing]. + */ +@WorkflowUiExperimentalApi +public fun ScreenViewHolder.show( + screen: ScreenT, + environment: ViewEnvironment +) { + // Why is this an extension rather than part of the interface? + // When wrapping, we need to prevent recursive calls from clobbering + // `environment[Showing]` with the nested rendering type. + runner.showRendering(screen, environment + (Showing to screen)) +} + +/** + * Returns the [Screen] most recently used to update the receiver's [view] + * via a call to [show]. + */ +@WorkflowUiExperimentalApi +public val ScreenViewHolder<*>.showing: Screen + get() = environment[Showing] + +@WorkflowUiExperimentalApi +public fun ScreenViewHolder( + initialEnvironment: ViewEnvironment, + view: View, + viewRunner: ScreenViewRunner +): ScreenViewHolder { + return RealScreenViewHolder(initialEnvironment, view, viewRunner) +} 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 deleted file mode 100644 index 018315fd7b..0000000000 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewRunner.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.squareup.workflow1.ui - -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.annotation.LayoutRes -import androidx.viewbinding.ViewBinding - -@WorkflowUiExperimentalApi -public typealias ViewBindingInflater = (LayoutInflater, ViewGroup?, Boolean) -> BindingT - -/** - * 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]. - * - * 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]. - */ -@WorkflowUiExperimentalApi -public fun interface ScreenViewRunner { - public fun showRendering( - rendering: RenderingT, - viewEnvironment: ViewEnvironment - ) - - public companion object { - /** - * Creates a [ScreenViewFactory] that [inflates][bindingInflater] a [ViewBinding] ([BindingT]) - * to show renderings of type [RenderingT] : [Screen], using [a lambda][showRendering]. - * - * val HelloViewFactory: ScreenViewFactory = - * ScreenViewRunner.bind(HelloGoodbyeViewBinding::inflate) { rendering, viewEnvironment -> - * helloMessage.text = rendering.message - * helloMessage.setOnClickListener { rendering.onClick(Unit) } - * } - * - * If you need to initialize your view before [showRendering] is called, - * implement [ScreenViewRunner] and create a binding using the `bind` variant - * that accepts a `(ViewBinding) -> ScreenViewRunner` function, below. - */ - public inline fun bind( - noinline bindingInflater: ViewBindingInflater, - crossinline showRendering: BindingT.(RenderingT, ViewEnvironment) -> Unit - ): ScreenViewFactory = bind(bindingInflater) { binding -> - ScreenViewRunner { rendering, viewEnvironment -> - binding.showRendering(rendering, viewEnvironment) - } - } - - /** - * Creates a [ScreenViewFactory] that [inflates][bindingInflater] a [ViewBinding] ([BindingT]) - * to show renderings of type [RenderingT] : [Screen], using a [ScreenViewRunner] - * created by [constructor]. Handy if you need to perform some set up before - * [showRendering] is called. - * - * class HelloScreenRunner( - * private val binding: HelloGoodbyeViewBinding - * ) : ScreenViewRunner { - * - * override fun showRendering(rendering: HelloScreen) { - * binding.messageView.text = rendering.message - * binding.messageView.setOnClickListener { rendering.onClick(Unit) } - * } - * - * companion object : ScreenViewFactory by bind( - * HelloGoodbyeViewBinding::inflate, ::HelloScreenRunner - * ) - * } - * - * If the view doesn't need to be initialized before [showRendering] is called, - * use the variant above which just takes a lambda. - */ - public inline fun bind( - noinline bindingInflater: ViewBindingInflater, - noinline constructor: (BindingT) -> ScreenViewRunner - ): ScreenViewFactory = - ViewBindingScreenViewFactory(RenderingT::class, bindingInflater, constructor) - - /** - * Creates a [ScreenViewFactory] that inflates [layoutId] to show renderings of - * type [RenderingT] : [Screen], using a [ScreenViewRunner] created by [constructor]. - * Avoids any use of [AndroidX ViewBinding][ViewBinding]. - */ - public inline fun bind( - @LayoutRes layoutId: Int, - noinline constructor: (View) -> ScreenViewRunner - ): ScreenViewFactory = - 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. - */ - @Suppress("unused") - public inline fun bindNoRunner( - @LayoutRes layoutId: Int - ): ScreenViewFactory = bind(layoutId) { ScreenViewRunner { _, _ -> } } - } -} - -internal fun Context.viewBindingLayoutInflater(container: ViewGroup?) = - LayoutInflater.from(container?.context ?: this) - .cloneInContext(this) 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..c9ab9cd129 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 @@ -1,7 +1,6 @@ package com.squareup.workflow1.ui import android.content.Context -import android.view.View import android.view.ViewGroup import androidx.viewbinding.ViewBinding import kotlin.reflect.KClass @@ -15,19 +14,12 @@ internal class ViewBindingScreenViewFactory { override fun buildView( initialRendering: RenderingT, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, + initialEnvironment: 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) - } + ): ScreenViewHolder = + bindingInflater(context.viewBindingLayoutInflater(container), container, false) + .let { binding -> + ScreenViewHolder(initialEnvironment, binding.root, runnerConstructor(binding)) } - .root } 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..10f9436299 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 ScreenViewHolder") 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/WorkflowLayout.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowLayout.kt index ba4a080676..7f6c15c038 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowLayout.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowLayout.kt @@ -59,7 +59,7 @@ public class WorkflowLayout( * make their own choices about how exactly to consume a stream of renderings. */ public fun show(rootScreen: Screen) { - showing.show(rootScreen, rootScreen.withEnvironment().viewEnvironment) + showing.show(rootScreen, rootScreen.withEnvironment().environment) restoredChildState?.let { restoredState -> restoredChildState = null showing.actual.restoreHierarchyState(restoredState) 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..d9518c7d8f 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 @@ -15,18 +15,19 @@ internal sealed class WorkflowViewState { abstract val environment: ViewEnvironment abstract val showRendering: ViewShowRendering - /** [bindShowRendering] has been called, [start] has not. */ + /** [bindShowRendering] has been called, [startShowing] has not. */ data class New( override val showing: RenderingT, override val environment: ViewEnvironment, override val showRendering: ViewShowRendering, val starter: (View) -> Unit = { view -> + @Suppress("DEPRECATION") view.showRendering(view.getRendering()!!, view.environment!!) } ) : WorkflowViewState() - /** [start] has been called. It's safe to call [showRendering] now. */ + /** [startShowing] has been called. It's safe to call [showRendering] now. */ data class Started( override val showing: RenderingT, override val environment: ViewEnvironment, @@ -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..fe1c6b4dc1 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 @@ -176,7 +177,8 @@ public class WorkflowViewStub @JvmOverloads constructor( viewEnvironment: ViewEnvironment ): View { @Suppress("DEPRECATION") - return show(asScreen(rendering), viewEnvironment) + show(asScreen(rendering), viewEnvironment) + return holder!!.view } /** @@ -197,57 +199,40 @@ public class WorkflowViewStub @JvmOverloads constructor( * @return the view that showed [rendering] * * @throws IllegalArgumentException if no binding can be found for the type of [rendering] - * - * @throws IllegalStateException if the matching - * [ViewFactory][com.squareup.workflow1.ui.ViewFactory] fails to call - * [View.bindShowRendering][com.squareup.workflow1.ui.bindShowRendering] - * when constructing the view */ 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) { - WorkflowLifecycleOwner.get(actual)?.destroyOnDetach() + 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(it)?.destroyOnDetach() } - return rendering.buildView( - viewEnvironment, - parent.context, - parent, - viewStarter = { view, doStart -> + holder = rendering.toViewFactory(viewEnvironment) + .startShowing(rendering, viewEnvironment, parent.context, parent) { view, doStart -> WorkflowLifecycleOwner.installOn(view) doStart() - } - ) - .also { newView -> - newView.start() + }.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 } } 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..285efca205 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 @@ -27,13 +27,12 @@ import com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner.Companion.insta * This type is meant to help integrate with [ViewTreeLifecycleOwner] by allowing the creation of a * tree of [LifecycleOwner]s that mirrors the view tree. * - * Custom container views that use [Screen.buildView][com.squareup.workflow1.ui.buildView] - * to create their children _must_ ensure - * they call [destroyOnDetach] on the outgoing view before they replace children with new views. - * If this is not done, then certain processes that are started by that view's subtree may continue - * to run long after the view has been detached, and memory and other resources may be leaked. - * Note that [WorkflowViewStub][com.squareup.workflow1.ui.WorkflowViewStub] takes care of - * this chore itself. + * Custom container views that use [ScreenViewFactory.startShowing] to create their children + * _must_ ensure they call [destroyOnDetach] on the outgoing view before they replace children + * with new views. If this is not done, then certain processes that are started by that view's + * subtree may continue to run long after the view has been detached, and memory and other + * resources may be leaked. Note that [WorkflowViewStub][com.squareup.workflow1.ui.WorkflowViewStub] + * takes care of this chore itself. * * Set a [WorkflowLifecycleOwner] on a view by calling [installOn], and read it back using [get]. */ @@ -74,7 +73,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..2934c88616 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,12 @@ 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.toUnwrappingViewFactory +import com.squareup.workflow1.ui.toViewFactory /** * Adds optional back button handling to a [wrapped] rendering, possibly overriding that @@ -27,24 +28,30 @@ public class BackButtonScreen( public val wrapped: W, 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) { - // Place our handler before invoking innerShowRendering, so that - // its later calls to view.backPressedHandler will take precedence - // over ours. - view.backPressedHandler = outerRendering.onBackPressed - } +) : AndroidScreen> { - innerShowRendering.invoke(outerRendering.wrapped, viewEnvironment) + override val viewFactory: ScreenViewFactory> = + ScreenViewFactory.fromCode { initialRendering, initialEnv, context, container -> + initialRendering.wrapped.toViewFactory(initialEnv) + .toUnwrappingViewFactory, W>( + unwrap = { it.wrapped }, + showWrapperScreen = { view, backButtonScreen, env, showUnwrapped -> + if (!backButtonScreen.shadow) { + // Place our handler before invoking innerShowRendering, so that + // its later calls to view.backPressedHandler will take precedence + // over ours. + view.backPressedHandler = backButtonScreen.onBackPressed + } - if (outerRendering.shadow) { - // Place our handler after invoking innerShowRendering, so that ours wins. - view.backPressedHandler = outerRendering.onBackPressed - } + // Show the wrapped Screen. + showUnwrapped(backButtonScreen.wrapped, env) + + if (backButtonScreen.shadow) { + // Place our handler after invoking innerShowRendering, so that ours wins. + view.backPressedHandler = backButtonScreen.onBackPressed + } + } + ) + .buildView(initialRendering, initialEnv, context, container) } - ) } 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..54c0008c23 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,22 @@ 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.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.canShow 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.show +import com.squareup.workflow1.ui.startShowing +import com.squareup.workflow1.ui.toViewFactory /** * A container view that can display a stream of [BackStackScreen] instances. @@ -48,13 +49,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[ScreenViewHolder.Showing]) + val config = if (newRendering.backStack.isEmpty()) First else Other val environment = newViewEnvironment + config @@ -63,19 +74,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).startShowing( + initialRendering = named.top, + initialEnvironment = environment, contextForNewView = this.context, container = this, viewStarter = { view, doStart -> @@ -83,39 +95,39 @@ 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 [update] (via [ScreenViewHolder.show] 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 +135,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 +150,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 +178,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..f7fc5c253d 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,22 @@ 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.ScreenViewHolder 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.fromCode( + buildView = { _, initialEnvironment, context, _ -> BackStackContainer(context) - .apply { - id = R.id.workflow_back_stack_container - layoutParams = (LayoutParams(MATCH_PARENT, MATCH_PARENT)) - bindShowRendering(initialRendering, initialEnv, ::update) + .let { view -> + view.id = R.id.workflow_back_stack_container + view.layoutParams = (LayoutParams(MATCH_PARENT, MATCH_PARENT)) + ScreenViewHolder(initialEnvironment, view) { rendering, environment -> + view.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..486049a36d 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.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewHolder import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub import com.squareup.workflow1.ui.androidx.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[ScreenViewHolder.Showing]) + 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() { @@ -171,14 +179,16 @@ internal class BodyAndModalsContainer @JvmOverloads constructor( } companion object : ScreenViewFactory> - by ManualScreenViewFactory( - type = BodyAndModalsScreen::class, - viewConstructor = { initialRendering, initialEnv, context, _ -> + by ScreenViewFactory.fromCode( + buildView = { _, initialEnvironment, context, _ -> BodyAndModalsContainer(context) - .apply { - id = R.id.workflow_body_and_modals_container - layoutParams = (LayoutParams(MATCH_PARENT, MATCH_PARENT)) - bindShowRendering(initialRendering, initialEnv, ::update) + .let { view -> + view.id = R.id.workflow_body_and_modals_container + view.layoutParams = (LayoutParams(MATCH_PARENT, MATCH_PARENT)) + + ScreenViewHolder(initialEnvironment, view) { rendering, environment -> + view.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..46612e67ae 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,26 @@ package com.squareup.workflow1.ui.container -import com.squareup.workflow1.ui.DecorativeScreenViewFactory +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromCode import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.merge +import com.squareup.workflow1.ui.toUnwrappingViewFactory +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(): + ScreenViewFactory> { + return fromCode { initialEnvScreen, initialEnvironment, context, container -> + val mergedInitialEnvironment = initialEnvironment merge initialEnvScreen.environment + + initialEnvScreen.wrapped.toViewFactory(mergedInitialEnvironment) + .toUnwrappingViewFactory, WrappedT>( + unwrap = { it.wrapped }, + showWrapperScreen = { _, envScreen, environment, showUnwrapped -> + showUnwrapped(envScreen.wrapped, environment merge envScreen.environment) + } + ) + .buildView(initialEnvScreen, mergedInitialEnvironment, context, container) } -) +} 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..0b505c87ad 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,14 @@ 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.show +import com.squareup.workflow1.ui.startShowing +import com.squareup.workflow1.ui.toViewFactory import kotlin.reflect.KClass /** @@ -72,21 +73,19 @@ 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() - // 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 = { } - } + val contentViewHolder = wrappedContentRendering.toViewFactory(initialEnvironment) + .startShowing(wrappedContentRendering, 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 (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 +95,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 +114,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..d710940c05 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 @@ -10,11 +10,13 @@ import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting.PRIVATE import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.ViewTreeSavedStateRegistryOwner +import com.squareup.workflow1.ui.Compatible.Companion.keyFor 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 +import com.squareup.workflow1.ui.showing /** * Handles persistence chores for container views that manage a set of [NamedScreen] renderings, @@ -74,10 +76,10 @@ internal constructor( */ public fun update( retainedRenderings: Collection>, - oldViewMaybe: View?, - newView: View + oldHolderMaybe: ScreenViewHolder>?, + newHolder: ScreenViewHolder> ) { - val newKey = newView.namedKey + val newKey = keyFor(newHolder.showing) val hiddenKeys = retainedRenderings.asSequence() .map { it.compatibilityKey } .toSet() @@ -88,19 +90,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) { + keyFor(oldHolderMaybe.showing).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 +208,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..e82a31114c 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,8 @@ - + + + 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..af0640805d 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 @@ -3,13 +3,10 @@ package com.squareup.workflow1.ui import android.content.Context -import android.view.View 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 +28,8 @@ internal class ScreenViewFactoryTest { } val error = assertFailsWith { - fooScreen.buildView(env, mock()) + fooScreen.toViewFactory(env) + .startShowing(fooScreen, env, mock()) } assertThat(error.message).isEqualTo( "A ScreenViewFactory should have been registered to display " + @@ -44,36 +42,40 @@ internal class ScreenViewFactoryTest { val env = ViewEnvironment.EMPTY + ViewRegistry() val screen = MyAndroidScreen() - screen.buildView(env, mock()) - assertThat(screen.viewFactory.called).isTrue() + screen.toViewFactory(env) + .startShowing(screen, 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.toViewFactory(env) + .startShowing(screen, 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, + initialEnvironment: ViewEnvironment, + context: Context, container: ViewGroup? - ): View { - called = true + ): ScreenViewHolder { + built = true - return mock { - on { - getTag(eq(R.id.workflow_ui_view_state)) - } doReturn (WorkflowViewState.New(initialRendering, initialViewEnvironment, { _, _ -> })) + return ScreenViewHolder(initialEnvironment, mock()) { _, _ -> + updated = true } } } diff --git a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/container/EnvironmentScreenAndroidIntegrationTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/container/EnvironmentScreenAndroidIntegrationTest.kt index 59ada52651..fd110a050e 100644 --- a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/container/EnvironmentScreenAndroidIntegrationTest.kt +++ b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/container/EnvironmentScreenAndroidIntegrationTest.kt @@ -6,52 +6,45 @@ import com.google.common.truth.Truth.assertThat import com.squareup.workflow1.ui.ViewEnvironment.Companion.EMPTY import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.buildView -import com.squareup.workflow1.ui.environment -import com.squareup.workflow1.ui.getRendering import com.squareup.workflow1.ui.plus -import com.squareup.workflow1.ui.showRendering -import com.squareup.workflow1.ui.start +import com.squareup.workflow1.ui.show +import com.squareup.workflow1.ui.startShowing +import com.squareup.workflow1.ui.toViewFactory import org.junit.Test import org.mockito.kotlin.mock internal class EnvironmentScreenAndroidIntegrationTest { @Test fun mergingWorksForBuild() { - // By putting altFactory into the environment in envScreen, - // we expect it to build the view for wrappedScreen instead of the hard - // coded default, wrappedScreen.viewFactory val altFactory = WrappedFactory() val env = EMPTY + (SomeEnvValue to "hi") + ViewRegistry(altFactory) val wrappedScreen = WrappedScreen() val envScreen = wrappedScreen.withEnvironment(env) - val view = envScreen.buildView(EMPTY, mock()) + val holder = envScreen.toViewFactory(EMPTY) + .startShowing(envScreen, EMPTY, mock()) + // By putting altFactory into the environment in envScreen, + // we expect it to have built the view for wrappedScreen instead of the hard + // coded default. assertThat(wrappedScreen.viewFactory.lastView).isNull() - - // altFactory made the view. - assertThat(view).isSameInstanceAs(altFactory.lastView) + assertThat(holder.view).isSameInstanceAs(altFactory.lastView) // The wrapper env reached the inner view factory. assertThat(altFactory.lastEnv!![SomeEnvValue]).isEqualTo("hi") // The wrapper env is on the view. - assertThat(view.environment!![SomeEnvValue]).isEqualTo("hi") - // The wrapper rendering is on the view. - assertThat(view.getRendering()).isEqualTo(envScreen) + assertThat(holder.environment[SomeEnvValue]).isEqualTo("hi") } @Test fun mergingWorksForUpdate() { val wrappedScreen = WrappedScreen() - val view = wrappedScreen.withEnvironment(EMPTY + (SomeEnvValue to "hi")) - .buildView(EMPTY, mock()) - assertThat(view.environment!![SomeEnvValue]).isEqualTo("hi") + val withEnvironment = wrappedScreen.withEnvironment(EMPTY + (SomeEnvValue to "hi")) + val holder = withEnvironment.toViewFactory(EMPTY) + .startShowing(withEnvironment, EMPTY, mock()) + assertThat(holder.environment[SomeEnvValue]).isEqualTo("hi") - view.start() - view.showRendering(wrappedScreen.withEnvironment(EMPTY + (SomeEnvValue to "bye")), EMPTY) + holder.show(wrappedScreen.withEnvironment(EMPTY + (SomeEnvValue to "bye")), EMPTY) assertThat(wrappedScreen.viewFactory.lastEnv!![SomeEnvValue]).isEqualTo("bye") - - // TODO To be fixed or obviated by https://github.com/square/workflow-kotlin/pull/703 - // assertThat(view.environment!![SomeEnvValue]).isEqualTo("bye") + assertThat(holder.environment[SomeEnvValue]).isEqualTo("bye") } } diff --git a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/container/NamedScreenAndroidIntegrationTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/container/NamedScreenAndroidIntegrationTest.kt index 460f79ee67..20111ec8fe 100644 --- a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/container/NamedScreenAndroidIntegrationTest.kt +++ b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/container/NamedScreenAndroidIntegrationTest.kt @@ -6,10 +6,10 @@ import com.google.common.truth.Truth.assertThat import com.squareup.workflow1.ui.NamedScreen import com.squareup.workflow1.ui.ViewEnvironment.Companion.EMPTY import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.buildView -import com.squareup.workflow1.ui.getRendering -import com.squareup.workflow1.ui.showRendering -import com.squareup.workflow1.ui.start +import com.squareup.workflow1.ui.show +import com.squareup.workflow1.ui.showing +import com.squareup.workflow1.ui.startShowing +import com.squareup.workflow1.ui.toViewFactory import org.junit.Test import org.mockito.kotlin.mock @@ -18,20 +18,20 @@ internal class NamedScreenAndroidIntegrationTest { val wrappedScreen = WrappedScreen() val named = NamedScreen(wrappedScreen, "fred") - val view = named.buildView(EMPTY, mock()) - assertThat(view).isSameInstanceAs(wrappedScreen.viewFactory.lastView) - assertThat(view.getRendering()).isSameInstanceAs(named) + val holder = named.toViewFactory(EMPTY) + .startShowing(named, EMPTY, mock()) + assertThat(holder.view).isSameInstanceAs(wrappedScreen.viewFactory.lastView) + assertThat(holder.showing).isSameInstanceAs(named) } @Test fun updatesOkay() { val wrappedScreen = WrappedScreen() val named = NamedScreen(wrappedScreen, "fred") + val holder = named.toViewFactory(EMPTY) + .startShowing(named, EMPTY, mock()) - val view = named.buildView(EMPTY, mock()) - view.start() - - view.showRendering(NamedScreen(wrappedScreen, "fred"), EMPTY) - assertThat(view).isSameInstanceAs(view) - assertThat(view.getRendering()).isNotSameInstanceAs(named) + holder.show(NamedScreen(wrappedScreen, "fred"), EMPTY) + assertThat(holder).isSameInstanceAs(holder) + assertThat(holder.showing).isNotSameInstanceAs(named) } } diff --git a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/container/ScreenViewFactoryTestUtil.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/container/ScreenViewFactoryTestUtil.kt index 1fba90e0ae..6ffb7718a6 100644 --- a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/container/ScreenViewFactoryTestUtil.kt +++ b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/container/ScreenViewFactoryTestUtil.kt @@ -6,11 +6,12 @@ import android.content.Context import android.view.View import android.view.ViewGroup import com.squareup.workflow1.ui.AndroidScreen +import com.squareup.workflow1.ui.RealScreenViewHolder import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewHolder import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.ViewEnvironmentKey import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.bindShowRendering import org.mockito.kotlin.isA import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -47,16 +48,16 @@ internal class WrappedFactory : ScreenViewFactory { override fun buildView( initialRendering: WrappedScreen, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, + initialEnvironment: ViewEnvironment, + context: Context, container: ViewGroup? - ): View { - lastEnv = initialViewEnvironment - return mockView().also { view -> - view.bindShowRendering(initialRendering, initialViewEnvironment) { _, environment -> - lastEnv = environment - } - lastView = view + ): ScreenViewHolder { + lastEnv = initialEnvironment + return RealScreenViewHolder( + view = mockView().also { lastView = it }, + initialEnvironment = initialEnvironment + ) { _, newEnvironment -> + lastEnv = newEnvironment } } } diff --git a/workflow-ui/core-common/api/core-common.api b/workflow-ui/core-common/api/core-common.api index 8b97adc3b9..25bf2f98cb 100644 --- a/workflow-ui/core-common/api/core-common.api +++ b/workflow-ui/core-common/api/core-common.api @@ -108,6 +108,10 @@ public abstract class com/squareup/workflow1/ui/ViewEnvironmentKey { public fun toString ()Ljava/lang/String; } +public final class com/squareup/workflow1/ui/ViewEnvironmentKt { + public static final synthetic fun ViewEnvironmentKey (Lkotlin/jvm/functions/Function0;)Lcom/squareup/workflow1/ui/ViewEnvironmentKey; +} + public abstract interface class com/squareup/workflow1/ui/ViewRegistry { public static final field Companion Lcom/squareup/workflow1/ui/ViewRegistry$Companion; public abstract fun getEntryFor (Lkotlin/reflect/KClass;)Lcom/squareup/workflow1/ui/ViewRegistry$Entry; @@ -218,8 +222,8 @@ public final class com/squareup/workflow1/ui/container/EnvironmentScreen : com/s public fun (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V public synthetic fun (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun getCompatibilityKey ()Ljava/lang/String; - public final fun getScreen ()Lcom/squareup/workflow1/ui/Screen; - public final fun getViewEnvironment ()Lcom/squareup/workflow1/ui/ViewEnvironment; + public final fun getEnvironment ()Lcom/squareup/workflow1/ui/ViewEnvironment; + public final fun getWrapped ()Lcom/squareup/workflow1/ui/Screen; } public final class com/squareup/workflow1/ui/container/EnvironmentScreenKt { 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/ViewEnvironment.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/ViewEnvironment.kt index d0a39fa080..3a51ff3105 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/ViewEnvironment.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/ViewEnvironment.kt @@ -69,3 +69,13 @@ public abstract class ViewEnvironmentKey( return "${this::class.simpleName}(${type.simpleName})" } } + +@WorkflowUiExperimentalApi +public inline fun ViewEnvironmentKey( + crossinline produceDefault: () -> T, +): ViewEnvironmentKey { + return object : ViewEnvironmentKey(T::class) { + override val default: T + get() = produceDefault() + } +} diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/EnvironmentScreen.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/EnvironmentScreen.kt index 56759b1b4a..52511f2ba5 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/EnvironmentScreen.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/EnvironmentScreen.kt @@ -8,26 +8,26 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.merge /** - * Pairs a [screen] rendering with a [viewEnvironment] to support its display. + * Pairs a [wrapped] rendering with a [environment] to support its display. * Typically the rendering type (`RenderingT`) of the root of a UI workflow, * but can be used at any point to modify the [ViewEnvironment] received from * a parent view. */ @WorkflowUiExperimentalApi public class EnvironmentScreen( - public val screen: V, - public val viewEnvironment: ViewEnvironment = ViewEnvironment.EMPTY + public val wrapped: V, + public val environment: ViewEnvironment = ViewEnvironment.EMPTY ) : Compatible, Screen { /** * Ensures that we make the decision to update or replace the root view based on - * the wrapped [screen]. + * the wrapped [wrapped]. */ - override val compatibilityKey: String = Compatible.keyFor(screen, "EnvironmentScreen") + override val compatibilityKey: String = Compatible.keyFor(wrapped, "EnvironmentScreen") } /** * Returns an [EnvironmentScreen] derived from the receiver, whose - * [EnvironmentScreen.viewEnvironment] includes [viewRegistry]. + * [EnvironmentScreen.environment] includes [viewRegistry]. * * If the receiver is an [EnvironmentScreen], uses [ViewRegistry.merge] * to preserve the [ViewRegistry] entries of both. @@ -39,7 +39,7 @@ public fun Screen.withRegistry(viewRegistry: ViewRegistry): EnvironmentScreen<*> /** * Returns an [EnvironmentScreen] derived from the receiver, - * whose [EnvironmentScreen.viewEnvironment] includes the values in the given [environment]. + * whose [EnvironmentScreen.environment] includes the values in the given [environment]. * * If the receiver is an [EnvironmentScreen], uses [ViewEnvironment.merge] * to preserve the [ViewRegistry] entries of both. @@ -51,7 +51,7 @@ public fun Screen.withEnvironment( return when (this) { is EnvironmentScreen<*> -> { if (environment.map.isEmpty()) this - else EnvironmentScreen(screen, this.viewEnvironment merge environment) + else EnvironmentScreen(wrapped, this.environment merge environment) } else -> EnvironmentScreen(this, environment) } diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/ScreenOverlay.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/ScreenOverlay.kt index bcb60e19e9..2a757ef8b3 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/ScreenOverlay.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/ScreenOverlay.kt @@ -9,8 +9,8 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi * An [Overlay] built around a root [content] [Screen]. */ @WorkflowUiExperimentalApi -public interface ScreenOverlay : Overlay, Compatible { - public val content: C +public interface ScreenOverlay : Overlay, Compatible { + public val content: ContentT override val compatibilityKey: String get() = keyFor(content, "ScreenOverlay") } diff --git a/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/container/EnvironmentScreenTest.kt b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/container/EnvironmentScreenTest.kt index b90565bd3e..406c3cd0f3 100644 --- a/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/container/EnvironmentScreenTest.kt +++ b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/container/EnvironmentScreenTest.kt @@ -36,10 +36,10 @@ internal class EnvironmentScreenTest { val viewRegistry = ViewRegistry(fooFactory) val envScreen = FooScreen.withRegistry(viewRegistry) - assertThat(envScreen.viewEnvironment[ViewRegistry][FooScreen::class]) + assertThat(envScreen.environment[ViewRegistry][FooScreen::class]) .isSameInstanceAs(fooFactory) - assertThat(envScreen.viewEnvironment[ViewRegistry][BarScreen::class]) + assertThat(envScreen.environment[ViewRegistry][BarScreen::class]) .isNull() } @@ -50,11 +50,11 @@ internal class EnvironmentScreenTest { EMPTY + viewRegistry + TestValue("foo") ) - assertThat(envScreen.viewEnvironment[ViewRegistry][FooScreen::class]) + assertThat(envScreen.environment[ViewRegistry][FooScreen::class]) .isSameInstanceAs(fooFactory) - assertThat(envScreen.viewEnvironment[ViewRegistry][BarScreen::class]) + assertThat(envScreen.environment[ViewRegistry][BarScreen::class]) .isNull() - assertThat(envScreen.viewEnvironment[TestValue]) + assertThat(envScreen.environment[TestValue]) .isEqualTo(TestValue("foo")) } @@ -66,10 +66,10 @@ internal class EnvironmentScreenTest { val left = FooScreen.withRegistry(ViewRegistry(fooFactory1, barFactory)) val union = left.withRegistry(ViewRegistry(fooFactory2)) - assertThat(union.viewEnvironment[ViewRegistry][FooScreen::class]) + assertThat(union.environment[ViewRegistry][FooScreen::class]) .isSameInstanceAs(fooFactory2) - assertThat(union.viewEnvironment[ViewRegistry][BarScreen::class]) + assertThat(union.environment[ViewRegistry][BarScreen::class]) .isSameInstanceAs(barFactory) } @@ -86,11 +86,11 @@ internal class EnvironmentScreenTest { EMPTY + ViewRegistry(fooFactory2) + TestValue("right") ) - assertThat(union.viewEnvironment[ViewRegistry][FooScreen::class]) + assertThat(union.environment[ViewRegistry][FooScreen::class]) .isSameInstanceAs(fooFactory2) - assertThat(union.viewEnvironment[ViewRegistry][BarScreen::class]) + assertThat(union.environment[ViewRegistry][BarScreen::class]) .isSameInstanceAs(barFactory) - assertThat(union.viewEnvironment[TestValue]) + assertThat(union.environment[TestValue]) .isEqualTo(TestValue("right")) } 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 224a224b2c..b725b7b153 100644 --- a/workflow-ui/internal-testing-android/api/internal-testing-android.api +++ b/workflow-ui/internal-testing-android/api/internal-testing-android.api @@ -18,13 +18,13 @@ public class com/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivi public field rendering Ljava/lang/Object; public fun (Landroid/content/Context;)V public final fun getRendering ()Ljava/lang/Object; - public final fun getViewObserver$wf1_internal_testing_android ()Lcom/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity$ViewObserver; + public final fun getViewObserver ()Lcom/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity$ViewObserver; protected fun onAttachedToWindow ()V protected fun onDetachedFromWindow ()V protected fun onRestoreInstanceState (Landroid/os/Parcelable;)V protected fun onSaveInstanceState ()Landroid/os/Parcelable; - public final fun setRendering$wf1_internal_testing_android (Ljava/lang/Object;)V - public final fun setViewObserver$wf1_internal_testing_android (Lcom/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity$ViewObserver;)V + public final fun setRendering (Ljava/lang/Object;)V + public final fun setViewObserver (Lcom/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity$ViewObserver;)V } public abstract interface class com/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity$ViewObserver { 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..d98aba8c3c 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,19 @@ 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.ScreenViewHolder 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,18 +92,26 @@ 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( + initialRendering: R, + initialEnvironment: ViewEnvironment, + context: Context, + container: ViewGroup? + ): ScreenViewHolder { + val view = viewConstructor(context).apply { this.viewObserver = viewObserver viewObserver.onViewCreated(this, initialRendering) + } - bindShowRendering(initialRendering, initialViewEnvironment) { rendering, _ -> - this.rendering = rendering - viewObserver.onShowRendering(this, rendering) - } + return ScreenViewHolder(initialEnvironment, view) { r, _ -> + view.rendering = r + viewObserver.onShowRendering(view, r) } } + } protected fun lifecycleLoggingViewObserver( describeRendering: (R) -> String @@ -178,11 +186,11 @@ public abstract class AbstractLifecycleTestActivity : WorkflowUiTestActivity() { context: Context ) : FrameLayout(context) { - internal var viewObserver: ViewObserver? = null + public var viewObserver: ViewObserver? = null // We can't rely on getRendering() in case it's wrapped with Named. public lateinit var rendering: R - internal set + public set private val lifecycleObserver = LifecycleEventObserver { _, event -> viewObserver?.onViewTreeLifecycleStateChanged(rendering, event) 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(