diff --git a/samples/containers/android/src/main/java/com/squareup/sample/container/BackButtonViewFactory.kt b/samples/containers/android/src/main/java/com/squareup/sample/container/BackButtonViewFactory.kt index 7e4ba2b328..635fc963ae 100644 --- a/samples/containers/android/src/main/java/com/squareup/sample/container/BackButtonViewFactory.kt +++ b/samples/containers/android/src/main/java/com/squareup/sample/container/BackButtonViewFactory.kt @@ -1,31 +1,43 @@ package com.squareup.sample.container -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.acceptRenderings import com.squareup.workflow1.ui.backPressedHandler +import com.squareup.workflow1.ui.buildView +import com.squareup.workflow1.ui.withShowScreen /** - * [ScreenViewFactory] that performs the work required by [BackButtonScreen]. + * [ScreenViewFactory] that performs the work required by [BackButtonScreen], demonstrating + * some fancy [ScreenViewHolder][com.squareup.workflow1.ui.ScreenViewHolder] tricks in + * the process. */ @WorkflowUiExperimentalApi -object BackButtonViewFactory : ScreenViewFactory> -by DecorativeScreenViewFactory( - type = BackButtonScreen::class, - map = { outer -> outer.wrapped }, - doShowRendering = { view, innerShowRendering, outerRendering, viewEnvironment -> - if (!outerRendering.override) { - // Place our handler before invoking innerShowRendering, so that - // its later calls to view.backPressedHandler will take precedence - // over ours. - view.backPressedHandler = outerRendering.onBackPressed - } +val BackButtonViewFactory: ScreenViewFactory> = + ScreenViewFactory.of { initialRendering, initialViewEnvironment, context, container -> + initialRendering.wrapped + // Build the view for the wrapped rendering. + .buildView(initialViewEnvironment, context, container) + // Transform it to accept BackButtonScreen directly + .acceptRenderings(initialRendering) { backButtonScreen -> backButtonScreen.wrapped } + // Replace the showScreen method with one that can do a bit of pre- and post-processing + // on the view. + .withShowScreen { backButtonScreen, viewEnvironment -> + if (!backButtonScreen.override) { + // Place our handler before invoking the real showRendering method, so that + // its later calls to view.backPressedHandler will take precedence + // over ours. + view.backPressedHandler = backButtonScreen.onBackPressed + } - innerShowRendering.invoke(outerRendering.wrapped, viewEnvironment) + // The receiver of this lambda is the one that received the withShowScreen call, + // so we're able to call the "real" showScreen method. + showScreen(backButtonScreen, viewEnvironment) - if (outerRendering.override) { - // Place our handler after invoking innerShowRendering, so that ours wins. - view.backPressedHandler = outerRendering.onBackPressed - } + if (backButtonScreen.override) { + // Place our handler after invoking innerShowRendering, so that ours wins. + view.backPressedHandler = backButtonScreen.onBackPressed + } + } } -) 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 e30b9f9837..ce7a49d9de 100644 --- a/samples/containers/android/src/main/java/com/squareup/sample/container/ScrimContainer.kt +++ b/samples/containers/android/src/main/java/com/squareup/sample/container/ScrimContainer.kt @@ -7,11 +7,10 @@ import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat import com.squareup.sample.container.panel.ScrimScreen -import com.squareup.workflow1.ui.ManualScreenViewFactory import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.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 @@ -33,7 +32,7 @@ internal class ScrimContainer @JvmOverloads constructor( private val child: View get() = getChildAt(0) - ?: error("Child must be set immediately upon creation.") + ?: error("Child must be set immediately upon creation.") var isDimmed: Boolean = false set(value) { @@ -84,30 +83,29 @@ internal class ScrimContainer @JvmOverloads constructor( ValueAnimator.ofFloat(1f, 0f) }.apply { duration = resources.getInteger(android.R.integer.config_shortAnimTime) - .toLong() + .toLong() addUpdateListener { animation -> scrim.alpha = animation.animatedValue as Float } start() } } @OptIn(WorkflowUiExperimentalApi::class) - companion object : ScreenViewFactory> by ManualScreenViewFactory( - type = ScrimScreen::class, - viewConstructor = { initialRendering, initialViewEnvironment, contextForNewView, _ -> - val stub = WorkflowViewStub(contextForNewView) + companion object : ScreenViewFactory> by ScreenViewFactory.of( + viewConstructor = { initialRendering, initialViewEnvironment, contextForNewView, _ -> + val stub = WorkflowViewStub(contextForNewView) - ScrimContainer(contextForNewView) - .also { view -> - view.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) - view.addView(stub) + ScrimContainer(contextForNewView) + .let { 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( + initialRendering, initialViewEnvironment, view + ) { rendering, environment -> + stub.show(rendering.content, environment) + view.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 47b8b2c17c..0820c37b53 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 @@ -8,7 +8,7 @@ import com.squareup.sample.container.overviewdetail.OverviewDetailConfig.Detail import com.squareup.sample.container.overviewdetail.OverviewDetailConfig.Overview import com.squareup.sample.container.overviewdetail.OverviewDetailConfig.Single import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewUpdater import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub @@ -25,7 +25,7 @@ import com.squareup.workflow1.ui.container.withBackStackStateKeyPrefix * with [OverviewDetailScreen.overviewRendering] as the base of the stack. */ @OptIn(WorkflowUiExperimentalApi::class) -class OverviewDetailContainer(view: View) : ScreenViewRunner { +class OverviewDetailContainer(view: View) : ScreenViewUpdater { private val overviewStub: WorkflowViewStub? = view.findViewById(R.id.overview_stub) private val detailStub: WorkflowViewStub? = view.findViewById(R.id.detail_stub) @@ -60,14 +60,14 @@ class OverviewDetailContainer(view: View) : ScreenViewRunner - detailStub!!.actual.visibility = VISIBLE + detailStub!!.delegateHolder.view.visibility = VISIBLE detailStub.show( detail, viewEnvironment + (OverviewDetailConfig to Detail) ) } ?: run { - detailStub!!.actual.visibility = INVISIBLE + detailStub!!.delegateHolder.view.visibility = INVISIBLE } } } @@ -84,11 +84,9 @@ class OverviewDetailContainer(view: View) : ScreenViewRunner by ScreenViewRunner.bind( - layoutId = R.layout.overview_detail, - constructor = ::OverviewDetailContainer + companion object : ScreenViewFactory by ScreenViewFactory.ofLayout(R.layout.overview_detail, + { it: View -> OverviewDetailContainer(it) } ) { private const val OverviewBackStackKey = "overview" - private const val DetailBackStackKey = "detail" } } diff --git a/samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelOverlayDialogFactory.kt b/samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelOverlayDialogFactory.kt index 6a697a7e20..34da57d713 100644 --- a/samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelOverlayDialogFactory.kt +++ b/samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelOverlayDialogFactory.kt @@ -4,8 +4,8 @@ import android.app.Dialog import android.graphics.Rect import android.graphics.drawable.ColorDrawable import android.util.TypedValue -import android.view.View import com.squareup.sample.container.R +import com.squareup.workflow1.ui.ScreenViewHolder import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.container.ModalScreenOverlayDialogFactory import com.squareup.workflow1.ui.container.setBounds @@ -17,10 +17,10 @@ import com.squareup.workflow1.ui.container.setBounds internal object PanelOverlayDialogFactory : ModalScreenOverlayDialogFactory>( type = PanelOverlay::class ) { - override fun buildDialogWithContentView(contentView: View): Dialog { - val context = contentView.context + override fun buildDialogWithContent(content: ScreenViewHolder<*>): Dialog { + val context = content.view.context return Dialog(context, R.style.PanelDialog).also { dialog -> - dialog.setContentView(contentView) + dialog.setContentView(content.view) // Welcome to Android. Nothing workflow-related here, this is just how one // finds the window background color for the theme. I sure hope it's better in Compose. diff --git a/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListScreen.kt b/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListScreen.kt index c6cfb50dac..02ebcd2617 100644 --- a/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListScreen.kt +++ b/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListScreen.kt @@ -12,7 +12,8 @@ import com.squareup.sample.container.overviewdetail.OverviewDetailConfig.Overvie import com.squareup.sample.container.poetryapp.R import com.squareup.sample.poetry.model.Poem import com.squareup.workflow1.ui.AndroidScreen -import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewUpdater import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi @@ -22,14 +23,13 @@ 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.ofLayout( + R.layout.list + ) { it: View -> PoemListLayoutUpdater(it) } } @OptIn(WorkflowUiExperimentalApi::class) -private class PoemListLayoutRunner(view: View) : ScreenViewRunner { +private class PoemListLayoutUpdater(view: View) : ScreenViewUpdater { init { view.findViewById(R.id.list_toolbar) .apply { 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/HelloBackButtonLayoutUpdater.kt similarity index 78% rename from samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonLayoutRunner.kt rename to samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonLayoutUpdater.kt index 5250b429f2..dbfb3d8152 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/HelloBackButtonLayoutUpdater.kt @@ -4,7 +4,7 @@ import android.view.View import android.widget.TextView import com.squareup.workflow1.ui.AndroidScreen import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewUpdater import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.backPressedHandler @@ -15,13 +15,13 @@ data class HelloBackButtonScreen( val onClick: () -> Unit, val onBackPressed: (() -> Unit)? ) : AndroidScreen { - override val viewFactory: ScreenViewFactory = ScreenViewRunner.bind( - R.layout.hello_back_button_layout, ::HelloBackButtonLayoutRunner - ) + override val viewFactory: ScreenViewFactory = ScreenViewFactory.ofLayout( + R.layout.hello_back_button_layout + ) { it: View -> HelloBackButtonLayoutUpdater(it) } } @OptIn(WorkflowUiExperimentalApi::class) -private class HelloBackButtonLayoutRunner(view: View) : ScreenViewRunner { +private class HelloBackButtonLayoutUpdater(view: View) : ScreenViewUpdater { private val messageView: TextView = view.findViewById(R.id.hello_message) override fun showRendering( 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..9395c16b93 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 @@ -12,7 +12,7 @@ import com.squareup.sample.container.overviewdetail.OverviewDetailConfig.Overvie import com.squareup.sample.container.poetry.R import com.squareup.workflow1.ui.AndroidScreen import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewUpdater import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.backPressedHandler @@ -28,14 +28,13 @@ 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 = ScreenViewFactory.ofLayout( + R.layout.list + ) { it: View -> StanzaListLayoutUpdater(it) } } @OptIn(WorkflowUiExperimentalApi::class) -private class StanzaListLayoutRunner(view: View) : ScreenViewRunner { +private class StanzaListLayoutUpdater(view: View) : ScreenViewUpdater { private val toolbar = view.findViewById(R.id.list_toolbar) private val recyclerView = view.findViewById(R.id.list_body) .apply { layoutManager = LinearLayoutManager(context) } 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 7f3bdf8a57..d907548756 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 @@ -14,7 +14,7 @@ import com.squareup.sample.container.poetry.R 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.ScreenViewUpdater import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.backPressedHandler @@ -32,14 +32,13 @@ 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 = ScreenViewFactory.ofLayout( + R.layout.stanza_layout + ) { it: View -> StanzaLayoutUpdater(it) } } @OptIn(WorkflowUiExperimentalApi::class) -private class StanzaLayoutRunner(private val view: View) : ScreenViewRunner { +private class StanzaLayoutUpdater(private val view: View) : ScreenViewUpdater { private val tabSize = TypedValue .applyDimension(TypedValue.COMPLEX_UNIT_SP, 24f, view.resources.displayMetrics) .toInt() @@ -114,8 +113,7 @@ private class StanzaLayoutRunner(private val view: View) : ScreenViewRunner by ScreenViewRunner.bind( - R.layout.stanza_layout, - ::StanzaLayoutRunner + companion object : ScreenViewFactory by ScreenViewFactory.ofLayout(R.layout.stanza_layout, + { it: View -> StanzaLayoutUpdater(it) } ) } 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 bdb070c080..4bb4919ecb 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,10 +82,15 @@ class BoardView(context: Context) : View(context) { } @OptIn(WorkflowUiExperimentalApi::class) - companion object : ScreenViewFactory by ManualScreenViewFactory( - type = Board::class, + companion object : ScreenViewFactory by ScreenViewFactory.of( viewConstructor = { initialRendering, initialEnv, contextForNewView, _ -> BoardView(contextForNewView) - .apply { bindShowRendering(initialRendering, initialEnv) { r, _ -> update(r) } } + .let { view -> + ScreenViewHolder( + initialRendering, + initialEnv, + view + ) { r, _ -> view.update(r) } + } }) } 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/BoardsListLayoutUpdater.kt similarity index 90% rename from samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardsListLayoutRunner.kt rename to samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardsListLayoutUpdater.kt index 24db11abde..d27e7a5b30 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/BoardsListLayoutUpdater.kt @@ -11,8 +11,7 @@ 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.ScreenViewRunner -import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bind +import com.squareup.workflow1.ui.ScreenViewUpdater import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub @@ -24,7 +23,7 @@ import com.squareup.workflow1.ui.WorkflowViewStub * a `RecyclerView`. */ @OptIn(WorkflowUiExperimentalApi::class) -class BoardsListLayoutRunner(rootView: View) : ScreenViewRunner { +class BoardsListLayoutUpdater(rootView: View) : ScreenViewUpdater { /** * Used to associate a single [ViewEnvironment] and [DisplayBoardsListScreen.onBoardSelected] @@ -48,7 +47,7 @@ class BoardsListLayoutRunner(rootView: View) : ScreenViewRunner val card: CardView = view.findViewById(R.id.board_card) val boardNameView: TextView = view.findViewById(R.id.board_name) - // The board preview is actually rendered using the same ScreenViewRunner as the actual + // The board preview is actually rendered using the same ScreenViewUpdater as the actual // live game. It's easy to delegate to it by just putting a WorkflowViewStub in our // layout and giving it the Board. val boardPreviewView: WorkflowViewStub = view.findViewById(R.id.board_preview_stub) @@ -57,7 +56,7 @@ class BoardsListLayoutRunner(rootView: View) : ScreenViewRunner by bind( - R.layout.boards_list_layout, ::BoardsListLayoutRunner + companion object : ScreenViewFactory by ScreenViewFactory.ofLayout( + R.layout.boards_list_layout, + { it: View -> BoardsListLayoutUpdater(it) } ) } 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 a3d5be2c97..f2df2f6157 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 @@ -7,7 +7,7 @@ import android.os.Vibrator import androidx.appcompat.app.AppCompatActivity import com.squareup.sample.dungeon.DungeonAppWorkflow.State.LoadingBoardList import com.squareup.sample.dungeon.GameSessionWorkflow.State.Loading -import com.squareup.sample.timemachine.shakeable.ShakeableTimeMachineLayoutRunner +import com.squareup.sample.timemachine.shakeable.ShakeableTimeMachineLayoutUpdater import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.modal.AlertContainer @@ -24,11 +24,11 @@ class Component(context: AppCompatActivity) { @OptIn(WorkflowUiExperimentalApi::class) val viewRegistry = ViewRegistry( - ShakeableTimeMachineLayoutRunner, + ShakeableTimeMachineLayoutUpdater, LoadingBinding(R.string.loading_boards_list), - BoardsListLayoutRunner, + BoardsListLayoutUpdater, LoadingBinding(R.string.loading_board), - GameLayoutRunner, + GameLayoutUpdater, 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/GameLayoutUpdater.kt similarity index 89% rename from samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameLayoutRunner.kt rename to samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameLayoutUpdater.kt index 7a096283f1..67d85f8aa9 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/GameLayoutUpdater.kt @@ -11,8 +11,7 @@ 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.ScreenViewRunner -import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bind +import com.squareup.workflow1.ui.ScreenViewUpdater import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub @@ -22,7 +21,7 @@ import com.squareup.workflow1.ui.WorkflowViewStub * the player. */ @OptIn(WorkflowUiExperimentalApi::class) -class GameLayoutRunner(view: View) : ScreenViewRunner { +class GameLayoutUpdater(view: View) : ScreenViewUpdater { private val boardView: WorkflowViewStub = view.findViewById(R.id.board_stub) private val moveLeft: View = view.findViewById(R.id.move_left) @@ -66,7 +65,8 @@ class GameLayoutRunner(view: View) : ScreenViewRunner { } } - companion object : ScreenViewFactory by bind( - R.layout.game_layout, ::GameLayoutRunner + companion object : ScreenViewFactory by ScreenViewFactory.ofLayout( + R.layout.game_layout, + { it: View -> GameLayoutUpdater(it) } ) } 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 019c97c2dc..fa9630cd8a 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,7 @@ 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.ScreenViewRunner -import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bind +import com.squareup.workflow1.ui.ScreenViewUpdater import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi @@ -23,13 +22,18 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi inline fun LoadingBinding( @StringRes loadingLabelRes: Int ): ScreenViewFactory = - bind(R.layout.loading_layout) { view -> LoadingLayoutRunner(loadingLabelRes, view) } + ScreenViewFactory.ofLayout(R.layout.loading_layout) { view: View -> + LoadingLayoutUpdater( + loadingLabelRes, + view + ) + } @PublishedApi -internal class LoadingLayoutRunner( +internal class LoadingLayoutUpdater( @StringRes private val labelRes: Int, view: View -) : ScreenViewRunner { +) : ScreenViewUpdater { init { view.findViewById(R.id.loading_label) 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/ShakeableTimeMachineLayoutUpdater.kt similarity index 91% rename from samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineLayoutRunner.kt rename to samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineLayoutUpdater.kt index c955a91404..a5f25ccca0 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/ShakeableTimeMachineLayoutUpdater.kt @@ -8,8 +8,7 @@ import androidx.constraintlayout.widget.Group import androidx.transition.TransitionManager import com.squareup.sample.timemachine.shakeable.internal.GlassFrameLayout import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.ScreenViewRunner -import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bind +import com.squareup.workflow1.ui.ScreenViewUpdater import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.WorkflowViewStub @@ -22,9 +21,9 @@ import kotlin.time.ExperimentalTime * [renderings][ShakeableTimeMachineScreen]. */ @OptIn(ExperimentalTime::class, WorkflowUiExperimentalApi::class) -class ShakeableTimeMachineLayoutRunner( +class ShakeableTimeMachineLayoutUpdater( private val view: View -) : ScreenViewRunner { +) : ScreenViewUpdater { private val glassView: GlassFrameLayout = view.findViewById(R.id.glass_view) private val childStub: WorkflowViewStub = view.findViewById(R.id.child_stub) @@ -87,7 +86,8 @@ class ShakeableTimeMachineLayoutRunner( private fun Duration.toUiString(): String = toString() - companion object : ScreenViewFactory by bind( - R.layout.shakeable_time_machine_layout, ::ShakeableTimeMachineLayoutRunner + companion object : ScreenViewFactory by ScreenViewFactory.ofLayout( + R.layout.shakeable_time_machine_layout, + { it: View -> ShakeableTimeMachineLayoutUpdater(it) } ) } diff --git a/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineWorkflow.kt b/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineWorkflow.kt index 0a3fea30fe..988a39a9b0 100644 --- a/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineWorkflow.kt +++ b/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineWorkflow.kt @@ -18,7 +18,7 @@ import kotlin.time.Duration import kotlin.time.ExperimentalTime /** - * A wrapper around a [TimeMachineWorkflow] that uses [ShakeableTimeMachineLayoutRunner] to render + * A wrapper around a [TimeMachineWorkflow] that uses [ShakeableTimeMachineLayoutUpdater] to render * the [delegate workflow][TimeMachineWorkflow.delegateWorkflow]'s rendering, but wrap it in a * UI to scrub around the recorded timeline when the device is shaken. * 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..8a614fae41 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 @@ -1,9 +1,11 @@ package com.squareup.sample.helloworkflowfragment +import android.view.LayoutInflater +import android.view.ViewGroup 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.ScreenViewUpdater import com.squareup.workflow1.ui.WorkflowUiExperimentalApi @OptIn(WorkflowUiExperimentalApi::class) @@ -12,8 +14,16 @@ 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() } + ScreenViewFactory.ofViewBinding({ inflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean -> + HelloGoodbyeLayoutBinding.inflate( + inflater, + parent, + attachToParent + ) + }) { binding -> + ScreenViewUpdater { rendering, viewEnvironment -> + binding.helloMessage.text = "${rendering.message} Fragment" + binding.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..6532e16763 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 @@ -1,9 +1,11 @@ package com.squareup.sample.helloworkflow +import android.view.LayoutInflater +import android.view.ViewGroup 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.ScreenViewUpdater import com.squareup.workflow1.ui.WorkflowUiExperimentalApi @OptIn(WorkflowUiExperimentalApi::class) @@ -12,8 +14,16 @@ data class HelloRendering( val onClick: () -> Unit ) : AndroidScreen { override val viewFactory: ScreenViewFactory = - ScreenViewRunner.bind(HelloGoodbyeLayoutBinding::inflate) { r, _ -> - helloMessage.text = r.message - helloMessage.setOnClickListener { r.onClick() } + ScreenViewFactory.ofViewBinding({ inflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean -> + HelloGoodbyeLayoutBinding.inflate( + inflater, + parent, + attachToParent + ) + }) { binding -> + ScreenViewUpdater { rendering, viewEnvironment -> + binding.helloMessage.text = rendering.message + binding.helloMessage.setOnClickListener { rendering.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..e18d07bb8d 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,25 +19,26 @@ data class ClickyTextRendering( val visible: Boolean = true, val onClick: (() -> Unit)? = null ) : AndroidScreen { - override val viewFactory = ManualScreenViewFactory( - type = ClickyTextRendering::class, + override val viewFactory = ScreenViewFactory.of( viewConstructor = { initialRendering, initialEnv, context, _ -> - TextView(context).also { textView -> + TextView(context).let { 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(initialRendering, initialEnv, textView) { r, _ -> textView.update(r) } } } ) } +@OptIn(WorkflowUiExperimentalApi::class) +private fun TextView.update(clickyText: ClickyTextRendering) { + text = clickyText.message + isVisible = clickyText.visible + setOnClickListener( + clickyText.onClick?.let { oc -> OnClickListener { oc() } } + ) +} + private var View.isVisible: Boolean get() = visibility == VISIBLE set(value) { 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..60ef5489fe 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 @@ -1,9 +1,11 @@ package com.squareup.sample.stubvisibility +import android.view.LayoutInflater +import android.view.ViewGroup 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.ScreenViewUpdater import com.squareup.workflow1.ui.WorkflowUiExperimentalApi @OptIn(WorkflowUiExperimentalApi::class) @@ -12,8 +14,16 @@ data class OuterRendering( val bottom: ClickyTextRendering ) : AndroidScreen { override val viewFactory: ScreenViewFactory = - ScreenViewRunner.bind(StubVisibilityLayoutBinding::inflate) { rendering, env -> - shouldBeFilledStub.show(rendering.top, env) - shouldBeWrappedStub.show(rendering.bottom, env) + ScreenViewFactory.ofViewBinding({ inflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean -> + StubVisibilityLayoutBinding.inflate( + inflater, + parent, + attachToParent + ) + }) { binding -> + ScreenViewUpdater { rendering, viewEnvironment -> + binding.shouldBeFilledStub.show(rendering.top, viewEnvironment) + binding.shouldBeWrappedStub.show(rendering.bottom, viewEnvironment) + } } } 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..5a1e8afa21 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 @@ -1,12 +1,22 @@ package com.squareup.sample.authworkflow +import android.view.LayoutInflater +import android.view.ViewGroup import com.squareup.sample.tictactoe.databinding.AuthorizingLayoutBinding import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewUpdater import com.squareup.workflow1.ui.WorkflowUiExperimentalApi @OptIn(WorkflowUiExperimentalApi::class) internal val AuthorizingViewFactory: ScreenViewFactory = - ScreenViewRunner.bind(AuthorizingLayoutBinding::inflate) { rendering, _ -> - authorizingMessage.text = rendering.message + ScreenViewFactory.ofViewBinding({ inflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean -> + AuthorizingLayoutBinding.inflate( + inflater, + parent, + attachToParent + ) + }) { binding -> + ScreenViewUpdater { rendering, viewEnvironment -> + binding.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..5c835f7311 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 @@ -1,19 +1,26 @@ package com.squareup.sample.authworkflow +import android.view.LayoutInflater +import android.view.ViewGroup import com.squareup.sample.tictactoe.databinding.LoginLayoutBinding import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewUpdater import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backPressedHandler @OptIn(WorkflowUiExperimentalApi::class) internal val LoginViewFactory: ScreenViewFactory = - ScreenViewRunner.bind(LoginLayoutBinding::inflate) { rendering, _ -> - loginErrorMessage.text = rendering.errorMessage - - loginButton.setOnClickListener { - rendering.onLogin(loginEmail.text.toString(), loginPassword.text.toString()) + ScreenViewFactory.ofViewBinding({ inflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean -> + LoginLayoutBinding.inflate( + inflater, + parent, + attachToParent + ) + }) { binding -> + ScreenViewUpdater { rendering, viewEnvironment -> + binding.loginErrorMessage.text = rendering.errorMessage + binding.loginButton.setOnClickListener { + rendering.onLogin(binding.loginEmail.text.toString(), binding.loginPassword.text.toString()) + } + binding.root.backPressedHandler = { rendering.onCancel() } } - - root.backPressedHandler = { rendering.onCancel() } } 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..1a0e974535 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 @@ -1,20 +1,27 @@ package com.squareup.sample.authworkflow +import android.view.LayoutInflater +import android.view.ViewGroup import com.squareup.sample.tictactoe.databinding.SecondFactorLayoutBinding import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewUpdater import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backPressedHandler @OptIn(WorkflowUiExperimentalApi::class) internal val SecondFactorViewFactory: ScreenViewFactory = - ScreenViewRunner.bind(SecondFactorLayoutBinding::inflate) { rendering, _ -> - root.backPressedHandler = { rendering.onCancel() } - secondFactorToolbar.setNavigationOnClickListener { rendering.onCancel() } - - secondFactorErrorMessage.text = rendering.errorMessage - - secondFactorSubmitButton.setOnClickListener { - rendering.onSubmit(secondFactor.text.toString()) + ScreenViewFactory.ofViewBinding({ inflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean -> + SecondFactorLayoutBinding.inflate( + inflater, + parent, + attachToParent + ) + }) { binding -> + ScreenViewUpdater { rendering, viewEnvironment -> + binding.root.backPressedHandler = { rendering.onCancel() } + binding.secondFactorToolbar.setNavigationOnClickListener { rendering.onCancel() } + binding.secondFactorErrorMessage.text = rendering.errorMessage + binding.secondFactorSubmitButton.setOnClickListener { + rendering.onSubmit(binding.secondFactor.text.toString()) + } } } 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/GameOverLayoutUpdater.kt similarity index 83% rename from samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GameOverLayoutRunner.kt rename to samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GameOverLayoutUpdater.kt index 2a2bf057f1..20d9ed6bd2 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/GameOverLayoutUpdater.kt @@ -1,6 +1,8 @@ package com.squareup.sample.gameworkflow +import android.view.LayoutInflater import android.view.MenuItem +import android.view.ViewGroup import androidx.appcompat.widget.Toolbar import com.squareup.sample.gameworkflow.Ending.Draw import com.squareup.sample.gameworkflow.Ending.Quitted @@ -11,16 +13,15 @@ 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.ScreenViewRunner -import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bind +import com.squareup.workflow1.ui.ScreenViewUpdater import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.backPressedHandler @OptIn(WorkflowUiExperimentalApi::class) -internal class GameOverLayoutRunner( +internal class GameOverLayoutUpdater( private val binding: GamePlayLayoutBinding -) : ScreenViewRunner { +) : ScreenViewUpdater { private val saveItem: MenuItem = binding.gamePlayToolbar.menu.add("") .apply { @@ -102,7 +103,17 @@ internal class GameOverLayoutRunner( } /** Note how easily we're sharing this layout with [GamePlayViewFactory]. */ - companion object : ScreenViewFactory by bind( - GamePlayLayoutBinding::inflate, ::GameOverLayoutRunner - ) + companion object : ScreenViewFactory by ScreenViewFactory.ofViewBinding( + { inflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean -> + GamePlayLayoutBinding.inflate( + inflater, + parent, + attachToParent + ) + }) { binding -> + ScreenViewUpdater + { rendering, viewEnvironment -> + GameOverLayoutUpdater(it) + } + } } 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 d1eace7626..1979870e9b 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 @@ -1,21 +1,28 @@ package com.squareup.sample.gameworkflow +import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.squareup.sample.tictactoe.databinding.GamePlayLayoutBinding import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewUpdater import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.backPressedHandler @OptIn(WorkflowUiExperimentalApi::class) internal val GamePlayViewFactory: ScreenViewFactory = - ScreenViewRunner.bind(GamePlayLayoutBinding::inflate) { rendering, _ -> - renderBanner(rendering.gameState, rendering.playerInfo) - rendering.gameState.board.render(gamePlayBoard.root) - - setCellClickListeners(gamePlayBoard.root, rendering.gameState, rendering.onClick) - root.backPressedHandler = rendering.onQuit + ScreenViewFactory.ofViewBinding({ inflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean -> + GamePlayLayoutBinding.inflate( + inflater, + parent, + attachToParent + ) + }) { binding -> + ScreenViewUpdater { rendering, viewEnvironment -> + binding.renderBanner(rendering.gameState, rendering.playerInfo) + rendering.gameState.board.render(binding.gamePlayBoard.root) + setCellClickListeners(binding.gamePlayBoard.root, rendering.gameState, rendering.onClick) + binding.root.backPressedHandler = rendering.onQuit + } } private fun setCellClickListeners( 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 53a4341e72..80e9b798fc 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 @@ -1,20 +1,27 @@ package com.squareup.sample.gameworkflow +import android.view.LayoutInflater +import android.view.ViewGroup import com.squareup.sample.tictactoe.databinding.NewGameLayoutBinding import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewUpdater import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.backPressedHandler @OptIn(WorkflowUiExperimentalApi::class) internal val NewGameViewFactory: ScreenViewFactory = - ScreenViewRunner.bind(NewGameLayoutBinding::inflate) { rendering, _ -> - if (playerX.text.isBlank()) playerX.setText(rendering.defaultNameX) - if (playerO.text.isBlank()) playerO.setText(rendering.defaultNameO) - - startGame.setOnClickListener { - rendering.onStartGame(playerX.text.toString(), playerO.text.toString()) + ScreenViewFactory.ofViewBinding({ inflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean -> + NewGameLayoutBinding.inflate( + inflater, + parent, + attachToParent + ) + }) { binding -> + ScreenViewUpdater { rendering, viewEnvironment -> + if (binding.playerX.text.isBlank()) binding.playerX.setText(rendering.defaultNameX) + if (binding.playerO.text.isBlank()) binding.playerO.setText(rendering.defaultNameO) + binding.startGame.setOnClickListener { + rendering.onStartGame(binding.playerX.text.toString(), binding.playerO.text.toString()) + } + binding.root.backPressedHandler = { rendering.onCancel() } } - - root.backPressedHandler = { rendering.onCancel() } } 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/TicTacToeViewBindings.kt index 5835cc3d00..8c2408a8d4 100644 --- a/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/TicTacToeViewBindings.kt +++ b/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/TicTacToeViewBindings.kt @@ -7,5 +7,5 @@ import com.squareup.workflow1.ui.ViewRegistry val TicTacToeViewFactories = ViewRegistry( NewGameViewFactory, GamePlayViewFactory, - GameOverLayoutRunner + GameOverLayoutUpdater ) 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..0d6d759c66 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 @@ -2,13 +2,14 @@ package com.squareup.sample.todo import android.content.Context.INPUT_METHOD_SERVICE +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup 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.ScreenViewRunner -import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bind +import com.squareup.workflow1.ui.ScreenViewUpdater import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.backPressedHandler @@ -25,13 +26,19 @@ data class TodoEditorScreen( ) : AndroidScreen, Compatible { override val compatibilityKey = Compatible.keyFor(this, "${session.id}") - override val viewFactory = bind(TodoEditorLayoutBinding::inflate, ::Runner) + override val viewFactory = ofViewBinding({ inflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean -> + TodoEditorLayoutBinding.inflate( + inflater, + parent, + attachToParent + ) + }) { Updater(it) } } @OptIn(WorkflowUiExperimentalApi::class) -private class Runner( +private class Updater( private val binding: TodoEditorLayoutBinding -) : ScreenViewRunner { +) : ScreenViewUpdater { private val itemListView = ItemListView.fromLinearLayout(binding.itemContainer) 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..1aad4ebc8f 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 @@ -1,13 +1,14 @@ package com.squareup.sample.todo import android.view.LayoutInflater +import android.view.ViewGroup import android.widget.TextView import com.squareup.sample.container.overviewdetail.OverviewDetailConfig import com.squareup.sample.container.overviewdetail.OverviewDetailConfig.Overview 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.ScreenViewUpdater import com.squareup.workflow1.ui.WorkflowUiExperimentalApi /** @@ -26,17 +27,25 @@ data class TodoListsScreen( val selection: Int = -1 ) : AndroidScreen { override val viewFactory: ScreenViewFactory = - bind(TodoListsLayoutBinding::inflate) { rendering, viewEnvironment -> - for ((index, list) in rendering.lists.withIndex()) { - addRow( - index, - list, - selectable = viewEnvironment[OverviewDetailConfig] == Overview, - selected = index == rendering.selection && - viewEnvironment[OverviewDetailConfig] == Overview - ) { rendering.onRowClicked(index) } + ScreenViewFactory.ofViewBinding({ inflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean -> + TodoListsLayoutBinding.inflate( + inflater, + parent, + attachToParent + ) + }) { binding -> + ScreenViewUpdater { rendering, viewEnvironment -> + for ((index, list) in rendering.lists.withIndex()) { + binding.addRow( + index, + list, + selectable = viewEnvironment[OverviewDetailConfig] == Overview, + selected = index == rendering.selection && + viewEnvironment[OverviewDetailConfig] == Overview + ) { rendering.onRowClicked(index) } + } + binding.pruneDeadRowsFrom(rendering.lists.size) } - pruneDeadRowsFrom(rendering.lists.size) } } 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 68b2541d6e..17bd1514d8 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 @@ -6,16 +6,15 @@ import android.content.Context import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import com.squareup.workflow1.ui.asScreen 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.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.asScreen import com.squareup.workflow1.ui.internal.test.AbstractLifecycleTestActivity import com.squareup.workflow1.ui.modal.HasModals import com.squareup.workflow1.ui.modal.ModalViewContainer @@ -31,11 +30,13 @@ internal class ModalViewContainerLifecycleActivity : AbstractLifecycleTestActivi override fun buildView( initialRendering: BaseRendering, initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, + context: Context, container: ViewGroup? - ): View = View(contextForNewView).apply { - bindShowRendering(initialRendering, initialViewEnvironment) { _, _ -> /* Noop */ } - } + ) = ScreenViewHolder( + initialRendering, + initialViewEnvironment, + View(context) + ) { _, _ -> /* Noop */ } } data class TestModals( @@ -55,19 +56,22 @@ internal class ModalViewContainerLifecycleActivity : AbstractLifecycleTestActivi override val viewRegistry: ViewRegistry = ViewRegistry( ModalViewContainer.binding(), BaseRendering, - leafViewBinding(LeafRendering::class, lifecycleLoggingViewObserver { it.name }), - ManualScreenViewFactory(RecurseRendering::class) { initialRendering, + leafViewBinding(lifecycleLoggingViewObserver { it.name }), + ScreenViewFactory.of { initialRendering, initialViewEnvironment, - contextForNewView, _ -> - FrameLayout(contextForNewView).also { container -> + contextForNewView, + _ -> + FrameLayout(contextForNewView).let { container -> val stub = WorkflowViewStub(contextForNewView) container.addView(stub) - container.bindShowRendering( - initialRendering, - initialViewEnvironment - ) { rendering, env -> - stub.show(asScreen(TestModals(listOf(rendering.wrapped))), env) - } + ScreenViewHolder( + initialRendering = initialRendering, + initialViewEnvironment = initialViewEnvironment, + view = container, + updater = { rendering, env -> + stub.show(asScreen(TestModals(listOf(rendering.wrapped))), env) + } + ) } }, ) diff --git a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/AlertContainer.kt b/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/AlertContainer.kt index c534825294..9008621c6b 100644 --- a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/AlertContainer.kt +++ b/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/AlertContainer.kt @@ -10,11 +10,10 @@ import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import androidx.annotation.StyleRes import androidx.appcompat.app.AlertDialog -import com.squareup.workflow1.ui.ManualScreenViewFactory import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewHolder import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.bindShowRendering import com.squareup.workflow1.ui.container.R import com.squareup.workflow1.ui.modal.AlertScreen.Button import com.squareup.workflow1.ui.modal.AlertScreen.Button.NEGATIVE @@ -83,14 +82,13 @@ public class AlertContainer @JvmOverloads constructor( private class AlertContainerViewFactory( @StyleRes private val dialogThemeResId: Int = 0 - ) : ScreenViewFactory> by ManualScreenViewFactory( - type = AlertContainerScreen::class, + ) : ScreenViewFactory> by ScreenViewFactory.of( viewConstructor = { initialRendering, initialEnv, context, _ -> AlertContainer(context, dialogThemeResId = dialogThemeResId) - .apply { - id = R.id.workflow_alert_container - layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) - bindShowRendering(initialRendering, initialEnv, ::update) + .let { view -> + view.id = R.id.workflow_alert_container + view.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) + ScreenViewHolder(initialRendering, initialEnv, view, view::update) } } ) 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 e2b9e7986c..52ed331c72 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 @@ -12,18 +12,17 @@ import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import androidx.annotation.IdRes -import com.squareup.workflow1.ui.asScreen import com.squareup.workflow1.ui.BuilderViewFactory 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.backPressedHandler import com.squareup.workflow1.ui.bindShowRendering import com.squareup.workflow1.ui.buildView 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 kotlin.reflect.KClass /** @@ -65,45 +64,47 @@ public open class ModalViewContainer @JvmOverloads constructor( initialModalRendering: Any, initialViewEnvironment: ViewEnvironment ): DialogRef { - val view = asScreen(initialModalRendering).buildView( - viewEnvironment = initialViewEnvironment, - contextForNewView = this.context, - container = this - ) - .apply { - start() - // If the modal's root 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 view = asScreen(initialModalRendering) + .buildView( + viewEnvironment = initialViewEnvironment, + context = this.context, + container = this + ) + .let { holder -> + holder.start() + holder.view + } + // If the modal's root 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 buildDialogForView(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 - // when back is pressed, so long as it doesn't look past this modal window for handlers. - // - // Here, we handle the ACTION_UP portion of a KEYCODE_BACK key event, and below - // we make sure that the root view has a backPressedHandler that will consume the - // onBackPressed call if no child of the root modal view does. + .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 + // when back is pressed, so long as it doesn't look past this modal window for handlers. + // + // Here, we handle the ACTION_UP portion of a KEYCODE_BACK key event, and below + // we make sure that the root view has a backPressedHandler that will consume the + // onBackPressed call if no child of the root modal view does. - setOnKeyListener { _, keyCode, keyEvent -> - if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.action == ACTION_UP) { - view.context.onBackPressedDispatcherOwnerOrNull() - ?.onBackPressedDispatcher - ?.let { - if (it.hasEnabledCallbacks()) it.onBackPressed() - } - true - } else { - false - } + setOnKeyListener { _, keyCode, keyEvent -> + if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.action == ACTION_UP) { + view.context.onBackPressedDispatcherOwnerOrNull() + ?.onBackPressedDispatcher + ?.let { + if (it.hasEnabledCallbacks()) it.onBackPressed() + } + true + } else { + false } } - .run { - DialogRef(initialModalRendering, initialViewEnvironment, this, view) - } + } + .run { + DialogRef(initialModalRendering, initialViewEnvironment, this, view) + } } override fun updateDialog(dialogRef: DialogRef) { @@ -118,14 +119,14 @@ public open class ModalViewContainer @JvmOverloads constructor( type: KClass ) : com.squareup.workflow1.ui.ViewFactory by BuilderViewFactory( - type = type, - viewConstructor = { initialRendering, initialEnv, context, _ -> - ModalViewContainer(context).apply { - this.id = id - layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) - bindShowRendering(initialRendering, initialEnv, ::update) - } + type = type, + viewConstructor = { initialRendering, initialEnv, context, _ -> + ModalViewContainer(context).apply { + this.id = id + layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) + bindShowRendering(initialRendering, initialEnv, ::update) } + } ) public companion object { diff --git a/workflow-ui/core-android/api/core-android.api b/workflow-ui/core-android/api/core-android.api index 456cd95b6d..6311ec4e9d 100644 --- a/workflow-ui/core-android/api/core-android.api +++ b/workflow-ui/core-android/api/core-android.api @@ -100,12 +100,12 @@ public final class com/squareup/workflow1/ui/ScreenViewFactoryKt { 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 abstract interface class com/squareup/workflow1/ui/ScreenViewRunner { - public static final field Companion Lcom/squareup/workflow1/ui/ScreenViewRunner$Companion; +public abstract interface class com/squareup/workflow1/ui/ScreenViewUpdater { + public static final field Companion Lcom/squareup/workflow1/ui/ScreenViewUpdater$Companion; public abstract fun showRendering (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V } -public final class com/squareup/workflow1/ui/ScreenViewRunner$Companion { +public final class com/squareup/workflow1/ui/ScreenViewUpdater$Companion { } public final class com/squareup/workflow1/ui/SnapshotParcelsKt { 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/ScreenViewHolderTest.kt similarity index 50% rename from workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeScreenViewFactoryTest.kt rename to workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/ScreenViewHolderTest.kt index b14c273762..c98fced2d3 100644 --- a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeScreenViewFactoryTest.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/ScreenViewHolderTest.kt @@ -2,48 +2,39 @@ 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 { +internal class ScreenViewHolderTest { 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 innerViewFactory = + ScreenViewFactory.of { initialRendering, initialViewEnvironment, context, _ -> + ScreenViewHolder(initialRendering, initialViewEnvironment, InnerView(context)) { s, _ -> + events += "inner showRendering $s" } } - } val envString = object : ViewEnvironmentKey(String::class) { override val default: String get() = "Not set" } - val outerViewFactory = DecorativeScreenViewFactory( - type = OuterRendering::class, - map = { 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 outerViewFactory = ScreenViewFactory + .of { initialRendering, initialViewEnvironment, context, container -> + initialRendering.wrapped.buildView(initialViewEnvironment, context, container) + .acceptRenderings(initialRendering) { initialRendering.wrapped } + .withStarter { viewHolder, doStart -> + events += "viewStarter ${viewHolder.screen} ${viewHolder.environment[envString]}" + doStart() + events += "exit viewStarter" + } } - ) + val viewRegistry = ViewRegistry(innerViewFactory, outerViewFactory) val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) @@ -63,27 +54,23 @@ internal class DecorativeScreenViewFactoryTest { @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 innerViewFactory = + ScreenViewFactory.of { initialRendering, initialViewEnvironment, context, _ -> + ScreenViewHolder(initialRendering, initialViewEnvironment, InnerView(context)) { s, _ -> + events += "inner showRendering $s" } } - } - val outerViewFactory = DecorativeScreenViewFactory( - type = OuterRendering::class, - map = { outer -> outer.wrapped }, - doShowRendering = { _, innerShowRendering, outerRendering, env -> - events += "doShowRendering $outerRendering" - innerShowRendering(outerRendering.wrapped, env) + + val outerViewFactory = ScreenViewFactory + .of { initialRendering, initialViewEnvironment, context, container -> + initialRendering.wrapped.buildView(initialViewEnvironment, context, container) + .acceptRenderings(initialRendering) { it.wrapped } + .withShowScreen { outerRendering, viewEnvironment -> + events += "doShowRendering $outerRendering" + showScreen(outerRendering, viewEnvironment) + } } - ) + val viewRegistry = ViewRegistry(innerViewFactory, outerViewFactory) val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) @@ -102,50 +89,46 @@ internal class DecorativeScreenViewFactoryTest { @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 innerViewFactory = ScreenViewFactory + .of { initialRendering, initialViewEnvironment, context, _ -> + ScreenViewHolder(initialRendering, initialViewEnvironment, InnerView(context)) { s, _ -> + events += "inner showRendering $s" } } - } val envString = object : ViewEnvironmentKey(String::class) { override val default: String get() = "Not set" } - val outerViewFactory = DecorativeScreenViewFactory( - type = OuterRendering::class, - map = { 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 outerViewFactory = + ScreenViewFactory.of { initialRendering, initialViewEnvironment, context, container -> + initialRendering.wrapped.buildView(initialViewEnvironment, context, container) + .acceptRenderings(initialRendering) { it.wrapped } + .updateEnvironment { env -> + env + (envString to "Outer Updated environment" + + " SHOULD NOT SEE THIS! It will be clobbered by WayOutRendering") + } + .withStarter { viewHolder, doStart -> + events += "outer viewStarter ${viewHolder.screen} ${viewHolder.environment[envString]}" + doStart() + events += "exit outer viewStarter" + } } - ) - val wayOutViewFactory = DecorativeScreenViewFactory( - type = WayOutRendering::class, - map = { 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 wayOutViewFactory = ScreenViewFactory + .of { initialRendering, initialViewEnvironment, context, container -> + initialRendering.wrapped.buildView(initialViewEnvironment, context, container) + .acceptRenderings(initialRendering) { it.wrapped } + .updateEnvironment { env -> + env + (envString to "Way Out Updated environment triumphs over all") + } + .withStarter { viewHolder, doStart -> + events += "way out viewStarter ${viewHolder.screen} ${viewHolder.environment[envString]}" + doStart() + events += "exit way out viewStarter" + } } - ) + val viewRegistry = ViewRegistry(innerViewFactory, outerViewFactory, wayOutViewFactory) val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) @@ -178,37 +161,33 @@ internal class DecorativeScreenViewFactoryTest { @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 innerViewFactory = + ScreenViewFactory.of { initialRendering, initialViewEnvironment, context, _ -> + ScreenViewHolder(initialRendering, initialViewEnvironment, InnerView(context)) { s, _ -> + events += "inner showRendering $s" } } - } - val outerViewFactory = DecorativeScreenViewFactory( - type = OuterRendering::class, - map = { outer -> outer.wrapped }, - doShowRendering = { _, innerShowRendering, outerRendering, env -> - events += "doShowRendering $outerRendering" - innerShowRendering(outerRendering.wrapped, env) + + val outerViewFactory = ScreenViewFactory + .of { initialRendering, initialViewEnvironment, context, container -> + initialRendering.wrapped.buildView(initialViewEnvironment, context, container) + .acceptRenderings(initialRendering) { it.wrapped } + .withShowScreen { outerRendering, viewEnvironment -> + events += "doShowRendering $outerRendering" + showScreen(outerRendering, viewEnvironment) + } } - ) + val viewRegistry = ViewRegistry(innerViewFactory, outerViewFactory) val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) - val view = OuterRendering("out1", InnerRendering("in1")).buildView( + val viewHolder = OuterRendering("out1", InnerRendering("in1")).buildView( viewEnvironment, instrumentation.context ).apply { start() } events.clear() - view.showRendering(OuterRendering("out2", InnerRendering("in2")), viewEnvironment) + viewHolder.showScreen(OuterRendering("out2", InnerRendering("in2")), viewEnvironment) assertThat(events).containsExactly( "doShowRendering OuterRendering(outerData=out2, wrapped=InnerRendering(innerData=in2))", 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..75d21e2689 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 @@ -19,16 +19,17 @@ internal class WorkflowViewStubLifecycleActivity : AbstractLifecycleTestActivity } override val viewRegistry: ViewRegistry = ViewRegistry( - leafViewBinding(LeafRendering::class, lifecycleLoggingViewObserver { it.name }), - ManualScreenViewFactory(RecurseRendering::class) { initialRendering, + leafViewBinding(lifecycleLoggingViewObserver { it.name }), + ScreenViewFactory.of { initialRendering, initialViewEnvironment, contextForNewView, _ -> - FrameLayout(contextForNewView).also { container -> + FrameLayout(contextForNewView).let { container -> val stub = WorkflowViewStub(contextForNewView) container.addView(stub) - container.bindShowRendering( + ScreenViewHolder( initialRendering, - initialViewEnvironment + initialViewEnvironment, + container ) { rendering, env -> stub.show(rendering.wrapped, env) } 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 e480065127..c9a650b991 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 @@ -255,20 +255,19 @@ internal class WorkflowViewStubLifecycleTest { } data class RegistrySetter(val wrapped: TestRendering) : ViewRendering() { - override val viewFactory: ScreenViewFactory = ManualScreenViewFactory( - RegistrySetter::class - ) { initialRendering, initialViewEnvironment, context, _ -> - val stub = WorkflowViewStub(context) - ViewTreeSavedStateRegistryOwner.set(stub, expectedRegistryOwner) + override val viewFactory = + ScreenViewFactory.of { initialRendering, initialViewEnvironment, context, _ -> + val stub = WorkflowViewStub(context) + ViewTreeSavedStateRegistryOwner.set(stub, expectedRegistryOwner) - FrameLayout(context).apply { - addView(stub) + FrameLayout(context).let { view -> + view.addView(stub) - bindShowRendering(initialRendering, initialViewEnvironment) { r, e -> - stub.show(r.wrapped, e) + ScreenViewHolder(initialRendering, initialViewEnvironment, view) { r, e -> + stub.show(r.wrapped, e) + } } } - } } var initialRegistryOwner: SavedStateRegistryOwner? = null @@ -298,61 +297,58 @@ internal class WorkflowViewStubLifecycleTest { const val Tag = "counter" } - override val viewFactory: ScreenViewFactory = ManualScreenViewFactory( - CounterRendering::class - ) { initialRendering, initialViewEnvironment, context, _ -> - var counter = 0 - Button(context).apply button@{ - tag = Tag + override val viewFactory = + ScreenViewFactory.of { initialRendering, initialViewEnvironment, context, _ -> + var counter = 0 + Button(context).apply button@{ + tag = Tag - fun updateText() { - text = "Counter: $counter" - } + fun updateText() { + text = "Counter: $counter" + } - addOnAttachStateChangeListener(object : OnAttachStateChangeListener { - lateinit var registryOwner: SavedStateRegistryOwner - lateinit var lifecycleObserver: LifecycleObserver - - override fun onViewAttachedToWindow(v: View) { - onViewAttached(this@button) - registryOwner = ViewTreeSavedStateRegistryOwner.get(this@button)!! - lifecycleObserver = object : LifecycleEventObserver { - override fun onStateChanged( - source: LifecycleOwner, - event: Event - ) { - if (event == ON_CREATE) { - source.lifecycle.removeObserver(this) - registryOwner.savedStateRegistry.consumeRestoredStateForKey("counter") - ?.let { restoredState -> - counter = restoredState.getInt("value") - updateText() - } + addOnAttachStateChangeListener(object : OnAttachStateChangeListener { + lateinit var registryOwner: SavedStateRegistryOwner + lateinit var lifecycleObserver: LifecycleObserver + + override fun onViewAttachedToWindow(v: View) { + onViewAttached(this@button) + registryOwner = ViewTreeSavedStateRegistryOwner.get(this@button)!! + lifecycleObserver = object : LifecycleEventObserver { + override fun onStateChanged( + source: LifecycleOwner, + event: Event + ) { + if (event == ON_CREATE) { + source.lifecycle.removeObserver(this) + registryOwner.savedStateRegistry.consumeRestoredStateForKey("counter") + ?.let { restoredState -> + counter = restoredState.getInt("value") + updateText() + } + } } } + registryOwner.lifecycle.addObserver(lifecycleObserver) + registryOwner.savedStateRegistry.registerSavedStateProvider("counter") { + Bundle().apply { putInt("value", counter) } + } } - registryOwner.lifecycle.addObserver(lifecycleObserver) - registryOwner.savedStateRegistry.registerSavedStateProvider("counter") { - Bundle().apply { putInt("value", counter) } - } - } - override fun onViewDetachedFromWindow(v: View) { - registryOwner.lifecycle.removeObserver(lifecycleObserver) - registryOwner.savedStateRegistry.unregisterSavedStateProvider("counter") - } - }) + override fun onViewDetachedFromWindow(v: View) { + registryOwner.lifecycle.removeObserver(lifecycleObserver) + registryOwner.savedStateRegistry.unregisterSavedStateProvider("counter") + } + }) - updateText() - setOnClickListener { - counter++ updateText() - } - - bindShowRendering(initialRendering, initialViewEnvironment) { _, _ -> - // Noop + setOnClickListener { + counter++ + updateText() + } + }.let { + ScreenViewHolder(initialRendering, initialViewEnvironment, it) { _, _ -> /* no-op*/ } } } - } } } 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 524e138c87..e350d47059 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 @@ -8,18 +8,17 @@ import androidx.test.core.app.ActivityScenario 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.ManualScreenViewFactory import com.squareup.workflow1.ui.Compatible import com.squareup.workflow1.ui.Screen -import com.squareup.workflow1.ui.ViewEnvironment 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.container.BackStackScreen import com.squareup.workflow1.ui.container.fixtures.BackStackContainerLifecycleActivity.TestRendering.LeafRendering import com.squareup.workflow1.ui.container.fixtures.BackStackContainerLifecycleActivity.TestRendering.RecurseRendering -import com.squareup.workflow1.ui.bindShowRendering -import com.squareup.workflow1.ui.container.BackStackScreen import com.squareup.workflow1.ui.internal.test.AbstractLifecycleTestActivity import com.squareup.workflow1.ui.internal.test.inAnyView import org.hamcrest.Matcher @@ -37,10 +36,13 @@ internal class BackStackContainerLifecycleActivity : AbstractLifecycleTestActivi override fun buildView( initialRendering: BaseRendering, initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, + context: Context, container: ViewGroup? - ): View = View(contextForNewView).apply { - bindShowRendering(initialRendering, initialViewEnvironment) { _, _ -> /* Noop */ } + ) = ScreenViewHolder( + initialRendering, + initialViewEnvironment, + View(context) + ) { _, _ -> /* Noop */ } } @@ -108,16 +110,17 @@ internal class BackStackContainerLifecycleActivity : AbstractLifecycleTestActivi override val viewRegistry: ViewRegistry = ViewRegistry( NoTransitionBackStackContainer, BaseRendering, - leafViewBinding(LeafRendering::class, viewObserver, viewConstructor = ::ViewStateTestView), - ManualScreenViewFactory(RecurseRendering::class) { initialRendering, + leafViewBinding(viewObserver, viewConstructor = ::ViewStateTestView), + ScreenViewFactory.of { initialRendering, initialViewEnvironment, contextForNewView, _ -> - FrameLayout(contextForNewView).also { container -> + FrameLayout(contextForNewView).let { container -> val stub = WorkflowViewStub(contextForNewView) container.addView(stub) - container.bindShowRendering( + ScreenViewHolder( initialRendering, - initialViewEnvironment + initialViewEnvironment, + container ) { rendering, env -> stub.show(rendering.wrappedBackstack.toBackstackWithBase(), env) } @@ -153,6 +156,6 @@ internal fun ActivityScenario.viewForScreen @OptIn(WorkflowUiExperimentalApi::class) internal fun waitForScreen(name: String) { - inAnyView(withTagValue(equalTo(name)) as Matcher) + inAnyView(withTagValue(equalTo(name)) as Matcher) .check(matches(isCompletelyDisplayed())) } 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 026848fd26..4d6e364ea2 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 @@ -3,13 +3,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.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 -import com.squareup.workflow1.ui.R /** * A subclass of [BackStackContainer] that disables transitions to make it simpler to test the @@ -24,14 +23,13 @@ internal class NoTransitionBackStackContainer(context: Context) : BackStackConta } companion object : ScreenViewFactory> - by ManualScreenViewFactory( - type = BackStackScreen::class, + by ScreenViewFactory.of( viewConstructor = { initialRendering, initialEnv, context, _ -> NoTransitionBackStackContainer(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(initialRendering, initialEnv, view, view::update) } } ) 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..7b44ef48db 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 @@ -5,8 +5,8 @@ package com.squareup.workflow1.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 + * use [ScreenViewUpdater.ofViewBinding] to work with XML layout resources, or + * [BuilderViewFactory] to create views from code. See [ScreenViewUpdater] for more * details. * * @OptIn(WorkflowUiExperimentalApi::class) @@ -15,7 +15,7 @@ package com.squareup.workflow1.ui * val onClick: () -> Unit * ) : AndroidScreen { * override val viewFactory = - * ScreenViewRunner.bind(HelloGoodbyeLayoutBinding::inflate) { screen, _ -> + * ScreenViewUpdater.bind(HelloGoodbyeLayoutBinding::inflate) { screen, _ -> * helloMessage.text = screen.message * helloMessage.setOnClickListener { screen.onClick() } * } 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 52b765db0a..34d2544c3b 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewRegistry.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewRegistry.kt @@ -26,8 +26,19 @@ public fun ) } -@Deprecated("Use getEntryFor()") @WorkflowUiExperimentalApi +@Deprecated("Use ScreenViewHolder.Starter") +public fun interface ViewStarter { + /** Called from [View.start]. [doStart] must be invoked. */ + public fun startView( + view: View, + doStart: () -> Unit + ) +} + +@Suppress("DeprecatedCallableAddReplaceWith") +@WorkflowUiExperimentalApi +@Deprecated("Use getEntryFor()") public fun ViewRegistry.getFactoryFor( renderingType: KClass ): ViewFactory? { 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..36d08ca1ee 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 @@ -3,8 +3,7 @@ package com.squareup.workflow1.ui @WorkflowUiExperimentalApi @Suppress("DEPRECATION") internal object AsScreenViewFactory : ScreenViewFactory> -by ManualScreenViewFactory( - type = AsScreen::class, +by ScreenViewFactory.of( viewConstructor = { initialRendering, initialViewEnvironment, context, container -> initialViewEnvironment[ViewRegistry] .buildView( @@ -12,12 +11,13 @@ by ManualScreenViewFactory( initialViewEnvironment, context, container - ).also { view -> + ).let { view -> val legacyShowRendering = view.getShowRendering()!! - view.bindShowRendering( + ScreenViewHolder( initialRendering, - initialViewEnvironment + initialViewEnvironment, + view ) { rendering, env -> legacyShowRendering(rendering.rendering, env) } } } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/BaseScreenViewHolder.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/BaseScreenViewHolder.kt new file mode 100644 index 0000000000..51bcb7645b --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/BaseScreenViewHolder.kt @@ -0,0 +1,29 @@ +package com.squareup.workflow1.ui + +import android.view.View + +@WorkflowUiExperimentalApi +internal class BaseScreenViewHolder( + initialRendering: ScreenT, + initialViewEnvironment: ViewEnvironment, + override val view: View, + private val updater: ScreenViewUpdater +) : ScreenViewHolder { + private var currentRendering: ScreenT = initialRendering + private var currentEnvironment: ViewEnvironment = initialViewEnvironment + + override val screen: ScreenT + get() = currentRendering + override val environment: ViewEnvironment + get() = currentEnvironment + + override fun start() { + showScreen(currentRendering, currentEnvironment) + } + + override fun showScreen(screen: ScreenT, environment: ViewEnvironment) { + currentRendering = screen + currentEnvironment = environment + updater.showRendering(screen, environment) + } +} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/BetterScreenViewHolder.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/BetterScreenViewHolder.kt new file mode 100644 index 0000000000..a75514e9ba --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/BetterScreenViewHolder.kt @@ -0,0 +1,46 @@ +package com.squareup.workflow1.ui + +import android.view.View + +@WorkflowUiExperimentalApi +public class BetterScreenViewHolder private constructor( + public val view: View, + private val updater: ScreenViewUpdater, + internal val screenGetter: () -> ScreenT, + internal val envGetter: () -> ViewEnvironment, + internal val startWrapper: BetterScreenViewHolder.(() -> Unit) -> Unit, +) { + public constructor( + view: View, + updater: ScreenViewUpdater, + screen: ScreenT, + env: ViewEnvironment + ) : this( + view = view, + updater = updater, + screenGetter = { screen }, + envGetter = { env }, + startWrapper = { doStart -> doStart() } + ) + + public val screen: ScreenT get() = screenGetter() + public val environment: ViewEnvironment get() = envGetter() + + public fun start() { + startWrapper.invoke(this) { updater.showRendering(screen, environment) } + } + + public fun canShowScreen(screen: Screen): Boolean = compatible(this.screen, screen) + + public fun withStarter( + startWrapper: BetterScreenViewHolder.(() -> Unit) -> Unit + ): BetterScreenViewHolder { + return BetterScreenViewHolder(view, updater, screenGetter, envGetter) { doStart -> + startWrapper(doStart) + } + } + + public fun withShowScreen( + + ) +} 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..74327aff5d 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.of") @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 bc730bd23d..0000000000 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeScreenViewFactory.kt +++ /dev/null @@ -1,181 +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 [OuterT] that delegates view construction responsibilities - * to the factory registered for [InnerT]. Makes it convenient for [OuterT] to wrap - * instances of [InnerT] 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 mapping function: - * - * class OriginalRendering(val data: String) : AndroidScreen { - * ... - * } - * class AliasRendering(val similarData: String) - * - * object DecorativeScreenViewFactory : ScreenViewFactory - * by DecorativeScreenViewFactory( - * type = AliasRendering::class, map = { alias -> - * OriginalRendering(alias.similarData) - * } - * ) - * - * To make a wrapper that adds information to the [ViewEnvironment]: - * - * class NeutronFlowPolarity(val reversed: Boolean) : Screen { - * 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, - * map = { 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, - * map = { 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, - * map = { outer -> outer.wrapped }, - * doShowRendering = { view, innerShowRendering, outerRendering, viewEnvironment -> - * if (!outerRendering.override) { - * // Place our handler before invoking innerShowRendering, so that - * // its later calls to view.backPressedHandler will take precedence - * // over ours. - * view.backPressedHandler = outerRendering.onBackPressed - * } - * - * innerShowRendering.invoke(outerRendering.wrapped, viewEnvironment) - * - * if (outerRendering.override) { - * // Place our handler after invoking innerShowRendering, so that ours wins. - * view.backPressedHandler = outerRendering.onBackPressed - * } - * } - * ) - * - * @param map called to convert instances of [OuterT] to [InnerT], 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 - * [InnerT], allowing pre- and post-processing. Default implementation simply - * uses [map] to extract the [InnerT] instance from [OuterT] and makes the function call. - */ -@WorkflowUiExperimentalApi -public class DecorativeScreenViewFactory( - override val type: KClass, - private val map: (OuterT, ViewEnvironment) -> Pair, - private val viewStarter: ViewStarter? = null, - private val doShowRendering: ( - view: View, - innerShowRendering: ViewShowRendering, - outerRendering: OuterT, - env: ViewEnvironment - ) -> Unit = { _, innerShowRendering, outerRendering, viewEnvironment -> - val (innerRendering, processedEnv) = map(outerRendering, viewEnvironment) - innerShowRendering(innerRendering, processedEnv) - } -) : ScreenViewFactory { - - /** - * Convenience constructor for cases requiring no changes to the [ViewEnvironment]. - */ - public constructor( - type: KClass, - map: (OuterT) -> InnerT, - viewStarter: ViewStarter? = null, - doShowRendering: ( - view: View, - innerShowRendering: ViewShowRendering, - outerRendering: OuterT, - env: ViewEnvironment - ) -> Unit = { _, innerShowRendering, outerRendering, viewEnvironment -> - innerShowRendering(map(outerRendering), viewEnvironment) - } - ) : this( - type, - map = { outer, viewEnvironment -> Pair(map(outer), viewEnvironment) }, - viewStarter = viewStarter, - doShowRendering = doShowRendering - ) - - override fun buildView( - initialRendering: OuterT, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, - container: ViewGroup? - ): View { - val (innerInitialRendering, processedInitialEnv) = map(initialRendering, initialViewEnvironment) - - return innerInitialRendering.buildView( - processedInitialEnv, - contextForNewView, - container, - viewStarter - ) - .also { view -> - val innerShowRendering: ViewShowRendering = view.getShowRendering()!! - - view.bindShowRendering( - initialRendering, - processedInitialEnv - ) { rendering, env -> doShowRendering(view, innerShowRendering, 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..ef8b40d3c5 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 @@ -5,8 +5,111 @@ import android.view.View import android.view.ViewGroup import kotlin.reflect.KClass +/** + * A [ViewFactory] for [OuterT] that delegates view construction responsibilities + * to the factory registered for [InnerT]. Makes it convenient for [OuterT] to wrap + * instances of [InnerT] 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 example below illustrates this. + * + * ## Examples + * + * To make one rendering type an "alias" for another -- that is, to use the same [ViewFactory] + * to display it -- provide nothing but a single-arg mapping function: + * + * class OriginalRendering(val data: String) : AndroidViewRendering {...} + * class AliasRendering(val similarData: String) + * + * object DecorativeViewFactory : ViewFactory + * by DecorativeViewFactory( + * type = AliasRendering::class, map = { alias -> OriginalRendering(alias.similarData) } + * ) + * + * To make a decorator type 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 + * ) : Compatible { + * override val compatibilityKey: String = Compatible.keyFor(wrapped) + * } + * + * object NeutronFlowPolarityViewFactory : ViewFactory> + * by DecorativeViewFactory( + * type = NeutronFlowPolarityOverride::class, + * map = { override, env -> + * Pair(override.wrapped, env + (NeutronFlowPolarity to override.polarity)) + * } + * ) + * + * To make a decorator type that customizes [View] initialization: + * + * class WithTutorialTips(val wrapped: W) : Compatible { + * override val compatibilityKey: String = Compatible.keyFor(wrapped) + * } + * + * object WithTutorialTipsViewFactory : ViewFactory> + * by DecorativeViewFactory( + * type = WithTutorialTips::class, + * map = { withTips -> withTips.wrapped }, + * viewStarter = { view, doStart -> + * TutorialTipRunner.run(view) + * doStart() + * } + * ) + * + * To make a decorator type that adds pre- or post-processing to [View] updates: + * + * class BackButtonScreen( + * val wrapped: W, + * val override: Boolean = false, + * val onBackPressed: (() -> Unit)? = null + * ) : Compatible { + * override val compatibilityKey: String = Compatible.keyFor(wrapped) + * } + * + * object BackButtonViewFactory : ViewFactory> + * by DecorativeViewFactory( + * type = BackButtonScreen::class, + * map = { outer -> outer.wrapped }, + * doShowRendering = { view, innerShowRendering, outerRendering, viewEnvironment -> + * if (!outerRendering.override) { + * // Place our handler before invoking innerShowRendering, so that + * // its later calls to view.backPressedHandler will take precedence + * // over ours. + * view.backPressedHandler = outerRendering.onBackPressed + * } + * + * innerShowRendering.invoke(outerRendering.wrapped, viewEnvironment) + * + * if (outerRendering.override) { + * // Place our handler after invoking innerShowRendering, so that ours wins. + * view.backPressedHandler = outerRendering.onBackPressed + * } + * }) + * + * @param map called to convert instances of [OuterT] to [InnerT], 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 + * [InnerT], allowing pre- and post-processing. Default implementation simply + * uses [map] to extract the [InnerT] instance from [OuterT] and makes the function call. + */ @Suppress("DEPRECATION") -@Deprecated("Use DecorativeScreenViewFactory") +@Deprecated("Use ScreenViewFactory.of and the extension methods on ScreenViewHolder") @WorkflowUiExperimentalApi public class DecorativeViewFactory( override val type: KClass, diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunner.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunner.kt index 0300b09b43..e27c1db201 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunner.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunner.kt @@ -5,7 +5,7 @@ import androidx.annotation.LayoutRes import androidx.viewbinding.ViewBinding @Suppress("DEPRECATION") -@Deprecated("Use ScreenViewRunner") +@Deprecated("Use ScreenViewUpdater") @WorkflowUiExperimentalApi public fun interface LayoutRunner { public fun showRendering( 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..53907c3dca 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 @@ -6,31 +6,27 @@ import android.view.ViewGroup import androidx.annotation.LayoutRes import kotlin.reflect.KClass -/** - * A [ScreenViewFactory] that ties a [layout resource][layoutId] to a - * [ViewRunner factory][runnerConstructor] function. See [ScreenViewRunner] for - * details. - */ @WorkflowUiExperimentalApi @PublishedApi -internal class LayoutScreenViewFactory( - override val type: KClass, +internal class LayoutScreenViewFactory( + override val type: KClass, @LayoutRes private val layoutId: Int, - private val runnerConstructor: (View) -> ScreenViewRunner -) : ScreenViewFactory { + private val updaterConstructor: (View) -> ScreenViewUpdater +) : ScreenViewFactory { override fun buildView( - initialRendering: RenderingT, + initialRendering: ScreenT, initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, + context: Context, container: ViewGroup? - ): View { - return contextForNewView.viewBindingLayoutInflater(container) - .inflate(layoutId, container, false) - .also { view -> - val runner = runnerConstructor(view) - view.bindShowRendering(initialRendering, initialViewEnvironment) { rendering, environment -> - runner.showRendering(rendering, environment) - } - } + ): ScreenViewHolder { + val view = + context.viewBindingLayoutInflater(container).inflate(layoutId, container, false) + + return BaseScreenViewHolder( + initialRendering = initialRendering, + initialViewEnvironment = initialViewEnvironment, + view = view, + updater = updaterConstructor(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 index 97147b970e..1390fddbe9 100644 --- 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 @@ -1,43 +1,25 @@ 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, +@PublishedApi +internal class ManualScreenViewFactory( + override val type: KClass, private val viewConstructor: ( - initialRendering: RenderingT, + initialRendering: ScreenT, initialViewEnvironment: ViewEnvironment, contextForNewView: Context, container: ViewGroup? - ) -> View -) : ScreenViewFactory { + ) -> ScreenViewHolder +) : ScreenViewFactory { override fun buildView( - initialRendering: RenderingT, + initialRendering: ScreenT, initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, + context: Context, container: ViewGroup? - ): View = viewConstructor(initialRendering, initialViewEnvironment, contextForNewView, container) + ): ScreenViewHolder = + viewConstructor(initialRendering, initialViewEnvironment, context, 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..4222b04a93 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/NamedScreenViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/NamedScreenViewFactory.kt @@ -5,5 +5,8 @@ package com.squareup.workflow1.ui * to the factory for [NamedScreen.wrapped]. */ @WorkflowUiExperimentalApi -internal object NamedScreenViewFactory : ScreenViewFactory> -by DecorativeScreenViewFactory(NamedScreen::class, { named -> named.wrapped }) +internal val NamedScreenViewFactory: ScreenViewFactory> = + ScreenViewFactory.of { initialRendering, initialViewEnvironment, context, container -> + initialRendering.wrapped.buildView(initialViewEnvironment, context, container) + .acceptRenderings(initialRendering) { named -> named.wrapped } + } 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 a674dfbbf6..827eba1402 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 @@ -3,6 +3,8 @@ package com.squareup.workflow1.ui import android.content.Context import android.view.View import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.viewbinding.ViewBinding import com.squareup.workflow1.ui.container.BackStackScreen import com.squareup.workflow1.ui.container.BackStackScreenViewFactory import com.squareup.workflow1.ui.container.BodyAndModalsContainer @@ -11,46 +13,125 @@ import com.squareup.workflow1.ui.container.EnvironmentScreen import com.squareup.workflow1.ui.container.EnvironmentScreenViewFactory /** - * Factory for [View] instances that can show renderings of type [RenderingT] : [Screen]. - * - * Two concrete [ScreenViewFactory] implementations are provided: - * - * - The various [bind][ScreenViewRunner.bind] methods on [ScreenViewRunner] allow easy use of - * Android XML layout resources and [AndroidX ViewBinding][androidx.viewbinding.ViewBinding]. - * - * - [ManualScreenViewFactory] allows views to be built from code. + * Factory for Android [View] instances that can show renderings of type [ScreenT] : [Screen]. * * 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]. + * + * - Use [ScreenViewFactory.ofViewBinding] to create a factory for an + * [AndroidX ViewBinding][ViewBinding] + * - Use [ScreenViewFactory.ofLayout] or [ScreenViewFactory.ofStaticLayout] to create + * a factory for an XML layout resource + * - Use [ScreenViewFactory.of] to create a factory entirely at runtime. */ @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]. + * Returns a [ScreenViewHolder] ready to display [initialRendering] (and any succeeding values). + * Callers of this method must call [ScreenViewHolder.start] exactly once before + * calling [ScreenViewHolder.showScreen]. */ public fun buildView( - initialRendering: RenderingT, + initialRendering: ScreenT, initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, + 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 = + * ScreenViewUpdater.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 [ScreenViewUpdater] and create a binding using the `bind` variant + * that accepts a `(ViewBinding) -> ScreenViewUpdater` function, below. + */ + public inline fun ofViewBinding( + noinline bindingInflater: ViewBindingInflater, + crossinline showRendering: BindingT.(ScreenT, ViewEnvironment) -> Unit + ): ScreenViewFactory = ofViewBinding(bindingInflater) { binding -> + ScreenViewUpdater { rendering, viewEnvironment -> + binding.showRendering(rendering, viewEnvironment) + } + } + + /** + * Creates a [ScreenViewFactory] that [inflates][bindingInflater] a [ViewBinding] ([BindingT]) + * to show renderings of type [ScreenT] : [Screen], using a [ScreenViewUpdater] + * created by [constructor]. Handy if you need to perform some set up before + * [showRendering] is called. + * + * class HelloScreenRunner( + * private val binding: HelloGoodbyeViewBinding + * ) : ScreenViewUpdater { + * + * 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 ofViewBinding( + noinline bindingInflater: ViewBindingInflater, + noinline constructor: (BindingT) -> ScreenViewUpdater + ): ScreenViewFactory = + ViewBindingScreenViewFactory(ScreenT::class, bindingInflater, constructor) + + /** + * Creates a [ScreenViewFactory] that inflates [layoutId] to show renderings of + * type [ScreenT] : [Screen], using a [ScreenViewUpdater] created by [constructor]. + * Avoids any use of [AndroidX ViewBinding][ViewBinding]. + */ + public inline fun ofLayout( + @LayoutRes layoutId: Int, + noinline constructor: (View) -> ScreenViewUpdater + ): ScreenViewFactory = + LayoutScreenViewFactory(ScreenT::class, layoutId, constructor) + + /** + * Creates a [ScreenViewFactory] that inflates [layoutId] to "show" renderings of type [ScreenT], + * with a no-op [ScreenViewUpdater]. Handy for showing static views, e.g. when prototyping. + */ + @Suppress("unused") + public inline fun ofStaticLayout( + @LayoutRes layoutId: Int + ): ScreenViewFactory = ofLayout(layoutId) { ScreenViewUpdater { _, _ -> } } + + /** Creates a [ScreenViewFactory] entirely from code. */ + public inline fun of( + noinline viewConstructor: ( + initialRendering: ScreenT, + initialViewEnvironment: ViewEnvironment, + context: Context, + container: ViewGroup? + ) -> ScreenViewHolder + ): ScreenViewFactory = ManualScreenViewFactory(ScreenT::class, viewConstructor) + } } /** - * It is usually more convenient to use [WorkflowViewStub] or [DecorativeScreenViewFactory] - * than to call this method directly. + * It is usually more convenient to use [WorkflowViewStub] than to call this method directly. * * Finds a [ScreenViewFactory] to create a [View] to display the receiving [Screen]. * The caller is responsible for calling [View.start] on the new [View]. After that, * [View.showRendering] can be used to update it with new renderings that * are [compatible] with this [Screen]. [WorkflowViewStub] takes care of this chore itself. * - * @param viewStarter An optional wrapper for the function invoked when [View.start] - * is called, allowing for last second initialization of a newly built [View]. - * See [ViewStarter] for details. - * * @throws IllegalArgumentException if no builder can be found for type [ScreenT] * * @throws IllegalStateException if the matching [ScreenViewFactory] fails to call @@ -59,42 +140,11 @@ public interface ScreenViewFactory : ViewRegistry.Entry< @WorkflowUiExperimentalApi public fun ScreenT.buildView( viewEnvironment: ViewEnvironment, - contextForNewView: Context, - container: ViewGroup? = null, - viewStarter: ViewStarter? = null, -): View { - val viewFactory = viewEnvironment.getViewFactoryForRendering(this) - - 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) } - } - } - } -} - -/** - * 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. - */ -@WorkflowUiExperimentalApi -public fun interface ViewStarter { - /** Called from [View.start]. [doStart] must be invoked. */ - public fun startView( - view: View, - doStart: () -> Unit + context: Context, + container: ViewGroup? = null +): ScreenViewHolder { + return viewEnvironment.getViewFactoryForRendering(this).buildView( + this, viewEnvironment, context, container ) } 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..f4090894c8 --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewHolder.kt @@ -0,0 +1,126 @@ +package com.squareup.workflow1.ui + +import android.view.View + +/** + * Wraps an [Android view][view] with: + * + * - the current [Screen] [screen] it's displaying + * - the [environment] that [screen] was shown with + * - a [showScreen] method to refresh it + * + * Instances are created via [ScreenViewFactory.buildView]. [start] must be called + * exactly once before [showScreen], to initialize the new view. Use [withStarter] to + * customize what the [start] method does. + */ +@WorkflowUiExperimentalApi +public interface ScreenViewHolder { + public val screen: ScreenT + public val environment: ViewEnvironment + public val view: View + + public fun start() + + public fun showScreen( + screen: ScreenT, + environment: ViewEnvironment + ) + + @WorkflowUiExperimentalApi + public fun interface Starter { + /** Called from [start]. [doStart] must be invoked. */ + public fun startView( + viewHolder: ScreenViewHolder, + doStart: () -> Unit + ) + } +} + +/** Wraps [view] in a [ScreenViewHolder], mainly for use from [ScreenViewFactory.of]. */ +@Suppress("FunctionName") +@WorkflowUiExperimentalApi +public fun ScreenViewHolder( + initialRendering: ScreenT, + initialViewEnvironment: ViewEnvironment, + view: View, + updater: ScreenViewUpdater +): ScreenViewHolder { + return BaseScreenViewHolder(initialRendering, initialViewEnvironment, view, updater) +} + +@WorkflowUiExperimentalApi +public fun ScreenViewHolder<*>.canShowScreen(screen: Screen): Boolean { + return compatible(this.screen, screen) +} + +@WorkflowUiExperimentalApi +public fun ScreenViewHolder.withStarter( + starter: ScreenViewHolder.Starter +): ScreenViewHolder { + return object : ScreenViewHolder by this { + override fun start() { + starter.startView(this@withStarter, this@withStarter::start) + } + } +} + +/** + * @param onShowScreen Function to be called in place of the receiver's + * [ScreenViewHolder.showScreen] method. When invoked, `this` is the + * original [ScreenViewHolder] that received the [withShowScreen] call, + * so [onShowScreen] has access to the original [ScreenViewHolder.showScreen] method. + */ +@WorkflowUiExperimentalApi +public fun ScreenViewHolder.withShowScreen( + onShowScreen: ScreenViewHolder.(ScreenT, ViewEnvironment) -> Unit +): ScreenViewHolder { + return object : ScreenViewHolder by this { + override fun showScreen(screen: ScreenT, environment: ViewEnvironment) { + this@withShowScreen.onShowScreen(screen, environment) + } + } +} + +/** + * Transforms the [ScreenViewHolder.showScreen] method of the receiver to accept [NewS] + * instead of [OriginalS], by applying the [given function][transform] to convert + * [NewS] to [OriginalS]. + */ +@WorkflowUiExperimentalApi +public fun ScreenViewHolder.acceptRenderings( + initialRendering: NewS, + transform: (NewS) -> OriginalS +): ScreenViewHolder { + return object : ScreenViewHolder { + var untransformed: NewS = initialRendering + + override val screen: NewS + get() = untransformed + + override val environment: ViewEnvironment + get() = this@acceptRenderings.environment + + override val view: View + get() = this@acceptRenderings.view + + override fun start() { + this@acceptRenderings.start() + } + + override fun showScreen(screen: NewS, environment: ViewEnvironment) { + untransformed = screen + this@acceptRenderings.showScreen(transform(screen), environment) + } + } +} + +@WorkflowUiExperimentalApi +public fun ScreenViewHolder.updateEnvironment( + updater: (ViewEnvironment) -> ViewEnvironment +): ScreenViewHolder { + return object : ScreenViewHolder by this { + override fun showScreen(screen: ScreenT, environment: ViewEnvironment) { + this@updateEnvironment.showScreen(screen, updater(environment)) + } + } +} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewRunner.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewRunner.kt deleted file mode 100644 index 52e51238fa..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/ScreenViewUpdater.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewUpdater.kt new file mode 100644 index 0000000000..c8d96869cf --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewUpdater.kt @@ -0,0 +1,22 @@ +package com.squareup.workflow1.ui + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.viewbinding.ViewBinding + +@WorkflowUiExperimentalApi +public typealias ViewBindingInflater = (LayoutInflater, ViewGroup?, Boolean) -> BindingT + +/** Function that updates the UI built by a [ScreenViewFactory]. */ +@WorkflowUiExperimentalApi +public fun interface ScreenViewUpdater { + public fun showRendering( + rendering: ScreenT, + viewEnvironment: ViewEnvironment + ) +} + +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..87800872da 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,33 +1,31 @@ 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 @WorkflowUiExperimentalApi @PublishedApi -internal class ViewBindingScreenViewFactory( - override val type: KClass, +internal class ViewBindingScreenViewFactory( + override val type: KClass, private val bindingInflater: ViewBindingInflater, - private val runnerConstructor: (BindingT) -> ScreenViewRunner -) : ScreenViewFactory { + private val updaterConstructor: (BindingT) -> ScreenViewUpdater +) : ScreenViewFactory { override fun buildView( - initialRendering: RenderingT, + initialRendering: ScreenT, initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, + 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) - } - } - .root + ): ScreenViewHolder { + val binding = bindingInflater( + context.viewBindingLayoutInflater(container), container, false + ) + return BaseScreenViewHolder( + initialRendering, + initialViewEnvironment, + binding.root, + updater = updaterConstructor(binding) + ) + } } 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 9b9a9d295e..45c2eda109 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 @@ -28,7 +28,9 @@ public typealias ViewShowRendering = * @see ViewFactory * @see DecorativeViewFactory */ +@Suppress("DeprecatedCallableAddReplaceWith") @WorkflowUiExperimentalApi +@Deprecated("Use ScreenViewHolder") public fun View.bindShowRendering( initialRendering: RenderingT, initialViewEnvironment: ViewEnvironment, diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowLayout.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowLayout.kt index 1cb6b2bcc7..05b5f5cf4e 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 @@ -39,7 +39,7 @@ public class WorkflowLayout( if (id == NO_ID) id = R.id.workflow_layout } - private val showing: WorkflowViewStub = WorkflowViewStub(context).also { rootStub -> + private val stub: WorkflowViewStub = WorkflowViewStub(context).also { rootStub -> rootStub.updatesVisibility = false addView(rootStub, ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)) } @@ -103,17 +103,19 @@ public class WorkflowLayout( } private fun show(rootScreen: EnvironmentScreen<*>) { - showing.show(rootScreen.screen, rootScreen.viewEnvironment) + stub.show(rootScreen.screen, rootScreen.viewEnvironment) restoredChildState?.let { restoredState -> restoredChildState = null - showing.actual.restoreHierarchyState(restoredState) + stub.delegateHolder.view.restoreHierarchyState(restoredState) } } override fun onSaveInstanceState(): Parcelable { return SavedState( super.onSaveInstanceState()!!, - SparseArray().also { array -> showing.actual.saveHierarchyState(array) } + SparseArray().also { array -> + stub.delegateHolder.view.saveHierarchyState(array) + } ) } 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..e9b703ad7f 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 @@ -59,8 +59,8 @@ import com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner * Use [updatesVisibility] and [setBackground] for more control of how [update] * effects the visibility and backgrounds of created views. * - * Use [replaceOldViewInParent] to customize replacing [actual] with a new view, e.g. - * for animated transitions. + * Use [replaceOldViewInParent] to customize replacing the current view with a new one during + * [show], e.g. for animated transitions. */ @WorkflowUiExperimentalApi public class WorkflowViewStub @JvmOverloads constructor( @@ -69,17 +69,41 @@ public class WorkflowViewStub @JvmOverloads constructor( defStyle: Int = 0, defStyleRes: Int = 0 ) : View(context, attributeSet, defStyle, defStyleRes) { - /** - * 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 + /** Returns null if [update] hasn't been called yet. */ + public val delegateHolderOrNull: ScreenViewHolder? + get() { + // can be null when called from the constructor. + @Suppress("UNNECESSARY_SAFE_CALL") + return delegateHolder?.takeUnless { it.view === this } + } + + public var delegateHolder: ScreenViewHolder = object : ScreenViewHolder { + override val screen: Screen = object : Screen {} + override val environment: ViewEnvironment = ViewEnvironment(mapOf()) + + override val view: View = this@WorkflowViewStub + + override fun start() { + throw UnsupportedOperationException() + } + + override fun showScreen( + screen: Screen, + environment: ViewEnvironment + ) { + throw UnsupportedOperationException() + } + } private set + @Deprecated("Use delegateHolder.view", ReplaceWith("delegateHolder.view")) + public val actual: View + get() = delegateHolder.view + /** - * If true, the visibility of views created by [update] will be copied - * from that of [actual]. Bear in mind that the initial value of - * [actual] is this stub. + * If true, the visibility of new delegate views created by [update] will be copied + * from the current one. The first delegate created will copy the visibility of + * this stub. */ public var updatesVisibility: Boolean = true @@ -114,14 +138,15 @@ public class WorkflowViewStub @JvmOverloads constructor( } /** - * Function called from [update] to replace this stub, or the current [actual], + * Function called from [update] to replace this stub, or its current delegate, * with a new view. Can be updated to provide custom transition effects. * * Note that this method is responsible for copying the [layoutParams][getLayoutParams] - * from the stub to the new view. Also note that in a [WorkflowViewStub] that has never - * been updated, [actual] is the stub itself. + * from the stub to the new view. */ public var replaceOldViewInParent: (ViewGroup, View) -> Unit = { parent, newView -> + val actual = delegateHolder.view + val index = parent.indexOfChild(actual) parent.removeView(actual) actual.layoutParams @@ -130,44 +155,30 @@ public class WorkflowViewStub @JvmOverloads constructor( } /** - * Sets the visibility of [actual]. If [updatesVisibility] is true, the visibility of - * new views created by [update] will copied from [actual]. (Bear in mind that the initial - * value of [actual] is this stub.) + * Sets the visibility of the delegate, or of this stub if [show] has not yet been called. + * + * @see updatesVisibility */ override fun setVisibility(visibility: Int) { super.setVisibility(visibility) - // actual can be null when called from the constructor. - @Suppress("SENSELESS_COMPARISON") - if (actual != this && actual != null) { - actual.visibility = visibility - } + delegateHolderOrNull?.let { it.view.visibility = visibility } } /** - * Returns the visibility of [actual]. (Bear in mind that the initial value of - * [actual] is this stub.) + * Returns the visibility of the delegate, or of this stub if [show] has not yet been called. */ override fun getVisibility(): Int { - // actual can be null when called from the constructor. - @Suppress("SENSELESS_NULL_IN_WHEN") - return when (actual) { - this, null -> super.getVisibility() - else -> actual.visibility - } + return delegateHolderOrNull?.view?.visibility ?: super.getVisibility() } /** - * Sets the background of this stub as usual, and also that of [actual] - * if the given [background] is not null. Any new views created by [update] + * Sets the background of this stub as usual, and also that of the delegate view, + * if the given [background] is not null. Any new delegates created by [update] * will be assigned this background, again if it is not null. */ override fun setBackground(background: Drawable?) { super.setBackground(background) - // actual can be null when called from the constructor. - @Suppress("SENSELESS_COMPARISON") - if (actual != this && actual != null && background != null) { - actual.background = background - } + if (background != null) delegateHolderOrNull?.view?.background = background } @Deprecated("Use show()", ReplaceWith("show(rendering, viewEnvironment)")) @@ -176,79 +187,73 @@ public class WorkflowViewStub @JvmOverloads constructor( viewEnvironment: ViewEnvironment ): View { @Suppress("DEPRECATION") - return show(asScreen(rendering), viewEnvironment) + show(asScreen(rendering), viewEnvironment) + return delegateHolder.view } /** - * Replaces this view with one that can display [rendering]. If the receiver - * has already been replaced, updates the replacement if it [canShowRendering]. - * If the current replacement can't handle [rendering], a new view is put in its place. + * Replaces this view with a [delegate][delegateHolder] that can display [screen]. + * If [show] has already been called previously, updates the current delegate if it + * [canShowScreen]. If the current delegate can't handle [screen], a new view + * is put in its place. * - * The [id][View.setId] of any view created by this method will be set to to [inflatedId], + * The [id][View.setId] of any delegate view created by this method will be set to to [inflatedId], * unless that value is [View.NO_ID]. * - * The [background][setBackground] of any view created by this method will be copied + * The [background][setBackground] of any delegate view created by this method will be copied * from [getBackground], if that value is non-null. * - * If [updatesVisibility] is true, the [visibility][setVisibility] of any view created by - * this method will be copied from [actual]. (Bear in mind that the initial value of - * [actual] is this stub.) - * - * @return the view that showed [rendering] + * If [updatesVisibility] is true, the [visibility][setVisibility] of any delegate view created + * by this method will be copied from [getVisibility]. * - * @throws IllegalArgumentException if no binding can be found for the type of [rendering] + * @return the view that showed [screen] * - * @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 + * @throws IllegalArgumentException if no binding can be found for the type of [screen] */ public fun show( - rendering: Screen, + screen: Screen, viewEnvironment: ViewEnvironment - ): View { - actual.takeIf { it.canShowRendering(rendering) } + ) { + delegateHolder.takeIf { it.canShowScreen(screen) } ?.let { - it.showRendering(rendering, viewEnvironment) - return it + it.showScreen(screen, viewEnvironment) + return } - val parent = actual.parent as? ViewGroup + val parent = delegateHolder.view.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 we have a delegate view, 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() + // If there isn't a delegate, 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. + delegateHolderOrNull?.let { + WorkflowLifecycleOwner.get(it.view)?.destroyOnDetach() } - return rendering.buildView( + val newViewHolder = screen.buildView( viewEnvironment, parent.context, - parent, - viewStarter = { view, doStart -> - WorkflowLifecycleOwner.installOn(view) - doStart() - } - ) - .also { newView -> - newView.start() + parent + ).withStarter { view, doStart -> + WorkflowLifecycleOwner.installOn(view.view) + doStart() + } + newViewHolder.start() - if (inflatedId != NO_ID) newView.id = inflatedId - if (updatesVisibility) newView.visibility = visibility - background?.let { newView.background = it } - propagateSavedStateRegistryOwner(newView) - replaceOldViewInParent(parent, newView) - actual = newView - } + val newAndroidView = newViewHolder.view + + if (inflatedId != NO_ID) newAndroidView.id = inflatedId + if (updatesVisibility) newAndroidView.visibility = visibility + background?.let { newAndroidView.background = it } + propagateSavedStateRegistryOwner(newAndroidView) + replaceOldViewInParent(parent, newAndroidView) + delegateHolder = newViewHolder } /** diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackContainer.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackContainer.kt index be81a2590f..79cab56c9e 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 @@ -27,7 +27,7 @@ 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.showRendering -import com.squareup.workflow1.ui.start +import com.squareup.workflow1.ui.withStarter /** * A container view that can display a stream of [BackStackScreen] instances. @@ -97,19 +97,18 @@ public open class BackStackContainer @JvmOverloads constructor( val newView = named.top.buildView( viewEnvironment = environment, - contextForNewView = this.context, - container = this, - viewStarter = { view, doStart -> - WorkflowLifecycleOwner.installOn(view) - doStart() - } - ) + context = this.context, + container = this + ).withStarter { view, doStart -> + WorkflowLifecycleOwner.installOn(view.view) + doStart() + } newView.start() - viewStateCache.update(named.backStack, oldViewMaybe, newView) + viewStateCache.update(named.backStack, oldViewMaybe, newView.view) val popped = currentRendering?.backStack?.any { compatible(it, named.top) } == true - performTransition(oldViewMaybe, newView, popped) + performTransition(oldViewMaybe, newView.view, popped) // Notify the view we're about to replace that it's going away. oldViewMaybe?.let(WorkflowLifecycleOwner::get)?.destroyOnDetach() 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..6887785494 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,20 @@ 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, +by ScreenViewFactory.of( viewConstructor = { initialRendering, initialEnv, 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(initialRendering, initialEnv, view, view::update) } } ) diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndModalsContainer.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndModalsContainer.kt index 8b37b6f6fe..ee72f2bc2e 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 @@ -12,16 +12,12 @@ import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewTreeObserver.OnGlobalLayoutListener import android.widget.FrameLayout -import com.squareup.workflow1.ui.ManualScreenViewFactory +import com.squareup.workflow1.ui.BaseScreenViewHolder import com.squareup.workflow1.ui.R import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub -import com.squareup.workflow1.ui.bindShowRendering -import com.squareup.workflow1.ui.environment -import com.squareup.workflow1.ui.getRendering -import com.squareup.workflow1.ui.showRendering import kotlinx.coroutines.flow.MutableStateFlow @WorkflowUiExperimentalApi @@ -97,7 +93,9 @@ internal class BodyAndModalsContainer @JvmOverloads constructor( viewTreeObserver.addOnGlobalLayoutListener(boundsListener) // Ugly, but here in case a strange parent detaches and re-attaches us. // https://github.com/square/workflow-kotlin/issues/314 - showRendering(getRendering()!!, environment!!) + baseViewStub.delegateHolderOrNull?.let { + it.showScreen(it.screen, it.environment) + } } override fun onDetachedFromWindow() { @@ -167,14 +165,13 @@ internal class BodyAndModalsContainer @JvmOverloads constructor( } companion object : ScreenViewFactory> - by ManualScreenViewFactory( - type = BodyAndModalsScreen::class, + by ScreenViewFactory.of( viewConstructor = { initialRendering, initialEnv, 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)) + BaseScreenViewHolder(initialRendering, initialEnv, view, view::update) } } ) diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/EnvironmentScreenViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/EnvironmentScreenViewFactory.kt index c07a482b06..390dfaff9b 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,22 @@ package com.squareup.workflow1.ui.container -import com.squareup.workflow1.ui.DecorativeScreenViewFactory import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.acceptRenderings +import com.squareup.workflow1.ui.buildView import com.squareup.workflow1.ui.updateFrom +import com.squareup.workflow1.ui.withShowScreen @WorkflowUiExperimentalApi -internal object EnvironmentScreenViewFactory : ScreenViewFactory> -by DecorativeScreenViewFactory( - type = EnvironmentScreen::class, - map = { withEnvironment, inheritedEnvironment -> - Pair( - withEnvironment.screen, - inheritedEnvironment.updateFrom(withEnvironment.viewEnvironment) - ) +internal val EnvironmentScreenViewFactory: ScreenViewFactory> = + ScreenViewFactory.of { initialRendering, initialViewEnvironment, context, container -> + initialRendering.screen + // Build the view for the wrapped rendering. + .buildView(initialViewEnvironment, context, container) + // Transform it to accept EnvironmentScreen directly. + .acceptRenderings(initialRendering) { it.screen } + // When showScreen is called, enhance the viewEnvironment with the one in environmentScreen. + .withShowScreen { environmentScreen, viewEnvironment -> + showScreen(environmentScreen, viewEnvironment.updateFrom(environmentScreen.viewEnvironment)) + } } -) 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 6cdc8045e2..a78f9d4d28 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactory.kt @@ -11,13 +11,12 @@ import android.view.View import android.view.Window import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL import com.squareup.workflow1.ui.R +import com.squareup.workflow1.ui.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 java.lang.IllegalStateException import kotlin.reflect.KClass @@ -37,9 +36,9 @@ public abstract class ModalScreenOverlayDialogFactory>( /** * Called from [buildDialog]. Builds (but does not show) the [Dialog] to - * display a [contentView] built for a [ScreenOverlay.content]. + * display a [content] view built for a [ScreenOverlay.content]. */ - public abstract fun buildDialogWithContentView(contentView: View): Dialog + public abstract fun buildDialogWithContent(content: ScreenViewHolder<*>): Dialog /** * If the [ScreenOverlay] displayed by a [dialog] created by this @@ -63,21 +62,21 @@ public abstract class ModalScreenOverlayDialogFactory>( initialEnvironment: ViewEnvironment, context: Context ): Dialog { - val contentView = initialRendering.content.buildView(initialEnvironment, context).apply { + val contentHolder = initialRendering.content.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 = { } + if (view.backPressedHandler == null) view.backPressedHandler = { } } - return buildDialogWithContentView(contentView).also { dialog -> + return buildDialogWithContent(contentHolder).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) + window.peekDecorView()?.setTag(R.id.workflow_modal_dialog_content, contentHolder) ?: throw IllegalStateException("Expected decorView to have been built.") val realWindowCallback = window.callback @@ -87,15 +86,15 @@ public abstract class ModalScreenOverlayDialogFactory>( event.action == ACTION_UP return when { - isBackPress -> contentView.environment?.get(ModalScreenOverlayOnBackPressed) - ?.onBackPressed(contentView) == true + isBackPress -> contentHolder.environment[ModalScreenOverlayOnBackPressed] + .onBackPressed(contentHolder) 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(contentHolder.view) { d, b -> updateBounds(d, Rect(b)) } } } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed.kt index 3641e1f44e..7cc0cddc0f 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed.kt @@ -1,6 +1,6 @@ package com.squareup.workflow1.ui.container -import android.view.View +import com.squareup.workflow1.ui.ScreenViewHolder import com.squareup.workflow1.ui.ViewEnvironmentKey import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.container.ModalScreenOverlayOnBackPressed.Handler @@ -26,11 +26,11 @@ public object ModalScreenOverlayOnBackPressed : ViewEnvironmentKey( * * @return true if the back press event was consumed */ - public fun onBackPressed(contentView: View): Boolean + public fun onBackPressed(content: ScreenViewHolder<*>): Boolean } - override val default: Handler = Handler { view -> - view.context.onBackPressedDispatcherOwnerOrNull() + override val default: Handler = Handler { viewHolder -> + viewHolder.view.context.onBackPressedDispatcherOwnerOrNull() ?.onBackPressedDispatcher ?.let { if (it.hasEnabledCallbacks()) it.onBackPressed() 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 d650b901f9..5a4606d2ed 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 @@ -67,7 +67,7 @@ internal class ScreenViewFactoryTest { override fun buildView( initialRendering: T, initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, + context: Context, container: ViewGroup? ): View { called = true diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Screen.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Screen.kt index 37b586e8aa..ffcac814cf 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Screen.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Screen.kt @@ -1,7 +1,7 @@ package com.squareup.workflow1.ui /** - * Marker interface implemented by renderings that map to a UI system's 2d view class. + * Marker interface implemented by renderings that model a UI system's 2d box / view class. */ @WorkflowUiExperimentalApi public interface Screen diff --git a/workflow-ui/internal-testing-android/src/main/java/com/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity.kt b/workflow-ui/internal-testing-android/src/main/java/com/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity.kt index 5e7115d128..9878e189b5 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 @@ -11,14 +11,13 @@ 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 kotlin.reflect.KClass /** @@ -89,19 +88,18 @@ public abstract class AbstractLifecycleTestActivity : WorkflowUiTestActivity() { lifecycleEvents += message } - protected fun leafViewBinding( - type: KClass, + protected inline fun leafViewBinding( viewObserver: ViewObserver, - viewConstructor: (Context) -> LeafView = ::LeafView + noinline viewConstructor: (Context) -> LeafView = ::LeafView ): ScreenViewFactory = - ManualScreenViewFactory(type) { initialRendering, initialViewEnvironment, context, _ -> - viewConstructor(context).apply { - this.viewObserver = viewObserver - viewObserver.onViewCreated(this, initialRendering) - - bindShowRendering(initialRendering, initialViewEnvironment) { rendering, _ -> - this.rendering = rendering - viewObserver.onShowRendering(this, rendering) + ScreenViewFactory.of { initialRendering, initialViewEnvironment, context, _ -> + viewConstructor(context).let { view -> + view.viewObserver = viewObserver + viewObserver.onViewCreated(view, initialRendering) + + ScreenViewHolder(initialRendering, initialViewEnvironment, view) { rendering, _ -> + view.rendering = rendering + viewObserver.onShowRendering(view, rendering) } } } @@ -179,11 +177,10 @@ 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 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..2a625206d8 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 @@ -45,7 +45,7 @@ public open class WorkflowUiTestActivity : AppCompatActivity() { /** * The [View] that was created to display the last rendering passed to [setRendering]. */ - public val rootRenderedView: View get() = rootStub.actual + public val rootRenderedView: View get() = rootStub.delegateHolder.view /** * Key-value store for custom values that should be retained across configuration changes. @@ -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.delegateHolder.view } private class NonConfigurationData(