diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposebinding/HelloBindingActivity.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposebinding/HelloBindingActivity.kt index 12821d38f7..4e99b10bcf 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposebinding/HelloBindingActivity.kt +++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposebinding/HelloBindingActivity.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package com.squareup.sample.compose.hellocomposebinding import android.os.Bundle diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflowActivity.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflowActivity.kt index f666a1690f..22e91a3844 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflowActivity.kt +++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/hellocomposeworkflow/HelloComposeWorkflowActivity.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package com.squareup.sample.compose.hellocomposeworkflow import android.os.Bundle diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingActivity.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingActivity.kt index ae13885466..1435fe3fcd 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingActivity.kt +++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingActivity.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package com.squareup.sample.compose.inlinerendering import android.os.Bundle diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingWorkflow.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingWorkflow.kt index 17badd16ce..727da411ab 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingWorkflow.kt +++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingWorkflow.kt @@ -1,3 +1,4 @@ +@file:Suppress("DEPRECATION") @file:OptIn(WorkflowUiExperimentalApi::class) package com.squareup.sample.compose.inlinerendering diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/LegacyRunner.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/LegacyRunner.kt index 12f785019e..bb56b5ad75 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/LegacyRunner.kt +++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/LegacyRunner.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package com.squareup.sample.compose.nestedrenderings import androidx.compose.foundation.layout.fillMaxSize diff --git a/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/NestedRenderingsActivity.kt b/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/NestedRenderingsActivity.kt index 0eecc3cbde..dd4849b9f4 100644 --- a/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/NestedRenderingsActivity.kt +++ b/samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/NestedRenderingsActivity.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package com.squareup.sample.compose.nestedrenderings import android.os.Bundle diff --git a/samples/containers/android/src/main/java/com/squareup/sample/container/BackButtonScreen.kt b/samples/containers/android/src/main/java/com/squareup/sample/container/BackButtonScreen.kt index d6314e309b..7627ab75c0 100644 --- a/samples/containers/android/src/main/java/com/squareup/sample/container/BackButtonScreen.kt +++ b/samples/containers/android/src/main/java/com/squareup/sample/container/BackButtonScreen.kt @@ -1,5 +1,6 @@ package com.squareup.sample.container +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi /** @@ -16,8 +17,8 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi * is pressed, or null to set no handler. Defaults to `null`. */ @WorkflowUiExperimentalApi -data class BackButtonScreen( +data class BackButtonScreen( val wrapped: W, val override: Boolean = false, val onBackPressed: (() -> Unit)? = null -) +) : Screen 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 214b977529..7e4ba2b328 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,30 +1,31 @@ package com.squareup.sample.container -import com.squareup.workflow1.ui.DecorativeViewFactory -import com.squareup.workflow1.ui.ViewFactory +import com.squareup.workflow1.ui.DecorativeScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.backPressedHandler /** - * [ViewFactory] that performs the work required by [BackButtonScreen]. + * [ScreenViewFactory] that performs the work required by [BackButtonScreen]. */ @WorkflowUiExperimentalApi -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 - } +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) + innerShowRendering.invoke(outerRendering.wrapped, viewEnvironment) - if (outerRendering.override) { - // Place our handler after invoking innerShowRendering, so that ours wins. - view.backPressedHandler = outerRendering.onBackPressed - } - }) + if (outerRendering.override) { + // Place our handler after invoking innerShowRendering, so that ours wins. + view.backPressedHandler = outerRendering.onBackPressed + } + } +) 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 3c83be2696..47b8b2c17c 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 @@ -7,13 +7,13 @@ import com.squareup.sample.container.R 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.LayoutRunner +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewRunner import com.squareup.workflow1.ui.ViewEnvironment -import com.squareup.workflow1.ui.ViewFactory import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub -import com.squareup.workflow1.ui.backstack.BackStackScreen -import com.squareup.workflow1.ui.backstack.withBackStackStateKeyPrefix +import com.squareup.workflow1.ui.container.BackStackScreen +import com.squareup.workflow1.ui.container.withBackStackStateKeyPrefix /** * Displays [OverviewDetailScreen] renderings in either split pane or single pane @@ -25,7 +25,7 @@ import com.squareup.workflow1.ui.backstack.withBackStackStateKeyPrefix * with [OverviewDetailScreen.overviewRendering] as the base of the stack. */ @OptIn(WorkflowUiExperimentalApi::class) -class OverviewDetailContainer(view: View) : LayoutRunner { +class OverviewDetailContainer(view: View) : ScreenViewRunner { private val overviewStub: WorkflowViewStub? = view.findViewById(R.id.overview_stub) private val detailStub: WorkflowViewStub? = view.findViewById(R.id.detail_stub) @@ -53,15 +53,15 @@ class OverviewDetailContainer(view: View) : LayoutRunner { if (rendering.detailRendering == null && rendering.selectDefault != null) { rendering.selectDefault!!.invoke() } else { - // Since we have two sibling backstacks, we need to give them each different + // Since we have two sibling back stacks, we need to give them each different // SavedStateRegistry key prefixes. val overviewViewEnvironment = viewEnvironment .withBackStackStateKeyPrefix(OverviewBackStackKey) + (OverviewDetailConfig to Overview) - overviewStub!!.update(rendering.overviewRendering, overviewViewEnvironment) + overviewStub!!.show(rendering.overviewRendering, overviewViewEnvironment) rendering.detailRendering ?.let { detail -> detailStub!!.actual.visibility = VISIBLE - detailStub.update( + detailStub.show( detail, viewEnvironment + (OverviewDetailConfig to Detail) ) @@ -81,10 +81,10 @@ class OverviewDetailContainer(view: View) : LayoutRunner { ?.let { rendering.overviewRendering + it } ?: rendering.overviewRendering - stub.update(combined, viewEnvironment + (OverviewDetailConfig to Single)) + stub.show(combined, viewEnvironment + (OverviewDetailConfig to Single)) } - companion object : ViewFactory by LayoutRunner.bind( + companion object : ScreenViewFactory by ScreenViewRunner.bind( layoutId = R.layout.overview_detail, constructor = ::OverviewDetailContainer ) { diff --git a/samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelContainer.kt b/samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelContainer.kt index 08081526b9..9fadf2f180 100644 --- a/samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelContainer.kt +++ b/samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelContainer.kt @@ -10,8 +10,8 @@ import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import com.squareup.sample.container.R -import com.squareup.workflow1.ui.BuilderViewFactory -import com.squareup.workflow1.ui.ViewFactory +import com.squareup.workflow1.ui.ManualScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.bindShowRendering import com.squareup.workflow1.ui.modal.ModalViewContainer @@ -59,7 +59,7 @@ class PanelContainer @JvmOverloads constructor( } } - companion object : ViewFactory> by BuilderViewFactory( + companion object : ScreenViewFactory> by ManualScreenViewFactory( type = PanelContainerScreen::class, viewConstructor = { initialRendering, initialEnv, contextForNewView, _ -> PanelContainer(contextForNewView).apply { diff --git a/samples/containers/android/src/main/java/com/squareup/sample/container/panel/ScrimContainer.kt b/samples/containers/android/src/main/java/com/squareup/sample/container/panel/ScrimContainer.kt index e10338ba2a..2bd40d659e 100644 --- a/samples/containers/android/src/main/java/com/squareup/sample/container/panel/ScrimContainer.kt +++ b/samples/containers/android/src/main/java/com/squareup/sample/container/panel/ScrimContainer.kt @@ -7,9 +7,9 @@ import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat import com.squareup.sample.container.R -import com.squareup.workflow1.ui.BuilderViewFactory +import com.squareup.workflow1.ui.ManualScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.ViewFactory import com.squareup.workflow1.ui.WorkflowViewStub import com.squareup.workflow1.ui.bindShowRendering @@ -91,7 +91,7 @@ class ScrimContainer @JvmOverloads constructor( } @OptIn(WorkflowUiExperimentalApi::class) - companion object : ViewFactory> by BuilderViewFactory( + companion object : ScreenViewFactory> by ManualScreenViewFactory( type = ScrimContainerScreen::class, viewConstructor = { initialRendering, initialViewEnvironment, contextForNewView, _ -> val stub = WorkflowViewStub(contextForNewView) @@ -104,7 +104,7 @@ class ScrimContainer @JvmOverloads constructor( bindShowRendering( initialRendering, initialViewEnvironment ) { rendering, environment -> - stub.update(rendering.wrapped, environment) + stub.show(rendering.wrapped, environment) isDimmed = rendering.dimmed } } diff --git a/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListRendering.kt b/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListScreen.kt similarity index 86% rename from samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListRendering.kt rename to samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListScreen.kt index 27ec18e244..c6cfb50dac 100644 --- a/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListRendering.kt +++ b/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListScreen.kt @@ -11,25 +11,25 @@ import com.squareup.sample.container.overviewdetail.OverviewDetailConfig import com.squareup.sample.container.overviewdetail.OverviewDetailConfig.Overview import com.squareup.sample.container.poetryapp.R import com.squareup.sample.poetry.model.Poem -import com.squareup.workflow1.ui.AndroidViewRendering -import com.squareup.workflow1.ui.LayoutRunner +import com.squareup.workflow1.ui.AndroidScreen +import com.squareup.workflow1.ui.ScreenViewRunner import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi @OptIn(WorkflowUiExperimentalApi::class) -data class PoemListRendering( +data class PoemListScreen( val poems: List, val onPoemSelected: (Int) -> Unit, val selection: Int = -1 -) : AndroidViewRendering { - override val viewFactory = LayoutRunner.bind( +) : AndroidScreen { + override val viewFactory = ScreenViewRunner.bind( R.layout.list, ::PoemListLayoutRunner ) } @OptIn(WorkflowUiExperimentalApi::class) -private class PoemListLayoutRunner(view: View) : LayoutRunner { +private class PoemListLayoutRunner(view: View) : ScreenViewRunner { init { view.findViewById(R.id.list_toolbar) .apply { @@ -44,7 +44,7 @@ private class PoemListLayoutRunner(view: View) : LayoutRunner private val adapter = Adapter() override fun showRendering( - rendering: PoemListRendering, + rendering: PoemListScreen, viewEnvironment: ViewEnvironment ) { adapter.rendering = rendering @@ -58,7 +58,7 @@ private class PoemListLayoutRunner(view: View) : LayoutRunner private class ViewHolder(val view: TextView) : RecyclerView.ViewHolder(view) private class Adapter : RecyclerView.Adapter() { - lateinit var rendering: PoemListRendering + lateinit var rendering: PoemListScreen lateinit var environment: ViewEnvironment override fun onCreateViewHolder( diff --git a/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListWorkflow.kt b/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListWorkflow.kt index 9daebd00e5..932f97d1be 100644 --- a/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListWorkflow.kt +++ b/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListWorkflow.kt @@ -6,13 +6,13 @@ import com.squareup.workflow1.StatelessWorkflow /** * Renders a given ordered list of [Poem]s. Reports the index of any that are clicked. */ -object PoemListWorkflow : StatelessWorkflow, Int, PoemListRendering>() { +object PoemListWorkflow : StatelessWorkflow, Int, PoemListScreen>() { override fun render( renderProps: List, context: RenderContext - ): PoemListRendering { - return PoemListRendering( + ): PoemListScreen { + return PoemListScreen( poems = renderProps, onPoemSelected = context.eventHandler { index -> setOutput(index) } ) diff --git a/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemsBrowserWorkflow.kt b/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemsBrowserWorkflow.kt index 13d039a7c8..3faaa3f4f6 100644 --- a/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemsBrowserWorkflow.kt +++ b/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemsBrowserWorkflow.kt @@ -8,7 +8,7 @@ import com.squareup.workflow1.StatefulWorkflow import com.squareup.workflow1.action import com.squareup.workflow1.parse import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backstack.BackStackScreen +import com.squareup.workflow1.ui.container.BackStackScreen typealias SelectedPoem = Int diff --git a/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoetryActivity.kt b/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoetryActivity.kt index bd29b51d64..5a1f7cde26 100644 --- a/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoetryActivity.kt +++ b/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoetryActivity.kt @@ -8,16 +8,17 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.squareup.sample.container.SampleContainers import com.squareup.sample.poetry.model.Poem +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowLayout import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backstack.BackStackContainer -import com.squareup.workflow1.ui.plus +import com.squareup.workflow1.ui.container.withRegistry import com.squareup.workflow1.ui.renderWorkflowIn import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map import timber.log.Timber @OptIn(WorkflowUiExperimentalApi::class) -private val viewRegistry = SampleContainers + BackStackContainer +private val viewRegistry = SampleContainers class PoetryActivity : AppCompatActivity() { @OptIn(WorkflowUiExperimentalApi::class) @@ -26,7 +27,7 @@ class PoetryActivity : AppCompatActivity() { val model: PoetryModel by viewModels() setContentView( - WorkflowLayout(this).apply { start(model.renderings, viewRegistry) } + WorkflowLayout(this).apply { take(model.renderings.map { it.withRegistry(viewRegistry) }) } ) } @@ -39,7 +40,7 @@ class PoetryActivity : AppCompatActivity() { class PoetryModel(savedState: SavedStateHandle) : ViewModel() { @OptIn(WorkflowUiExperimentalApi::class) - val renderings: StateFlow by lazy { + val renderings: StateFlow by lazy { renderWorkflowIn( workflow = PoemsBrowserWorkflow, scope = viewModelScope, diff --git a/samples/containers/app-raven/src/main/java/com/squareup/sample/ravenapp/RavenActivity.kt b/samples/containers/app-raven/src/main/java/com/squareup/sample/ravenapp/RavenActivity.kt index 05dd90088f..fa4f43653a 100644 --- a/samples/containers/app-raven/src/main/java/com/squareup/sample/ravenapp/RavenActivity.kt +++ b/samples/containers/app-raven/src/main/java/com/squareup/sample/ravenapp/RavenActivity.kt @@ -1,3 +1,5 @@ +@file:OptIn(WorkflowUiExperimentalApi::class) + package com.squareup.sample.ravenapp import android.os.Bundle @@ -10,27 +12,26 @@ import androidx.lifecycle.viewModelScope import com.squareup.sample.container.SampleContainers import com.squareup.sample.poetry.PoemWorkflow import com.squareup.sample.poetry.model.Raven +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowLayout import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backstack.BackStackContainer -import com.squareup.workflow1.ui.plus +import com.squareup.workflow1.ui.container.withRegistry import com.squareup.workflow1.ui.renderWorkflowIn import kotlinx.coroutines.Job import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import timber.log.Timber -@OptIn(WorkflowUiExperimentalApi::class) -private val viewRegistry = SampleContainers + BackStackContainer +private val viewRegistry = SampleContainers class RavenActivity : AppCompatActivity() { - @OptIn(WorkflowUiExperimentalApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val model: RavenModel by viewModels() setContentView( - WorkflowLayout(this).apply { start(model.renderings, viewRegistry) } + WorkflowLayout(this).apply { take(model.renderings.map { it.withRegistry(viewRegistry) }) } ) lifecycleScope.launch { @@ -49,8 +50,7 @@ class RavenActivity : AppCompatActivity() { class RavenModel(savedState: SavedStateHandle) : ViewModel() { private val running = Job() - @OptIn(WorkflowUiExperimentalApi::class) - val renderings: StateFlow by lazy { + val renderings: StateFlow by lazy { renderWorkflowIn( workflow = PoemWorkflow, scope = viewModelScope, diff --git a/samples/containers/common/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailScreen.kt b/samples/containers/common/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailScreen.kt index fce395bfb8..a8ce8860c7 100644 --- a/samples/containers/common/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailScreen.kt +++ b/samples/containers/common/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailScreen.kt @@ -1,7 +1,8 @@ package com.squareup.sample.container.overviewdetail +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backstack.BackStackScreen +import com.squareup.workflow1.ui.container.BackStackScreen /** * Rendering type for overview / detail containers, with [BackStackScreen] in both roles. @@ -16,13 +17,13 @@ import com.squareup.workflow1.ui.backstack.BackStackScreen */ @OptIn(WorkflowUiExperimentalApi::class) class OverviewDetailScreen private constructor( - val overviewRendering: BackStackScreen, - val detailRendering: BackStackScreen? = null, + val overviewRendering: BackStackScreen, + val detailRendering: BackStackScreen? = null, val selectDefault: (() -> Unit)? = null -) { +) : Screen { constructor( - overviewRendering: BackStackScreen, - detailRendering: BackStackScreen + overviewRendering: BackStackScreen, + detailRendering: BackStackScreen ) : this(overviewRendering, detailRendering, null) /** @@ -30,12 +31,12 @@ class OverviewDetailScreen private constructor( * that a selection be made to fill a null [detailRendering]. */ constructor( - overviewRendering: BackStackScreen, + overviewRendering: BackStackScreen, selectDefault: (() -> Unit)? = null ) : this(overviewRendering, null, selectDefault) - operator fun component1(): BackStackScreen = overviewRendering - operator fun component2(): BackStackScreen? = detailRendering + operator fun component1(): BackStackScreen = overviewRendering + operator fun component2(): BackStackScreen? = detailRendering /** * Returns a new [OverviewDetailScreen] appending the [overviewRendering] and @@ -45,8 +46,8 @@ class OverviewDetailScreen private constructor( operator fun plus(other: OverviewDetailScreen): OverviewDetailScreen { val newOverview = overviewRendering + other.overviewRendering val newDetail = detailRendering - ?.let { it + other.detailRendering } - ?: other.detailRendering + ?.let { it + other.detailRendering } + ?: other.detailRendering return if (newDetail == null) OverviewDetailScreen(newOverview, other.selectDefault) else OverviewDetailScreen(newOverview, newDetail) @@ -59,8 +60,8 @@ class OverviewDetailScreen private constructor( other as OverviewDetailScreen return overviewRendering == other.overviewRendering && - detailRendering == other.detailRendering && - selectDefault == other.selectDefault + detailRendering == other.detailRendering && + selectDefault == other.selectDefault } override fun hashCode(): Int { @@ -72,7 +73,7 @@ class OverviewDetailScreen private constructor( override fun toString(): String { return "OverviewDetailScreen(overviewRendering=$overviewRendering, " + - "detailRendering=$detailRendering, " + - "selectDefault=$selectDefault)" + "detailRendering=$detailRendering, " + + "selectDefault=$selectDefault)" } } diff --git a/samples/containers/common/src/main/java/com/squareup/sample/container/panel/PanelContainerScreen.kt b/samples/containers/common/src/main/java/com/squareup/sample/container/panel/PanelContainerScreen.kt index c9a22e0bf9..9743298435 100644 --- a/samples/containers/common/src/main/java/com/squareup/sample/container/panel/PanelContainerScreen.kt +++ b/samples/containers/common/src/main/java/com/squareup/sample/container/panel/PanelContainerScreen.kt @@ -1,7 +1,8 @@ package com.squareup.sample.container.panel +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backstack.BackStackScreen +import com.squareup.workflow1.ui.container.BackStackScreen import com.squareup.workflow1.ui.modal.HasModals /** @@ -22,14 +23,14 @@ import com.squareup.workflow1.ui.modal.HasModals * tasks which take multiple steps and involve going backward and forward. */ @OptIn(WorkflowUiExperimentalApi::class) -data class PanelContainerScreen constructor( +data class PanelContainerScreen constructor( val baseScreen: B, override val modals: List> = emptyList() -) : HasModals, BackStackScreen> { +) : Screen, HasModals, BackStackScreen> { override val beneathModals: ScrimContainerScreen get() = ScrimContainerScreen( - wrapped = baseScreen, - dimmed = modals.isNotEmpty() + wrapped = baseScreen, + dimmed = modals.isNotEmpty() ) } @@ -37,7 +38,9 @@ data class PanelContainerScreen constructor( * Shows the receiving [BackStackScreen] in the only panel over [baseScreen]. */ @OptIn(WorkflowUiExperimentalApi::class) -fun BackStackScreen.inPanelOver(baseScreen: B): PanelContainerScreen { +fun BackStackScreen.inPanelOver( + baseScreen: B +): PanelContainerScreen { return PanelContainerScreen(baseScreen, listOf(this)) } @@ -45,6 +48,6 @@ fun BackStackScreen.inPanelOver(baseScreen: B): PanelConta * Shows the receiver as the only panel over [baseScreen], with no back stack. */ @OptIn(WorkflowUiExperimentalApi::class) -fun T.firstInPanelOver(baseScreen: B): PanelContainerScreen { +fun T.firstInPanelOver(baseScreen: B): PanelContainerScreen { return BackStackScreen(this, emptyList()).inPanelOver(baseScreen) } diff --git a/samples/containers/common/src/main/java/com/squareup/sample/container/panel/ScrimContainerScreen.kt b/samples/containers/common/src/main/java/com/squareup/sample/container/panel/ScrimContainerScreen.kt index eb5f6b202d..ae5e86b6aa 100644 --- a/samples/containers/common/src/main/java/com/squareup/sample/container/panel/ScrimContainerScreen.kt +++ b/samples/containers/common/src/main/java/com/squareup/sample/container/panel/ScrimContainerScreen.kt @@ -1,10 +1,14 @@ package com.squareup.sample.container.panel +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + /** * Show a scrim over the [wrapped] item, which is invisible if [dimmed] is false, * dark if it is true. */ -class ScrimContainerScreen( +@OptIn(WorkflowUiExperimentalApi::class) +class ScrimContainerScreen( val wrapped: T, val dimmed: Boolean -) +) : Screen diff --git a/samples/containers/common/src/test/java/com/squareup/sample/container/overviewdetail/OverviewDetailScreenTest.kt b/samples/containers/common/src/test/java/com/squareup/sample/container/overviewdetail/OverviewDetailScreenTest.kt index cfdd58e783..f65601ed88 100644 --- a/samples/containers/common/src/test/java/com/squareup/sample/container/overviewdetail/OverviewDetailScreenTest.kt +++ b/samples/containers/common/src/test/java/com/squareup/sample/container/overviewdetail/OverviewDetailScreenTest.kt @@ -1,64 +1,68 @@ package com.squareup.sample.container.overviewdetail import com.google.common.truth.Truth.assertThat +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backstack.BackStackScreen +import com.squareup.workflow1.ui.container.BackStackScreen import org.junit.Test @OptIn(WorkflowUiExperimentalApi::class) -class OverviewDetailScreenTest { +internal class OverviewDetailScreenTest { + data class S(val value: T) : Screen @Test fun `minimal structure`() { - val screen = OverviewDetailScreen(BackStackScreen(1)) - assertThat(screen.overviewRendering).isEqualTo(BackStackScreen(1)) + val screen = OverviewDetailScreen(BackStackScreen(S(1))) + assertThat(screen.overviewRendering).isEqualTo(BackStackScreen(S(1))) assertThat(screen.detailRendering).isNull() assertThat(screen.selectDefault).isNull() } @Test fun `minimal equality`() { - val screen = OverviewDetailScreen(BackStackScreen(1)) - assertThat(screen).isEqualTo(OverviewDetailScreen(BackStackScreen(1))) - assertThat(screen).isNotEqualTo(OverviewDetailScreen(BackStackScreen(2))) + val screen = OverviewDetailScreen(BackStackScreen(S(1))) + assertThat(screen).isEqualTo(OverviewDetailScreen(BackStackScreen(S(1)))) + assertThat(screen).isNotEqualTo(OverviewDetailScreen(BackStackScreen(S(2)))) } @Test fun `minimal hash`() { - val screen = OverviewDetailScreen(BackStackScreen(1)) - assertThat(screen.hashCode()).isEqualTo(OverviewDetailScreen(BackStackScreen(1)).hashCode()) - assertThat(screen.hashCode()).isNotEqualTo(OverviewDetailScreen(BackStackScreen(2)).hashCode()) + val screen = OverviewDetailScreen(BackStackScreen(S(1))) + assertThat(screen.hashCode()).isEqualTo(OverviewDetailScreen(BackStackScreen(S(1))).hashCode()) + assertThat(screen.hashCode()) + .isNotEqualTo(OverviewDetailScreen(BackStackScreen(S(2))).hashCode()) } @Test fun `combine minimal`() { - val left = OverviewDetailScreen(BackStackScreen(1, 2)) - val right = OverviewDetailScreen(BackStackScreen(11, 12)) + val left = OverviewDetailScreen(BackStackScreen(S(1), S(2))) + val right = OverviewDetailScreen(BackStackScreen(S(11), S(12))) - assertThat(left + right).isEqualTo(OverviewDetailScreen(BackStackScreen(1, 2, 11, 12))) + assertThat(left + right) + .isEqualTo(OverviewDetailScreen(BackStackScreen(S(1), S(2), S(11), S(12)))) } @Test fun `full structure`() { val screen = OverviewDetailScreen( - overviewRendering = BackStackScreen(1, 2), - detailRendering = BackStackScreen(3, 4) + overviewRendering = BackStackScreen(S(1), S(2)), + detailRendering = BackStackScreen(S(3), S(4)) ) - assertThat(screen.overviewRendering).isEqualTo(BackStackScreen(1, 2)) - assertThat(screen.detailRendering).isEqualTo(BackStackScreen(3, 4)) + assertThat(screen.overviewRendering).isEqualTo(BackStackScreen(S(1), S(2))) + assertThat(screen.detailRendering).isEqualTo(BackStackScreen(S(3), S(4))) assertThat(screen.selectDefault).isNull() } @Test fun `full equality`() { val screen1 = OverviewDetailScreen( - overviewRendering = BackStackScreen(1, 2), - detailRendering = BackStackScreen(3, 4) + overviewRendering = BackStackScreen(S(1), S(2)), + detailRendering = BackStackScreen(S(3), S(4)) ) val screen2 = OverviewDetailScreen( - overviewRendering = BackStackScreen(1, 2), - detailRendering = BackStackScreen(3, 4) + overviewRendering = BackStackScreen(S(1), S(2)), + detailRendering = BackStackScreen(S(3), S(4)) ) val screen3 = OverviewDetailScreen( - overviewRendering = BackStackScreen(1, 2), - detailRendering = BackStackScreen(3, 4, 5) + overviewRendering = BackStackScreen(S(1), S(2)), + detailRendering = BackStackScreen(S(3), S(4), S(5)) ) assertThat(screen1).isEqualTo(screen2) @@ -67,18 +71,18 @@ class OverviewDetailScreenTest { @Test fun `full hash`() { val screen1 = OverviewDetailScreen( - overviewRendering = BackStackScreen(1, 2), - detailRendering = BackStackScreen(3, 4) + overviewRendering = BackStackScreen(S(1), S(2)), + detailRendering = BackStackScreen(S(3), S(4)) ) val screen2 = OverviewDetailScreen( - overviewRendering = BackStackScreen(1, 2), - detailRendering = BackStackScreen(3, 4) + overviewRendering = BackStackScreen(S(1), S(2)), + detailRendering = BackStackScreen(S(3), S(4)) ) val screen3 = OverviewDetailScreen( - overviewRendering = BackStackScreen(1, 2), - detailRendering = BackStackScreen(3, 4, 5) + overviewRendering = BackStackScreen(S(1), S(2)), + detailRendering = BackStackScreen(S(3), S(4), S(5)) ) assertThat(screen1.hashCode()).isEqualTo(screen2.hashCode()) @@ -87,18 +91,18 @@ class OverviewDetailScreenTest { @Test fun `combine full`() { val left = OverviewDetailScreen( - overviewRendering = BackStackScreen(1, 2), - detailRendering = BackStackScreen(3, 4) + overviewRendering = BackStackScreen(S(1), S(2)), + detailRendering = BackStackScreen(S(3), S(4)) ) val right = OverviewDetailScreen( - overviewRendering = BackStackScreen(11, 12), - detailRendering = BackStackScreen(13, 14) + overviewRendering = BackStackScreen(S(11), S(12)), + detailRendering = BackStackScreen(S(13), S(14)) ) assertThat(left + right).isEqualTo( OverviewDetailScreen( - overviewRendering = BackStackScreen(1, 2, 11, 12), - detailRendering = BackStackScreen(3, 4, 13, 14) + overviewRendering = BackStackScreen(S(1), S(2), S(11), S(12)), + detailRendering = BackStackScreen(S(3), S(4), S(13), S(14)) ) ) } @@ -106,11 +110,11 @@ class OverviewDetailScreenTest { @Test fun `selectDefault structure`() { val selectDefault = {} val screen = OverviewDetailScreen( - overviewRendering = BackStackScreen(1, 2), + overviewRendering = BackStackScreen(S(1), S(2)), selectDefault = selectDefault ) - assertThat(screen.overviewRendering).isEqualTo(BackStackScreen(1, 2)) + assertThat(screen.overviewRendering).isEqualTo(BackStackScreen(S(1), S(2))) assertThat(screen.detailRendering).isNull() assertThat(screen.selectDefault).isEqualTo(selectDefault) } @@ -119,17 +123,17 @@ class OverviewDetailScreenTest { val selectDefault = {} val screen1 = OverviewDetailScreen( - overviewRendering = BackStackScreen(1, 2), + overviewRendering = BackStackScreen(S(1), S(2)), selectDefault = selectDefault ) val screen2 = OverviewDetailScreen( - overviewRendering = BackStackScreen(1, 2), + overviewRendering = BackStackScreen(S(1), S(2)), selectDefault = selectDefault ) val screen3 = OverviewDetailScreen( - overviewRendering = BackStackScreen(1, 2), + overviewRendering = BackStackScreen(S(1), S(2)), selectDefault = {} ) @@ -141,17 +145,17 @@ class OverviewDetailScreenTest { val selectDefault = {} val screen1 = OverviewDetailScreen( - overviewRendering = BackStackScreen(1, 2), + overviewRendering = BackStackScreen(S(1), S(2)), selectDefault = selectDefault ) val screen2 = OverviewDetailScreen( - overviewRendering = BackStackScreen(1, 2), + overviewRendering = BackStackScreen(S(1), S(2)), selectDefault = selectDefault ) val screen3 = OverviewDetailScreen( - overviewRendering = BackStackScreen(1, 2), + overviewRendering = BackStackScreen(S(1), S(2)), selectDefault = {} ) @@ -162,18 +166,18 @@ class OverviewDetailScreenTest { @Test fun `combine selectDefault`() { val selectDefaultLeft = {} val left = OverviewDetailScreen( - overviewRendering = BackStackScreen(1, 2), + overviewRendering = BackStackScreen(S(1), S(2)), selectDefault = selectDefaultLeft ) val selectDefaultRight = {} val right = OverviewDetailScreen( - overviewRendering = BackStackScreen(11, 12), + overviewRendering = BackStackScreen(S(11), S(12)), selectDefault = selectDefaultRight ) assertThat(left + right).isEqualTo( OverviewDetailScreen( - overviewRendering = BackStackScreen(1, 2, 11, 12), + overviewRendering = BackStackScreen(S(1), S(2), S(11), S(12)), selectDefault = selectDefaultRight ) ) diff --git a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonActivity.kt b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonActivity.kt index 084658a45a..a324a88f1a 100644 --- a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonActivity.kt +++ b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonActivity.kt @@ -1,3 +1,5 @@ +@file:OptIn(WorkflowUiExperimentalApi::class) + package com.squareup.sample.hellobackbutton import android.os.Bundle @@ -8,26 +10,27 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewModelScope import com.squareup.sample.container.SampleContainers +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowLayout import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.container.withRegistry import com.squareup.workflow1.ui.modal.AlertContainer import com.squareup.workflow1.ui.plus import com.squareup.workflow1.ui.renderWorkflowIn import kotlinx.coroutines.Job import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -@OptIn(WorkflowUiExperimentalApi::class) private val viewRegistry = SampleContainers + AlertContainer class HelloBackButtonActivity : AppCompatActivity() { - @OptIn(WorkflowUiExperimentalApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val model: HelloBackButtonModel by viewModels() setContentView( - WorkflowLayout(this).apply { start(model.renderings, viewRegistry) } + WorkflowLayout(this).apply { take(model.renderings.map { it.withRegistry(viewRegistry) }) } ) lifecycleScope.launch { @@ -40,8 +43,7 @@ class HelloBackButtonActivity : AppCompatActivity() { class HelloBackButtonModel(savedState: SavedStateHandle) : ViewModel() { private val running = Job() - @OptIn(WorkflowUiExperimentalApi::class) - val renderings: StateFlow by lazy { + val renderings: StateFlow by lazy { renderWorkflowIn( workflow = AreYouSureWorkflow, scope = viewModelScope, diff --git a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonLayoutRunner.kt b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonLayoutRunner.kt index c8d81522ec..5250b429f2 100644 --- a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonLayoutRunner.kt +++ b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonLayoutRunner.kt @@ -2,30 +2,30 @@ package com.squareup.sample.hellobackbutton import android.view.View import android.widget.TextView -import com.squareup.workflow1.ui.AndroidViewRendering -import com.squareup.workflow1.ui.LayoutRunner +import com.squareup.workflow1.ui.AndroidScreen +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewRunner import com.squareup.workflow1.ui.ViewEnvironment -import com.squareup.workflow1.ui.ViewFactory import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.backPressedHandler @OptIn(WorkflowUiExperimentalApi::class) -data class HelloBackButtonRendering( +data class HelloBackButtonScreen( val message: String, val onClick: () -> Unit, val onBackPressed: (() -> Unit)? -) : AndroidViewRendering { - override val viewFactory: ViewFactory = LayoutRunner.bind( +) : AndroidScreen { + override val viewFactory: ScreenViewFactory = ScreenViewRunner.bind( R.layout.hello_back_button_layout, ::HelloBackButtonLayoutRunner ) } @OptIn(WorkflowUiExperimentalApi::class) -private class HelloBackButtonLayoutRunner(view: View) : LayoutRunner { +private class HelloBackButtonLayoutRunner(view: View) : ScreenViewRunner { private val messageView: TextView = view.findViewById(R.id.hello_message) override fun showRendering( - rendering: HelloBackButtonRendering, + rendering: HelloBackButtonScreen, viewEnvironment: ViewEnvironment ) { messageView.text = rendering.message diff --git a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonWorkflow.kt b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonWorkflow.kt index a8f28e7117..8f9524cf1d 100644 --- a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonWorkflow.kt +++ b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonWorkflow.kt @@ -15,7 +15,7 @@ object HelloBackButtonWorkflow : StatefulWorkflow< Unit, State, Nothing, - HelloBackButtonRendering + HelloBackButtonScreen >() { @Parcelize @@ -34,8 +34,8 @@ object HelloBackButtonWorkflow : StatefulWorkflow< renderProps: Unit, renderState: State, context: RenderContext - ): HelloBackButtonRendering { - return HelloBackButtonRendering( + ): HelloBackButtonScreen { + return HelloBackButtonScreen( message = "$renderState", onClick = context.eventHandler { state = when (state) { diff --git a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoemWorkflow.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoemWorkflow.kt index 9fd63ae03e..af3d9bb4d3 100644 --- a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoemWorkflow.kt +++ b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoemWorkflow.kt @@ -16,13 +16,14 @@ import com.squareup.workflow1.StatefulWorkflow import com.squareup.workflow1.WorkflowAction import com.squareup.workflow1.WorkflowAction.Companion.noAction import com.squareup.workflow1.parse +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backstack.BackStackScreen -import com.squareup.workflow1.ui.backstack.toBackStackScreen +import com.squareup.workflow1.ui.container.BackStackScreen +import com.squareup.workflow1.ui.container.toBackStackScreen /** - * Renders a [Poem] as a [OverviewDetailScreen], whose overview is a [StanzaListRendering] - * for the poem, and whose detail traverses through [StanzaRendering]s. + * Renders a [Poem] as a [OverviewDetailScreen], whose overview is a [StanzaListScreen] + * for the poem, and whose detail traverses through [StanzaScreen]s. */ object PoemWorkflow : StatefulWorkflow() { object ClosePoem @@ -41,7 +42,7 @@ object PoemWorkflow : StatefulWorkflow = + val previousStanzas: List = if (renderState == -1) emptyList() else renderProps.stanzas.subList(0, renderState) .mapIndexed { index, _ -> @@ -66,7 +67,7 @@ object PoemWorkflow : StatefulWorkflow() + (previousStanzas + visibleStanza).toBackStackScreen() } val stanzaIndex = @@ -74,7 +75,7 @@ object PoemWorkflow : StatefulWorkflow(it) } + .let { BackStackScreen(it) } return stackedStanzas ?.let { OverviewDetailScreen(overviewRendering = stanzaIndex, detailRendering = it) } diff --git a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListRendering.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListScreen.kt similarity index 82% rename from samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListRendering.kt rename to samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListScreen.kt index 3ce403f503..ec5ebe70c6 100644 --- a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListRendering.kt +++ b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListScreen.kt @@ -10,32 +10,32 @@ import androidx.recyclerview.widget.RecyclerView import com.squareup.sample.container.overviewdetail.OverviewDetailConfig import com.squareup.sample.container.overviewdetail.OverviewDetailConfig.Overview import com.squareup.sample.container.poetry.R -import com.squareup.workflow1.ui.AndroidViewRendering -import com.squareup.workflow1.ui.LayoutRunner +import com.squareup.workflow1.ui.AndroidScreen +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewRunner import com.squareup.workflow1.ui.ViewEnvironment -import com.squareup.workflow1.ui.ViewFactory import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.backPressedHandler -import com.squareup.workflow1.ui.backstack.BackStackConfig -import com.squareup.workflow1.ui.backstack.BackStackConfig.Other +import com.squareup.workflow1.ui.container.BackStackConfig +import com.squareup.workflow1.ui.container.BackStackConfig.Other @OptIn(WorkflowUiExperimentalApi::class) -data class StanzaListRendering( +data class StanzaListScreen( val title: String, val subtitle: String, val firstLines: List, val onStanzaSelected: (Int) -> Unit, val onExit: () -> Unit, val selection: Int = -1 -) : AndroidViewRendering { - override val viewFactory: ViewFactory = LayoutRunner.bind( +) : AndroidScreen { + override val viewFactory: ScreenViewFactory = ScreenViewRunner.bind( R.layout.list, ::StanzaListLayoutRunner ) } @OptIn(WorkflowUiExperimentalApi::class) -private class StanzaListLayoutRunner(view: View) : LayoutRunner { +private class StanzaListLayoutRunner(view: View) : ScreenViewRunner { private val toolbar = view.findViewById(R.id.list_toolbar) private val recyclerView = view.findViewById(R.id.list_body) .apply { layoutManager = LinearLayoutManager(context) } @@ -43,7 +43,7 @@ private class StanzaListLayoutRunner(view: View) : LayoutRunner() { - lateinit var view: StanzaListRendering + lateinit var view: StanzaListScreen lateinit var environment: ViewEnvironment override fun onCreateViewHolder( diff --git a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListWorkflow.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListWorkflow.kt index b111760192..425adc179d 100644 --- a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListWorkflow.kt +++ b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListWorkflow.kt @@ -8,13 +8,13 @@ import com.squareup.workflow1.StatelessWorkflow * * Output is the index of a clicked stanza, or -1 on exit. */ -object StanzaListWorkflow : StatelessWorkflow() { +object StanzaListWorkflow : StatelessWorkflow() { override fun render( renderProps: Poem, context: RenderContext - ): StanzaListRendering { - return StanzaListRendering( + ): StanzaListScreen { + return StanzaListScreen( title = renderProps.title, subtitle = renderProps.poet.fullName, firstLines = renderProps.initialStanzas, diff --git a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaRendering.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaScreen.kt similarity index 84% rename from samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaRendering.kt rename to samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaScreen.kt index c429755257..7f3bdf8a57 100644 --- a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaRendering.kt +++ b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaScreen.kt @@ -11,35 +11,35 @@ import androidx.appcompat.widget.Toolbar import com.squareup.sample.container.overviewdetail.OverviewDetailConfig import com.squareup.sample.container.overviewdetail.OverviewDetailConfig.Detail import com.squareup.sample.container.poetry.R -import com.squareup.workflow1.ui.AndroidViewRendering +import com.squareup.workflow1.ui.AndroidScreen import com.squareup.workflow1.ui.Compatible -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.LayoutRunner -import com.squareup.workflow1.ui.ViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewRunner import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.backPressedHandler -import com.squareup.workflow1.ui.backstack.BackStackConfig -import com.squareup.workflow1.ui.backstack.BackStackConfig.None +import com.squareup.workflow1.ui.container.BackStackConfig +import com.squareup.workflow1.ui.container.BackStackConfig.None @OptIn(WorkflowUiExperimentalApi::class) -data class StanzaRendering( +data class StanzaScreen( val title: String, val stanzaNumber: Int, val lines: List, val onGoUp: () -> Unit, val onGoBack: (() -> Unit)? = null, val onGoForth: (() -> Unit)? = null -) : AndroidViewRendering, Compatible { +) : AndroidScreen, Compatible { override val compatibilityKey = "$title: $stanzaNumber" - override val viewFactory: ViewFactory = LayoutRunner.bind( + override val viewFactory: ScreenViewFactory = ScreenViewRunner.bind( R.layout.stanza_layout, ::StanzaLayoutRunner ) } @OptIn(WorkflowUiExperimentalApi::class) -private class StanzaLayoutRunner(private val view: View) : LayoutRunner { +private class StanzaLayoutRunner(private val view: View) : ScreenViewRunner { private val tabSize = TypedValue .applyDimension(TypedValue.COMPLEX_UNIT_SP, 24f, view.resources.displayMetrics) .toInt() @@ -52,7 +52,7 @@ private class StanzaLayoutRunner(private val view: View) : LayoutRunner(R.id.stanza_back) override fun showRendering( - rendering: StanzaRendering, + rendering: StanzaScreen, viewEnvironment: ViewEnvironment ) { if (viewEnvironment[OverviewDetailConfig] == Detail) { @@ -114,7 +114,7 @@ private class StanzaLayoutRunner(private val view: View) : LayoutRunner by LayoutRunner.bind( + companion object : ScreenViewFactory by ScreenViewRunner.bind( R.layout.stanza_layout, ::StanzaLayoutRunner ) diff --git a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaWorkflow.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaWorkflow.kt index d3333ebdd6..9a9147cfaf 100644 --- a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaWorkflow.kt +++ b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaWorkflow.kt @@ -8,7 +8,7 @@ import com.squareup.sample.poetry.StanzaWorkflow.Props import com.squareup.sample.poetry.model.Poem import com.squareup.workflow1.StatelessWorkflow -object StanzaWorkflow : StatelessWorkflow() { +object StanzaWorkflow : StatelessWorkflow() { data class Props( val poem: Poem, val index: Int @@ -23,7 +23,7 @@ object StanzaWorkflow : StatelessWorkflow() { override fun render( renderProps: Props, context: RenderContext - ): StanzaRendering { + ): StanzaScreen { with(renderProps) { val onGoBack: (() -> Unit)? = when (index) { 0 -> null @@ -39,7 +39,7 @@ object StanzaWorkflow : StatelessWorkflow() { } } - return StanzaRendering( + return StanzaScreen( onGoUp = context.eventHandler { setOutput(CloseStanzas) }, title = poem.title, stanzaNumber = index + 1, 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 8dff8f2d3b..bdb070c080 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,9 +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.BuilderViewFactory +import com.squareup.workflow1.ui.ManualScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.ViewFactory import com.squareup.workflow1.ui.bindShowRendering import kotlin.math.abs import kotlin.math.min @@ -83,10 +83,10 @@ class BoardView(context: Context) : View(context) { } @OptIn(WorkflowUiExperimentalApi::class) - companion object : ViewFactory by BuilderViewFactory( - type = Board::class, - viewConstructor = { initialRendering, initialEnv, contextForNewView, _ -> - BoardView(contextForNewView) - .apply { bindShowRendering(initialRendering, initialEnv) { r, _ -> update(r) } } - }) + companion object : ScreenViewFactory by ManualScreenViewFactory( + type = Board::class, + viewConstructor = { initialRendering, initialEnv, contextForNewView, _ -> + BoardView(contextForNewView) + .apply { bindShowRendering(initialRendering, initialEnv) { r, _ -> 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/BoardsListLayoutRunner.kt index 240e15d8ad..24db11abde 100644 --- a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardsListLayoutRunner.kt +++ b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardsListLayoutRunner.kt @@ -10,10 +10,10 @@ import com.squareup.cycler.Recycler import com.squareup.cycler.toDataSource import com.squareup.sample.dungeon.DungeonAppWorkflow.DisplayBoardsListScreen import com.squareup.sample.dungeon.board.Board -import com.squareup.workflow1.ui.LayoutRunner -import com.squareup.workflow1.ui.LayoutRunner.Companion.bind +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bind import com.squareup.workflow1.ui.ViewEnvironment -import com.squareup.workflow1.ui.ViewFactory import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub @@ -24,7 +24,7 @@ import com.squareup.workflow1.ui.WorkflowViewStub * a `RecyclerView`. */ @OptIn(WorkflowUiExperimentalApi::class) -class BoardsListLayoutRunner(rootView: View) : LayoutRunner { +class BoardsListLayoutRunner(rootView: View) : ScreenViewRunner { /** * Used to associate a single [ViewEnvironment] and [DisplayBoardsListScreen.onBoardSelected] @@ -48,13 +48,13 @@ class BoardsListLayoutRunner(rootView: View) : LayoutRunner 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 LayoutRunner as the actual + // The board preview is actually rendered using the same ScreenViewRunner 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) boardNameView.text = item.board.metadata.name - boardPreviewView.update(item.board, item.viewEnvironment) + boardPreviewView.show(item.board, item.viewEnvironment) // Gratuitous, hacky, inline test of WorkflowViewStub features. check(boardPreviewView.actual.visibility == INVISIBLE) { @@ -101,7 +101,7 @@ class BoardsListLayoutRunner(rootView: View) : LayoutRunner by bind( + companion object : ScreenViewFactory by bind( R.layout.boards_list_layout, ::BoardsListLayoutRunner ) } diff --git a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/DungeonActivity.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/DungeonActivity.kt index 4ce4b130b1..51c9612564 100644 --- a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/DungeonActivity.kt +++ b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/DungeonActivity.kt @@ -5,6 +5,8 @@ import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import com.squareup.workflow1.ui.WorkflowLayout import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.container.withRegistry +import kotlinx.coroutines.flow.map class DungeonActivity : AppCompatActivity() { @@ -16,8 +18,9 @@ class DungeonActivity : AppCompatActivity() { val component = Component(this) val model: TimeMachineModel by viewModels { component.timeMachineModelFactory } - setContentView( - WorkflowLayout(this).apply { start(model.renderings, component.viewRegistry) } - ) + val contentView = WorkflowLayout(this).apply { + take(model.renderings.map { it.withRegistry(component.viewRegistry) }) + } + setContentView(contentView) } } diff --git a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/DungeonAppWorkflow.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/DungeonAppWorkflow.kt index 01490e2693..5aa3973fcd 100644 --- a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/DungeonAppWorkflow.kt +++ b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/DungeonAppWorkflow.kt @@ -11,6 +11,7 @@ import com.squareup.workflow1.StatefulWorkflow import com.squareup.workflow1.action import com.squareup.workflow1.renderChild import com.squareup.workflow1.runningWorker +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.modal.AlertContainerScreen @@ -23,7 +24,7 @@ class DungeonAppWorkflow( data class Props(val paused: Boolean = false) sealed class State { - object LoadingBoardList : State() + object LoadingBoardList : State(), Screen data class ChoosingBoard(val boards: List>) : State() data class PlayingGame(val boardPath: BoardPath) : State() } @@ -31,7 +32,7 @@ class DungeonAppWorkflow( data class DisplayBoardsListScreen( val boards: List, val onBoardSelected: (index: Int) -> Unit - ) + ) : Screen override fun initialState( props: Props, diff --git a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameLayoutRunner.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameLayoutRunner.kt index 84cdf4e456..7a096283f1 100644 --- a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameLayoutRunner.kt +++ b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameLayoutRunner.kt @@ -1,5 +1,6 @@ package com.squareup.sample.dungeon +import android.annotation.SuppressLint import android.view.MotionEvent.ACTION_DOWN import android.view.MotionEvent.ACTION_MASK import android.view.MotionEvent.ACTION_UP @@ -9,11 +10,11 @@ import com.squareup.sample.dungeon.Direction.LEFT 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.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.LayoutRunner -import com.squareup.workflow1.ui.LayoutRunner.Companion.bind -import com.squareup.workflow1.ui.ViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bind import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub /** @@ -21,7 +22,7 @@ import com.squareup.workflow1.ui.WorkflowViewStub * the player. */ @OptIn(WorkflowUiExperimentalApi::class) -class GameLayoutRunner(view: View) : LayoutRunner { +class GameLayoutRunner(view: View) : ScreenViewRunner { private val boardView: WorkflowViewStub = view.findViewById(R.id.board_stub) private val moveLeft: View = view.findViewById(R.id.move_left) @@ -42,7 +43,7 @@ class GameLayoutRunner(view: View) : LayoutRunner { rendering: GameRendering, viewEnvironment: ViewEnvironment ) { - boardView.update(rendering.board, viewEnvironment) + boardView.show(rendering.board, viewEnvironment) this.rendering = rendering // Disable the views if we don't have an event handler, e.g. when the game has finished. @@ -53,6 +54,7 @@ class GameLayoutRunner(view: View) : LayoutRunner { moveDown.isEnabled = controlsEnabled } + @SuppressLint("ClickableViewAccessibility") private fun View.registerPlayerEventHandlers(direction: Direction) { setOnTouchListener { _, motionEvent -> when (motionEvent.action and ACTION_MASK) { @@ -64,7 +66,7 @@ class GameLayoutRunner(view: View) : LayoutRunner { } } - companion object : ViewFactory by bind( + companion object : ScreenViewFactory by bind( R.layout.game_layout, ::GameLayoutRunner ) } diff --git a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameSessionWorkflow.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameSessionWorkflow.kt index 9ad76f98dd..f655f455c7 100644 --- a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameSessionWorkflow.kt +++ b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameSessionWorkflow.kt @@ -15,6 +15,7 @@ import com.squareup.workflow1.WorkflowAction import com.squareup.workflow1.WorkflowAction.Companion.noAction import com.squareup.workflow1.action import com.squareup.workflow1.runningWorker +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.modal.AlertContainerScreen import com.squareup.workflow1.ui.modal.AlertScreen @@ -38,7 +39,7 @@ class GameSessionWorkflow( ) sealed class State { - object Loading : State() + object Loading : State(), Screen data class Running(val board: Board) : State() data class GameOver(val board: Board) : State() } 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 8f1a1b3cef..019c97c2dc 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 @@ -1,13 +1,16 @@ +@file:OptIn(WorkflowUiExperimentalApi::class) + package com.squareup.sample.dungeon import android.view.View import android.widget.TextView import androidx.annotation.StringRes -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.LayoutRunner -import com.squareup.workflow1.ui.LayoutRunner.Companion.bind -import com.squareup.workflow1.ui.ViewFactory +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.ViewEnvironment +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi /** * Factory function for [ViewFactory]s that show a full-screen loading indicator with some text @@ -16,19 +19,17 @@ import com.squareup.workflow1.ui.ViewEnvironment * The binding is parameterized on two things: the type of the rendering that this binding is * keyed off of, and the resource ID of the string to use for the label. */ -@OptIn(WorkflowUiExperimentalApi::class) @Suppress("FunctionName") -inline fun LoadingBinding( +inline fun LoadingBinding( @StringRes loadingLabelRes: Int -): ViewFactory = - bind(R.layout.loading_layout) { view -> LoadingLayoutRunner(loadingLabelRes, view) } +): ScreenViewFactory = + bind(R.layout.loading_layout) { view -> LoadingLayoutRunner(loadingLabelRes, view) } -@OptIn(WorkflowUiExperimentalApi::class) @PublishedApi -internal class LoadingLayoutRunner( +internal class LoadingLayoutRunner( @StringRes private val labelRes: Int, view: View -) : LayoutRunner { +) : ScreenViewRunner { init { view.findViewById(R.id.loading_label) diff --git a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/TimeMachineAppWorkflow.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/TimeMachineAppWorkflow.kt index b6fb62ca70..eb011836a0 100644 --- a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/TimeMachineAppWorkflow.kt +++ b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/TimeMachineAppWorkflow.kt @@ -3,7 +3,7 @@ package com.squareup.sample.dungeon import android.content.Context import com.squareup.sample.dungeon.DungeonAppWorkflow.Props import com.squareup.sample.timemachine.TimeMachineWorkflow -import com.squareup.sample.timemachine.shakeable.ShakeableTimeMachineRendering +import com.squareup.sample.timemachine.shakeable.ShakeableTimeMachineScreen import com.squareup.sample.timemachine.shakeable.ShakeableTimeMachineWorkflow import com.squareup.sample.timemachine.shakeable.ShakeableTimeMachineWorkflow.PropsFactory import com.squareup.workflow1.StatelessWorkflow @@ -20,7 +20,7 @@ class TimeMachineAppWorkflow( appWorkflow: DungeonAppWorkflow, clock: TimeSource, context: Context -) : StatelessWorkflow() { +) : StatelessWorkflow() { private val timeMachineWorkflow = ShakeableTimeMachineWorkflow(TimeMachineWorkflow(appWorkflow, clock), context) @@ -28,7 +28,7 @@ class TimeMachineAppWorkflow( override fun render( renderProps: BoardPath, context: RenderContext - ): ShakeableTimeMachineRendering { + ): ShakeableTimeMachineScreen { val propsFactory = PropsFactory { recording -> Props(paused = !recording) } diff --git a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/TimeMachineModel.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/TimeMachineModel.kt index 0c231feaf6..992f3cdbc0 100644 --- a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/TimeMachineModel.kt +++ b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/TimeMachineModel.kt @@ -6,17 +6,20 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.savedstate.SavedStateRegistryOwner import com.squareup.workflow1.diagnostic.tracing.TracingWorkflowInterceptor +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.renderWorkflowIn import kotlinx.coroutines.flow.StateFlow import java.io.File +import kotlin.time.ExperimentalTime class TimeMachineModel( private val savedState: SavedStateHandle, private val workflow: TimeMachineAppWorkflow, private val traceFilesDir: File ) : ViewModel() { - val renderings: StateFlow by lazy { + @OptIn(WorkflowUiExperimentalApi::class, ExperimentalTime::class) + val renderings: StateFlow by lazy { val traceFile = traceFilesDir.resolve("workflow-trace-dungeon.json") @OptIn(WorkflowUiExperimentalApi::class) diff --git a/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/GameWorkflow.kt b/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/GameWorkflow.kt index 4c9c11ac2e..56932e1def 100644 --- a/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/GameWorkflow.kt +++ b/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/GameWorkflow.kt @@ -21,6 +21,8 @@ import com.squareup.workflow1.Worker import com.squareup.workflow1.action import com.squareup.workflow1.renderChild import com.squareup.workflow1.runningWorker +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -60,12 +62,13 @@ class GameWorkflow( object PlayerWasEaten : Output() } + @OptIn(WorkflowUiExperimentalApi::class) data class GameRendering( val board: Board, val gameOver: Boolean = false, val onStartMoving: (Direction) -> Unit, val onStopMoving: (Direction) -> Unit - ) + ) : Screen override fun initialState( props: Props, diff --git a/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/board/Board.kt b/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/board/Board.kt index a459bf772b..9ea482883d 100644 --- a/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/board/Board.kt +++ b/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/board/Board.kt @@ -1,5 +1,7 @@ package com.squareup.sample.dungeon.board +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import kotlinx.serialization.Serializable /** @@ -17,12 +19,13 @@ data class BoardMetadata(val name: String) * * @see parseBoard */ +@OptIn(WorkflowUiExperimentalApi::class) data class Board( val metadata: BoardMetadata, val width: Int, val height: Int, val cells: List -) { +) : Screen { data class Location( val x: Int, val y: Int @@ -50,10 +53,10 @@ data class Board( override fun toString(): String { return cells.asSequence() - .chunked(width) - .joinToString(separator = "\n") { - it.joinToString(separator = "") - } + .chunked(width) + .joinToString(separator = "\n") { + it.joinToString(separator = "") + } } private fun cellIndexOf( @@ -70,9 +73,9 @@ data class Board( rows: List> ): Board { val width = rows.map { it.size } - .distinct() - .singleOrNull() - ?: throw IllegalArgumentException("Expected all rows to be the same length.") + .distinct() + .singleOrNull() + ?: throw IllegalArgumentException("Expected all rows to be the same length.") val height = rows.size require(width == height) { "Expected board to be square, but was $width × $height" } val cells = rows.reduce { acc, row -> acc + row } diff --git a/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineLayoutRunner.kt b/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineLayoutRunner.kt index f65d42daf1..c955a91404 100644 --- a/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineLayoutRunner.kt +++ b/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineLayoutRunner.kt @@ -8,10 +8,10 @@ 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.LayoutRunner -import com.squareup.workflow1.ui.LayoutRunner.Companion.bind +import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bind import com.squareup.workflow1.ui.ViewEnvironment -import com.squareup.workflow1.ui.ViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.WorkflowViewStub import com.squareup.workflow1.ui.backPressedHandler import kotlin.time.Duration @@ -19,12 +19,12 @@ import kotlin.time.ExperimentalTime /** * Renders [ShakeableTimeMachineWorkflow][ShakeableTimeMachineWorkflow] - * [renderings][ShakeableTimeMachineRendering]. + * [renderings][ShakeableTimeMachineScreen]. */ @OptIn(ExperimentalTime::class, WorkflowUiExperimentalApi::class) class ShakeableTimeMachineLayoutRunner( private val view: View -) : LayoutRunner { +) : ScreenViewRunner { private val glassView: GlassFrameLayout = view.findViewById(R.id.glass_view) private val childStub: WorkflowViewStub = view.findViewById(R.id.child_stub) @@ -39,7 +39,7 @@ class ShakeableTimeMachineLayoutRunner( } override fun showRendering( - rendering: ShakeableTimeMachineRendering, + rendering: ShakeableTimeMachineScreen, viewEnvironment: ViewEnvironment ) { // Only handle back presses explicitly if in playback mode. @@ -79,7 +79,7 @@ class ShakeableTimeMachineLayoutRunner( } // Show the child screen. - childStub.update(rendering.rendering, viewEnvironment) + childStub.show(rendering.rendering, viewEnvironment) } private fun Duration.toProgressInt(): Int = this.inWholeMilliseconds.toInt() @@ -87,7 +87,7 @@ class ShakeableTimeMachineLayoutRunner( private fun Duration.toUiString(): String = toString() - companion object : ViewFactory by bind( + companion object : ScreenViewFactory by bind( R.layout.shakeable_time_machine_layout, ::ShakeableTimeMachineLayoutRunner ) } diff --git a/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineRendering.kt b/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineScreen.kt similarity index 81% rename from samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineRendering.kt rename to samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineScreen.kt index e48a7907ed..0f5d9332b0 100644 --- a/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineRendering.kt +++ b/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineScreen.kt @@ -1,5 +1,7 @@ package com.squareup.sample.timemachine.shakeable +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import kotlin.time.Duration import kotlin.time.ExperimentalTime @@ -15,12 +17,13 @@ import kotlin.time.ExperimentalTime * @param onResumeRecording Event handler that will be called when [recording] is false and the user * wants to go back to the live delegate workflow. */ +@OptIn(WorkflowUiExperimentalApi::class) @ExperimentalTime -data class ShakeableTimeMachineRendering( - val rendering: Any, +data class ShakeableTimeMachineScreen( + val rendering: Screen, val totalDuration: Duration, val playbackPosition: Duration, val recording: Boolean, val onSeek: (Duration) -> Unit = {}, val onResumeRecording: () -> Unit -) +) : Screen 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 f5f94f8db2..0a3fea30fe 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 @@ -12,6 +12,8 @@ import com.squareup.workflow1.StatefulWorkflow import com.squareup.workflow1.WorkflowAction import com.squareup.workflow1.action import com.squareup.workflow1.runningWorker +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import kotlin.time.Duration import kotlin.time.ExperimentalTime @@ -22,11 +24,12 @@ import kotlin.time.ExperimentalTime * * This workflow takes a [PropsFactory] as its props. See that class for more documentation. */ +@OptIn(WorkflowUiExperimentalApi::class) @ExperimentalTime -class ShakeableTimeMachineWorkflow( +class ShakeableTimeMachineWorkflow( private val timeMachineWorkflow: TimeMachineWorkflow, context: Context -) : StatefulWorkflow, State, O, ShakeableTimeMachineRendering>() { +) : StatefulWorkflow, State, O, ShakeableTimeMachineScreen>() { /** * A factory that knows how to create the props for a [TimeMachineWorkflow.delegateWorkflow], @@ -54,7 +57,7 @@ class ShakeableTimeMachineWorkflow( renderProps: PropsFactory

, renderState: State, context: RenderContext - ): ShakeableTimeMachineRendering { + ): ShakeableTimeMachineScreen { // Only listen to shakes when recording. if (renderState === Recording) context.runningWorker(shakeWorker) { onShake } @@ -70,7 +73,7 @@ class ShakeableTimeMachineWorkflow( forwardOutput(output) } - return ShakeableTimeMachineRendering( + return ShakeableTimeMachineScreen( rendering = timeMachineRendering.value, totalDuration = timeMachineRendering.totalDuration, playbackPosition = if (renderState is PlayingBack) { 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 f7f7ca6407..3757079052 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,18 +1,18 @@ package com.squareup.sample.helloworkflowfragment import com.squareup.sample.helloworkflowfragment.databinding.HelloGoodbyeLayoutBinding -import com.squareup.workflow1.ui.AndroidViewRendering -import com.squareup.workflow1.ui.LayoutRunner -import com.squareup.workflow1.ui.ViewFactory +import com.squareup.workflow1.ui.AndroidScreen +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewRunner import com.squareup.workflow1.ui.WorkflowUiExperimentalApi @OptIn(WorkflowUiExperimentalApi::class) data class HelloRendering( val message: String, val onClick: () -> Unit -) : AndroidViewRendering { - override val viewFactory: ViewFactory = - LayoutRunner.bind(HelloGoodbyeLayoutBinding::inflate) { r, _ -> +) : AndroidScreen { + override val viewFactory: ScreenViewFactory = + ScreenViewRunner.bind(HelloGoodbyeLayoutBinding::inflate) { r, _ -> helloMessage.text = "${r.message} Fragment" helloMessage.setOnClickListener { r.onClick() } } diff --git a/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragment.kt b/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragment.kt index ccd6787034..ec31c50f95 100644 --- a/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragment.kt +++ b/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragment.kt @@ -26,7 +26,7 @@ class HelloWorkflowFragment : Fragment() { val model: HelloViewModel = ViewModelProvider(this).get(HelloViewModel::class.java) return WorkflowLayout(inflater.context).apply { - start(model.renderings) + take(model.renderings) } } } 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 a6a0d7add8..ee34b15391 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,18 +1,18 @@ package com.squareup.sample.helloworkflow import com.squareup.sample.helloworkflow.databinding.HelloGoodbyeLayoutBinding -import com.squareup.workflow1.ui.AndroidViewRendering -import com.squareup.workflow1.ui.LayoutRunner -import com.squareup.workflow1.ui.ViewFactory +import com.squareup.workflow1.ui.AndroidScreen +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewRunner import com.squareup.workflow1.ui.WorkflowUiExperimentalApi @OptIn(WorkflowUiExperimentalApi::class) data class HelloRendering( val message: String, val onClick: () -> Unit -) : AndroidViewRendering { - override val viewFactory: ViewFactory = - LayoutRunner.bind(HelloGoodbyeLayoutBinding::inflate) { r, _ -> +) : AndroidScreen { + override val viewFactory: ScreenViewFactory = + ScreenViewRunner.bind(HelloGoodbyeLayoutBinding::inflate) { r, _ -> helloMessage.text = r.message helloMessage.setOnClickListener { r.onClick() } } diff --git a/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflowActivity.kt b/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflowActivity.kt index 99318b4eb0..5170f60f62 100644 --- a/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflowActivity.kt +++ b/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflowActivity.kt @@ -21,7 +21,7 @@ class HelloWorkflowActivity : AppCompatActivity() { // succeeding calls. val model: HelloViewModel by viewModels() setContentView( - WorkflowLayout(this).apply { start(model.renderings) } + WorkflowLayout(this).apply { take(model.renderings) } ) } } 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 c89d0a0185..2a68a552a5 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 @@ -8,8 +8,8 @@ import android.view.View.VISIBLE import android.view.ViewGroup.LayoutParams import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.TextView -import com.squareup.workflow1.ui.AndroidViewRendering -import com.squareup.workflow1.ui.BuilderViewFactory +import com.squareup.workflow1.ui.AndroidScreen +import com.squareup.workflow1.ui.ManualScreenViewFactory import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.bindShowRendering @@ -18,8 +18,8 @@ data class ClickyTextRendering( val message: String, val visible: Boolean = true, val onClick: (() -> Unit)? = null -) : AndroidViewRendering { - override val viewFactory = BuilderViewFactory( +) : AndroidScreen { + override val viewFactory = ManualScreenViewFactory( type = ClickyTextRendering::class, viewConstructor = { initialRendering, initialEnv, context, _ -> TextView(context).also { textView -> 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 4f15b0cfb3..ac22b64f52 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,18 +1,19 @@ package com.squareup.sample.stubvisibility + import com.squareup.sample.stubvisibility.databinding.StubVisibilityLayoutBinding -import com.squareup.workflow1.ui.AndroidViewRendering -import com.squareup.workflow1.ui.LayoutRunner -import com.squareup.workflow1.ui.ViewFactory +import com.squareup.workflow1.ui.AndroidScreen +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewRunner import com.squareup.workflow1.ui.WorkflowUiExperimentalApi @OptIn(WorkflowUiExperimentalApi::class) data class OuterRendering( val top: ClickyTextRendering, val bottom: ClickyTextRendering -) : AndroidViewRendering { - override val viewFactory: ViewFactory = - LayoutRunner.bind(StubVisibilityLayoutBinding::inflate) { rendering, env -> - shouldBeFilledStub.update(rendering.top, env) - shouldBeWrappedStub.update(rendering.bottom, env) +) : AndroidScreen { + override val viewFactory: ScreenViewFactory = + ScreenViewRunner.bind(StubVisibilityLayoutBinding::inflate) { rendering, env -> + shouldBeFilledStub.show(rendering.top, env) + shouldBeWrappedStub.show(rendering.bottom, env) } } diff --git a/samples/stub-visibility/src/main/java/com/squareup/sample/stubvisibility/StubVisibilityActivity.kt b/samples/stub-visibility/src/main/java/com/squareup/sample/stubvisibility/StubVisibilityActivity.kt index 82d29114cc..a68ed589ab 100644 --- a/samples/stub-visibility/src/main/java/com/squareup/sample/stubvisibility/StubVisibilityActivity.kt +++ b/samples/stub-visibility/src/main/java/com/squareup/sample/stubvisibility/StubVisibilityActivity.kt @@ -1,3 +1,5 @@ +@file:OptIn(WorkflowUiExperimentalApi::class) + package com.squareup.sample.stubvisibility import android.os.Bundle @@ -6,25 +8,25 @@ import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowLayout import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.renderWorkflowIn import kotlinx.coroutines.flow.StateFlow -@OptIn(WorkflowUiExperimentalApi::class) class StubVisibilityActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val model: StubVisibilityModel by viewModels() setContentView( - WorkflowLayout(this).apply { start(model.renderings) } + WorkflowLayout(this).apply { take(model.renderings) } ) } } class StubVisibilityModel(savedState: SavedStateHandle) : ViewModel() { - val renderings: StateFlow by lazy { + val renderings: StateFlow by lazy { @OptIn(WorkflowUiExperimentalApi::class) renderWorkflowIn( workflow = StubVisibilityWorkflow, 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 c20f8bce08..a9f2411280 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,12 @@ package com.squareup.sample.authworkflow import com.squareup.sample.tictactoe.databinding.AuthorizingLayoutBinding +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewRunner import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.LayoutRunner -import com.squareup.workflow1.ui.ViewFactory @OptIn(WorkflowUiExperimentalApi::class) -internal val AuthorizingViewFactory: ViewFactory = - LayoutRunner.bind(AuthorizingLayoutBinding::inflate) { rendering, _ -> +internal val AuthorizingViewFactory: ScreenViewFactory = + ScreenViewRunner.bind(AuthorizingLayoutBinding::inflate) { rendering, _ -> authorizingMessage.text = rendering.message } diff --git a/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/LoginViewFactory.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/LoginViewFactory.kt index ccbb5715bc..b16254b150 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,14 +1,14 @@ package com.squareup.sample.authworkflow import com.squareup.sample.tictactoe.databinding.LoginLayoutBinding +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewRunner import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.LayoutRunner -import com.squareup.workflow1.ui.ViewFactory import com.squareup.workflow1.ui.backPressedHandler @OptIn(WorkflowUiExperimentalApi::class) -internal val LoginViewFactory: ViewFactory = - LayoutRunner.bind(LoginLayoutBinding::inflate) { rendering, _ -> +internal val LoginViewFactory: ScreenViewFactory = + ScreenViewRunner.bind(LoginLayoutBinding::inflate) { rendering, _ -> loginErrorMessage.text = rendering.errorMessage loginButton.setOnClickListener { diff --git a/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/SecondFactorViewFactory.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/SecondFactorViewFactory.kt index 687780e38a..411a8db473 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,14 +1,14 @@ package com.squareup.sample.authworkflow import com.squareup.sample.tictactoe.databinding.SecondFactorLayoutBinding +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewRunner import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.LayoutRunner -import com.squareup.workflow1.ui.ViewFactory import com.squareup.workflow1.ui.backPressedHandler @OptIn(WorkflowUiExperimentalApi::class) -internal val SecondFactorViewFactory: ViewFactory = - LayoutRunner.bind(SecondFactorLayoutBinding::inflate) { rendering, _ -> +internal val SecondFactorViewFactory: ScreenViewFactory = + ScreenViewRunner.bind(SecondFactorLayoutBinding::inflate) { rendering, _ -> root.backPressedHandler = { rendering.onCancel() } secondFactorToolbar.setNavigationOnClickListener { rendering.onCancel() } diff --git a/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GameOverLayoutRunner.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GameOverLayoutRunner.kt index 25ef50f297..2a2bf057f1 100644 --- a/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GameOverLayoutRunner.kt +++ b/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GameOverLayoutRunner.kt @@ -10,17 +10,17 @@ import com.squareup.sample.gameworkflow.SyncState.SAVE_FAILED 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.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.LayoutRunner -import com.squareup.workflow1.ui.LayoutRunner.Companion.bind +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bind import com.squareup.workflow1.ui.ViewEnvironment -import com.squareup.workflow1.ui.ViewFactory +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.backPressedHandler @OptIn(WorkflowUiExperimentalApi::class) internal class GameOverLayoutRunner( private val binding: GamePlayLayoutBinding -) : LayoutRunner { +) : ScreenViewRunner { private val saveItem: MenuItem = binding.gamePlayToolbar.menu.add("") .apply { @@ -102,7 +102,7 @@ internal class GameOverLayoutRunner( } /** Note how easily we're sharing this layout with [GamePlayViewFactory]. */ - companion object : ViewFactory by bind( + companion object : ScreenViewFactory by bind( GamePlayLayoutBinding::inflate, ::GameOverLayoutRunner ) } diff --git a/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GamePlayViewFactory.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GamePlayViewFactory.kt index 11c6c1cb03..d1eace7626 100644 --- a/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GamePlayViewFactory.kt +++ b/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GamePlayViewFactory.kt @@ -4,13 +4,13 @@ import android.view.View import android.view.ViewGroup import com.squareup.sample.tictactoe.databinding.GamePlayLayoutBinding import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.LayoutRunner -import com.squareup.workflow1.ui.ViewFactory +import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.backPressedHandler @OptIn(WorkflowUiExperimentalApi::class) -internal val GamePlayViewFactory: ViewFactory = - LayoutRunner.bind(GamePlayLayoutBinding::inflate) { rendering, _ -> +internal val GamePlayViewFactory: ScreenViewFactory = + ScreenViewRunner.bind(GamePlayLayoutBinding::inflate) { rendering, _ -> renderBanner(rendering.gameState, rendering.playerInfo) rendering.gameState.board.render(gamePlayBoard.root) diff --git a/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/NewGameViewFactory.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/NewGameViewFactory.kt index 4d01e1b622..53a4341e72 100644 --- a/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/NewGameViewFactory.kt +++ b/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/NewGameViewFactory.kt @@ -2,13 +2,13 @@ package com.squareup.sample.gameworkflow import com.squareup.sample.tictactoe.databinding.NewGameLayoutBinding import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.LayoutRunner -import com.squareup.workflow1.ui.ViewFactory +import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.backPressedHandler @OptIn(WorkflowUiExperimentalApi::class) -internal val NewGameViewFactory: ViewFactory = - LayoutRunner.bind(NewGameLayoutBinding::inflate) { rendering, _ -> +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) diff --git a/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/TicTacToeActivity.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/TicTacToeActivity.kt index 38490140e2..a31890d891 100644 --- a/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/TicTacToeActivity.kt +++ b/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/TicTacToeActivity.kt @@ -10,10 +10,11 @@ import com.squareup.sample.container.SampleContainers import com.squareup.sample.gameworkflow.TicTacToeViewFactories import com.squareup.workflow1.ui.WorkflowLayout import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backstack.BackStackContainer +import com.squareup.workflow1.ui.container.withRegistry import com.squareup.workflow1.ui.modal.AlertContainer import com.squareup.workflow1.ui.plus import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import timber.log.Timber @@ -31,7 +32,7 @@ class TicTacToeActivity : AppCompatActivity() { idlingResource = component.idlingResource setContentView( - WorkflowLayout(this).apply { start(model.renderings, viewRegistry) } + WorkflowLayout(this).apply { take(model.renderings.map { it.withRegistry(viewRegistry) }) } ) lifecycleScope.launch { @@ -47,7 +48,6 @@ class TicTacToeActivity : AppCompatActivity() { val viewRegistry = SampleContainers + AuthViewFactories + TicTacToeViewFactories + - BackStackContainer + AlertContainer } } diff --git a/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/TicTacToeModel.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/TicTacToeModel.kt index 0cfb0c459c..9e56bd4f6d 100644 --- a/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/TicTacToeModel.kt +++ b/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/TicTacToeModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope import androidx.savedstate.SavedStateRegistryOwner import com.squareup.sample.mainworkflow.TicTacToeWorkflow import com.squareup.workflow1.diagnostic.tracing.TracingWorkflowInterceptor +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.renderWorkflowIn import kotlinx.coroutines.Job @@ -21,7 +22,7 @@ class TicTacToeModel( private val running = Job() @OptIn(WorkflowUiExperimentalApi::class) - val renderings: StateFlow by lazy { + val renderings: StateFlow by lazy { val traceFile = traceFilesDir.resolve("workflow-trace-tictactoe.json") renderWorkflowIn( diff --git a/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthWorkflow.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthWorkflow.kt index 67c8d405ca..c1a474be36 100644 --- a/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthWorkflow.kt +++ b/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthWorkflow.kt @@ -15,15 +15,16 @@ import com.squareup.workflow1.Workflow import com.squareup.workflow1.action import com.squareup.workflow1.runningWorker import com.squareup.workflow1.rx2.asWorker +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backstack.BackStackScreen +import com.squareup.workflow1.ui.container.BackStackScreen /** * We define this otherwise redundant typealias to keep composite workflows * that build on [AuthWorkflow] decoupled from it, for ease of testing. */ @OptIn(WorkflowUiExperimentalApi::class) -typealias AuthWorkflow = Workflow> +typealias AuthWorkflow = Workflow> sealed class AuthState { internal data class LoginPrompt(val errorMessage: String = "") : AuthState() @@ -61,7 +62,7 @@ sealed class AuthResult { */ @OptIn(WorkflowUiExperimentalApi::class) class RealAuthWorkflow(private val authService: AuthService) : AuthWorkflow, - StatefulWorkflow>() { + StatefulWorkflow>() { override fun initialState( props: Unit, @@ -72,7 +73,7 @@ class RealAuthWorkflow(private val authService: AuthService) : AuthWorkflow, renderProps: Unit, renderState: AuthState, context: RenderContext - ): BackStackScreen = when (renderState) { + ): BackStackScreen = when (renderState) { is LoginPrompt -> { BackStackScreen( LoginScreen( diff --git a/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthorizingScreen.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthorizingScreen.kt index 795d7fe933..41878b251b 100644 --- a/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthorizingScreen.kt +++ b/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthorizingScreen.kt @@ -1,5 +1,9 @@ package com.squareup.sample.authworkflow +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +@OptIn(WorkflowUiExperimentalApi::class) data class AuthorizingScreen( val message: String -) +) : Screen diff --git a/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/LoginScreen.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/LoginScreen.kt index 9d45becfc5..8f375ad0b3 100644 --- a/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/LoginScreen.kt +++ b/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/LoginScreen.kt @@ -1,7 +1,11 @@ package com.squareup.sample.authworkflow +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +@OptIn(WorkflowUiExperimentalApi::class) data class LoginScreen( val errorMessage: String = "", val onLogin: (email: String, password: String) -> Unit = { _, _ -> }, val onCancel: () -> Unit = {} -) +) : Screen diff --git a/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/SecondFactorScreen.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/SecondFactorScreen.kt index de0c37d728..7595cd8f6a 100644 --- a/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/SecondFactorScreen.kt +++ b/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/SecondFactorScreen.kt @@ -1,7 +1,11 @@ package com.squareup.sample.authworkflow +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +@OptIn(WorkflowUiExperimentalApi::class) data class SecondFactorScreen( val errorMessage: String = "", val onSubmit: (String) -> Unit = {}, val onCancel: () -> Unit = {} -) +) : Screen diff --git a/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/GameOverScreen.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/GameOverScreen.kt index 3a4ed377d3..2b22ca2831 100644 --- a/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/GameOverScreen.kt +++ b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/GameOverScreen.kt @@ -1,8 +1,12 @@ package com.squareup.sample.gameworkflow +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +@OptIn(WorkflowUiExperimentalApi::class) data class GameOverScreen( val endGameState: RunGameState.GameOver, val onTrySaveAgain: () -> Unit, val onPlayAgain: () -> Unit, val onExit: () -> Unit -) +) : Screen diff --git a/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/GamePlayScreen.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/GamePlayScreen.kt index 42a928ce9a..6f2c9042f8 100644 --- a/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/GamePlayScreen.kt +++ b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/GamePlayScreen.kt @@ -1,8 +1,12 @@ package com.squareup.sample.gameworkflow +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +@OptIn(WorkflowUiExperimentalApi::class) data class GamePlayScreen( val playerInfo: PlayerInfo = PlayerInfo(), val gameState: Turn = Turn(), val onQuit: () -> Unit = {}, val onClick: (row: Int, col: Int) -> Unit = { _, _ -> } -) +) : Screen diff --git a/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/NewGameScreen.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/NewGameScreen.kt index e5b27e59fb..bb16547123 100644 --- a/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/NewGameScreen.kt +++ b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/NewGameScreen.kt @@ -1,8 +1,12 @@ package com.squareup.sample.gameworkflow +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +@OptIn(WorkflowUiExperimentalApi::class) data class NewGameScreen( val defaultNameX: String, val defaultNameO: String, val onCancel: () -> Unit, val onStartGame: (x: String, o: String) -> Unit -) +) : Screen diff --git a/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt index 0d6cc223aa..7b39099048 100644 --- a/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt +++ b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt @@ -1,3 +1,5 @@ +@file:OptIn(WorkflowUiExperimentalApi::class) + package com.squareup.sample.gameworkflow import com.squareup.sample.container.panel.PanelContainerScreen @@ -21,6 +23,7 @@ import com.squareup.workflow1.Workflow import com.squareup.workflow1.action import com.squareup.workflow1.runningWorker import com.squareup.workflow1.rx2.asWorker +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.modal.AlertContainerScreen import com.squareup.workflow1.ui.modal.AlertScreen @@ -35,8 +38,7 @@ enum class RunGameResult { FinishedPlaying } -@OptIn(WorkflowUiExperimentalApi::class) -typealias RunGameScreen = AlertContainerScreen> +typealias RunGameScreen = AlertContainerScreen> /** * We define this otherwise redundant typealias to keep composite workflows @@ -49,20 +51,18 @@ typealias RunGameWorkflow = Workflow * confirm quit screen, and offers a chance to play again. Delegates to [TakeTurnsWorkflow] * for the actual playing of the game. */ -@OptIn(WorkflowUiExperimentalApi::class) class RealRunGameWorkflow( private val takeTurnsWorkflow: TakeTurnsWorkflow, private val gameLog: GameLog ) : RunGameWorkflow, - StatefulWorkflow() { + StatefulWorkflow() { override fun initialState( props: Unit, snapshot: Snapshot? ): RunGameState { return snapshot?.let { RunGameState.fromSnapshot(snapshot.bytes) } - ?: NewGame() + ?: NewGame() } override fun render( @@ -74,13 +74,13 @@ class RealRunGameWorkflow( val emptyGameScreen = GamePlayScreen() subflowScreen( - base = emptyGameScreen, - subflow = NewGameScreen( - renderState.defaultXName, - renderState.defaultOName, - onCancel = context.eventHandler { setOutput(CanceledStart) }, - onStartGame = context.eventHandler { x, o -> state = Playing(PlayerInfo(x, o)) } - ) + base = emptyGameScreen, + subflow = NewGameScreen( + renderState.defaultXName, + renderState.defaultOName, + onCancel = context.eventHandler { setOutput(CanceledStart) }, + onStartGame = context.eventHandler { x, o -> state = Playing(PlayerInfo(x, o)) } + ) ) } @@ -89,10 +89,10 @@ class RealRunGameWorkflow( // already going. TakeTurnsWorkflow.render is immediately called, // and the GamePlayScreen it renders is immediately returned. val takeTurnsScreen = context.renderChild( - takeTurnsWorkflow, - props = renderState.resume - ?.let { TakeTurnsProps.resumeGame(renderState.playerInfo, it) } - ?: TakeTurnsProps.newGame(renderState.playerInfo) + takeTurnsWorkflow, + props = renderState.resume + ?.let { TakeTurnsProps.resumeGame(renderState.playerInfo, it) } + ?: TakeTurnsProps.newGame(renderState.playerInfo) ) { stopPlaying(it) } simpleScreen(takeTurnsScreen) @@ -100,41 +100,41 @@ class RealRunGameWorkflow( is MaybeQuitting -> { alertScreen( - base = GamePlayScreen(renderState.playerInfo, renderState.completedGame.lastTurn), - alert = maybeQuitScreen( - confirmQuit = context.eventHandler { - (state as? MaybeQuitting)?.let { oldState -> - state = MaybeQuittingForSure(oldState.playerInfo, oldState.completedGame) - } - }, - continuePlaying = context.eventHandler { - (state as? MaybeQuitting)?.let { oldState -> - state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn) - } - } - ) + base = GamePlayScreen(renderState.playerInfo, renderState.completedGame.lastTurn), + alert = maybeQuitScreen( + confirmQuit = context.eventHandler { + (state as? MaybeQuitting)?.let { oldState -> + state = MaybeQuittingForSure(oldState.playerInfo, oldState.completedGame) + } + }, + continuePlaying = context.eventHandler { + (state as? MaybeQuitting)?.let { oldState -> + state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn) + } + } + ) ) } is MaybeQuittingForSure -> { nestedAlertsScreen( - GamePlayScreen(renderState.playerInfo, renderState.completedGame.lastTurn), - maybeQuitScreen(), - maybeQuitScreen( - message = "Really?", - positive = "Yes!!", - negative = "Sigh, no", - confirmQuit = context.eventHandler { - (state as? MaybeQuittingForSure)?.let { oldState -> - state = GameOver(oldState.playerInfo, oldState.completedGame) - } - }, - continuePlaying = context.eventHandler { - (state as? MaybeQuittingForSure)?.let { oldState -> - state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn) - } - } - ) + GamePlayScreen(renderState.playerInfo, renderState.completedGame.lastTurn), + maybeQuitScreen(), + maybeQuitScreen( + message = "Really?", + positive = "Yes!!", + negative = "Sigh, no", + confirmQuit = context.eventHandler { + (state as? MaybeQuittingForSure)?.let { oldState -> + state = GameOver(oldState.playerInfo, oldState.completedGame) + } + }, + continuePlaying = context.eventHandler { + (state as? MaybeQuittingForSure)?.let { oldState -> + state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn) + } + } + ) ) } @@ -147,9 +147,9 @@ class RealRunGameWorkflow( GameOverScreen( renderState, - onTrySaveAgain = context.trySaveAgain(), - onPlayAgain = context.playAgain(), - onExit = context.eventHandler { setOutput(FinishedPlaying) } + onTrySaveAgain = context.trySaveAgain(), + onPlayAgain = context.playAgain(), + onExit = context.eventHandler { setOutput(FinishedPlaying) } ).let(::simpleScreen) } } @@ -181,7 +181,7 @@ class RealRunGameWorkflow( (state as? GameOver)?.let { oldState -> check(oldState.syncState == SAVE_FAILED) { "Should only fire trySaveAgain in syncState $SAVE_FAILED, " + - "was ${oldState.syncState}" + "was ${oldState.syncState}" } state = oldState.copy(syncState = SAVING) } @@ -190,31 +190,31 @@ class RealRunGameWorkflow( override fun snapshotState(state: RunGameState): Snapshot = state.toSnapshot() private fun nestedAlertsScreen( - base: Any, + base: Screen, vararg alerts: AlertScreen ): RunGameScreen { return AlertContainerScreen( - PanelContainerScreen(base), *alerts + PanelContainerScreen(base), *alerts ) } private fun alertScreen( - base: Any, + base: Screen, alert: AlertScreen ): RunGameScreen { return AlertContainerScreen( - PanelContainerScreen(base), alert + PanelContainerScreen(base), alert ) } private fun subflowScreen( - base: Any, - subflow: Any + base: Screen, + subflow: Screen ): RunGameScreen { return AlertContainerScreen(subflow.firstInPanelOver(base)) } - private fun simpleScreen(screen: Any): RunGameScreen { + private fun simpleScreen(screen: Screen): RunGameScreen { return AlertContainerScreen(PanelContainerScreen(screen)) } @@ -226,21 +226,21 @@ class RealRunGameWorkflow( continuePlaying: () -> Unit = { } ): AlertScreen { return AlertScreen( - buttons = mapOf( - POSITIVE to positive, - NEGATIVE to negative - ), - message = message, - onEvent = { alertEvent -> - when (alertEvent) { - is ButtonClicked -> when (alertEvent.button) { - POSITIVE -> confirmQuit() - NEGATIVE -> continuePlaying() - NEUTRAL -> throw IllegalArgumentException() - } - Canceled -> continuePlaying() + buttons = mapOf( + POSITIVE to positive, + NEGATIVE to negative + ), + message = message, + onEvent = { alertEvent -> + when (alertEvent) { + is ButtonClicked -> when (alertEvent.button) { + POSITIVE -> confirmQuit() + NEGATIVE -> continuePlaying() + NEUTRAL -> throw IllegalArgumentException() } + Canceled -> continuePlaying() } + } ) } } diff --git a/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/TicTacToeWorkflow.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/TicTacToeWorkflow.kt index 5b3b5d4485..be84a6e1d8 100644 --- a/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/TicTacToeWorkflow.kt +++ b/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/TicTacToeWorkflow.kt @@ -1,3 +1,4 @@ +@file:OptIn(WorkflowUiExperimentalApi::class) @file:Suppress("DEPRECATION") package com.squareup.sample.mainworkflow @@ -47,7 +48,6 @@ class TicTacToeWorkflow( ): MainState = snapshot?.let { MainState.fromSnapshot(snapshot.bytes) } ?: Authenticating - @OptIn(WorkflowUiExperimentalApi::class) override fun render( renderProps: Unit, renderState: MainState, @@ -61,7 +61,7 @@ class TicTacToeWorkflow( // Probably due to https://youtrack.jetbrains.com/issue/KT-32869 @Suppress("RemoveExplicitTypeArguments") (AlertContainerScreen( - authScreen.inPanelOver(emptyGameScreen) + authScreen.inPanelOver(emptyGameScreen) )) } diff --git a/samples/tictactoe/common/src/test/java/com/squareup/sample/mainworkflow/TicTacToeWorkflowTest.kt b/samples/tictactoe/common/src/test/java/com/squareup/sample/mainworkflow/TicTacToeWorkflowTest.kt index d3dcc788fd..52f46c2363 100644 --- a/samples/tictactoe/common/src/test/java/com/squareup/sample/mainworkflow/TicTacToeWorkflowTest.kt +++ b/samples/tictactoe/common/src/test/java/com/squareup/sample/mainworkflow/TicTacToeWorkflowTest.kt @@ -14,8 +14,9 @@ import com.squareup.workflow1.rendering import com.squareup.workflow1.runningWorker import com.squareup.workflow1.stateless import com.squareup.workflow1.testing.launchForTestingFromStartWith +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backstack.BackStackScreen +import com.squareup.workflow1.ui.container.BackStackScreen import org.junit.Test /** @@ -27,19 +28,19 @@ class TicTacToeWorkflowTest { @Test fun `starts in auth over empty game`() { TicTacToeWorkflow(authWorkflow(), runGameWorkflow()).launchForTestingFromStartWith { awaitNextRendering() - .let { screen -> - assertThat(screen.panels).hasSize(1) - assertThat(screen.panels[0]).isEqualTo(DEFAULT_AUTH) + .let { screen -> + assertThat(screen.panels).hasSize(1) + assertThat(screen.panels[0]).isEqualTo(S(DEFAULT_AUTH)) - // This GamePlayScreen() is emitted by MainWorkflow itself. - assertThat(screen.body).isEqualTo(GamePlayScreen()) - } + // This GamePlayScreen() is emitted by MainWorkflow itself. + assertThat(screen.body).isEqualTo(GamePlayScreen()) + } } } @Test fun `starts game on auth`() { val authWorkflow: AuthWorkflow = Workflow.stateless { - runningWorker(Worker.from { Unit }) { + runningWorker(Worker.from { }) { action { setOutput(Authorized("auth")) } } authScreen() @@ -47,19 +48,21 @@ class TicTacToeWorkflowTest { TicTacToeWorkflow(authWorkflow, runGameWorkflow()).launchForTestingFromStartWith { awaitNextRendering() - .let { screen -> - assertThat(screen.panels).isEmpty() - assertThat(screen.body).isEqualTo(DEFAULT_RUN_GAME) - } + .let { screen -> + assertThat(screen.panels).isEmpty() + assertThat(screen.body).isEqualTo(S(DEFAULT_RUN_GAME)) + } } } + private data class S(val value: T) : Screen + private fun runGameScreen( body: String = DEFAULT_RUN_GAME - ) = RunGameScreen(PanelContainerScreen(body)) + ) = RunGameScreen(PanelContainerScreen(S(body))) private fun authScreen(wrapped: String = DEFAULT_AUTH) = - BackStackScreen(wrapped) + BackStackScreen(S(wrapped)) private val RunGameScreen.panels: List get() = beneathModals.modals.map { it.top } private val RunGameScreen.body: Any get() = beneathModals.beneathModals.wrapped diff --git a/samples/todo-android/app/src/main/java/com/squareup/sample/todo/ToDoActivity.kt b/samples/todo-android/app/src/main/java/com/squareup/sample/todo/ToDoActivity.kt index ce33f281cf..b8df57a76c 100644 --- a/samples/todo-android/app/src/main/java/com/squareup/sample/todo/ToDoActivity.kt +++ b/samples/todo-android/app/src/main/java/com/squareup/sample/todo/ToDoActivity.kt @@ -1,3 +1,5 @@ +@file:OptIn(WorkflowUiExperimentalApi::class) + package com.squareup.sample.todo import android.os.Bundle @@ -8,15 +10,16 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.squareup.sample.container.overviewdetail.OverviewDetailContainer import com.squareup.workflow1.diagnostic.tracing.TracingWorkflowInterceptor +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowLayout import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backstack.BackStackContainer +import com.squareup.workflow1.ui.container.withRegistry import com.squareup.workflow1.ui.renderWorkflowIn import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map import java.io.File -@OptIn(WorkflowUiExperimentalApi::class) class ToDoActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -26,21 +29,20 @@ class ToDoActivity : AppCompatActivity() { setContentView( WorkflowLayout(this).apply { - start(model.ensureWorkflow(traceFilesDir = filesDir), viewRegistry) + take(model.ensureWorkflow(traceFilesDir = filesDir).map { it.withRegistry(viewRegistry) }) } ) } private companion object { - val viewRegistry = ViewRegistry(OverviewDetailContainer, BackStackContainer) + val viewRegistry = ViewRegistry(OverviewDetailContainer) } } class ToDoModel(private val savedState: SavedStateHandle) : ViewModel() { - private var renderings: StateFlow? = null + private var renderings: StateFlow? = null - @OptIn(WorkflowUiExperimentalApi::class) - fun ensureWorkflow(traceFilesDir: File): StateFlow { + fun ensureWorkflow(traceFilesDir: File): StateFlow { if (renderings == null) { val traceFile = traceFilesDir.resolve("workflow-trace-todo.json") 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 ca72af9356..72ef00d114 100644 --- a/samples/todo-android/app/src/main/java/com/squareup/sample/todo/TodoEditorScreen.kt +++ b/samples/todo-android/app/src/main/java/com/squareup/sample/todo/TodoEditorScreen.kt @@ -1,18 +1,19 @@ +@file:OptIn(WorkflowUiExperimentalApi::class) package com.squareup.sample.todo import android.content.Context.INPUT_METHOD_SERVICE import android.view.View import android.view.inputmethod.InputMethodManager import com.squareup.sample.todo.databinding.TodoEditorLayoutBinding -import com.squareup.workflow1.ui.AndroidViewRendering +import com.squareup.workflow1.ui.AndroidScreen import com.squareup.workflow1.ui.Compatible -import com.squareup.workflow1.ui.LayoutRunner -import com.squareup.workflow1.ui.LayoutRunner.Companion.bind +import com.squareup.workflow1.ui.ScreenViewRunner +import com.squareup.workflow1.ui.ScreenViewRunner.Companion.bind import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.backPressedHandler -import com.squareup.workflow1.ui.backstack.BackStackConfig -import com.squareup.workflow1.ui.backstack.BackStackConfig.Other +import com.squareup.workflow1.ui.container.BackStackConfig +import com.squareup.workflow1.ui.container.BackStackConfig.Other import com.squareup.workflow1.ui.control @OptIn(WorkflowUiExperimentalApi::class) @@ -21,16 +22,16 @@ data class TodoEditorScreen( val onCheckboxClicked: (index: Int) -> Unit, val onDeleteClicked: (index: Int) -> Unit, val onGoBackClicked: () -> Unit -) : AndroidViewRendering, Compatible { +) : AndroidScreen, Compatible { override val compatibilityKey = Compatible.keyFor(this, "${session.id}") - override val viewFactory = bind(TodoEditorLayoutBinding::inflate, ::TodoEditorLayoutRunner) + override val viewFactory = bind(TodoEditorLayoutBinding::inflate, ::Runner) } @OptIn(WorkflowUiExperimentalApi::class) -private class TodoEditorLayoutRunner( +private class Runner( private val binding: TodoEditorLayoutBinding -) : LayoutRunner { +) : ScreenViewRunner { private val itemListView = ItemListView.fromLinearLayout(binding.itemContainer) diff --git a/samples/todo-android/app/src/main/java/com/squareup/sample/todo/TodoListsAppWorkflow.kt b/samples/todo-android/app/src/main/java/com/squareup/sample/todo/TodoListsAppWorkflow.kt index b4db736b45..d49b9ad092 100644 --- a/samples/todo-android/app/src/main/java/com/squareup/sample/todo/TodoListsAppWorkflow.kt +++ b/samples/todo-android/app/src/main/java/com/squareup/sample/todo/TodoListsAppWorkflow.kt @@ -10,7 +10,7 @@ import com.squareup.workflow1.Snapshot import com.squareup.workflow1.StatefulWorkflow import com.squareup.workflow1.action import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backstack.BackStackScreen +import com.squareup.workflow1.ui.container.BackStackScreen sealed class TodoListsAppState { abstract val lists: List 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 90bcdaf57d..573dfd40c9 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 @@ -5,9 +5,9 @@ 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.AndroidViewRendering -import com.squareup.workflow1.ui.LayoutRunner.Companion.bind -import com.squareup.workflow1.ui.ViewFactory +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.WorkflowUiExperimentalApi /** @@ -24,8 +24,8 @@ data class TodoListsScreen( val lists: List, val onRowClicked: (Int) -> Unit, val selection: Int = -1 -) : AndroidViewRendering { - override val viewFactory: ViewFactory = +) : AndroidScreen { + override val viewFactory: ScreenViewFactory = bind(TodoListsLayoutBinding::inflate) { rendering, viewEnvironment -> for ((index, list) in rendering.lists.withIndex()) { addRow( diff --git a/workflow-ui/compose-tooling/src/main/java/com/squareup/workflow1/ui/compose/tooling/PreviewViewEnvironment.kt b/workflow-ui/compose-tooling/src/main/java/com/squareup/workflow1/ui/compose/tooling/PreviewViewEnvironment.kt index 85168c257c..40b98e51b8 100644 --- a/workflow-ui/compose-tooling/src/main/java/com/squareup/workflow1/ui/compose/tooling/PreviewViewEnvironment.kt +++ b/workflow-ui/compose-tooling/src/main/java/com/squareup/workflow1/ui/compose/tooling/PreviewViewEnvironment.kt @@ -1,3 +1,4 @@ +@file:Suppress("DEPRECATION") @file:OptIn(WorkflowUiExperimentalApi::class) package com.squareup.workflow1.ui.compose.tooling @@ -48,7 +49,7 @@ private class PreviewViewRegistry( override val keys: Set> get() = mainFactory?.let { setOf(it.type) } ?: emptySet() @Suppress("UNCHECKED_CAST") - override fun getFactoryFor( + override fun getEntryFor( renderingType: KClass ): ViewFactory = when (renderingType) { mainFactory?.type -> mainFactory diff --git a/workflow-ui/compose-tooling/src/main/java/com/squareup/workflow1/ui/compose/tooling/ViewFactories.kt b/workflow-ui/compose-tooling/src/main/java/com/squareup/workflow1/ui/compose/tooling/ViewFactories.kt index ed5d6050c1..42c5fd3cbb 100644 --- a/workflow-ui/compose-tooling/src/main/java/com/squareup/workflow1/ui/compose/tooling/ViewFactories.kt +++ b/workflow-ui/compose-tooling/src/main/java/com/squareup/workflow1/ui/compose/tooling/ViewFactories.kt @@ -1,3 +1,4 @@ +@file:Suppress("DEPRECATION", "FunctionName") package com.squareup.workflow1.ui.compose.tooling import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy diff --git a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewFactoryTest.kt b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewFactoryTest.kt index b334a0e3d6..82d7e56eb2 100644 --- a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewFactoryTest.kt +++ b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewFactoryTest.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package com.squareup.workflow1.ui.compose import android.content.Context diff --git a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt index a2338173a5..e88e443f66 100644 --- a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt +++ b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package com.squareup.workflow1.ui.compose import android.content.Context @@ -25,14 +27,15 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import com.google.common.truth.Truth.assertThat import com.squareup.workflow1.ui.AndroidViewRendering +import com.squareup.workflow1.ui.asScreen import com.squareup.workflow1.ui.Compatible -import com.squareup.workflow1.ui.Named +import com.squareup.workflow1.ui.NamedScreen import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.ViewFactory import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backstack.BackStackScreen import com.squareup.workflow1.ui.bindShowRendering +import com.squareup.workflow1.ui.container.BackStackScreen import com.squareup.workflow1.ui.internal.test.IdleAfterTestRule import com.squareup.workflow1.ui.internal.test.WorkflowUiTestActivity import com.squareup.workflow1.ui.modal.HasModals @@ -355,7 +358,7 @@ internal class ComposeViewTreeIntegrationTest { } @Test fun composition_is_restored_in_modal_after_config_change() { - val firstScreen = ComposeRendering(compatibilityKey = "") { + val firstScreen = asScreen(ComposeRendering(compatibilityKey = "") { var counter by rememberSaveable { mutableStateOf(0) } BasicText( "Counter: $counter", @@ -363,15 +366,13 @@ internal class ComposeViewTreeIntegrationTest { .clickable { counter++ } .testTag(CounterTag) ) - } + }) // Show first screen to initialize state. scenario.onActivity { it.setRendering( - TestModalScreen( - listOf( - BackStackScreen(EmptyRendering, firstScreen) - ) + asScreen( + TestModalScreen(listOf(BackStackScreen(EmptyRendering, firstScreen))) ) ) } @@ -388,7 +389,7 @@ internal class ComposeViewTreeIntegrationTest { } @Test fun composition_is_restored_in_multiple_modals_after_config_change() { - val firstScreen = ComposeRendering(compatibilityKey = "first") { + val firstScreen = asScreen(ComposeRendering(compatibilityKey = "first") { var counter by rememberSaveable { mutableStateOf(0) } BasicText( "Counter: $counter", @@ -396,8 +397,8 @@ internal class ComposeViewTreeIntegrationTest { .clickable { counter++ } .testTag(CounterTag) ) - } - val secondScreen = ComposeRendering(compatibilityKey = "second") { + }) + val secondScreen = asScreen(ComposeRendering(compatibilityKey = "second") { var counter by rememberSaveable { mutableStateOf(0) } BasicText( "Counter2: $counter", @@ -405,18 +406,20 @@ internal class ComposeViewTreeIntegrationTest { .clickable { counter++ } .testTag(CounterTag2) ) - } + }) // Show first screen to initialize state. scenario.onActivity { it.setRendering( - TestModalScreen( - listOf( - // Name each BackStackScreen to give them unique state registry keys. - // TODO(https://github.com/square/workflow-kotlin/issues/469) Should this naming be - // done automatically in ModalContainer? - Named(BackStackScreen(EmptyRendering, firstScreen), "modal1"), - Named(BackStackScreen(EmptyRendering, secondScreen), "modal2") + asScreen( + TestModalScreen( + listOf( + // Name each BackStackScreen to give them unique state registry keys. + // TODO(https://github.com/square/workflow-kotlin/issues/469) Should this naming be + // done automatically in ModalContainer? + NamedScreen(BackStackScreen(EmptyRendering, firstScreen), "modal1"), + NamedScreen(BackStackScreen(EmptyRendering, secondScreen), "modal2") + ) ) ) ) @@ -442,7 +445,7 @@ internal class ComposeViewTreeIntegrationTest { } private fun WorkflowUiTestActivity.setBackstack(vararg backstack: ComposeRendering) { - setRendering(BackStackScreen(EmptyRendering, backstack.asList())) + setRendering(BackStackScreen(EmptyRendering, backstack.asList().map { asScreen(it) })) } data class TestModalScreen( @@ -485,7 +488,7 @@ internal class ComposeViewTreeIntegrationTest { companion object { // Use a ComposeView here because the Compose test infra doesn't like it if there are no // Compose views at all. See https://issuetracker.google.com/issues/179455327. - val EmptyRendering = ComposeRendering(compatibilityKey = "") {} + val EmptyRendering = asScreen(ComposeRendering(compatibilityKey = "") {}) const val CounterTag = "counter" const val CounterTag2 = "counter2" diff --git a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/NoTransitionBackStackContainer.kt b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/NoTransitionBackStackContainer.kt index fc4008ffc0..517965d87c 100644 --- a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/NoTransitionBackStackContainer.kt +++ b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/NoTransitionBackStackContainer.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package com.squareup.workflow1.ui.compose import android.content.Context @@ -6,10 +8,11 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT import com.squareup.workflow1.ui.BuilderViewFactory import com.squareup.workflow1.ui.ViewFactory import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backstack.BackStackContainer import com.squareup.workflow1.ui.backstack.BackStackScreen -import com.squareup.workflow1.ui.container.R +import com.squareup.workflow1.ui.backstack.asNonLegacy import com.squareup.workflow1.ui.bindShowRendering +import com.squareup.workflow1.ui.container.BackStackContainer +import com.squareup.workflow1.ui.container.R /** * A subclass of [BackStackContainer] that disables transitions to make it simpler to test the @@ -20,7 +23,11 @@ import com.squareup.workflow1.ui.bindShowRendering @OptIn(WorkflowUiExperimentalApi::class) internal class NoTransitionBackStackContainer(context: Context) : BackStackContainer(context) { - override fun performTransition(oldViewMaybe: View?, newView: View, popped: Boolean) { + override fun performTransition( + oldViewMaybe: View?, + newView: View, + popped: Boolean + ) { oldViewMaybe?.let(::removeView) addView(newView) } @@ -33,7 +40,13 @@ internal class NoTransitionBackStackContainer(context: Context) : BackStackConta .apply { id = R.id.workflow_back_stack_container layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT) - bindShowRendering(initialRendering, initialEnv, ::update) + bindShowRendering( + initialRendering, + initialEnv, + { newRendering, newViewEnvironment -> + update(newRendering.asNonLegacy(), newViewEnvironment) + } + ) } } ) diff --git a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/WorkflowRenderingTest.kt b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/WorkflowRenderingTest.kt index a09e87c32b..8573e35998 100644 --- a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/WorkflowRenderingTest.kt +++ b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/WorkflowRenderingTest.kt @@ -1,4 +1,4 @@ -@file:Suppress("TestFunctionName") +@file:Suppress("TestFunctionName", "DEPRECATION") package com.squareup.workflow1.ui.compose diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeRendering.kt b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeRendering.kt index 9fbc6d3f66..dd7d88da8a 100644 --- a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeRendering.kt +++ b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeRendering.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package com.squareup.workflow1.ui.compose import androidx.compose.runtime.Composable diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeViewFactory.kt b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeViewFactory.kt index bf4b5144ac..46804e2b14 100644 --- a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeViewFactory.kt +++ b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeViewFactory.kt @@ -1,5 +1,5 @@ // See https://youtrack.jetbrains.com/issue/KT-31734 -@file:Suppress("RemoveEmptyParenthesesFromAnnotationEntry") +@file:Suppress("RemoveEmptyParenthesesFromAnnotationEntry", "DEPRECATION") package com.squareup.workflow1.ui.compose diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/CompositionRoot.kt b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/CompositionRoot.kt index 110fdce67c..73db130338 100644 --- a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/CompositionRoot.kt +++ b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/CompositionRoot.kt @@ -1,4 +1,4 @@ -@file:Suppress("RemoveEmptyParenthesesFromAnnotationEntry") +@file:Suppress("RemoveEmptyParenthesesFromAnnotationEntry", "DEPRECATION", "FunctionName") package com.squareup.workflow1.ui.compose @@ -7,7 +7,6 @@ import androidx.annotation.VisibleForTesting.PRIVATE import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.staticCompositionLocalOf -import com.squareup.workflow1.ui.AndroidViewRendering import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.ViewFactory import com.squareup.workflow1.ui.ViewRegistry @@ -84,7 +83,7 @@ public fun ViewRegistry.withCompositionRoot(root: CompositionRoot): ViewRegistry /** * Applies [transform] to each [ViewFactory] in this registry. Transformations are applied lazily, - * at the time of lookup via [ViewRegistry.getFactoryFor]. + * at the time of lookup via [ViewRegistry.getEntryFor]. */ @WorkflowUiExperimentalApi private fun ViewRegistry.mapFactories( @@ -92,15 +91,11 @@ private fun ViewRegistry.mapFactories( ): ViewRegistry = object : ViewRegistry { override val keys: Set> get() = this@mapFactories.keys - override fun getFactoryFor( + override fun getEntryFor( renderingType: KClass - ): ViewFactory { - val factoryFor = - this@mapFactories.getFactoryFor(renderingType) ?: throw IllegalArgumentException( - "A ${ViewFactory::class.qualifiedName} should have been registered to display " + - "${renderingType.qualifiedName} instances, or that class should implement " + - "${AndroidViewRendering::class.simpleName}<${renderingType.simpleName}>." - ) + ): ViewFactory? { + val factoryFor = (this@mapFactories.getEntryFor(renderingType) as? ViewFactory<*>) + ?: return null val transformedFactory = transform(factoryFor) check(transformedFactory.type == renderingType) { "Expected transform to return a ViewFactory that is compatible with $renderingType, " + diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt index 7b086a53a7..cd5a45b1ff 100644 --- a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt +++ b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package com.squareup.workflow1.ui.compose import android.view.View diff --git a/workflow-ui/container-android/api/container-android.api b/workflow-ui/container-android/api/container-android.api index db97d77857..995c5d5166 100644 --- a/workflow-ui/container-android/api/container-android.api +++ b/workflow-ui/container-android/api/container-android.api @@ -1,30 +1,6 @@ -public final class com/squareup/workflow1/ui/backstack/BackStackConfig : java/lang/Enum { - public static final field Companion Lcom/squareup/workflow1/ui/backstack/BackStackConfig$Companion; - public static final field First Lcom/squareup/workflow1/ui/backstack/BackStackConfig; - public static final field None Lcom/squareup/workflow1/ui/backstack/BackStackConfig; - public static final field Other Lcom/squareup/workflow1/ui/backstack/BackStackConfig; - public static fun valueOf (Ljava/lang/String;)Lcom/squareup/workflow1/ui/backstack/BackStackConfig; - public static fun values ()[Lcom/squareup/workflow1/ui/backstack/BackStackConfig; -} - -public final class com/squareup/workflow1/ui/backstack/BackStackConfig$Companion : com/squareup/workflow1/ui/ViewEnvironmentKey { - public fun getDefault ()Lcom/squareup/workflow1/ui/backstack/BackStackConfig; - public synthetic fun getDefault ()Ljava/lang/Object; -} - -public class com/squareup/workflow1/ui/backstack/BackStackContainer : android/widget/FrameLayout { +public final class com/squareup/workflow1/ui/backstack/BackStackContainer { public static final field Companion Lcom/squareup/workflow1/ui/backstack/BackStackContainer$Companion; - public fun (Landroid/content/Context;)V - public fun (Landroid/content/Context;Landroid/util/AttributeSet;)V - public fun (Landroid/content/Context;Landroid/util/AttributeSet;I)V - public fun (Landroid/content/Context;Landroid/util/AttributeSet;II)V - public synthetic fun (Landroid/content/Context;Landroid/util/AttributeSet;IIILkotlin/jvm/internal/DefaultConstructorMarker;)V - protected fun onAttachedToWindow ()V - protected fun onDetachedFromWindow ()V - protected fun onRestoreInstanceState (Landroid/os/Parcelable;)V - protected fun onSaveInstanceState ()Landroid/os/Parcelable; - protected fun performTransition (Landroid/view/View;Landroid/view/View;Z)V - protected final fun update (Lcom/squareup/workflow1/ui/backstack/BackStackScreen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V + public fun ()V } public final class com/squareup/workflow1/ui/backstack/BackStackContainer$Companion : com/squareup/workflow1/ui/ViewFactory { @@ -33,44 +9,6 @@ public final class com/squareup/workflow1/ui/backstack/BackStackContainer$Compan public fun getType ()Lkotlin/reflect/KClass; } -public final class com/squareup/workflow1/ui/backstack/BackStackStateKeyKt { - public static final fun withBackStackStateKeyPrefix (Lcom/squareup/workflow1/ui/ViewEnvironment;Ljava/lang/String;)Lcom/squareup/workflow1/ui/ViewEnvironment; -} - -public final class com/squareup/workflow1/ui/backstack/ViewStateCache : android/os/Parcelable { - public static final field CREATOR Lcom/squareup/workflow1/ui/backstack/ViewStateCache$CREATOR; - public fun ()V - public final fun attachToParentRegistry (Ljava/lang/String;Landroidx/savedstate/SavedStateRegistryOwner;)V - public fun describeContents ()I - public final fun detachFromParentRegistry ()V - public final fun prune (Ljava/util/Collection;)V - public final fun restore (Lcom/squareup/workflow1/ui/backstack/ViewStateCache;)V - public final fun update (Ljava/util/Collection;Landroid/view/View;Landroid/view/View;)V - public fun writeToParcel (Landroid/os/Parcel;I)V -} - -public final class com/squareup/workflow1/ui/backstack/ViewStateCache$CREATOR : android/os/Parcelable$Creator { - public fun createFromParcel (Landroid/os/Parcel;)Lcom/squareup/workflow1/ui/backstack/ViewStateCache; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public fun newArray (I)[Lcom/squareup/workflow1/ui/backstack/ViewStateCache; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - -public final class com/squareup/workflow1/ui/backstack/ViewStateCache$SavedState : android/view/View$BaseSavedState { - public static final field CREATOR Lcom/squareup/workflow1/ui/backstack/ViewStateCache$SavedState$CREATOR; - public fun (Landroid/os/Parcel;)V - public fun (Landroid/os/Parcelable;Lcom/squareup/workflow1/ui/backstack/ViewStateCache;)V - public final fun getViewStateCache ()Lcom/squareup/workflow1/ui/backstack/ViewStateCache; - public fun writeToParcel (Landroid/os/Parcel;I)V -} - -public final class com/squareup/workflow1/ui/backstack/ViewStateCache$SavedState$CREATOR : android/os/Parcelable$Creator { - public fun createFromParcel (Landroid/os/Parcel;)Lcom/squareup/workflow1/ui/backstack/ViewStateCache$SavedState; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public fun newArray (I)[Lcom/squareup/workflow1/ui/backstack/ViewStateCache$SavedState; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - public final class com/squareup/workflow1/ui/modal/AlertContainer : com/squareup/workflow1/ui/modal/ModalContainer { public static final field Companion Lcom/squareup/workflow1/ui/modal/AlertContainer$Companion; public fun (Landroid/content/Context;)V @@ -82,11 +20,11 @@ public final class com/squareup/workflow1/ui/modal/AlertContainer : com/squareup public synthetic fun buildDialog (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/modal/ModalContainer$DialogRef; } -public final class com/squareup/workflow1/ui/modal/AlertContainer$Companion : com/squareup/workflow1/ui/ViewFactory { +public final class com/squareup/workflow1/ui/modal/AlertContainer$Companion : com/squareup/workflow1/ui/ScreenViewFactory { + public synthetic fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; public fun buildView (Lcom/squareup/workflow1/ui/modal/AlertContainerScreen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; - public synthetic fun buildView (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; - public final fun customThemeBinding (I)Lcom/squareup/workflow1/ui/ViewFactory; - public static synthetic fun customThemeBinding$default (Lcom/squareup/workflow1/ui/modal/AlertContainer$Companion;IILjava/lang/Object;)Lcom/squareup/workflow1/ui/ViewFactory; + public final fun customThemeBinding (I)Lcom/squareup/workflow1/ui/ScreenViewFactory; + public static synthetic fun customThemeBinding$default (Lcom/squareup/workflow1/ui/modal/AlertContainer$Companion;IILjava/lang/Object;)Lcom/squareup/workflow1/ui/ScreenViewFactory; public fun getType ()Lkotlin/reflect/KClass; } diff --git a/workflow-ui/container-android/src/androidTest/AndroidManifest.xml b/workflow-ui/container-android/src/androidTest/AndroidManifest.xml index 1f57b16026..59a52a2dd4 100644 --- a/workflow-ui/container-android/src/androidTest/AndroidManifest.xml +++ b/workflow-ui/container-android/src/androidTest/AndroidManifest.xml @@ -5,8 +5,5 @@ - diff --git a/workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/modal/test/ModalViewContainerLifecycleActivity.kt b/workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/modal/test/ModalViewContainerLifecycleActivity.kt index 05fc271c01..68b2541d6e 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 @@ -1,13 +1,17 @@ +@file:Suppress("DEPRECATION") + package com.squareup.workflow1.ui.modal.test import android.content.Context import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import com.squareup.workflow1.ui.BuilderViewFactory +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.ViewEnvironment -import com.squareup.workflow1.ui.ViewFactory import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub @@ -22,7 +26,7 @@ import kotlin.reflect.KClass @OptIn(WorkflowUiExperimentalApi::class) internal class ModalViewContainerLifecycleActivity : AbstractLifecycleTestActivity() { - object BaseRendering : ViewFactory { + object BaseRendering : Screen, ScreenViewFactory { override val type: KClass = BaseRendering::class override fun buildView( initialRendering: BaseRendering, @@ -40,7 +44,7 @@ internal class ModalViewContainerLifecycleActivity : AbstractLifecycleTestActivi override val beneathModals: BaseRendering get() = BaseRendering } - sealed class TestRendering { + sealed class TestRendering : Screen { data class LeafRendering(val name: String) : TestRendering(), Compatible { override val compatibilityKey: String get() = name } @@ -52,7 +56,7 @@ internal class ModalViewContainerLifecycleActivity : AbstractLifecycleTestActivi ModalViewContainer.binding(), BaseRendering, leafViewBinding(LeafRendering::class, lifecycleLoggingViewObserver { it.name }), - BuilderViewFactory(RecurseRendering::class) { initialRendering, + ManualScreenViewFactory(RecurseRendering::class) { initialRendering, initialViewEnvironment, contextForNewView, _ -> FrameLayout(contextForNewView).also { container -> @@ -62,11 +66,12 @@ internal class ModalViewContainerLifecycleActivity : AbstractLifecycleTestActivi initialRendering, initialViewEnvironment ) { rendering, env -> - stub.update(TestModals(listOf(rendering.wrapped)), env) + stub.show(asScreen(TestModals(listOf(rendering.wrapped))), env) } } }, ) - fun update(vararg modals: TestRendering) = setRendering(TestModals(modals.asList())) + fun update(vararg modals: TestRendering) = + setRendering(asScreen(TestModals(modals.asList()))) } diff --git a/workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/modal/test/ModalViewContainerLifecycleTest.kt b/workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/modal/test/ModalViewContainerLifecycleTest.kt index 48a2a7aec0..bf366a15eb 100644 --- a/workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/modal/test/ModalViewContainerLifecycleTest.kt +++ b/workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/modal/test/ModalViewContainerLifecycleTest.kt @@ -1,3 +1,5 @@ +@file:OptIn(WorkflowUiExperimentalApi::class) + package com.squareup.workflow1.ui.modal.test import androidx.lifecycle.Lifecycle.State.CREATED @@ -5,6 +7,7 @@ import androidx.lifecycle.Lifecycle.State.RESUMED import androidx.lifecycle.LifecycleOwner import androidx.test.ext.junit.rules.ActivityScenarioRule import com.google.common.truth.Truth.assertThat +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.modal.ModalViewContainer import com.squareup.workflow1.ui.modal.test.ModalViewContainerLifecycleActivity.TestRendering.LeafRendering import com.squareup.workflow1.ui.modal.test.ModalViewContainerLifecycleActivity.TestRendering.RecurseRendering diff --git a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/BackStackContainer.kt b/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/BackStackContainer.kt index 72bcd19d32..35ee23476c 100644 --- a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/BackStackContainer.kt +++ b/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/BackStackContainer.kt @@ -1,261 +1,21 @@ +@file:Suppress("DEPRECATION") + package com.squareup.workflow1.ui.backstack -import android.content.Context -import android.os.Parcelable -import android.util.AttributeSet -import android.util.Log -import android.view.Gravity -import android.view.View -import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import android.view.animation.AccelerateDecelerateInterpolator -import android.widget.FrameLayout -import androidx.savedstate.SavedStateRegistry -import androidx.savedstate.ViewTreeSavedStateRegistryOwner -import androidx.transition.Scene -import androidx.transition.Slide -import androidx.transition.TransitionManager -import androidx.transition.TransitionSet -import com.squareup.workflow1.ui.BuilderViewFactory -import com.squareup.workflow1.ui.Named -import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.DecorativeViewFactory import com.squareup.workflow1.ui.ViewFactory -import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.androidx.WorkflowAndroidXSupport.stateRegistryOwnerFromViewTreeOrContext -import com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner -import com.squareup.workflow1.ui.backstack.BackStackConfig.First -import com.squareup.workflow1.ui.backstack.BackStackConfig.Other -import com.squareup.workflow1.ui.bindShowRendering -import com.squareup.workflow1.ui.buildView -import com.squareup.workflow1.ui.canShowRendering -import com.squareup.workflow1.ui.compatible -import com.squareup.workflow1.ui.container.R -import com.squareup.workflow1.ui.getRendering -import com.squareup.workflow1.ui.showFirstRendering -import com.squareup.workflow1.ui.showRendering /** - * A container view that can display a stream of [BackStackScreen] instances. - * - * This container supports saving and restoring the view state of each of its subviews corresponding - * to the renderings in its [BackStackScreen]. It supports two distinct state mechanisms: - * 1. Classic view hierarchy state ([View.onSaveInstanceState]/[View.onRestoreInstanceState]) - * 2. AndroidX [SavedStateRegistry] via [ViewTreeSavedStateRegistryOwner]. - * - * ## A note about `SavedStateRegistry` support. - * - * The [SavedStateRegistry] API involves defining string keys to associate with state bundles. These - * keys must be unique relative to the instance of the registry they are saved in. To support this - * requirement, [BackStackContainer] tries to generate a best-effort unique key by combining its - * fully-qualified class name with both its [view ID][View.getId] and the - * [compatibility key][com.squareup.workflow1.ui.Compatible.compatibilityKey] of its rendering. - * This method isn't guaranteed to give a unique registry key, but it should be good enough: If you - * need to nest multiple [BackStackContainer]s under the same `SavedStateRegistry`, just wrap each - * [BackStackScreen] with a [Named], or give each [BackStackContainer] a unique view ID. + * BackStackContainer has been promoted to the core workflow-ui modules, + * and is now built into [ViewRegistry][com.squareup.workflow1.ui.ViewRegistry] by default. * - * There's a potential issue here where if our ID is changed to something else, then another - * [BackStackContainer] is added with our old ID, that container will overwrite our state. Since - * they'd both be using the same key, [SavedStateRegistry] would throw an exception. As long as this - * container is detached before its ID is changed, it shouldn't be a problem. + * This stub has been left in place to preserve the name of the legacy [ViewFactory], + * to ease conversion. */ +@Deprecated("Use com.squareup.workflow1.ui.container.BackStackContainer") @WorkflowUiExperimentalApi -public open class BackStackContainer @JvmOverloads constructor( - context: Context, - attributeSet: AttributeSet? = null, - defStyle: Int = 0, - defStyleRes: Int = 0 -) : FrameLayout(context, attributeSet, defStyle, defStyleRes) { - - private val viewStateCache = ViewStateCache() - - private val currentView: View? get() = if (childCount > 0) getChildAt(0) else null - private var currentRendering: BackStackScreen>? = null - private var stateRegistryKey: String? = null - - protected fun update( - newRendering: BackStackScreen<*>, - newViewEnvironment: ViewEnvironment - ) { - updateStateRegistryKey(newViewEnvironment) - - val config = if (newRendering.backStack.isEmpty()) First else Other - val environment = newViewEnvironment + (BackStackConfig to config) - - val named: BackStackScreen> = newRendering - // ViewStateCache requires that everything be Named. - // It's fine if client code is already using Named for its own purposes, recursion works. - .map { Named(it, "backstack") } - - val oldViewMaybe = currentView - - // If existing view is compatible, just update it. - oldViewMaybe - ?.takeIf { it.canShowRendering(named.top) } - ?.let { - viewStateCache.prune(named.frames) - it.showRendering(named.top, environment) - return - } - - val newView = environment[ViewRegistry].buildView( - initialRendering = named.top, - initialViewEnvironment = environment, - contextForNewView = this.context, - container = this, - initializeView = { - WorkflowLifecycleOwner.installOn(this) - showFirstRendering() - } - ) - viewStateCache.update(named.backStack, oldViewMaybe, newView) - - val popped = currentRendering?.backStack?.any { compatible(it, named.top) } == true - - performTransition(oldViewMaybe, newView, popped) - // Notify the view we're about to replace that it's going away. - oldViewMaybe?.let(WorkflowLifecycleOwner::get)?.destroyOnDetach() - - currentRendering = named - } - - /** - * Called from [View.showRendering] to swap between views. - * Subclasses can override to customize visual effects. There is no need to call super. - * Note that views are showing renderings of type [Named]`>`. - * - * @param oldViewMaybe the outgoing view, or null if this is the initial rendering. - * @param newView the view that should replace [oldViewMaybe] (if it exists), and become - * this view's only child - * @param popped true if we should give the appearance of popping "back" to a previous rendering, - * false if a new rendering is being "pushed". Should be ignored if [oldViewMaybe] is null. - */ - protected open fun performTransition( - oldViewMaybe: View?, - newView: View, - popped: Boolean - ) { - // Showing something already, transition with push or pop effect. - oldViewMaybe - ?.let { oldView -> - val oldBody: View? = oldView.findViewById(R.id.back_stack_body) - val newBody: View? = newView.findViewById(R.id.back_stack_body) - - val oldTarget: View - val newTarget: View - if (oldBody != null && newBody != null) { - oldTarget = oldBody - newTarget = newBody - } else { - oldTarget = oldView - newTarget = newView - } - - val (outEdge, inEdge) = when (popped) { - false -> Gravity.START to Gravity.END - true -> Gravity.END to Gravity.START - } - - val transition = TransitionSet() - .addTransition(Slide(outEdge).addTarget(oldTarget)) - .addTransition(Slide(inEdge).addTarget(newTarget)) - .setInterpolator(AccelerateDecelerateInterpolator()) - - TransitionManager.go(Scene(this, newView), transition) - return - } - - // This is the first view, just show it. - addView(newView) - } - - override fun onSaveInstanceState(): Parcelable { - return ViewStateCache.SavedState(super.onSaveInstanceState(), viewStateCache) - } - - override fun onRestoreInstanceState(state: Parcelable) { - (state as? ViewStateCache.SavedState) - ?.let { - viewStateCache.restore(it.viewStateCache) - super.onRestoreInstanceState(state.superState) - } - ?: super.onRestoreInstanceState(super.onSaveInstanceState()) - // Some other class wrote state, but we're not allowed to skip - // the call to super. Make a no-op call. - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - - // Wire up our viewStateCache to our parent SavedStateRegistry. - val parentRegistryOwner = stateRegistryOwnerFromViewTreeOrContext(this)!! - val key = checkNotNull(stateRegistryKey) { - "Expected stateRegistryKey to have been set – the view seems to be getting attached before " + - "its first update: $this" - } - - viewStateCache.attachToParentRegistry(key, parentRegistryOwner) - } - - override fun onDetachedFromWindow() { - // Disconnect our state cache from our parent SavedStateRegistry so that it doesn't get asked - // to save state anymore. - viewStateCache.detachFromParentRegistry() - super.onDetachedFromWindow() - } - - /** - * See the note about SavedStateRegistry support in this class's kdoc for some caveats. - */ - private fun getStateRegistryKey(): String { - val namedKeyOrNull = run { - val rendering = getRendering() as? Named<*> - rendering?.compatibilityKey - } - val nameSuffix = namedKeyOrNull?.let { "-$it" } ?: "" - val idSuffix = if (id == NO_ID) "" else "-$id" - return BackStackContainer::class.java.name + nameSuffix + idSuffix - } - - /** - * In order to save our state with a unique ID in our parent's registry, we use a combination - * of this class name, our [compatibility key][Named.compatibilityKey] if specified, and our view - * ID if specified. This method isn't guaranteed to give a unique registry key, but it should be - * good enough: If you need to nest multiple [BackStackContainer]s under the same - * `SavedStateRegistry`, just wrap each [BackStackScreen] with a [Named], or give each - * [BackStackContainer] a unique view ID. - * - * There's a potential issue here where if our ID is changed to something else, then another - * BackStackContainer is added with our old ID, that container will overwrite our state. Since - * they'd both be using the same key, SavedStateRegistry would throw an exception. That's a - * pretty unlikely situation though I think. And as long as this container is detached before - * its ID is changed, it won't be a problem. - */ - private fun updateStateRegistryKey(environment: ViewEnvironment) { - val idSuffix = if (id == NO_ID) "" else "-$id" - val keyPrefix = environment.getBackStackStateKeyPrefix - val newKey = keyPrefix + BackStackContainer::class.java.name + idSuffix - - if (stateRegistryKey != null && stateRegistryKey != newKey) { - Log.wtf( - "workflow1", - "BackStackContainer state registry key changed – view state may be lost:" + - " from $stateRegistryKey to $newKey" - ) - } - stateRegistryKey = newKey - } - +public class BackStackContainer { public companion object : ViewFactory> - by BuilderViewFactory( - type = BackStackScreen::class, - viewConstructor = { initialRendering, initialEnv, context, _ -> - BackStackContainer(context) - .apply { - id = R.id.workflow_back_stack_container - layoutParams = (ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)) - bindShowRendering(initialRendering, initialEnv, ::update) - } - } - ) + by DecorativeViewFactory(BackStackScreen::class, { legacy -> legacy.asNonLegacy() }) } 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 27e4acb165..e415ba0050 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 @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package com.squareup.workflow1.ui.modal import android.content.Context @@ -8,10 +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.BuilderViewFactory -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.ViewFactory +import com.squareup.workflow1.ui.ManualScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.bindShowRendering import com.squareup.workflow1.ui.container.R import com.squareup.workflow1.ui.modal.AlertScreen.Button @@ -38,7 +40,7 @@ public class AlertContainer @JvmOverloads constructor( initialViewEnvironment: ViewEnvironment ): DialogRef { val dialog = AlertDialog.Builder(context, dialogThemeResId) - .create() + .create() val ref = DialogRef(initialModalRendering, initialViewEnvironment, dialog) updateDialog(ref) return ref @@ -57,15 +59,15 @@ public class AlertContainer @JvmOverloads constructor( for (button in Button.values()) { rendering.buttons[button] - ?.let { name -> - dialog.setButton(button.toId(), name) { _, _ -> - rendering.onEvent(ButtonClicked(button)) - } - } - ?: run { - dialog.getButton(button.toId()) - ?.visibility = View.INVISIBLE + ?.let { name -> + dialog.setButton(button.toId(), name) { _, _ -> + rendering.onEvent(ButtonClicked(button)) } + } + ?: run { + dialog.getButton(button.toId()) + ?.visibility = View.INVISIBLE + } } dialog.setMessage(rendering.message) @@ -80,21 +82,22 @@ public class AlertContainer @JvmOverloads constructor( private class AlertContainerViewFactory( @StyleRes private val dialogThemeResId: Int = 0 - ) : ViewFactory> by BuilderViewFactory( - type = AlertContainerScreen::class, - 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) - } - } + ) : ScreenViewFactory> by ManualScreenViewFactory( + type = AlertContainerScreen::class, + 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) + } + } ) - public companion object : ViewFactory> by AlertContainerViewFactory() { + public companion object : ScreenViewFactory> + by AlertContainerViewFactory() { /** - * Creates a [ViewFactory] to show the [AlertScreen]s of an [AlertContainerScreen] + * Creates a [ScreenViewFactory] to show the [AlertScreen]s of an [AlertContainerScreen] * as Android `AlertDialog`s. * * @param dialogThemeResId the resource ID of the theme against which to inflate @@ -102,6 +105,6 @@ public class AlertContainer @JvmOverloads constructor( */ public fun customThemeBinding( @StyleRes dialogThemeResId: Int = 0 - ): ViewFactory> = AlertContainerViewFactory(dialogThemeResId) + ): ScreenViewFactory> = AlertContainerViewFactory(dialogThemeResId) } } diff --git a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalContainer.kt b/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalContainer.kt index 13ce2fadf5..23d0666061 100644 --- a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalContainer.kt +++ b/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalContainer.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package com.squareup.workflow1.ui.modal import android.app.Dialog @@ -17,11 +19,11 @@ import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import com.squareup.workflow1.ui.Compatible import com.squareup.workflow1.ui.ViewEnvironment -import com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub -import com.squareup.workflow1.ui.backstack.withBackStackStateKeyPrefix +import com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner import com.squareup.workflow1.ui.compatible +import com.squareup.workflow1.ui.container.withBackStackStateKeyPrefix /** * Base class for containers that show [HasModals.modals] in [Dialog] windows. 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 6df9057e12..7246739a88 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 @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package com.squareup.workflow1.ui.modal import android.app.Dialog @@ -10,10 +12,11 @@ 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.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.ViewRegistry +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.backPressedHandler import com.squareup.workflow1.ui.bindShowRendering import com.squareup.workflow1.ui.buildView @@ -61,16 +64,11 @@ public open class ModalViewContainer @JvmOverloads constructor( initialModalRendering: Any, initialViewEnvironment: ViewEnvironment ): DialogRef { - val view = initialViewEnvironment[ViewRegistry] - // Notice that we don't pass a custom initializeView function to set the - // WorkflowLifecycleOwner here. ModalContainer will do that itself, on the parent of the view - // created here. - .buildView( - initialRendering = initialModalRendering, - initialViewEnvironment = initialViewEnvironment, - contextForNewView = this.context, - container = this - ) + val view = asScreen(initialModalRendering).buildView( + viewEnvironment = initialViewEnvironment, + contextForNewView = this.context, + container = this + ) .apply { // 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 @@ -107,7 +105,9 @@ public open class ModalViewContainer @JvmOverloads constructor( } override fun updateDialog(dialogRef: DialogRef) { - with(dialogRef) { (extra as View).showRendering(modalRendering, viewEnvironment) } + with(dialogRef) { + (extra as View).showRendering(asScreen(modalRendering), viewEnvironment) + } } @PublishedApi diff --git a/workflow-ui/container-android/src/main/res/values/ids.xml b/workflow-ui/container-android/src/main/res/values/ids.xml index b58c7435bd..91877c61dc 100644 --- a/workflow-ui/container-android/src/main/res/values/ids.xml +++ b/workflow-ui/container-android/src/main/res/values/ids.xml @@ -1,14 +1,7 @@ - - - - - - + + diff --git a/workflow-ui/container-common/api/container-common.api b/workflow-ui/container-common/api/container-common.api index 5b00621a33..41a10a1408 100644 --- a/workflow-ui/container-common/api/container-common.api +++ b/workflow-ui/container-common/api/container-common.api @@ -14,11 +14,12 @@ public final class com/squareup/workflow1/ui/backstack/BackStackScreen { } public final class com/squareup/workflow1/ui/backstack/BackStackScreenKt { + public static final fun asNonLegacy (Lcom/squareup/workflow1/ui/backstack/BackStackScreen;)Lcom/squareup/workflow1/ui/container/BackStackScreen; public static final fun toBackStackScreen (Ljava/util/List;)Lcom/squareup/workflow1/ui/backstack/BackStackScreen; public static final fun toBackStackScreenOrNull (Ljava/util/List;)Lcom/squareup/workflow1/ui/backstack/BackStackScreen; } -public final class com/squareup/workflow1/ui/modal/AlertContainerScreen : com/squareup/workflow1/ui/modal/HasModals { +public final class com/squareup/workflow1/ui/modal/AlertContainerScreen : com/squareup/workflow1/ui/Screen, com/squareup/workflow1/ui/modal/HasModals { public fun (Ljava/lang/Object;Lcom/squareup/workflow1/ui/modal/AlertScreen;)V public fun (Ljava/lang/Object;Ljava/util/List;)V public synthetic fun (Ljava/lang/Object;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/workflow-ui/container-common/src/main/java/com/squareup/workflow1/ui/backstack/BackStackScreen.kt b/workflow-ui/container-common/src/main/java/com/squareup/workflow1/ui/backstack/BackStackScreen.kt index c736b0fc92..01e416154a 100644 --- a/workflow-ui/container-common/src/main/java/com/squareup/workflow1/ui/backstack/BackStackScreen.kt +++ b/workflow-ui/container-common/src/main/java/com/squareup/workflow1/ui/backstack/BackStackScreen.kt @@ -1,23 +1,14 @@ +@file:Suppress("DEPRECATION") + package com.squareup.workflow1.ui.backstack +import com.squareup.workflow1.ui.asScreen +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.container.BackStackScreen as NewBackStackScreen -/** - * Represents an active screen ([top]), and a set of previously visited screens to which we may - * return ([backStack]). By rendering the entire history we allow the UI to do things like maintain - * cached view state, implement drag-back gestures without waiting for the workflow, etc. - * - * Effectively a list that can never be empty. - * - * If multiple [BackStackScreen]s are used as sibling renderings within the same parent navigation - * container (either the root activity or another [BackStackScreen]), then the siblings must be - * distinguished by wrapping them in [Named][com.squareup.workflow1.ui.Named] renderings in order to - * correctly support AndroidX `SavedStateRegistry`. - * - * @param bottom the bottom-most entry in the stack - * @param rest the rest of the stack, empty by default - */ @WorkflowUiExperimentalApi +@Deprecated("Use com.squareup.workflow1.ui.container.BackStackScreen") public class BackStackScreen( bottom: StackedT, rest: List @@ -51,12 +42,12 @@ public class BackStackScreen( public fun map(transform: (StackedT) -> R): BackStackScreen { return frames.map(transform) - .toBackStackScreen() + .toBackStackScreen() } public fun mapIndexed(transform: (index: Int, StackedT) -> R): BackStackScreen { return frames.mapIndexed(transform) - .toBackStackScreen() + .toBackStackScreen() } override fun equals(other: Any?): Boolean { @@ -83,3 +74,14 @@ public fun List.toBackStackScreen(): BackStackScreen { require(isNotEmpty()) return BackStackScreen(first(), subList(1, size)) } + +@WorkflowUiExperimentalApi +public fun BackStackScreen<*>.asNonLegacy(): NewBackStackScreen { + return NewBackStackScreen( + bottom = asScreen(frames.first()), + rest = when (frames.size) { + 1 -> emptyList() + else -> frames.takeLast(frames.count() - 1).map { asScreen(it) } + } + ) +} diff --git a/workflow-ui/container-common/src/main/java/com/squareup/workflow1/ui/modal/AlertContainerScreen.kt b/workflow-ui/container-common/src/main/java/com/squareup/workflow1/ui/modal/AlertContainerScreen.kt index 6a029910e6..a315725e72 100644 --- a/workflow-ui/container-common/src/main/java/com/squareup/workflow1/ui/modal/AlertContainerScreen.kt +++ b/workflow-ui/container-common/src/main/java/com/squareup/workflow1/ui/modal/AlertContainerScreen.kt @@ -1,5 +1,6 @@ package com.squareup.workflow1.ui.modal +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi /** @@ -11,7 +12,7 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi public data class AlertContainerScreen( override val beneathModals: B, override val modals: List = emptyList() -) : HasModals { +) : Screen, HasModals { public constructor( baseScreen: B, alert: AlertScreen diff --git a/workflow-ui/container-common/src/test/java/com/squareup/workflow1/ui/backstack/BackStackScreenTest.kt b/workflow-ui/container-common/src/test/java/com/squareup/workflow1/ui/backstack/BackStackScreenTest.kt index 1e48651b92..82591f9892 100644 --- a/workflow-ui/container-common/src/test/java/com/squareup/workflow1/ui/backstack/BackStackScreenTest.kt +++ b/workflow-ui/container-common/src/test/java/com/squareup/workflow1/ui/backstack/BackStackScreenTest.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package com.squareup.workflow1.ui.backstack import com.google.common.truth.Truth.assertThat @@ -6,7 +8,7 @@ import org.junit.Test import kotlin.test.assertFailsWith @OptIn(WorkflowUiExperimentalApi::class) -class BackStackScreenTest { +internal class BackStackScreenTest { @Test fun `top is last`() { assertThat(BackStackScreen(1, 2, 3, 4).top).isEqualTo(4) } diff --git a/workflow-ui/core-android/api/core-android.api b/workflow-ui/core-android/api/core-android.api index 4cc208b366..f6e2eff984 100644 --- a/workflow-ui/core-android/api/core-android.api +++ b/workflow-ui/core-android/api/core-android.api @@ -7,6 +7,27 @@ public final class com/squareup/workflow1/ui/AndroidRenderWorkflowKt { public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; } +public abstract interface class com/squareup/workflow1/ui/AndroidScreen : com/squareup/workflow1/ui/Screen { + public abstract fun getViewFactory ()Lcom/squareup/workflow1/ui/ScreenViewFactory; +} + +public final class com/squareup/workflow1/ui/AndroidScreenKt { + public static final fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lkotlin/jvm/functions/Function1;)Landroid/view/View; + public static synthetic fun buildView$default (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Landroid/view/View; +} + +public final class com/squareup/workflow1/ui/AndroidViewEnvironmentKt { + public static final fun getViewFactoryForRendering (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/Screen;)Lcom/squareup/workflow1/ui/ScreenViewFactory; + public static final fun showFirstRendering (Landroid/view/View;)V +} + +public final class com/squareup/workflow1/ui/AndroidViewRegistryKt { + public static final fun buildView (Lcom/squareup/workflow1/ui/ViewRegistry;Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lkotlin/jvm/functions/Function1;)Landroid/view/View; + public static synthetic fun buildView$default (Lcom/squareup/workflow1/ui/ViewRegistry;Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Landroid/view/View; + public static final fun getFactoryFor (Lcom/squareup/workflow1/ui/ViewRegistry;Lkotlin/reflect/KClass;)Lcom/squareup/workflow1/ui/ViewFactory; + public static final fun getFactoryForRendering (Lcom/squareup/workflow1/ui/ViewRegistry;Ljava/lang/Object;)Lcom/squareup/workflow1/ui/ViewFactory; +} + public abstract interface class com/squareup/workflow1/ui/AndroidViewRendering { public abstract fun getViewFactory ()Lcom/squareup/workflow1/ui/ViewFactory; } @@ -23,6 +44,15 @@ public final class com/squareup/workflow1/ui/BuilderViewFactory : com/squareup/w public fun getType ()Lkotlin/reflect/KClass; } +public final class com/squareup/workflow1/ui/DecorativeScreenViewFactory : com/squareup/workflow1/ui/ScreenViewFactory { + public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;)V + public synthetic fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;)V + public synthetic fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; + public fun getType ()Lkotlin/reflect/KClass; +} + public final class com/squareup/workflow1/ui/DecorativeViewFactory : com/squareup/workflow1/ui/ViewFactory { public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;)V public synthetic fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;ILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -51,6 +81,34 @@ public final class com/squareup/workflow1/ui/LayoutRunnerViewFactory : com/squar public fun getType ()Lkotlin/reflect/KClass; } +public final class com/squareup/workflow1/ui/LayoutScreenViewFactory : com/squareup/workflow1/ui/ScreenViewFactory { + public fun (Lkotlin/reflect/KClass;ILkotlin/jvm/functions/Function1;)V + public fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; + public fun getType ()Lkotlin/reflect/KClass; +} + +public final class com/squareup/workflow1/ui/ManualScreenViewFactory : com/squareup/workflow1/ui/ScreenViewFactory { + public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function4;)V + public fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; + public fun getType ()Lkotlin/reflect/KClass; +} + +public abstract interface class com/squareup/workflow1/ui/ScreenViewFactory : com/squareup/workflow1/ui/ViewRegistry$Entry { + public abstract fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; +} + +public final class com/squareup/workflow1/ui/ScreenViewFactory$DefaultImpls { + public static synthetic fun buildView$default (Lcom/squareup/workflow1/ui/ScreenViewFactory;Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;ILjava/lang/Object;)Landroid/view/View; +} + +public abstract interface class com/squareup/workflow1/ui/ScreenViewRunner { + public static final field Companion Lcom/squareup/workflow1/ui/ScreenViewRunner$Companion; + public abstract fun showRendering (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V +} + +public final class com/squareup/workflow1/ui/ScreenViewRunner$Companion { +} + public final class com/squareup/workflow1/ui/ShowRenderingTag { public fun (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function2;)V public final fun component1 ()Ljava/lang/Object; @@ -74,64 +132,26 @@ public final class com/squareup/workflow1/ui/TextControllerControlEditTextKt { public static final fun control (Lcom/squareup/workflow1/ui/TextController;Landroid/widget/EditText;)V } -public final class com/squareup/workflow1/ui/ViewBindingViewFactory : com/squareup/workflow1/ui/ViewFactory { +public final class com/squareup/workflow1/ui/ViewBindingScreenViewFactory : com/squareup/workflow1/ui/ScreenViewFactory { public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;)V - public fun buildView (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; + public fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; public fun getType ()Lkotlin/reflect/KClass; } -public final class com/squareup/workflow1/ui/ViewEnvironment { - public fun ()V - public fun (Ljava/util/Map;)V - public synthetic fun (Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun equals (Ljava/lang/Object;)Z - public final fun get (Lcom/squareup/workflow1/ui/ViewEnvironmentKey;)Ljava/lang/Object; - public final fun getMap ()Ljava/util/Map; - public fun hashCode ()I - public final fun plus (Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/ViewEnvironment; - public final fun plus (Lkotlin/Pair;)Lcom/squareup/workflow1/ui/ViewEnvironment; - public fun toString ()Ljava/lang/String; -} - -public abstract class com/squareup/workflow1/ui/ViewEnvironmentKey { - public fun (Lkotlin/reflect/KClass;)V - public final fun equals (Ljava/lang/Object;)Z - public abstract fun getDefault ()Ljava/lang/Object; - public final fun hashCode ()I - public fun toString ()Ljava/lang/String; +public final class com/squareup/workflow1/ui/ViewBindingViewFactory : com/squareup/workflow1/ui/ViewFactory { + public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;)V + public fun buildView (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; + public fun getType ()Lkotlin/reflect/KClass; } -public abstract interface class com/squareup/workflow1/ui/ViewFactory { +public abstract interface class com/squareup/workflow1/ui/ViewFactory : com/squareup/workflow1/ui/ViewRegistry$Entry { public abstract fun buildView (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; - public abstract fun getType ()Lkotlin/reflect/KClass; } public final class com/squareup/workflow1/ui/ViewFactory$DefaultImpls { public static synthetic fun buildView$default (Lcom/squareup/workflow1/ui/ViewFactory;Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;ILjava/lang/Object;)Landroid/view/View; } -public abstract interface class com/squareup/workflow1/ui/ViewRegistry { - public static final field Companion Lcom/squareup/workflow1/ui/ViewRegistry$Companion; - public abstract fun getFactoryFor (Lkotlin/reflect/KClass;)Lcom/squareup/workflow1/ui/ViewFactory; - public abstract fun getKeys ()Ljava/util/Set; -} - -public final class com/squareup/workflow1/ui/ViewRegistry$Companion : com/squareup/workflow1/ui/ViewEnvironmentKey { - public fun getDefault ()Lcom/squareup/workflow1/ui/ViewRegistry; - public synthetic fun getDefault ()Ljava/lang/Object; -} - -public final class com/squareup/workflow1/ui/ViewRegistryKt { - public static final fun ViewRegistry ()Lcom/squareup/workflow1/ui/ViewRegistry; - public static final fun ViewRegistry ([Lcom/squareup/workflow1/ui/ViewFactory;)Lcom/squareup/workflow1/ui/ViewRegistry; - public static final fun buildView (Lcom/squareup/workflow1/ui/ViewRegistry;Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lkotlin/jvm/functions/Function1;)Landroid/view/View; - public static synthetic fun buildView$default (Lcom/squareup/workflow1/ui/ViewRegistry;Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Landroid/view/View; - public static final fun getFactoryForRendering (Lcom/squareup/workflow1/ui/ViewRegistry;Ljava/lang/Object;)Lcom/squareup/workflow1/ui/ViewFactory; - public static final fun plus (Lcom/squareup/workflow1/ui/ViewRegistry;Lcom/squareup/workflow1/ui/ViewFactory;)Lcom/squareup/workflow1/ui/ViewRegistry; - public static final fun plus (Lcom/squareup/workflow1/ui/ViewRegistry;Lcom/squareup/workflow1/ui/ViewRegistry;)Lcom/squareup/workflow1/ui/ViewRegistry; - public static final fun showFirstRendering (Landroid/view/View;)V -} - public final class com/squareup/workflow1/ui/ViewShowRenderingKt { public static final fun bindShowRendering (Landroid/view/View;Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function2;)V public static final fun canShowRendering (Landroid/view/View;Ljava/lang/Object;)Z @@ -147,6 +167,7 @@ public final class com/squareup/workflow1/ui/WorkflowLayout : android/widget/Fra public final fun start (Lkotlinx/coroutines/flow/Flow;Lcom/squareup/workflow1/ui/ViewEnvironment;)V public final fun start (Lkotlinx/coroutines/flow/Flow;Lcom/squareup/workflow1/ui/ViewRegistry;)V public static synthetic fun start$default (Lcom/squareup/workflow1/ui/WorkflowLayout;Lkotlinx/coroutines/flow/Flow;Lcom/squareup/workflow1/ui/ViewEnvironment;ILjava/lang/Object;)V + public final fun take (Lkotlinx/coroutines/flow/Flow;)V } public final class com/squareup/workflow1/ui/WorkflowViewStub : android/view/View { @@ -166,6 +187,7 @@ public final class com/squareup/workflow1/ui/WorkflowViewStub : android/view/Vie public final fun setReplaceOldViewInParent (Lkotlin/jvm/functions/Function2;)V public final fun setUpdatesVisibility (Z)V public fun setVisibility (I)V + public final fun show (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)Landroid/view/View; public final fun update (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;)Landroid/view/View; } @@ -186,3 +208,69 @@ public final class com/squareup/workflow1/ui/androidx/WorkflowLifecycleOwner$Com public static synthetic fun installOn$default (Lcom/squareup/workflow1/ui/androidx/WorkflowLifecycleOwner$Companion;Landroid/view/View;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V } +public final class com/squareup/workflow1/ui/container/BackStackConfig : java/lang/Enum { + public static final field Companion Lcom/squareup/workflow1/ui/container/BackStackConfig$Companion; + public static final field First Lcom/squareup/workflow1/ui/container/BackStackConfig; + public static final field None Lcom/squareup/workflow1/ui/container/BackStackConfig; + public static final field Other Lcom/squareup/workflow1/ui/container/BackStackConfig; + public static fun valueOf (Ljava/lang/String;)Lcom/squareup/workflow1/ui/container/BackStackConfig; + public static fun values ()[Lcom/squareup/workflow1/ui/container/BackStackConfig; +} + +public final class com/squareup/workflow1/ui/container/BackStackConfig$Companion : com/squareup/workflow1/ui/ViewEnvironmentKey { + public fun getDefault ()Lcom/squareup/workflow1/ui/container/BackStackConfig; + public synthetic fun getDefault ()Ljava/lang/Object; +} + +public class com/squareup/workflow1/ui/container/BackStackContainer : android/widget/FrameLayout { + public fun (Landroid/content/Context;)V + public fun (Landroid/content/Context;Landroid/util/AttributeSet;)V + public fun (Landroid/content/Context;Landroid/util/AttributeSet;I)V + public fun (Landroid/content/Context;Landroid/util/AttributeSet;II)V + public synthetic fun (Landroid/content/Context;Landroid/util/AttributeSet;IIILkotlin/jvm/internal/DefaultConstructorMarker;)V + protected fun onAttachedToWindow ()V + protected fun onDetachedFromWindow ()V + protected fun onRestoreInstanceState (Landroid/os/Parcelable;)V + protected fun onSaveInstanceState ()Landroid/os/Parcelable; + protected fun performTransition (Landroid/view/View;Landroid/view/View;Z)V + public final fun update (Lcom/squareup/workflow1/ui/container/BackStackScreen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V +} + +public final class com/squareup/workflow1/ui/container/BackStackStateKeyKt { + public static final fun withBackStackStateKeyPrefix (Lcom/squareup/workflow1/ui/ViewEnvironment;Ljava/lang/String;)Lcom/squareup/workflow1/ui/ViewEnvironment; +} + +public final class com/squareup/workflow1/ui/container/ViewStateCache : android/os/Parcelable { + public static final field CREATOR Lcom/squareup/workflow1/ui/container/ViewStateCache$CREATOR; + public fun ()V + public final fun attachToParentRegistry (Ljava/lang/String;Landroidx/savedstate/SavedStateRegistryOwner;)V + public fun describeContents ()I + public final fun detachFromParentRegistry ()V + public final fun prune (Ljava/util/Collection;)V + public final fun restore (Lcom/squareup/workflow1/ui/container/ViewStateCache;)V + public final fun update (Ljava/util/Collection;Landroid/view/View;Landroid/view/View;)V + public fun writeToParcel (Landroid/os/Parcel;I)V +} + +public final class com/squareup/workflow1/ui/container/ViewStateCache$CREATOR : android/os/Parcelable$Creator { + public fun createFromParcel (Landroid/os/Parcel;)Lcom/squareup/workflow1/ui/container/ViewStateCache; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public fun newArray (I)[Lcom/squareup/workflow1/ui/container/ViewStateCache; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class com/squareup/workflow1/ui/container/ViewStateCache$SavedState : android/view/View$BaseSavedState { + public static final field CREATOR Lcom/squareup/workflow1/ui/container/ViewStateCache$SavedState$CREATOR; + public fun (Landroid/os/Parcel;)V + public fun (Landroid/os/Parcelable;Lcom/squareup/workflow1/ui/container/ViewStateCache;)V + public final fun getViewStateCache ()Lcom/squareup/workflow1/ui/container/ViewStateCache; + public fun writeToParcel (Landroid/os/Parcel;I)V +} + +public final class com/squareup/workflow1/ui/container/ViewStateCache$SavedState$CREATOR : android/os/Parcelable$Creator { + public fun createFromParcel (Landroid/os/Parcel;)Lcom/squareup/workflow1/ui/container/ViewStateCache$SavedState; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public fun newArray (I)[Lcom/squareup/workflow1/ui/container/ViewStateCache$SavedState; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + diff --git a/workflow-ui/core-android/src/androidTest/AndroidManifest.xml b/workflow-ui/core-android/src/androidTest/AndroidManifest.xml index f09bff9472..988bfbc7fc 100644 --- a/workflow-ui/core-android/src/androidTest/AndroidManifest.xml +++ b/workflow-ui/core-android/src/androidTest/AndroidManifest.xml @@ -5,5 +5,8 @@ + diff --git a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeScreenViewFactoryTest.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeScreenViewFactoryTest.kt new file mode 100644 index 0000000000..70685b3770 --- /dev/null +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeScreenViewFactoryTest.kt @@ -0,0 +1,153 @@ +package com.squareup.workflow1.ui + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +@OptIn(WorkflowUiExperimentalApi::class) +internal class DecorativeScreenViewFactoryTest { + private val instrumentation = InstrumentationRegistry.getInstrumentation() + + @Test fun initializeView_is_only_call_to_showRendering() { + val events = mutableListOf() + + val innerViewFactory = object : ScreenViewFactory { + override val type = InnerRendering::class + override fun buildView( + initialRendering: InnerRendering, + initialViewEnvironment: ViewEnvironment, + contextForNewView: Context, + container: ViewGroup? + ): View = InnerView(contextForNewView).apply { + bindShowRendering(initialRendering, initialViewEnvironment) { rendering, _ -> + events += "inner showRendering $rendering" + } + } + } + + val envString = object : ViewEnvironmentKey(String::class) { + override val default: String get() = "Not set" + } + + val outerViewFactory = DecorativeScreenViewFactory( + type = OuterRendering::class, + map = { outer, env -> + val enhancedEnv = env + (envString to "Updated environment") + Pair(outer.wrapped, enhancedEnv) + }, + initializeView = { + val outerRendering = getRendering() + events += "initializeView $outerRendering ${environment!![envString]}" + showFirstRendering() + events += "exit initializeView" + } + ) + val viewRegistry = ViewRegistry(innerViewFactory) + val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) + + outerViewFactory.buildView( + OuterRendering("outer", InnerRendering("inner")), + viewEnvironment, + instrumentation.context + ) + + assertThat(events).containsExactly( + "initializeView OuterRendering(outerData=outer, wrapped=InnerRendering(innerData=inner)) " + + "Updated environment", + "inner showRendering InnerRendering(innerData=inner)", + "exit initializeView" + ) + } + + @Test fun initial_doShowRendering_calls_wrapped_showRendering() { + val events = mutableListOf() + + val innerViewFactory = object : ScreenViewFactory { + override val type = InnerRendering::class + override fun buildView( + initialRendering: InnerRendering, + initialViewEnvironment: ViewEnvironment, + contextForNewView: Context, + container: ViewGroup? + ): View = InnerView(contextForNewView).apply { + bindShowRendering(initialRendering, initialViewEnvironment) { rendering, _ -> + events += "inner showRendering $rendering" + } + } + } + val outerViewFactory = DecorativeScreenViewFactory( + type = OuterRendering::class, + map = { outer -> outer.wrapped }, + doShowRendering = { _, innerShowRendering, outerRendering, env -> + events += "doShowRendering $outerRendering" + innerShowRendering(outerRendering.wrapped, env) + } + ) + val viewRegistry = ViewRegistry(innerViewFactory) + val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) + + outerViewFactory.buildView( + OuterRendering("outer", InnerRendering("inner")), + viewEnvironment, + instrumentation.context + ) + + assertThat(events).containsExactly( + "doShowRendering OuterRendering(outerData=outer, wrapped=InnerRendering(innerData=inner))", + "inner showRendering InnerRendering(innerData=inner)" + ) + } + + @Test fun subsequent_showRendering_calls_wrapped_showRendering() { + val events = mutableListOf() + + val innerViewFactory = object : ScreenViewFactory { + override val type = InnerRendering::class + override fun buildView( + initialRendering: InnerRendering, + initialViewEnvironment: ViewEnvironment, + contextForNewView: Context, + container: ViewGroup? + ): View = InnerView(contextForNewView).apply { + bindShowRendering(initialRendering, initialViewEnvironment) { rendering, _ -> + events += "inner showRendering $rendering" + } + } + } + val outerViewFactory = DecorativeScreenViewFactory( + type = OuterRendering::class, + map = { outer -> outer.wrapped }, + doShowRendering = { _, innerShowRendering, outerRendering, env -> + events += "doShowRendering $outerRendering" + innerShowRendering(outerRendering.wrapped, env) + } + ) + val viewRegistry = ViewRegistry(innerViewFactory) + val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) + + val view = outerViewFactory.buildView( + OuterRendering("out1", InnerRendering("in1")), + viewEnvironment, + instrumentation.context + ) + events.clear() + + view.showRendering(OuterRendering("out2", InnerRendering("in2")), viewEnvironment) + + assertThat(events).containsExactly( + "doShowRendering OuterRendering(outerData=out2, wrapped=InnerRendering(innerData=in2))", + "inner showRendering InnerRendering(innerData=in2)" + ) + } + + private data class InnerRendering(val innerData: String) : Screen + private data class OuterRendering( + val outerData: String, + val wrapped: InnerRendering + ) : Screen + + private class InnerView(context: Context) : View(context) +} diff --git a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeViewFactoryTest.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeViewFactoryTest.kt index 8220c1b6a4..3f02ef81d0 100644 --- a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeViewFactoryTest.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeViewFactoryTest.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package com.squareup.workflow1.ui import android.content.Context diff --git a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/WorkflowViewStubLifecycleActivity.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/WorkflowViewStubLifecycleActivity.kt index d4bec1f911..9a1754bd2e 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 @@ -8,19 +8,19 @@ import com.squareup.workflow1.ui.internal.test.AbstractLifecycleTestActivity @OptIn(WorkflowUiExperimentalApi::class) internal class WorkflowViewStubLifecycleActivity : AbstractLifecycleTestActivity() { - sealed class TestRendering { + sealed class TestRendering : Screen { data class LeafRendering(val name: String) : TestRendering(), Compatible { override val compatibilityKey: String get() = name } data class RecurseRendering(val wrapped: TestRendering) : TestRendering() - abstract class ViewRendering> : TestRendering(), AndroidViewRendering + abstract class ViewRendering> : TestRendering(), AndroidScreen } override val viewRegistry: ViewRegistry = ViewRegistry( leafViewBinding(LeafRendering::class, lifecycleLoggingViewObserver { it.name }), - BuilderViewFactory(RecurseRendering::class) { initialRendering, + ManualScreenViewFactory(RecurseRendering::class) { initialRendering, initialViewEnvironment, contextForNewView, _ -> FrameLayout(contextForNewView).also { container -> @@ -30,7 +30,7 @@ internal class WorkflowViewStubLifecycleActivity : AbstractLifecycleTestActivity initialRendering, initialViewEnvironment ) { rendering, env -> - stub.update(rendering.wrapped, 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 af76b8ab9c..e480065127 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,7 +255,7 @@ internal class WorkflowViewStubLifecycleTest { } data class RegistrySetter(val wrapped: TestRendering) : ViewRendering() { - override val viewFactory: ViewFactory = BuilderViewFactory( + override val viewFactory: ScreenViewFactory = ManualScreenViewFactory( RegistrySetter::class ) { initialRendering, initialViewEnvironment, context, _ -> val stub = WorkflowViewStub(context) @@ -265,7 +265,7 @@ internal class WorkflowViewStubLifecycleTest { addView(stub) bindShowRendering(initialRendering, initialViewEnvironment) { r, e -> - stub.update(r.wrapped, e) + stub.show(r.wrapped, e) } } } @@ -298,7 +298,7 @@ internal class WorkflowViewStubLifecycleTest { const val Tag = "counter" } - override val viewFactory: ViewFactory = BuilderViewFactory( + override val viewFactory: ScreenViewFactory = ManualScreenViewFactory( CounterRendering::class ) { initialRendering, initialViewEnvironment, context, _ -> var counter = 0 diff --git a/workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/backstack/test/BackstackContainerTest.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/BackstackContainerTest.kt similarity index 96% rename from workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/backstack/test/BackstackContainerTest.kt rename to workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/BackstackContainerTest.kt index fed23dcc47..aaa1061d7b 100644 --- a/workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/backstack/test/BackstackContainerTest.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/BackstackContainerTest.kt @@ -1,4 +1,4 @@ -package com.squareup.workflow1.ui.backstack.test +package com.squareup.workflow1.ui.container import android.os.Build import android.view.View @@ -9,11 +9,11 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.filters.SdkSuppress import com.google.common.truth.Truth.assertThat import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backstack.test.fixtures.BackStackContainerLifecycleActivity -import com.squareup.workflow1.ui.backstack.test.fixtures.BackStackContainerLifecycleActivity.TestRendering.LeafRendering -import com.squareup.workflow1.ui.backstack.test.fixtures.BackStackContainerLifecycleActivity.TestRendering.RecurseRendering -import com.squareup.workflow1.ui.backstack.test.fixtures.viewForScreen -import com.squareup.workflow1.ui.backstack.test.fixtures.waitForScreen +import com.squareup.workflow1.ui.container.fixtures.BackStackContainerLifecycleActivity +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.container.fixtures.viewForScreen +import com.squareup.workflow1.ui.container.fixtures.waitForScreen import org.junit.Rule import org.junit.Test diff --git a/workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/backstack/test/ViewStateCacheTest.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/ViewStateCacheTest.kt similarity index 87% rename from workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/backstack/test/ViewStateCacheTest.kt rename to workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/ViewStateCacheTest.kt index 0483ccfbb4..090fe6f59f 100644 --- a/workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/backstack/test/ViewStateCacheTest.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/ViewStateCacheTest.kt @@ -1,4 +1,4 @@ -package com.squareup.workflow1.ui.backstack.test +package com.squareup.workflow1.ui.container import android.os.Parcel import android.os.Parcelable @@ -7,14 +7,12 @@ import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat -import com.squareup.workflow1.ui.Named +import com.squareup.workflow1.ui.NamedScreen +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backstack.KeyedStateRegistryOwner -import com.squareup.workflow1.ui.backstack.ViewStateCache -import com.squareup.workflow1.ui.backstack.ViewStateFrame -import com.squareup.workflow1.ui.backstack.test.fixtures.ViewStateTestView +import com.squareup.workflow1.ui.container.fixtures.ViewStateTestView import com.squareup.workflow1.ui.bindShowRendering import org.junit.Assert.fail import org.junit.Test @@ -32,8 +30,10 @@ internal class ViewStateCacheTest { private val instrumentation = InstrumentationRegistry.getInstrumentation() private val viewEnvironment = ViewEnvironment() + private object AScreen : Screen + @Test fun saves_and_restores_self() { - val rendering = Named(wrapped = Unit, name = "rendering") + val rendering = NamedScreen(wrapped = AScreen, name = "rendering") val childState = SparseArray().apply { put(0, TestChildState("hello world")) } @@ -57,8 +57,8 @@ internal class ViewStateCacheTest { @Test fun saves_and_restores_child_states_on_navigation() { val cache = ViewStateCache() - val firstRendering = Named(wrapped = Unit, name = "first") - val secondRendering = Named(wrapped = Unit, name = "second") + val firstRendering = NamedScreen(wrapped = AScreen, name = "first") + val secondRendering = NamedScreen(wrapped = AScreen, name = "second") // Android requires ID to be set for view hierarchy to be saved or restored. val firstView = createTestView(firstRendering, id = 1) val secondView = createTestView(secondRendering) @@ -84,8 +84,8 @@ internal class ViewStateCacheTest { @Test fun doesnt_restore_state_when_restored_view_id_is_different() { val cache = ViewStateCache() - val firstRendering = Named(wrapped = Unit, name = "first") - val secondRendering = Named(wrapped = Unit, name = "second") + val firstRendering = NamedScreen(wrapped = AScreen, name = "first") + val secondRendering = NamedScreen(wrapped = AScreen, name = "second") // Android requires ID to be set for view hierarchy to be saved or restored. val firstView = createTestView(firstRendering, id = 1) val secondView = createTestView(secondRendering) @@ -115,8 +115,8 @@ internal class ViewStateCacheTest { @Test fun doesnt_restore_state_when_view_id_not_set() { val cache = ViewStateCache() - val firstRendering = Named(wrapped = Unit, name = "first") - val secondRendering = Named(wrapped = Unit, name = "second") + val firstRendering = NamedScreen(wrapped = AScreen, name = "first") + val secondRendering = NamedScreen(wrapped = AScreen, name = "second") val firstView = createTestView(firstRendering) val secondView = createTestView(secondRendering) @@ -141,20 +141,20 @@ internal class ViewStateCacheTest { @Test fun throws_when_view_not_bound() { val cache = ViewStateCache() - val rendering = Named(wrapped = Unit, name = "duplicate") + val rendering = NamedScreen(wrapped = AScreen, name = "duplicate") val view = View(instrumentation.context) try { cache.update(listOf(rendering, rendering), null, view) fail("Expected exception.") } catch (e: IllegalStateException) { - assertThat(e.message).contains("to be showing a Named<*> rendering, found null") + assertThat(e.message).contains("to be showing a NamedScreen<*> rendering, found null") } } @Test fun throws_on_duplicate_renderings() { val cache = ViewStateCache() - val rendering = Named(wrapped = Unit, name = "duplicate") + val rendering = NamedScreen(wrapped = AScreen, name = "duplicate") val view = createTestView(rendering) try { @@ -166,7 +166,7 @@ internal class ViewStateCacheTest { } private fun createTestView( - firstRendering: Named, + firstRendering: NamedScreen<*>, id: Int? = null ) = ViewStateTestView(instrumentation.context).also { view -> id?.let { view.id = id } diff --git a/workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/backstack/test/fixtures/BackStackContainerLifecycleActivity.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/fixtures/BackStackContainerLifecycleActivity.kt similarity index 86% rename from workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/backstack/test/fixtures/BackStackContainerLifecycleActivity.kt rename to workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/fixtures/BackStackContainerLifecycleActivity.kt index ca84e4bf15..524e138c87 100644 --- a/workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/backstack/test/fixtures/BackStackContainerLifecycleActivity.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/fixtures/BackStackContainerLifecycleActivity.kt @@ -1,4 +1,4 @@ -package com.squareup.workflow1.ui.backstack.test.fixtures +package com.squareup.workflow1.ui.container.fixtures import android.content.Context import android.view.View @@ -8,17 +8,18 @@ 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.BuilderViewFactory +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.ViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub -import com.squareup.workflow1.ui.backstack.BackStackScreen -import com.squareup.workflow1.ui.backstack.test.fixtures.BackStackContainerLifecycleActivity.TestRendering.LeafRendering -import com.squareup.workflow1.ui.backstack.test.fixtures.BackStackContainerLifecycleActivity.TestRendering.RecurseRendering +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 @@ -31,7 +32,7 @@ internal class BackStackContainerLifecycleActivity : AbstractLifecycleTestActivi /** * Default rendering always shown in the backstack to simplify test configuration. */ - object BaseRendering : ViewFactory { + object BaseRendering : Screen, ScreenViewFactory { override val type: KClass = BaseRendering::class override fun buildView( initialRendering: BaseRendering, @@ -43,7 +44,7 @@ internal class BackStackContainerLifecycleActivity : AbstractLifecycleTestActivi } } - sealed class TestRendering { + sealed class TestRendering : Screen { data class LeafRendering(val name: String) : TestRendering(), Compatible { override val compatibilityKey: String get() = name } @@ -108,7 +109,7 @@ internal class BackStackContainerLifecycleActivity : AbstractLifecycleTestActivi NoTransitionBackStackContainer, BaseRendering, leafViewBinding(LeafRendering::class, viewObserver, viewConstructor = ::ViewStateTestView), - BuilderViewFactory(RecurseRendering::class) { initialRendering, + ManualScreenViewFactory(RecurseRendering::class) { initialRendering, initialViewEnvironment, contextForNewView, _ -> FrameLayout(contextForNewView).also { container -> @@ -118,7 +119,7 @@ internal class BackStackContainerLifecycleActivity : AbstractLifecycleTestActivi initialRendering, initialViewEnvironment ) { rendering, env -> - stub.update(rendering.wrappedBackstack.toBackstackWithBase(), env) + stub.show(rendering.wrappedBackstack.toBackstackWithBase(), env) } } }, diff --git a/workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/backstack/test/fixtures/NoTransitionBackStackContainer.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/fixtures/NoTransitionBackStackContainer.kt similarity index 72% rename from workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/backstack/test/fixtures/NoTransitionBackStackContainer.kt rename to workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/fixtures/NoTransitionBackStackContainer.kt index df2289c8b3..026848fd26 100644 --- a/workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/backstack/test/fixtures/NoTransitionBackStackContainer.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/fixtures/NoTransitionBackStackContainer.kt @@ -1,15 +1,15 @@ -package com.squareup.workflow1.ui.backstack.test.fixtures +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.BuilderViewFactory -import com.squareup.workflow1.ui.ViewFactory +import com.squareup.workflow1.ui.ManualScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backstack.BackStackContainer -import com.squareup.workflow1.ui.backstack.BackStackScreen import com.squareup.workflow1.ui.bindShowRendering -import com.squareup.workflow1.ui.container.R +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 @@ -23,8 +23,8 @@ internal class NoTransitionBackStackContainer(context: Context) : BackStackConta addView(newView) } - companion object : ViewFactory> - by BuilderViewFactory( + companion object : ScreenViewFactory> + by ManualScreenViewFactory( type = BackStackScreen::class, viewConstructor = { initialRendering, initialEnv, context, _ -> NoTransitionBackStackContainer(context) diff --git a/workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/backstack/test/fixtures/ViewStateTestView.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/fixtures/ViewStateTestView.kt similarity index 90% rename from workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/backstack/test/fixtures/ViewStateTestView.kt rename to workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/fixtures/ViewStateTestView.kt index c4ba553c6a..f42a41b0f5 100644 --- a/workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/backstack/test/fixtures/ViewStateTestView.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/fixtures/ViewStateTestView.kt @@ -1,11 +1,11 @@ -package com.squareup.workflow1.ui.backstack.test.fixtures +package com.squareup.workflow1.ui.container.fixtures import android.content.Context import android.os.Parcel import android.os.Parcelable import android.os.Parcelable.Creator import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backstack.test.fixtures.BackStackContainerLifecycleActivity.TestRendering.LeafRendering +import com.squareup.workflow1.ui.container.fixtures.BackStackContainerLifecycleActivity.TestRendering.LeafRendering import com.squareup.workflow1.ui.internal.test.AbstractLifecycleTestActivity.LeafView /** 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 new file mode 100644 index 0000000000..13a65d05ba --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidScreen.kt @@ -0,0 +1,95 @@ +package com.squareup.workflow1.ui + +import android.content.Context +import android.view.View +import android.view.ViewGroup + +/** + * Interface implemented by a rendering class to allow it to drive an Android UI + * via an appropriate [ScreenViewFactory] implementation. + * + * You will rarely, if ever, write a [ScreenViewFactory] yourself. Instead + * use [ScreenViewRunner.bind] to work with XML layout resources, or + * [BuilderViewFactory] to create views from code. See [ScreenViewRunner] for more + * details. + * + * @OptIn(WorkflowUiExperimentalApi::class) + * data class HelloScreen( + * val message: String, + * val onClick: () -> Unit + * ) : AndroidScreen { + * override val viewFactory = + * ScreenViewRunner.bind(HelloGoodbyeLayoutBinding::inflate) { screen, _ -> + * helloMessage.text = screen.message + * helloMessage.setOnClickListener { screen.onClick() } + * } + * } + * + * This is the simplest way to bridge the gap between your workflows and the UI, + * but using it requires your workflows code to reside in Android modules, instead + * of pure Kotlin. If this is a problem, or you need more flexibility for any other + * reason, you can use [ViewRegistry] to bind your renderings to [ScreenViewFactory] + * implementations at runtime. Also note that a [ViewRegistry] entry will override + * the [viewFactory] returned by an [AndroidScreen]. + */ +@WorkflowUiExperimentalApi +public interface AndroidScreen> : Screen { + /** + * Used to build instances of [android.view.View] as needed to + * display renderings of this type. + */ + public val viewFactory: ScreenViewFactory +} + +/** + * It is usually more convenient to use [WorkflowViewStub] or [DecorativeScreenViewFactory] + * than to call this method directly. + * + * Finds a [ScreenViewFactory] to create a [View] to display [this@buildView]. The new view + * can be updated via calls to [View.showRendering] -- that is, it is guaranteed that + * [bindShowRendering] has been called on this view. + * + * The returned view will have a + * [WorkflowLifecycleOwner][com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner] + * set on it. The returned view must EITHER: + * + * 1. Be attached at least once to ensure that the lifecycle eventually gets destroyed (because its + * parent is destroyed), or + * 2. Have its + * [WorkflowLifecycleOwner.destroyOnDetach][com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner.destroyOnDetach] + * called, which will either schedule the + * lifecycle to be destroyed if the view is attached, or destroy it immediately if it's detached. + * + * [WorkflowViewStub] takes care of this chore itself. + * + * @param initializeView Optional function invoked immediately after the [View] is + * created (that is, immediately after the call to [ScreenViewFactory.buildView]). + * [showRendering], [getRendering] and [environment] are all available when this is called. + * Defaults to a call to [View.showFirstRendering]. + * + * @throws IllegalArgumentException if no builder can be find for type [ScreenT] + * + * @throws IllegalStateException if the matching [ScreenViewFactory] fails to call + * [View.bindShowRendering] when constructing the view + */ +@WorkflowUiExperimentalApi +public fun ScreenT.buildView( + viewEnvironment: ViewEnvironment, + contextForNewView: Context, + container: ViewGroup? = null, + initializeView: View.() -> Unit = { showFirstRendering() } +): View { + val entry = viewEnvironment.getViewFactoryForRendering(this) + val viewFactory = (entry as? ScreenViewFactory) + ?: error("Require a ScreenViewFactory for $this, found $entry") + + return viewFactory.buildView( + this, viewEnvironment, contextForNewView, container + ).also { view -> + checkNotNull(view.showRenderingTag) { + "View.bindShowRendering should have been called for $view, typically by the " + + "${ScreenViewFactory::class.java.name} that created it." + } + initializeView.invoke(view) + } +} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewEnvironment.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewEnvironment.kt new file mode 100644 index 0000000000..1b07a68778 --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewEnvironment.kt @@ -0,0 +1,62 @@ +package com.squareup.workflow1.ui + +import android.view.View +import com.squareup.workflow1.ui.container.BackStackScreen +import com.squareup.workflow1.ui.container.BackStackScreenViewFactory +import com.squareup.workflow1.ui.container.WithEnvironmentViewFactory +import com.squareup.workflow1.ui.container.WithEnvironment + +/** + * It is usually more convenient to use [WorkflowViewStub] or [DecorativeScreenViewFactory] + * than to call this method directly. + * + * Returns the [ScreenViewFactory] that builds [View] instances suitable to display the given [rendering], + * via subsequent calls to [View.showRendering]. + * + * Prefers factories found via [ViewRegistry.getEntryFor]. If that returns null, falls + * back to the builder provided by the rendering's implementation of + * [AndroidScreen.viewFactory], if there is one. Note that this means that a + * compile time [AndroidScreen.viewFactory] binding can be overridden at runtime. + * + * The returned view will have a + * [WorkflowLifecycleOwner][com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner] + * set on it. The returned view must EITHER: + * + * 1. Be attached at least once to ensure that the lifecycle eventually gets destroyed (because its + * parent is destroyed), or + * 2. Have its + * [WorkflowLifecycleOwner.destroyOnDetach][com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner.destroyOnDetach] + * called, which will either schedule the + * lifecycle to be destroyed if the view is attached, or destroy it immediately if it's detached. + * + * @throws IllegalArgumentException if no builder can be find for type [ScreenT] + */ +@WorkflowUiExperimentalApi +public fun + ViewEnvironment.getViewFactoryForRendering(rendering: ScreenT): ScreenViewFactory { + @Suppress("UNCHECKED_CAST", "DEPRECATION") + return (get(ViewRegistry).getEntryFor(rendering::class) as? ScreenViewFactory) + ?: (rendering as? AndroidScreen<*>)?.viewFactory as? ScreenViewFactory + ?: (rendering as? AsScreen<*>)?.let { AsScreenViewFactory as ScreenViewFactory } + ?: (rendering as? BackStackScreen<*>)?.let { + BackStackScreenViewFactory as ScreenViewFactory + } + ?: (rendering as? NamedScreen<*>)?.let { NamedScreenViewFactory as ScreenViewFactory } + ?: (rendering as? WithEnvironment<*>)?.let { + WithEnvironmentViewFactory as ScreenViewFactory + } + ?: throw IllegalArgumentException( + "A ScreenViewFactory should have been registered to display $rendering, " + + "or that class should implement AndroidScreen." + ) +} + +/** + * Default implementation for the `initializeView` argument of [Screen.buildView], + * and for [DecorativeScreenViewFactory.initializeView]. Calls [showRendering] against + * [getRendering] and [environment]. + */ +@WorkflowUiExperimentalApi +public fun View.showFirstRendering() { + showRendering(getRendering()!!, environment!!) +} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewRegistry.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewRegistry.kt new file mode 100644 index 0000000000..631a14328e --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewRegistry.kt @@ -0,0 +1,56 @@ +@file:Suppress("DEPRECATION") + +package com.squareup.workflow1.ui + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import com.squareup.workflow1.ui.container.BackStackScreen +import kotlin.reflect.KClass + +@Deprecated("Use ViewEnvironment.getViewFactoryForRendering()") +@WorkflowUiExperimentalApi +public fun + ViewRegistry.getFactoryForRendering(rendering: RenderingT): ViewFactory { + @Suppress("UNCHECKED_CAST") + return getFactoryFor(rendering::class) + ?: (rendering as? AndroidViewRendering<*>)?.viewFactory as? ViewFactory + ?: (rendering as? AsScreen<*>)?.let { AsScreenLegacyViewFactory as ViewFactory } + ?: (rendering as? BackStackScreen<*>)?.let { + BackStackScreenLegacyViewFactory as ViewFactory + } + ?: (rendering as? Named<*>)?.let { NamedViewFactory as ViewFactory } + ?: throw IllegalArgumentException( + "A ViewFactory should have been registered to display $rendering, " + + "or that class should implement AndroidViewRendering." + ) +} + +@Deprecated("Use getEntryFor()") +@WorkflowUiExperimentalApi +public fun ViewRegistry.getFactoryFor( + renderingType: KClass +): ViewFactory? { + return getEntryFor(renderingType) as? ViewFactory +} + +@Suppress("DEPRECATION") +@Deprecated("Use ViewEnvironment.buildview") +@WorkflowUiExperimentalApi +public fun ViewRegistry.buildView( + initialRendering: RenderingT, + initialViewEnvironment: ViewEnvironment, + contextForNewView: Context, + container: ViewGroup? = null, + initializeView: View.() -> Unit = { showFirstRendering() } +): View { + return getFactoryForRendering(initialRendering).buildView( + initialRendering, initialViewEnvironment, contextForNewView, container + ).also { view -> + checkNotNull(view.showRenderingTag) { + "View.bindShowRendering should have been called for $view, typically by the " + + "${ViewFactory::class.java.name} that created it." + } + initializeView.invoke(view) + } +} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewRendering.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewRendering.kt index 35e551fe81..775c506faa 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewRendering.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewRendering.kt @@ -1,32 +1,7 @@ package com.squareup.workflow1.ui -/** - * Interface implemented by a rendering class to allow it to drive an Android UI - * via an appropriate [ViewFactory] implementation. - * - * You will rarely, if ever, write a [ViewFactory] yourself. Instead - * use [LayoutRunner.bind] to work with XML layout resources, or - * [BuilderViewFactory] to create views from code. See [LayoutRunner] for more - * details. - * - * @OptIn(WorkflowUiExperimentalApi::class) - * data class HelloView( - * val message: String, - * val onClick: () -> Unit - * ) : AndroidViewRendering { - * override val viewFactory: ViewFactory = - * LayoutRunner.bind(HelloGoodbyeLayoutBinding::inflate) { r, _ -> - * helloMessage.text = r.message - * helloMessage.setOnClickListener { r.onClick() } - * } - * } - * - * This is the simplest way to bridge the gap between your workflows and the UI, - * but using it requires your workflows code to reside in Android modules, instead - * of pure Kotlin. If this is a problem, or you need more flexibility for any other - * reason, you can use [ViewRegistry] to bind your renderings to [ViewFactory] - * implementations at runtime. - */ +@Suppress("DEPRECATION") +@Deprecated("Use AndroidScreen") @WorkflowUiExperimentalApi public interface AndroidViewRendering> { /** diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AsScreenLegacyViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AsScreenLegacyViewFactory.kt new file mode 100644 index 0000000000..2951307656 --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AsScreenLegacyViewFactory.kt @@ -0,0 +1,6 @@ +package com.squareup.workflow1.ui + +@Suppress("DEPRECATION") +@WorkflowUiExperimentalApi +internal object AsScreenLegacyViewFactory : ViewFactory> +by DecorativeViewFactory(AsScreen::class, { asScreen -> asScreen.rendering }) 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 new file mode 100644 index 0000000000..2363518a66 --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AsScreenViewFactory.kt @@ -0,0 +1,28 @@ +package com.squareup.workflow1.ui + +@WorkflowUiExperimentalApi +@Suppress("DEPRECATION") +internal object AsScreenViewFactory : ScreenViewFactory> +by ManualScreenViewFactory( + type = AsScreen::class, + viewConstructor = { initialRendering, initialViewEnvironment, context, container -> + initialViewEnvironment[ViewRegistry] + .buildView( + initialRendering.rendering, + initialViewEnvironment, + context, + container, + // Don't call showRendering yet, we need to wrap the function first. + initializeView = { } + ).also { view -> + val legacyShowRendering = view.getShowRendering()!! + + view.bindShowRendering( + initialRendering, + initialViewEnvironment + ) { rendering, env -> legacyShowRendering(rendering.rendering, env) } + + view.showFirstRendering() + } + } +) diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/BackStackScreenLegacyViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/BackStackScreenLegacyViewFactory.kt new file mode 100644 index 0000000000..c60da654bc --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/BackStackScreenLegacyViewFactory.kt @@ -0,0 +1,21 @@ +package com.squareup.workflow1.ui + +import android.view.ViewGroup.LayoutParams +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import com.squareup.workflow1.ui.container.BackStackContainer +import com.squareup.workflow1.ui.container.BackStackScreen + +@Suppress("DEPRECATION") +@WorkflowUiExperimentalApi +internal object BackStackScreenLegacyViewFactory : ViewFactory> +by BuilderViewFactory( + type = BackStackScreen::class, + viewConstructor = { initialRendering, initialEnv, context, _ -> + BackStackContainer(context) + .apply { + id = R.id.workflow_back_stack_container + layoutParams = (LayoutParams(MATCH_PARENT, MATCH_PARENT)) + bindShowRendering(initialRendering, initialEnv, ::update) + } + } +) 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 c3f24accc8..e18aa37185 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 @@ -5,25 +5,8 @@ import android.view.View import android.view.ViewGroup import kotlin.reflect.KClass -/** - * A [ViewFactory] that creates [View]s that need to be generated from code. - * (Use [LayoutRunner] to work with XML layout resources.) - * - * data class MyView(): AndroidViewRendering { - * val viewFactory = BuilderViewFactory( - * 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: MyView) { ... } - * } - */ +@Suppress("DEPRECATION") +@Deprecated("Use ManualScreenViewFactory") @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 new file mode 100644 index 0000000000..de2b3334cf --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeScreenViewFactory.kt @@ -0,0 +1,185 @@ +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 }, + * initializeView = { + * TutorialTipRunner.run(this) + * showFirstRendering>() + * } + * ) + * + * 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 initializeView Optional function invoked immediately after the [View] is + * created (that is, immediately after the call to [ScreenViewFactory.buildView]). + * [showRendering], [getRendering] and [environment] are all available when this is called. + * Defaults to a call to [View.showFirstRendering]. + * + * @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 initializeView: View.() -> Unit = { showFirstRendering() }, + 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, + initializeView: View.() -> Unit = { showFirstRendering() }, + 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) }, + initializeView = initializeView, + 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, + // Don't call showRendering yet, we need to wrap the function first. + initializeView = { } + ) + .also { view -> + val innerShowRendering: ViewShowRendering = view.getShowRendering()!! + + view.bindShowRendering( + initialRendering, + processedInitialEnv + ) { rendering, env -> doShowRendering(view, innerShowRendering, rendering, env) } + + view.initializeView() + } + } +} 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 346b708b59..dcd11eeba7 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,112 +5,8 @@ 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) - * class AliasRendering(val similarData: String) : Compatible { - * override val compatibilityKey: String = Compatible.keyFor(wrapped) - * } - * - * 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 }, - * initializeView = { - * TutorialTipRunner.run(this) - * showFirstRendering>() - * } - * ) - * - * 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 initializeView Optional function invoked immediately after the [View] is - * created (that is, immediately after the call to [ViewFactory.buildView]). - * [showRendering], [getRendering] and [environment] are all available when this is called. - * Defaults to a call to [View.showFirstRendering]. - * - * @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") @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 a39f1b0281..0300b09b43 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 @@ -1,24 +1,11 @@ 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] is ready to be displayed in a view inflated from a layout resource - * by a [ViewRegistry]. (Use [BuilderViewFactory] if you want to build views from code rather - * than layouts.) - * - * 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 [LayoutRunner.bind]. - */ +@Suppress("DEPRECATION") +@Deprecated("Use ScreenViewRunner") @WorkflowUiExperimentalApi public fun interface LayoutRunner { public fun showRendering( @@ -98,7 +85,3 @@ public fun interface LayoutRunner { ): ViewFactory = bind(layoutId) { LayoutRunner { _, _ -> } } } } - -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/LayoutRunnerViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunnerViewFactory.kt index 2a25ad5fc6..e471e19956 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunnerViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunnerViewFactory.kt @@ -11,6 +11,7 @@ import kotlin.reflect.KClass * [LayoutRunner factory][runnerConstructor] function. See [LayoutRunner] for * details. */ +@Suppress("DEPRECATION") @WorkflowUiExperimentalApi @PublishedApi internal class LayoutRunnerViewFactory( 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 new file mode 100644 index 0000000000..49a117406b --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutScreenViewFactory.kt @@ -0,0 +1,36 @@ +package com.squareup.workflow1.ui + +import android.content.Context +import android.view.View +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, + @LayoutRes private val layoutId: Int, + private val runnerConstructor: (View) -> ScreenViewRunner +) : ScreenViewFactory { + override fun buildView( + initialRendering: RenderingT, + initialViewEnvironment: ViewEnvironment, + contextForNewView: 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) + } + } + } +} 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 new file mode 100644 index 0000000000..97147b970e --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ManualScreenViewFactory.kt @@ -0,0 +1,43 @@ +package com.squareup.workflow1.ui + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import kotlin.reflect.KClass + +/** + * A [ScreenViewFactory] that creates [View]s that need to be generated from code. + * (Use [ScreenViewRunner] to work with XML layout resources.) + * + * data class MyScreen(): AndroidScreen { + * val viewFactory = ManualScreenViewFactory( + * type = MyScreen::class, + * viewConstructor = { initialRendering, _, context, _ -> + * MyFrame(context).apply { + * layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) + * bindShowRendering(initialRendering, ::update) + * } + * ) + * } + * + * private class MyFrame(context: Context) : FrameLayout(context, attributeSet) { + * private fun update(rendering: MyScreen) { ... } + * } + */ +@WorkflowUiExperimentalApi +public class ManualScreenViewFactory( + override val type: KClass, + private val viewConstructor: ( + initialRendering: RenderingT, + initialViewEnvironment: ViewEnvironment, + contextForNewView: Context, + container: ViewGroup? + ) -> View +) : ScreenViewFactory { + override fun buildView( + initialRendering: RenderingT, + initialViewEnvironment: ViewEnvironment, + contextForNewView: Context, + container: ViewGroup? + ): View = viewConstructor(initialRendering, initialViewEnvironment, contextForNewView, container) +} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/NamedScreenViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/NamedScreenViewFactory.kt new file mode 100644 index 0000000000..ddff8051a3 --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/NamedScreenViewFactory.kt @@ -0,0 +1,9 @@ +package com.squareup.workflow1.ui + +/** + * [ScreenViewFactory] that allows views to display instances of [NamedScreen]. Delegates + * to the factory for [NamedScreen.wrapped]. + */ +@WorkflowUiExperimentalApi +internal object NamedScreenViewFactory : ScreenViewFactory> +by DecorativeScreenViewFactory(NamedScreen::class, { named -> named.wrapped }) diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/NamedViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/NamedViewFactory.kt index 7b98144de0..3349140614 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/NamedViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/NamedViewFactory.kt @@ -4,6 +4,7 @@ package com.squareup.workflow1.ui * [ViewFactory] that allows views to display instances of [Named]. Delegates * to the factory for [Named.wrapped]. */ +@Suppress("DEPRECATION") @WorkflowUiExperimentalApi internal object NamedViewFactory : ViewFactory> by DecorativeViewFactory(Named::class, { 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 new file mode 100644 index 0000000000..507658c0ca --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactory.kt @@ -0,0 +1,33 @@ +package com.squareup.workflow1.ui + +import android.content.Context +import android.view.View +import android.view.ViewGroup + +/** + * 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. + * + * 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]. + */ +@WorkflowUiExperimentalApi +public interface ScreenViewFactory : ViewRegistry.Entry { + /** + * Returns a View ready to display [initialRendering] (and any succeeding values) + * via [View.showRendering]. + */ + public fun buildView( + initialRendering: RenderingT, + initialViewEnvironment: ViewEnvironment, + contextForNewView: Context, + container: ViewGroup? = null + ): View +} 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 new file mode 100644 index 0000000000..52e51238fa --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewRunner.kt @@ -0,0 +1,105 @@ +package com.squareup.workflow1.ui + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.viewbinding.ViewBinding + +@WorkflowUiExperimentalApi +public typealias ViewBindingInflater = (LayoutInflater, ViewGroup?, Boolean) -> BindingT + +/** + * A delegate that implements a [showRendering] method to be called when a workflow + * rendering of type [RenderingT] : [Screen] is ready to be displayed in a view created + * by a [ScreenViewFactory]. + * + * If you're using [AndroidX ViewBinding][ViewBinding] you likely won't need to + * implement this interface at all. For details, see the three overloads of [ScreenViewRunner.bind]. + */ +@WorkflowUiExperimentalApi +public fun interface ScreenViewRunner { + public fun showRendering( + rendering: RenderingT, + viewEnvironment: ViewEnvironment + ) + + public companion object { + /** + * Creates a [ScreenViewFactory] that [inflates][bindingInflater] a [ViewBinding] ([BindingT]) + * to show renderings of type [RenderingT] : [Screen], using [a lambda][showRendering]. + * + * val HelloViewFactory: ScreenViewFactory = + * ScreenViewRunner.bind(HelloGoodbyeViewBinding::inflate) { rendering, viewEnvironment -> + * helloMessage.text = rendering.message + * helloMessage.setOnClickListener { rendering.onClick(Unit) } + * } + * + * If you need to initialize your view before [showRendering] is called, + * implement [ScreenViewRunner] and create a binding using the `bind` variant + * that accepts a `(ViewBinding) -> ScreenViewRunner` function, below. + */ + public inline fun bind( + noinline bindingInflater: ViewBindingInflater, + crossinline showRendering: BindingT.(RenderingT, ViewEnvironment) -> Unit + ): ScreenViewFactory = bind(bindingInflater) { binding -> + ScreenViewRunner { rendering, viewEnvironment -> + binding.showRendering(rendering, viewEnvironment) + } + } + + /** + * Creates a [ScreenViewFactory] that [inflates][bindingInflater] a [ViewBinding] ([BindingT]) + * to show renderings of type [RenderingT] : [Screen], using a [ScreenViewRunner] + * created by [constructor]. Handy if you need to perform some set up before + * [showRendering] is called. + * + * class HelloScreenRunner( + * private val binding: HelloGoodbyeViewBinding + * ) : ScreenViewRunner { + * + * override fun showRendering(rendering: HelloScreen) { + * binding.messageView.text = rendering.message + * binding.messageView.setOnClickListener { rendering.onClick(Unit) } + * } + * + * companion object : ScreenViewFactory by bind( + * HelloGoodbyeViewBinding::inflate, ::HelloScreenRunner + * ) + * } + * + * If the view doesn't need to be initialized before [showRendering] is called, + * use the variant above which just takes a lambda. + */ + public inline fun bind( + noinline bindingInflater: ViewBindingInflater, + noinline constructor: (BindingT) -> ScreenViewRunner + ): ScreenViewFactory = + ViewBindingScreenViewFactory(RenderingT::class, bindingInflater, constructor) + + /** + * Creates a [ScreenViewFactory] that inflates [layoutId] to show renderings of + * type [RenderingT] : [Screen], using a [ScreenViewRunner] created by [constructor]. + * Avoids any use of [AndroidX ViewBinding][ViewBinding]. + */ + public inline fun bind( + @LayoutRes layoutId: Int, + noinline constructor: (View) -> ScreenViewRunner + ): ScreenViewFactory = + LayoutScreenViewFactory(RenderingT::class, layoutId, constructor) + + /** + * Creates a [ScreenViewFactory] that inflates [layoutId] to "show" renderings of type [RenderingT], + * with a no-op [ScreenViewRunner]. Handy for showing static views, e.g. when prototyping. + */ + @Suppress("unused") + public inline fun bindNoRunner( + @LayoutRes layoutId: Int + ): ScreenViewFactory = bind(layoutId) { ScreenViewRunner { _, _ -> } } + } +} + +internal fun Context.viewBindingLayoutInflater(container: ViewGroup?) = + LayoutInflater.from(container?.context ?: this) + .cloneInContext(this) diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewBindingScreenViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewBindingScreenViewFactory.kt new file mode 100644 index 0000000000..ab8d0b072a --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewBindingScreenViewFactory.kt @@ -0,0 +1,33 @@ +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, + private val bindingInflater: ViewBindingInflater, + private val runnerConstructor: (BindingT) -> ScreenViewRunner +) : ScreenViewFactory { + override fun buildView( + initialRendering: RenderingT, + initialViewEnvironment: ViewEnvironment, + contextForNewView: 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 +} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewBindingViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewBindingViewFactory.kt index 4b1f215e13..94c62f177c 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewBindingViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewBindingViewFactory.kt @@ -6,6 +6,7 @@ import android.view.ViewGroup import androidx.viewbinding.ViewBinding import kotlin.reflect.KClass +@Suppress("DEPRECATION") @WorkflowUiExperimentalApi @PublishedApi internal class ViewBindingViewFactory( diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewFactory.kt index 6a19e89035..ca7bb3c3c6 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewFactory.kt @@ -3,26 +3,10 @@ package com.squareup.workflow1.ui import android.content.Context import android.view.View import android.view.ViewGroup -import kotlin.reflect.KClass -/** - * Factory for [View] instances that can show renderings of type[RenderingT]. - * - * Two concrete [ViewFactory] implementations are provided: - * - * - The various [bind][LayoutRunner.bind] methods on [LayoutRunner] allow easy use of - * Android XML layout resources and [AndroidX ViewBinding][androidx.viewbinding.ViewBinding]. - * - * - [BuilderViewFactory] allows views to be built from code. - * - * It's simplest to have your rendering classes implement [AndroidViewRendering] to associate - * them with appropriate an appropriate [ViewFactory]. For more flexibility, and to - * avoid coupling your workflow directly to the Android runtime, see [ViewRegistry]. - */ +@Deprecated("Use ScreenViewFactory") @WorkflowUiExperimentalApi -public interface ViewFactory { - public val type: KClass - +public interface ViewFactory : ViewRegistry.Entry { /** * Returns a View ready to display [initialRendering] (and any succeeding values) * via [View.showRendering]. diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewRegistry.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewRegistry.kt deleted file mode 100644 index 6fd84cf762..0000000000 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewRegistry.kt +++ /dev/null @@ -1,206 +0,0 @@ -@file:Suppress("FunctionName") - -package com.squareup.workflow1.ui - -import android.content.Context -import android.view.View -import android.view.ViewGroup -import kotlin.reflect.KClass - -/** - * The [ViewEnvironment] service that can be used to display the stream of renderings - * from a workflow tree as [View] instances. This is the engine behind [AndroidViewRendering], - * [WorkflowViewStub] and [ViewFactory]. Most apps can ignore [ViewRegistry] as an implementation - * detail, by using [AndroidViewRendering] to tie their rendering classes to view code. - * - * To avoid that coupling between workflow code and the Android runtime, registries can - * be loaded with [ViewFactory] instances at runtime, and provided as an optional parameter to - * [WorkflowLayout.start]. - * - * For example: - * - * val AuthViewFactories = ViewRegistry( - * AuthorizingLayoutRunner, LoginLayoutRunner, SecondFactorLayoutRunner - * ) - * - * val TicTacToeViewFactories = ViewRegistry( - * NewGameLayoutRunner, GamePlayLayoutRunner, GameOverLayoutRunner - * ) - * - * val ApplicationViewFactories = ViewRegistry(ApplicationLayoutRunner) + - * AuthViewFactories + TicTacToeViewFactories - * - * override fun onCreate(savedInstanceState: Bundle?) { - * super.onCreate(savedInstanceState) - * - * val model: MyViewModel by viewModels() - * setContentView( - * WorkflowLayout(this).apply { start(model.renderings, ApplicationViewFactories) } - * ) - * } - * - * /** As always, use an androidx ViewModel for state that survives config change. */ - * class MyViewModel(savedState: SavedStateHandle) : ViewModel() { - * val renderings: StateFlow by lazy { - * renderWorkflowIn( - * workflow = rootWorkflow, - * scope = viewModelScope, - * savedStateHandle = savedState - * ) - * } - * } - * - * In the above example, it is assumed that the `companion object`s of the various - * decoupled [LayoutRunner] classes honor a convention of implementing [ViewFactory], in - * aid of this kind of assembly. - * - * class GamePlayLayoutRunner(view: View) : LayoutRunner { - * - * // ... - * - * companion object : ViewFactory by LayoutRunner.bind( - * R.layout.game_layout, ::GameLayoutRunner - * ) - * } - */ -@WorkflowUiExperimentalApi -public interface ViewRegistry { - /** - * The set of unique keys which this registry can derive from the renderings passed to [buildView] - * and for which it knows how to create views. - * - * Used to ensure that duplicate bindings are never registered. - */ - public val keys: Set> - - /** - * This method is not for general use, use [WorkflowViewStub] instead. - * - * Returns the [ViewFactory] that was registered for the given [renderingType], or null - * if none was found. - */ - public fun getFactoryFor( - renderingType: KClass - ): ViewFactory? - - public companion object : ViewEnvironmentKey(ViewRegistry::class) { - override val default: ViewRegistry get() = ViewRegistry() - } -} - -@WorkflowUiExperimentalApi -public fun ViewRegistry(vararg bindings: ViewFactory<*>): ViewRegistry = - TypedViewRegistry(*bindings) - -/** - * Returns a [ViewRegistry] that contains no bindings. - * - * Exists as a separate overload from the other two functions to disambiguate between them. - */ -@WorkflowUiExperimentalApi -public fun ViewRegistry(): ViewRegistry = TypedViewRegistry() - -/** - * It is usually more convenient to use [WorkflowViewStub] or [DecorativeViewFactory] - * than to call this method directly. - * - * Returns the [ViewFactory] that builds [View] instances suitable to display the given [rendering], - * via subsequent calls to [View.showRendering]. - * - * Prefers factories found via [ViewRegistry.getFactoryFor]. If that returns null, falls - * back to the factory provided by the rendering's implementation of - * [AndroidViewRendering.viewFactory], if there is one. Note that this means that a - * compile time [AndroidViewRendering.viewFactory] binding can be overridden at runtime. - * - * The returned view will have a - * [WorkflowLifecycleOwner][com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner] - * set on it. The returned view must EITHER: - * - * 1. Be attached at least once to ensure that the lifecycle eventually gets destroyed (because its - * parent is destroyed), or - * 2. Have its - * [WorkflowLifecycleOwner.destroyOnDetach][com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner.destroyOnDetach] - * called, which will either schedule the - * lifecycle to be destroyed if the view is attached, or destroy it immediately if it's detached. - * - * @throws IllegalArgumentException if no factory can be find for type [RenderingT] - */ -@WorkflowUiExperimentalApi -public fun - ViewRegistry.getFactoryForRendering(rendering: RenderingT): ViewFactory { - @Suppress("UNCHECKED_CAST") - return getFactoryFor(rendering::class) - ?: (rendering as? AndroidViewRendering<*>)?.viewFactory as? ViewFactory - ?: (rendering as? Named<*>)?.let { NamedViewFactory as ViewFactory } - ?: throw IllegalArgumentException( - "A ${ViewFactory::class.qualifiedName} should have been registered to display " + - "${rendering::class.qualifiedName} instances, or that class should implement " + - "${AndroidViewRendering::class.simpleName}<${rendering::class.simpleName}>." - ) -} - -/** - * It is usually more convenient to use [WorkflowViewStub] or [DecorativeViewFactory] - * than to call this method directly. - * - * Finds a [ViewFactory] to create a [View] to display [initialRendering]. The new view - * can be updated via calls to [View.showRendering] -- that is, it is guaranteed that - * [bindShowRendering] has been called on this view. - * - * The returned view will have a - * [WorkflowLifecycleOwner][com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner] - * set on it. The returned view must EITHER: - * - * 1. Be attached at least once to ensure that the lifecycle eventually gets destroyed (because its - * parent is destroyed), or - * 2. Have its - * [WorkflowLifecycleOwner.destroyOnDetach][com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner.destroyOnDetach] - * called, which will either schedule the - * lifecycle to be destroyed if the view is attached, or destroy it immediately if it's detached. - * - * @param initializeView Optional function invoked immediately after the [View] is - * created (that is, immediately after the call to [ViewFactory.buildView]). - * [showRendering], [getRendering] and [environment] are all available when this is called. - * Defaults to a call to [View.showFirstRendering]. - * - * @throws IllegalArgumentException if no factory can be find for type [RenderingT] - * - * @throws IllegalStateException if the matching [ViewFactory] fails to call - * [View.bindShowRendering] when constructing the view - */ -@WorkflowUiExperimentalApi -public fun ViewRegistry.buildView( - initialRendering: RenderingT, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, - container: ViewGroup? = null, - initializeView: View.() -> Unit = { showFirstRendering() } -): View { - return getFactoryForRendering(initialRendering).buildView( - initialRendering, initialViewEnvironment, contextForNewView, container - ).also { view -> - checkNotNull(view.showRenderingTag) { - "View.bindShowRendering should have been called for $view, typically by the " + - "${ViewFactory::class.java.name} that created it." - } - initializeView.invoke(view) - } -} - -@WorkflowUiExperimentalApi -public operator fun ViewRegistry.plus(binding: ViewFactory<*>): ViewRegistry = - this + ViewRegistry(binding) - -@WorkflowUiExperimentalApi -public operator fun ViewRegistry.plus(other: ViewRegistry): ViewRegistry = - CompositeViewRegistry(this, other) - -/** - * Default implementation for the `initializeView` argument of [ViewRegistry.buildView], - * and for [DecorativeViewFactory.initializeView]. Calls [showRendering] against - * [getRendering] and [environment]. - */ -@WorkflowUiExperimentalApi -public fun View.showFirstRendering() { - showRendering(getRendering()!!, environment!!) -} 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 bb84966898..9d21324903 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 @@ -2,10 +2,6 @@ package com.squareup.workflow1.ui import android.view.View -/** - * Function attached to a view created by [ViewFactory], to allow it - * to respond to [View.showRendering]. - */ @WorkflowUiExperimentalApi public typealias ViewShowRendering = (@UnsafeVariance RenderingT, ViewEnvironment) -> Unit @@ -87,7 +83,7 @@ public fun View.showRendering( } ?: error( "Expected $this to have a showRendering function to show $rendering. " + - "Perhaps it was not built by a ${ViewFactory::class.java.simpleName}, " + + "Perhaps it was not built by a ScreenViewFactory, " + "or perhaps the factory did not call View.bindShowRendering." ) } 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 071a0a105f..2f49c9893b 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 @@ -10,16 +10,19 @@ import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.FrameLayout +import com.squareup.workflow1.ui.container.WithEnvironment +import com.squareup.workflow1.ui.container.withEnvironment import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach /** - * A view that can be driven by a stream of renderings (and an optional [ViewRegistry]) - * passed to its [start] method. + * A view that can be driven by a stream of [Screen] renderings passed to its [take] method. + * To configure the [ViewEnvironment] in play, use [WithEnvironment] as your root rendering type. * * [id][setId] defaults to [R.id.workflow_layout], as a convenience to ensure that * view persistence will work without requiring authors to be immersed in Android arcana. @@ -44,34 +47,63 @@ public class WorkflowLayout( private var restoredChildState: SparseArray? = null /** - * Subscribes to [renderings], and uses [registry] to - * [build a new view][ViewRegistry.buildView] each time a new type of rendering is received, - * making that view the only child of this one. + * While this view is attached to a window, subscribes to [renderings] and display its values. + * + * To configure the [ViewEnvironment], use [WithEnvironment] as your rendering type. + * For example, to customize the UI via [ViewRegistry] entries: + * + * val registry = ViewRegistry(MuchBetterViewForFooScreen) + * val renderings: Flow> = renderWorkflowIn(...).map { + * it.withRegistry(registry) + * } + * workflowLayout.take(renderings) */ + public fun take(renderings: Flow) { + takeWhileAttached(renderings.map { it.withEnvironment() }) { show(it) } + } + + @Deprecated( + "Use take()", + ReplaceWith( + "take(renderings.map { asScreen(it).withRegistry(registry) })", + "com.squareup.workflow1.ui.ViewEnvironment", + "com.squareup.workflow1.ui.ViewRegistry", + "com.squareup.workflow1.ui.asScreen", + "com.squareup.workflow1.ui.container.withRegistry", + "kotlinx.coroutines.flow.map" + ) + ) public fun start( renderings: Flow, registry: ViewRegistry ) { + @Suppress("DEPRECATION") start(renderings, ViewEnvironment(mapOf(ViewRegistry to registry))) } - /** - * Subscribes to [renderings], and uses the [ViewRegistry] in the given [environment] to - * [build a new view][ViewRegistry.buildView] each time a new type of rendering is received, - * making that view the only child of this one. - */ + @Deprecated( + "Use take()", + ReplaceWith( + "take(renderings.map { asScreen(it).withEnvironment(environment) })", + "com.squareup.workflow1.ui.ViewEnvironment", + "com.squareup.workflow1.ui.ViewRegistry", + "com.squareup.workflow1.ui.asScreen", + "com.squareup.workflow1.ui.container.withEnvironment", + "kotlinx.coroutines.flow.map" + ) + ) public fun start( renderings: Flow, environment: ViewEnvironment = ViewEnvironment() ) { - takeWhileAttached(renderings) { show(it, environment) } + takeWhileAttached(renderings) { + @Suppress("DEPRECATION") + show(asScreen(it).withEnvironment(environment)) + } } - private fun show( - newRendering: Any, - environment: ViewEnvironment - ) { - showing.update(newRendering, environment) + private fun show(rootScreen: WithEnvironment<*>) { + showing.show(rootScreen.screen, rootScreen.viewEnvironment) restoredChildState?.let { restoredState -> restoredChildState = null showing.actual.restoreHierarchyState(restoredState) 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 a290e2149b..06fb08405a 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 @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package com.squareup.workflow1.ui import android.content.Context @@ -168,6 +170,15 @@ public class WorkflowViewStub @JvmOverloads constructor( } } + @Deprecated("Use show()", ReplaceWith("show(rendering, viewEnvironment)")) + public fun update( + rendering: Any, + viewEnvironment: ViewEnvironment + ): View { + @Suppress("DEPRECATION") + return show(asScreen(rendering), viewEnvironment) + } + /** * Replaces this view with one that can display [rendering]. If the receiver * has already been replaced, updates the replacement if it [canShowRendering]. @@ -192,8 +203,8 @@ public class WorkflowViewStub @JvmOverloads constructor( * [View.bindShowRendering][com.squareup.workflow1.ui.bindShowRendering] * when constructing the view */ - public fun update( - rendering: Any, + public fun show( + rendering: Screen, viewEnvironment: ViewEnvironment ): View { actual.takeIf { it.canShowRendering(rendering) } @@ -219,17 +230,15 @@ public class WorkflowViewStub @JvmOverloads constructor( WorkflowLifecycleOwner.get(actual)?.destroyOnDetach() } - return viewEnvironment[ViewRegistry] - .buildView( - rendering, - viewEnvironment, - parent.context, - parent, - initializeView = { - WorkflowLifecycleOwner.installOn(this) - showFirstRendering() - } - ) + return rendering.buildView( + viewEnvironment, + parent.context, + parent, + initializeView = { + WorkflowLifecycleOwner.installOn(this) + showFirstRendering() + } + ) .also { newView -> if (inflatedId != NO_ID) newView.id = inflatedId if (updatesVisibility) newView.visibility = visibility diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/androidx/WorkflowLifecycleOwner.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/androidx/WorkflowLifecycleOwner.kt index 3187f0a685..fefa4bccb9 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/androidx/WorkflowLifecycleOwner.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/androidx/WorkflowLifecycleOwner.kt @@ -27,7 +27,7 @@ import com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner.Companion.insta * This type is meant to help integrate with [ViewTreeLifecycleOwner] by allowing the creation of a * tree of [LifecycleOwner]s that mirrors the view tree. * - * Custom container views that use [ViewRegistry.buildView][com.squareup.workflow1.ui.buildView] + * Custom container views that use [Screen.buildView][com.squareup.workflow1.ui.buildView] * to create their children _must_ ensure * they call [destroyOnDetach] on the outgoing view before they replace children with new views. * If this is not done, then certain processes that are started by that view's subtree may continue diff --git a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/BackStackConfig.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackConfig.kt similarity index 83% rename from workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/BackStackConfig.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackConfig.kt index 5887cc52dd..53aa5f5652 100644 --- a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/BackStackConfig.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackConfig.kt @@ -1,9 +1,9 @@ -package com.squareup.workflow1.ui.backstack +package com.squareup.workflow1.ui.container import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.ViewEnvironmentKey -import com.squareup.workflow1.ui.backstack.BackStackConfig.First -import com.squareup.workflow1.ui.backstack.BackStackConfig.Other +import com.squareup.workflow1.ui.container.BackStackConfig.First +import com.squareup.workflow1.ui.container.BackStackConfig.Other /** * Informs views whether they're children of a [BackStackContainer], 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 new file mode 100644 index 0000000000..83f1b745a2 --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackContainer.kt @@ -0,0 +1,233 @@ +package com.squareup.workflow1.ui.container + +import android.content.Context +import android.os.Parcelable +import android.util.AttributeSet +import android.util.Log +import android.view.Gravity +import android.view.View +import android.view.animation.AccelerateDecelerateInterpolator +import android.widget.FrameLayout +import androidx.savedstate.SavedStateRegistry +import androidx.savedstate.ViewTreeSavedStateRegistryOwner +import androidx.transition.Scene +import androidx.transition.Slide +import androidx.transition.TransitionManager +import androidx.transition.TransitionSet +import com.squareup.workflow1.ui.NamedScreen +import com.squareup.workflow1.ui.R +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.androidx.WorkflowAndroidXSupport.stateRegistryOwnerFromViewTreeOrContext +import com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner +import com.squareup.workflow1.ui.buildView +import com.squareup.workflow1.ui.canShowRendering +import com.squareup.workflow1.ui.compatible +import com.squareup.workflow1.ui.container.BackStackConfig.First +import com.squareup.workflow1.ui.container.BackStackConfig.Other +import com.squareup.workflow1.ui.container.ViewStateCache.SavedState +import com.squareup.workflow1.ui.showFirstRendering +import com.squareup.workflow1.ui.showRendering + +/** + * A container view that can display a stream of [BackStackScreen] instances. + * + * This container supports saving and restoring the view state of each of its subviews corresponding + * to the renderings in its [BackStackScreen]. It supports two distinct state mechanisms: + * 1. Classic view hierarchy state ([View.onSaveInstanceState]/[View.onRestoreInstanceState]) + * 2. AndroidX [SavedStateRegistry] via [ViewTreeSavedStateRegistryOwner]. + * + * ## A note about `SavedStateRegistry` support. + * + * The [SavedStateRegistry] API involves defining string keys to associate with state bundles. These + * keys must be unique relative to the instance of the registry they are saved in. To support this + * requirement, [BackStackContainer] tries to generate a best-effort unique key by combining its + * fully-qualified class name with both its [view ID][View.getId] and the + * [compatibility key][com.squareup.workflow1.ui.Compatible.compatibilityKey] of its rendering. + * + * This method isn't guaranteed to give a unique registry key, but it should be good enough: If you + * need to nest multiple [BackStackContainer]s under the same `SavedStateRegistry`, just wrap each + * [BackStackScreen] with a [NamedScreen], or give each [BackStackContainer] a unique view ID. If that + * heuristic fails you, use [ViewEnvironment.withBackStackStateKeyPrefix] to add unique names to + * the [ViewEnvironment] used to show each [BackStackScreen]. + * + * There's a potential issue here where if our ID is changed to something else, then another + * [BackStackContainer] is added with our old ID, that container will overwrite our state. Since + * they'd both be using the same key, [SavedStateRegistry] would throw an exception. As long as this + * container is detached before its ID is changed, it shouldn't be a problem. + */ +@WorkflowUiExperimentalApi +public open class BackStackContainer @JvmOverloads constructor( + context: Context, + attributeSet: AttributeSet? = null, + defStyle: Int = 0, + defStyleRes: Int = 0 +) : FrameLayout(context, attributeSet, defStyle, defStyleRes) { + + private val viewStateCache = ViewStateCache() + + private val currentView: View? get() = if (childCount > 0) getChildAt(0) else null + private var currentRendering: BackStackScreen>? = null + private var stateRegistryKey: String? = null + + public fun update( + newRendering: BackStackScreen<*>, + newViewEnvironment: ViewEnvironment + ) { + updateStateRegistryKey(newViewEnvironment) + + val config = if (newRendering.backStack.isEmpty()) First else Other + val environment = newViewEnvironment + (BackStackConfig to config) + + val named: BackStackScreen> = newRendering + // ViewStateCache requires that everything be Named. + // It's fine if client code is already using Named for its own purposes, recursion works. + .map { NamedScreen(it, "backstack") } + + val oldViewMaybe = currentView + + // If existing view is compatible, just update it. + oldViewMaybe + ?.takeIf { it.canShowRendering(named.top) } + ?.let { + viewStateCache.prune(named.frames) + it.showRendering(named.top, environment) + return + } + + val newView = named.top.buildView( + viewEnvironment = environment, + contextForNewView = this.context, + container = this, + initializeView = { + WorkflowLifecycleOwner.installOn(this) + showFirstRendering() + } + ) + viewStateCache.update(named.backStack, oldViewMaybe, newView) + + val popped = currentRendering?.backStack?.any { compatible(it, named.top) } == true + + performTransition(oldViewMaybe, newView, popped) + // Notify the view we're about to replace that it's going away. + oldViewMaybe?.let(WorkflowLifecycleOwner::get)?.destroyOnDetach() + + currentRendering = named + } + + /** + * Called from [View.showRendering] to swap between views. + * Subclasses can override to customize visual effects. There is no need to call super. + * Note that views are showing renderings of type [NamedScreen]`>`. + * + * @param oldViewMaybe the outgoing view, or null if this is the initial rendering. + * @param newView the view that should replace [oldViewMaybe] (if it exists), and become + * this view's only child + * @param popped true if we should give the appearance of popping "back" to a previous rendering, + * false if a new rendering is being "pushed". Should be ignored if [oldViewMaybe] is null. + */ + protected open fun performTransition( + oldViewMaybe: View?, + newView: View, + popped: Boolean + ) { + // Showing something already, transition with push or pop effect. + oldViewMaybe + ?.let { oldView -> + val oldBody: View? = oldView.findViewById(R.id.back_stack_body) + val newBody: View? = newView.findViewById(R.id.back_stack_body) + + val oldTarget: View + val newTarget: View + if (oldBody != null && newBody != null) { + oldTarget = oldBody + newTarget = newBody + } else { + oldTarget = oldView + newTarget = newView + } + + val (outEdge, inEdge) = when (popped) { + false -> Gravity.START to Gravity.END + true -> Gravity.END to Gravity.START + } + + val transition = TransitionSet() + .addTransition(Slide(outEdge).addTarget(oldTarget)) + .addTransition(Slide(inEdge).addTarget(newTarget)) + .setInterpolator(AccelerateDecelerateInterpolator()) + + TransitionManager.go(Scene(this, newView), transition) + return + } + + // This is the first view, just show it. + addView(newView) + } + + override fun onSaveInstanceState(): Parcelable { + return SavedState(super.onSaveInstanceState(), viewStateCache) + } + + override fun onRestoreInstanceState(state: Parcelable) { + (state as? SavedState) + ?.let { + viewStateCache.restore(it.viewStateCache) + super.onRestoreInstanceState(state.superState) + } + ?: super.onRestoreInstanceState(super.onSaveInstanceState()) + // Some other class wrote state, but we're not allowed to skip + // the call to super. Make a no-op call. + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + + // Wire up our viewStateCache to our parent SavedStateRegistry. + val parentRegistryOwner = stateRegistryOwnerFromViewTreeOrContext(this)!! + val key = checkNotNull(stateRegistryKey) { + "Expected stateRegistryKey to have been set – the view seems to be getting attached before " + + "its first update: $this" + } + + viewStateCache.attachToParentRegistry(key, parentRegistryOwner) + } + + override fun onDetachedFromWindow() { + // Disconnect our state cache from our parent SavedStateRegistry so that it doesn't get asked + // to save state anymore. + viewStateCache.detachFromParentRegistry() + super.onDetachedFromWindow() + } + + /** + * In order to save our state with a unique ID in our parent's registry, we use a combination + * of this class name, our [compatibility key][NamedScreen.compatibilityKey] if specified, + * and our view ID if specified. + * + * This method isn't guaranteed to give a unique registry key, but it should be + * good enough: If you need to nest multiple [BackStackContainer]s under the same + * `SavedStateRegistry`, just wrap each [BackStackScreen] with a [NamedScreen], or give each + * [BackStackContainer] a unique view ID. + * + * There's a potential issue here where if our ID is changed to something else, then another + * BackStackContainer is added with our old ID, that container will overwrite our state. Since + * they'd both be using the same key, SavedStateRegistry would throw an exception. That's a + * pretty unlikely situation though I think. And as long as this container is detached before + * its ID is changed, it won't be a problem. + */ + private fun updateStateRegistryKey(environment: ViewEnvironment) { + val idSuffix = if (id == NO_ID) "" else "-$id" + val keyPrefix = environment.getBackStackStateKeyPrefix + val newKey = keyPrefix + BackStackContainer::class.java.name + idSuffix + + if (stateRegistryKey != null && stateRegistryKey != newKey) { + Log.wtf( + "workflow1", + "BackStackContainer state registry key changed – view state may be lost:" + + " from $stateRegistryKey to $newKey" + ) + } + stateRegistryKey = newKey + } +} 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 new file mode 100644 index 0000000000..6708071272 --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackScreenViewFactory.kt @@ -0,0 +1,23 @@ +package com.squareup.workflow1.ui.container + +import android.view.ViewGroup.LayoutParams +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import com.squareup.workflow1.ui.ManualScreenViewFactory +import com.squareup.workflow1.ui.R +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.bindShowRendering + +@WorkflowUiExperimentalApi +internal object BackStackScreenViewFactory : ScreenViewFactory> +by ManualScreenViewFactory( + type = BackStackScreen::class, + viewConstructor = { initialRendering, initialEnv, context, _ -> + BackStackContainer(context) + .apply { + id = R.id.workflow_back_stack_container + layoutParams = (LayoutParams(MATCH_PARENT, MATCH_PARENT)) + bindShowRendering(initialRendering, initialEnv, ::update) + } + } +) diff --git a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/BackStackStateKey.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackStateKey.kt similarity index 96% rename from workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/BackStackStateKey.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackStateKey.kt index b8d4be6177..dfbe933139 100644 --- a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/BackStackStateKey.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackStateKey.kt @@ -1,4 +1,4 @@ -package com.squareup.workflow1.ui.backstack +package com.squareup.workflow1.ui.container import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.ViewEnvironmentKey diff --git a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/KeyedStateRegistryOwner.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/KeyedStateRegistryOwner.kt similarity index 95% rename from workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/KeyedStateRegistryOwner.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/KeyedStateRegistryOwner.kt index bdd5bf0508..1da4503123 100644 --- a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/KeyedStateRegistryOwner.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/KeyedStateRegistryOwner.kt @@ -1,4 +1,4 @@ -package com.squareup.workflow1.ui.backstack +package com.squareup.workflow1.ui.container import android.view.View import androidx.lifecycle.LifecycleOwner @@ -8,7 +8,7 @@ import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.ViewTreeSavedStateRegistryOwner import com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backstack.KeyedStateRegistryOwner.Companion.installAsSavedStateRegistryOwnerOn +import com.squareup.workflow1.ui.container.KeyedStateRegistryOwner.Companion.installAsSavedStateRegistryOwnerOn /** * The implementation of [SavedStateRegistryOwner] that is installed on every immediate child view diff --git a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/StateRegistryAggregator.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/StateRegistryAggregator.kt similarity index 99% rename from workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/StateRegistryAggregator.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/StateRegistryAggregator.kt index c8467ae2b0..6683397cec 100644 --- a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/StateRegistryAggregator.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/StateRegistryAggregator.kt @@ -1,4 +1,4 @@ -package com.squareup.workflow1.ui.backstack +package com.squareup.workflow1.ui.container import android.os.Bundle import androidx.lifecycle.Lifecycle.Event diff --git a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/ViewStateCache.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ViewStateCache.kt similarity index 93% rename from workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/ViewStateCache.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ViewStateCache.kt index 0cbddc48de..7a46064515 100644 --- a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/ViewStateCache.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ViewStateCache.kt @@ -1,4 +1,4 @@ -package com.squareup.workflow1.ui.backstack +package com.squareup.workflow1.ui.container import android.os.Parcel import android.os.Parcelable @@ -10,13 +10,13 @@ import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting.PRIVATE import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.ViewTreeSavedStateRegistryOwner -import com.squareup.workflow1.ui.Named +import com.squareup.workflow1.ui.NamedScreen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backstack.ViewStateCache.SavedState +import com.squareup.workflow1.ui.container.ViewStateCache.SavedState import com.squareup.workflow1.ui.getRendering /** - * Handles persistence chores for container views that manage a set of [Named] renderings, + * Handles persistence chores for container views that manage a set of [NamedScreen] renderings, * showing a view for one at a time -- think back stacks or tab sets. * * This class implements [Parcelable] so that it can be preserved from @@ -61,7 +61,7 @@ internal constructor( * the same. Any cached view state held for renderings that are not * [compatible][com.squareup.workflow1.ui.compatible] those in [retaining] will be dropped. */ - public fun prune(retaining: Collection>) { + public fun prune(retaining: Collection>) { pruneKeys(retaining.map { it.compatibilityKey }) } @@ -77,18 +77,18 @@ internal constructor( * on a succeeding call to his method. Any other cached view state will be dropped. * * @param oldViewMaybe the view that is being removed, if any, which is expected to be showing - * a [Named] rendering. If that rendering is + * a [NamedScreen] rendering. If that rendering is * [compatible with][com.squareup.workflow1.ui.compatible] a member of * [retainedRenderings], its state will be [saved][View.saveHierarchyState]. * * @param newView the view that is about to be displayed, which must be showing a - * [Named] rendering. If [compatible][com.squareup.workflow1.ui.compatible] + * [NamedScreen] rendering. If [compatible][com.squareup.workflow1.ui.compatible] * view state is found in the cache, it is [restored][View.restoreHierarchyState]. * * @return true if [newView] has been restored. */ public fun update( - retainedRenderings: Collection>, + retainedRenderings: Collection>, oldViewMaybe: View?, newView: View ) { @@ -237,9 +237,9 @@ internal constructor( @WorkflowUiExperimentalApi private val View.namedKey: String get() { - val rendering = getRendering>() + val rendering = getRendering>() return checkNotNull(rendering?.compatibilityKey) { - "Expected $this to be showing a ${Named::class.java.simpleName}<*> rendering, " + + "Expected $this to be showing a ${NamedScreen::class.java.simpleName}<*> rendering, " + "found $rendering" } } diff --git a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/ViewStateFrame.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ViewStateFrame.kt similarity index 96% rename from workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/ViewStateFrame.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ViewStateFrame.kt index b20ab6f253..f25b0d84f8 100644 --- a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/ViewStateFrame.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ViewStateFrame.kt @@ -1,4 +1,4 @@ -package com.squareup.workflow1.ui.backstack +package com.squareup.workflow1.ui.container import android.os.Parcel import android.os.Parcelable diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/WithEnvironmentViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/WithEnvironmentViewFactory.kt new file mode 100644 index 0000000000..032b7874ef --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/WithEnvironmentViewFactory.kt @@ -0,0 +1,18 @@ +package com.squareup.workflow1.ui.container + +import com.squareup.workflow1.ui.DecorativeScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.updateFrom + +@WorkflowUiExperimentalApi +internal object WithEnvironmentViewFactory : ScreenViewFactory> +by DecorativeScreenViewFactory( + type = WithEnvironment::class, + map = { withEnvironment, inheritedEnvironment -> + Pair( + withEnvironment.screen, + inheritedEnvironment.updateFrom(withEnvironment.viewEnvironment) + ) + } +) diff --git a/workflow-ui/container-android/src/main/res/layout/view_stack_layout.xml b/workflow-ui/core-android/src/main/res/layout/view_stack_layout.xml similarity index 63% rename from workflow-ui/container-android/src/main/res/layout/view_stack_layout.xml rename to workflow-ui/core-android/src/main/res/layout/view_stack_layout.xml index 92150e5e30..99c295d23f 100644 --- a/workflow-ui/container-android/src/main/res/layout/view_stack_layout.xml +++ b/workflow-ui/core-android/src/main/res/layout/view_stack_layout.xml @@ -1,8 +1,8 @@ - - + diff --git a/workflow-ui/core-android/src/main/res/values/ids.xml b/workflow-ui/core-android/src/main/res/values/ids.xml index 1e13c033cf..996b3b662e 100644 --- a/workflow-ui/core-android/src/main/res/values/ids.xml +++ b/workflow-ui/core-android/src/main/res/values/ids.xml @@ -1,5 +1,14 @@ + + + + + + diff --git a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/AndroidViewEnvironmentTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/AndroidViewEnvironmentTest.kt new file mode 100644 index 0000000000..c5b5b49e05 --- /dev/null +++ b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/AndroidViewEnvironmentTest.kt @@ -0,0 +1,87 @@ +@file:OptIn(WorkflowUiExperimentalApi::class) + +package com.squareup.workflow1.ui + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import com.google.common.truth.Truth.assertThat +import com.squareup.workflow1.ui.ViewRegistry.Entry +import org.junit.Test +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import kotlin.reflect.KClass +import kotlin.test.assertFailsWith + +internal class AndroidViewEnvironmentTest { + + @OptIn(WorkflowUiExperimentalApi::class) + @Test fun missingBindingMessage_isUseful() { + val emptyReg = object : ViewRegistry { + override val keys: Set> = emptySet() + override fun getEntryFor( + renderingType: KClass + ): Entry? = null + } + val env = ViewEnvironment(mapOf(ViewRegistry to emptyReg)) + + val fooScreen = object : Screen { + override fun toString() = "FooScreen" + } + + val error = assertFailsWith { + fooScreen.buildView(env, mock()) + } + assertThat(error.message).isEqualTo( + "A ScreenViewFactory should have been registered to display " + + "FooScreen, or that class should implement AndroidScreen." + ) + } + + @Test fun `buildView honors AndroidScreen`() { + val registry = ViewRegistry() + val env = ViewEnvironment(mapOf(ViewRegistry to registry)) + val screen = MyAndroidScreen() + + screen.buildView(env, mock()) + assertThat(screen.viewFactory.called).isTrue() + } + + @Test fun `buildView prefers registry entries to AndroidViewRendering`() { + val registry = ViewRegistry(overrideViewRenderingFactory) + val env = ViewEnvironment(mapOf(ViewRegistry to registry)) + + val screen = MyAndroidScreen() + screen.buildView(env, mock()) + assertThat(screen.viewFactory.called).isFalse() + assertThat(overrideViewRenderingFactory.called).isTrue() + } + + private class TestViewFactory( + override val type: KClass + ) : ScreenViewFactory { + var called = false + + override fun buildView( + initialRendering: T, + initialViewEnvironment: ViewEnvironment, + contextForNewView: Context, + container: ViewGroup? + ): View { + called = true + + return mock { + on { + getTag(eq(R.id.view_show_rendering_function)) + } doReturn (ShowRenderingTag(initialRendering, initialViewEnvironment, { _, _ -> })) + } + } + } + + private class MyAndroidScreen : AndroidScreen { + override val viewFactory = TestViewFactory(MyAndroidScreen::class) + } + + private val overrideViewRenderingFactory = TestViewFactory(MyAndroidScreen::class) +} diff --git a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/LegacyAndroidViewRegistryTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/LegacyAndroidViewRegistryTest.kt new file mode 100644 index 0000000000..3ce5c3d5a8 --- /dev/null +++ b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/LegacyAndroidViewRegistryTest.kt @@ -0,0 +1,174 @@ +@file:Suppress("DEPRECATION") + +package com.squareup.workflow1.ui + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import com.google.common.truth.Truth.assertThat +import com.squareup.workflow1.ui.ViewRegistry.Entry +import org.junit.Test +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import kotlin.reflect.KClass +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +@OptIn(WorkflowUiExperimentalApi::class) +internal class LegacyAndroidViewRegistryTest { + + @OptIn(WorkflowUiExperimentalApi::class) + @Test fun missingBindingMessage_isUseful() { + val emptyReg = object : ViewRegistry { + override val keys: Set> = emptySet() + override fun getEntryFor( + renderingType: KClass + ): Entry? = null + } + + val error = assertFailsWith { + emptyReg.buildView("render this, bud") + } + assertThat(error.message).isEqualTo( + "A ViewFactory should have been registered to display " + + "render this, bud, or that class should implement AndroidViewRendering.") + } + + @Test fun `getFactoryFor delegates to composite registries`() { + val fooFactory = TestViewFactory(FooRendering::class) + val barFactory = TestViewFactory(BarRendering::class) + val bazFactory = TestViewFactory(BazRendering::class) + val fooBarRegistry = TestRegistry( + mapOf( + FooRendering::class to fooFactory, + BarRendering::class to barFactory + ) + ) + val bazRegistry = TestRegistry(factories = mapOf(BazRendering::class to bazFactory)) + val registry = fooBarRegistry + bazRegistry + + assertThat(registry.getEntryFor(FooRendering::class)) + .isSameInstanceAs(fooFactory) + assertThat(registry.getEntryFor(BarRendering::class)) + .isSameInstanceAs(barFactory) + assertThat(registry.getEntryFor(BazRendering::class)) + .isSameInstanceAs(bazFactory) + } + + @Test fun `getFactoryFor returns null on missing registry`() { + val fooRegistry = TestRegistry(setOf(FooRendering::class)) + val registry = ViewRegistry() + fooRegistry + + assertThat(registry.getEntryFor(BarRendering::class)).isNull() + } + + @Test fun `keys includes all composite registries' keys`() { + val fooBarRegistry = TestRegistry(setOf(FooRendering::class, BarRendering::class)) + val bazRegistry = TestRegistry(setOf(BazRendering::class)) + val registry = fooBarRegistry + bazRegistry + + assertThat(registry.keys).containsExactly( + FooRendering::class, + BarRendering::class, + BazRendering::class + ) + } + + @Test fun `keys from bindings`() { + val factory1 = TestViewFactory(FooRendering::class) + val factory2 = TestViewFactory(BarRendering::class) + val registry = ViewRegistry(factory1, factory2) + + assertThat(registry.keys).containsExactly(factory1.type, factory2.type) + } + + @Test fun `constructor throws on duplicates`() { + val factory1 = TestViewFactory(FooRendering::class) + val factory2 = TestViewFactory(FooRendering::class) + + val error = assertFailsWith { + ViewRegistry(factory1, factory2) + } + assertThat(error).hasMessageThat() + .endsWith("must not have duplicate entries.") + assertThat(error).hasMessageThat() + .contains(FooRendering::class.java.name) + } + + @Test fun `getFactoryFor works`() { + val fooFactory = TestViewFactory(FooRendering::class) + val registry = ViewRegistry(fooFactory) + + val factory = registry.getEntryFor(FooRendering::class) + assertThat(factory).isSameInstanceAs(fooFactory) + } + + @Test fun `getFactoryFor returns null on missing binding`() { + val fooFactory = TestViewFactory(FooRendering::class) + val registry = ViewRegistry(fooFactory) + + assertThat(registry.getEntryFor(BarRendering::class)).isNull() + } + + @Test fun `buildView honors AndroidViewRendering`() { + val registry = ViewRegistry() + registry.buildView(ViewRendering) + assertThat(ViewRendering.viewFactory.called).isTrue() + } + + @Test fun `buildView prefers registry entries to AndroidViewRendering`() { + val registry = ViewRegistry(overrideViewRenderingFactory) + registry.buildView(ViewRendering) + assertThat(ViewRendering.viewFactory.called).isFalse() + assertThat(overrideViewRenderingFactory.called).isTrue() + } + + @Test fun `ViewRegistry with no arguments infers type`() { + val registry = ViewRegistry() + assertTrue(registry.keys.isEmpty()) + } + + private object FooRendering + private object BarRendering + private object BazRendering + + private object ViewRendering : AndroidViewRendering { + override val viewFactory: TestViewFactory = TestViewFactory(ViewRendering::class) + } + private val overrideViewRenderingFactory = TestViewFactory(ViewRendering::class) + + private class TestRegistry(private val factories: Map, ViewFactory<*>>) : ViewRegistry { + constructor(keys: Set>) : this(keys.associateWith { TestViewFactory(it) }) + + override val keys: Set> get() = factories.keys + + @Suppress("UNCHECKED_CAST") + override fun getEntryFor( + renderingType: KClass + ): Entry = factories.getValue(renderingType) as Entry + } + + @OptIn(WorkflowUiExperimentalApi::class) + private fun ViewRegistry.buildView(rendering: R): View = + buildView(rendering, ViewEnvironment(mapOf(ViewRegistry to this)), mock()) + + @OptIn(WorkflowUiExperimentalApi::class) + private class TestViewFactory(override val type: KClass) : ViewFactory { + var called = false + + override fun buildView( + initialRendering: R, + initialViewEnvironment: ViewEnvironment, + contextForNewView: Context, + container: ViewGroup? + ): View { + called = true + return mock { + on { + getTag(eq(com.squareup.workflow1.ui.R.id.view_show_rendering_function)) + } doReturn (ShowRenderingTag(initialRendering, initialViewEnvironment, { _, _ -> })) + } + } + } +} diff --git a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/TestViewFactory.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/TestViewFactory.kt deleted file mode 100644 index a718275c8c..0000000000 --- a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/TestViewFactory.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.squareup.workflow1.ui - -import android.content.Context -import android.view.View -import android.view.ViewGroup -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import kotlin.reflect.KClass - -@OptIn(WorkflowUiExperimentalApi::class) -internal fun ViewRegistry.buildView(rendering: R): View = - buildView(rendering, ViewEnvironment(mapOf(ViewRegistry to this)), mock()) - -@OptIn(WorkflowUiExperimentalApi::class) -internal class TestViewFactory(override val type: KClass) : ViewFactory { - var called = false - - override fun buildView( - initialRendering: R, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, - container: ViewGroup? - ): View { - called = true - return mock { - on { - getTag(eq(com.squareup.workflow1.ui.R.id.view_show_rendering_function)) - } doReturn (ShowRenderingTag(initialRendering, initialViewEnvironment, { _, _ -> })) - } - } -} diff --git a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/TypedViewRegistryTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/TypedViewRegistryTest.kt deleted file mode 100644 index 88d5fa21da..0000000000 --- a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/TypedViewRegistryTest.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.squareup.workflow1.ui - -import com.google.common.truth.Truth.assertThat -import org.junit.Test -import kotlin.test.assertFailsWith -import kotlin.test.assertTrue - -@OptIn(WorkflowUiExperimentalApi::class) -class TypedViewRegistryTest { - - @Test fun `keys from bindings`() { - val factory1 = TestViewFactory(FooRendering::class) - val factory2 = TestViewFactory(BarRendering::class) - val registry = TypedViewRegistry(factory1, factory2) - - assertThat(registry.keys).containsExactly(factory1.type, factory2.type) - } - - @Test fun `constructor throws on duplicates`() { - val factory1 = TestViewFactory(FooRendering::class) - val factory2 = TestViewFactory(FooRendering::class) - - val error = assertFailsWith { - TypedViewRegistry(factory1, factory2) - } - assertThat(error).hasMessageThat() - .endsWith("must not have duplicate entries.") - assertThat(error).hasMessageThat() - .contains(FooRendering::class.java.name) - } - - @Test fun `getFactoryFor works`() { - val fooFactory = TestViewFactory(FooRendering::class) - val registry = TypedViewRegistry(fooFactory) - - val factory = registry.getFactoryFor(FooRendering::class) - assertThat(factory).isSameInstanceAs(fooFactory) - } - - @Test fun `getFactoryFor returns null on missing binding`() { - val fooFactory = TestViewFactory(FooRendering::class) - val registry = TypedViewRegistry(fooFactory) - - assertThat(registry.getFactoryFor(BarRendering::class)).isNull() - } - - @Test fun `buildView honors AndroidViewRendering`() { - val registry = TypedViewRegistry() - registry.buildView(ViewRendering) - assertThat(ViewRendering.viewFactory.called).isTrue() - } - - @Test fun `buildView prefers registry entries to AndroidViewRendering`() { - val registry = TypedViewRegistry(overrideViewRenderingFactory) - registry.buildView(ViewRendering) - assertThat(ViewRendering.viewFactory.called).isFalse() - assertThat(overrideViewRenderingFactory.called).isTrue() - } - - @Test fun `ViewRegistry with no arguments infers type`() { - val registry = ViewRegistry() - assertTrue(registry.keys.isEmpty()) - } - - private object FooRendering - private object BarRendering - - private object ViewRendering : AndroidViewRendering { - override val viewFactory: TestViewFactory = TestViewFactory(ViewRendering::class) - } - private val overrideViewRenderingFactory = TestViewFactory(ViewRendering::class) -} diff --git a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ViewRegistryTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ViewRegistryTest.kt deleted file mode 100644 index bec88b9d86..0000000000 --- a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ViewRegistryTest.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.squareup.workflow1.ui - -import com.google.common.truth.Truth.assertThat -import org.junit.Test -import kotlin.reflect.KClass -import kotlin.test.assertFailsWith - -@OptIn(WorkflowUiExperimentalApi::class) -internal class ViewRegistryTest { - - @OptIn(WorkflowUiExperimentalApi::class) - @Test fun missingBindingMessage_isUseful() { - val emptyReg = object : ViewRegistry { - override val keys: Set> = emptySet() - override fun getFactoryFor( - renderingType: KClass - ): ViewFactory? = null - } - - val error = assertFailsWith { - emptyReg.buildView("render this, bud") - } - assertThat(error.message).isEqualTo( - "A com.squareup.workflow1.ui.ViewFactory should have been registered to display " + - "kotlin.String instances, or that class should implement AndroidViewRendering.") - } -} diff --git a/workflow-ui/container-android/src/test/java/com/squareup/workflow1/ui/backstack/StateRegistryAggregatorTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/container/StateRegistryAggregatorTest.kt similarity index 71% rename from workflow-ui/container-android/src/test/java/com/squareup/workflow1/ui/backstack/StateRegistryAggregatorTest.kt rename to workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/container/StateRegistryAggregatorTest.kt index 7da5708f45..4c27791bf7 100644 --- a/workflow-ui/container-android/src/test/java/com/squareup/workflow1/ui/backstack/StateRegistryAggregatorTest.kt +++ b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/container/StateRegistryAggregatorTest.kt @@ -1,59 +1,60 @@ -package com.squareup.workflow1.ui.backstack +package com.squareup.workflow1.ui.container import android.os.Bundle import androidx.lifecycle.Lifecycle -import androidx.lifecycle.Lifecycle.Event.ON_CREATE -import androidx.lifecycle.Lifecycle.State.RESUMED import androidx.lifecycle.LifecycleRegistry import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner -import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import kotlin.test.assertFailsWith @RunWith(RobolectricTestRunner::class) -class StateRegistryAggregatorTest { +internal class StateRegistryAggregatorTest { - @Test fun `attach stops observing previous parent when called multiple times without detach`() { + @Test + fun `attach stops observing previous parent when called multiple times without detach`() { val aggregator = StateRegistryAggregator( - onWillSave = {}, - onRestored = {} + onWillSave = {}, + onRestored = {} ) val parent1 = SimpleStateRegistry() val parent2 = SimpleStateRegistry() aggregator.attachToParentRegistry("key", parent1) - assertThat(parent1.lifecycleRegistry.observerCount).isEqualTo(1) + Truth.assertThat(parent1.lifecycleRegistry.observerCount).isEqualTo(1) aggregator.attachToParentRegistry("key", parent2) - assertThat(parent1.lifecycleRegistry.observerCount).isEqualTo(0) + Truth.assertThat(parent1.lifecycleRegistry.observerCount).isEqualTo(0) } - @Test fun `attach throws more helpful exception when key already registered`() { + @Test + fun `attach throws more helpful exception when key already registered`() { val key = "fizzbuz" val aggregator = StateRegistryAggregator( - onWillSave = {}, - onRestored = {} + onWillSave = {}, + onRestored = {} ) val parent = SimpleStateRegistry().apply { stateRegistryController.savedStateRegistry.registerSavedStateProvider(key) { Bundle() } } val error = assertFailsWith { - aggregator.attachToParentRegistry(key, parent) + aggregator.attachToParentRegistry(key, parent) } - assertThat(error).hasMessageThat() + Truth.assertThat(error).hasMessageThat() .contains("Error registering StateRegistryHolder as SavedStateProvider with key \"$key\"") } - @Test fun `attach observes parent lifecycle`() { + @Test + fun `attach observes parent lifecycle`() { val aggregator = StateRegistryAggregator( - onWillSave = {}, - onRestored = {} + onWillSave = {}, + onRestored = {} ) val parent = SimpleStateRegistry().apply { stateRegistryController.performRestore(null) @@ -63,13 +64,14 @@ class StateRegistryAggregatorTest { // The androidx internals add 2 additional observers. This test could break if that changes, but // unfortunately there's no other public API to check. - assertThat(parent.lifecycleRegistry.observerCount).isEqualTo(3) + Truth.assertThat(parent.lifecycleRegistry.observerCount).isEqualTo(3) } - @Test fun `attach doesn't observe parent when already restored`() { + @Test + fun `attach doesn't observe parent when already restored`() { val aggregator = StateRegistryAggregator( - onWillSave = {}, - onRestored = {} + onWillSave = {}, + onRestored = {} ) val parent = SimpleStateRegistry().apply { stateRegistryController.performRestore(null) @@ -77,7 +79,7 @@ class StateRegistryAggregatorTest { // Restore the aggregator. aggregator.attachToParentRegistry("key", parent) - parent.lifecycleRegistry.currentState = RESUMED + parent.lifecycleRegistry.currentState = Lifecycle.State.RESUMED // Re-attach it. aggregator.detachFromParentRegistry() @@ -85,13 +87,14 @@ class StateRegistryAggregatorTest { // The androidx internals add 2 additional observers. This test could break if that changes, but // unfortunately there's no other public API to check. - assertThat(parent.lifecycleRegistry.observerCount).isEqualTo(1) + Truth.assertThat(parent.lifecycleRegistry.observerCount).isEqualTo(1) } - @Test fun `stops observing parent after ON_CREATED`() { + @Test + fun `stops observing parent after ON_CREATED`() { val aggregator = StateRegistryAggregator( - onWillSave = {}, - onRestored = {} + onWillSave = {}, + onRestored = {} ) val parent = SimpleStateRegistry().apply { // Must restore parent in order to advance lifecycle. @@ -99,30 +102,32 @@ class StateRegistryAggregatorTest { } aggregator.attachToParentRegistry("key", parent) - parent.lifecycleRegistry.handleLifecycleEvent(ON_CREATE) + parent.lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) // The androidx internals add 1 additional observer. This test could break if that changes, but // unfortunately there's no other public API to check. - assertThat(parent.lifecycleRegistry.observerCount).isEqualTo(1) + Truth.assertThat(parent.lifecycleRegistry.observerCount).isEqualTo(1) } - @Test fun `detach stops observing parent lifecycle`() { + @Test + fun `detach stops observing parent lifecycle`() { val aggregator = StateRegistryAggregator( - onWillSave = {}, - onRestored = {} + onWillSave = {}, + onRestored = {} ) val parent = SimpleStateRegistry() aggregator.attachToParentRegistry("key", parent) aggregator.detachFromParentRegistry() - assertThat(parent.lifecycleRegistry.observerCount).isEqualTo(0) + Truth.assertThat(parent.lifecycleRegistry.observerCount).isEqualTo(0) } - @Test fun `saveRegistryController saves when parent is restored`() { + @Test + fun `saveRegistryController saves when parent is restored`() { val aggregator = StateRegistryAggregator( - onWillSave = {}, - onRestored = {} + onWillSave = {}, + onRestored = {} ) val parent = SimpleStateRegistry().apply { // Must restore the parent controller in order to initialize the aggregator. @@ -132,22 +137,23 @@ class StateRegistryAggregatorTest { val child = SimpleStateRegistry().apply { savedStateRegistry.registerSavedStateProvider("key") { childSaveCount++ - Bundle() + Bundle() } } aggregator.attachToParentRegistry("parentKey", parent) // Advancing the lifecycle triggers restoration. - parent.lifecycleRegistry.currentState = RESUMED + parent.lifecycleRegistry.currentState = Lifecycle.State.RESUMED aggregator.saveRegistryController("childKey", child.stateRegistryController) - assertThat(childSaveCount).isEqualTo(1) + Truth.assertThat(childSaveCount).isEqualTo(1) } - @Test fun `saveRegistryController doesn't save if not restored`() { + @Test + fun `saveRegistryController doesn't save if not restored`() { val aggregator = StateRegistryAggregator( - onWillSave = {}, - onRestored = {} + onWillSave = {}, + onRestored = {} ) val parent = SimpleStateRegistry().apply { stateRegistryController.performRestore(null) @@ -156,7 +162,7 @@ class StateRegistryAggregatorTest { val child = SimpleStateRegistry().apply { savedStateRegistry.registerSavedStateProvider("key") { childSaveCount++ - Bundle() + Bundle() } } aggregator.attachToParentRegistry("parentKey", parent) @@ -164,13 +170,14 @@ class StateRegistryAggregatorTest { aggregator.saveRegistryController("childKey", child.stateRegistryController) - assertThat(childSaveCount).isEqualTo(0) + Truth.assertThat(childSaveCount).isEqualTo(0) } - @Test fun `restoreRegistryControllerIfReady restores when parent is restored`() { + @Test + fun `restoreRegistryControllerIfReady restores when parent is restored`() { val aggregator = StateRegistryAggregator( - onWillSave = {}, - onRestored = {} + onWillSave = {}, + onRestored = {} ) val parent = SimpleStateRegistry().apply { stateRegistryController.performRestore(null) @@ -178,17 +185,18 @@ class StateRegistryAggregatorTest { val child = SimpleStateRegistry() aggregator.attachToParentRegistry("parentKey", parent) // Advancing the lifecycle triggers restoration. - parent.lifecycleRegistry.currentState = RESUMED + parent.lifecycleRegistry.currentState = Lifecycle.State.RESUMED aggregator.restoreRegistryControllerIfReady("childKey", child.stateRegistryController) - assertThat(child.savedStateRegistry.isRestored).isTrue() + Truth.assertThat(child.savedStateRegistry.isRestored).isTrue() } - @Test fun `restoreRegistryControllerIfReady doesn't restore if not restored`() { + @Test + fun `restoreRegistryControllerIfReady doesn't restore if not restored`() { val aggregator = StateRegistryAggregator( - onWillSave = {}, - onRestored = {} + onWillSave = {}, + onRestored = {} ) val parent = SimpleStateRegistry().apply { stateRegistryController.performRestore(null) @@ -199,21 +207,22 @@ class StateRegistryAggregatorTest { aggregator.restoreRegistryControllerIfReady("childKey", child.stateRegistryController) - assertThat(child.savedStateRegistry.isRestored).isFalse() + Truth.assertThat(child.savedStateRegistry.isRestored).isFalse() } // This is really more of an integration test. - @Test fun `saves and restores child state controller`() { + @Test + fun `saves and restores child state controller`() { val holderToSave = StateRegistryAggregator( - onWillSave = {}, - onRestored = {} + onWillSave = {}, + onRestored = {} ) val parentToSave = SimpleStateRegistry().apply { // Need to call restore before moving lifecycle state past INITIALIZED. stateRegistryController.performRestore(null) } holderToSave.attachToParentRegistry("parentKey", parentToSave) - parentToSave.lifecycleRegistry.currentState = RESUMED + parentToSave.lifecycleRegistry.currentState = Lifecycle.State.RESUMED // Store some data in the system. val childToSave = stateRegistryOf("key" to bundleOf("data" to "value")) @@ -224,14 +233,14 @@ class StateRegistryAggregatorTest { // Create a whole new tree, restored from our bundle. val holderToRestore = StateRegistryAggregator( - onWillSave = {}, - onRestored = {} + onWillSave = {}, + onRestored = {} ) val parentToRestore = SimpleStateRegistry().apply { stateRegistryController.performRestore(parentSavedBundle) } holderToRestore.attachToParentRegistry("parentKey", parentToRestore) - parentToRestore.lifecycleRegistry.currentState = RESUMED + parentToRestore.lifecycleRegistry.currentState = Lifecycle.State.RESUMED val childToRestore = SimpleStateRegistry() holderToRestore.restoreRegistryControllerIfReady( "childKey", @@ -240,13 +249,14 @@ class StateRegistryAggregatorTest { val restoredChildContent = childToRestore.savedStateRegistry.consumeRestoredStateForKey("key")!! // Verify that our leaf data was restored. - assertThat(restoredChildContent.getString("data")).isEqualTo("value") + Truth.assertThat(restoredChildContent.getString("data")).isEqualTo("value") } - @Test fun `restores child state controller`() { + @Test + fun `restores child state controller`() { val aggregator = StateRegistryAggregator( - onWillSave = {}, - onRestored = {} + onWillSave = {}, + onRestored = {} ) val parent = stateRegistryOf( "parentKey" to bundleOf( @@ -261,29 +271,31 @@ class StateRegistryAggregatorTest { ) aggregator.attachToParentRegistry("parentKey", parent) - parent.lifecycleRegistry.handleLifecycleEvent(ON_CREATE) + parent.lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) val child = SimpleStateRegistry() aggregator.restoreRegistryControllerIfReady("childKey", child.stateRegistryController) val childState = child.savedStateRegistry.consumeRestoredStateForKey("key")!! - assertThat(childState.getString("data")).isEqualTo("value") + Truth.assertThat(childState.getString("data")).isEqualTo("value") } - @Test fun `detach doesn't throws when called without attach`() { + @Test + fun `detach doesn't throws when called without attach`() { val aggregator = StateRegistryAggregator( - onWillSave = {}, - onRestored = {} + onWillSave = {}, + onRestored = {} ) aggregator.detachFromParentRegistry() } - @Test fun `save callback is invoked`() { + @Test + fun `save callback is invoked`() { var saveCount = 0 val aggregator = StateRegistryAggregator( - onWillSave = { saveCount++ }, - onRestored = {} + onWillSave = { saveCount++ }, + onRestored = {} ) val parent = SimpleStateRegistry().apply { // Must restore the parent controller in order to initialize the aggregator. @@ -291,18 +303,19 @@ class StateRegistryAggregatorTest { } aggregator.attachToParentRegistry("parentKey", parent) // Advancing the lifecycle triggers restoration. - parent.lifecycleRegistry.currentState = RESUMED + parent.lifecycleRegistry.currentState = Lifecycle.State.RESUMED parent.saveToBundle() - assertThat(saveCount).isEqualTo(1) + Truth.assertThat(saveCount).isEqualTo(1) } - @Test fun `restore callback is invoked when restored`() { + @Test + fun `restore callback is invoked when restored`() { var restoreCount = 0 val aggregator = StateRegistryAggregator( - onWillSave = {}, - onRestored = { restoreCount++ } + onWillSave = {}, + onRestored = { restoreCount++ } ) val parent = SimpleStateRegistry().apply { // Must restore the parent controller in order to initialize the aggregator. @@ -310,16 +323,17 @@ class StateRegistryAggregatorTest { } aggregator.attachToParentRegistry("parentKey", parent) // Advancing the lifecycle triggers restoration. - parent.lifecycleRegistry.currentState = RESUMED + parent.lifecycleRegistry.currentState = Lifecycle.State.RESUMED - assertThat(restoreCount).isEqualTo(1) + Truth.assertThat(restoreCount).isEqualTo(1) } - @Test fun `restore callback is not invoked when attached`() { + @Test + fun `restore callback is not invoked when attached`() { var restoreCount = 0 val aggregator = StateRegistryAggregator( - onWillSave = {}, - onRestored = { restoreCount++ } + onWillSave = {}, + onRestored = { restoreCount++ } ) val parent = SimpleStateRegistry().apply { // Must restore the parent controller in order to initialize the aggregator. @@ -327,7 +341,7 @@ class StateRegistryAggregatorTest { } aggregator.attachToParentRegistry("parentKey", parent) - assertThat(restoreCount).isEqualTo(0) + Truth.assertThat(restoreCount).isEqualTo(0) } /** diff --git a/workflow-ui/core-common/api/core-common.api b/workflow-ui/core-common/api/core-common.api index 0dcc88401c..68fa18bcb5 100644 --- a/workflow-ui/core-common/api/core-common.api +++ b/workflow-ui/core-common/api/core-common.api @@ -1,3 +1,13 @@ +public final class com/squareup/workflow1/ui/AsScreen : com/squareup/workflow1/ui/Compatible, com/squareup/workflow1/ui/Screen { + public fun (Ljava/lang/Object;)V + public fun getCompatibilityKey ()Ljava/lang/String; + public final fun getRendering ()Ljava/lang/Object; +} + +public final class com/squareup/workflow1/ui/AsScreenKt { + public static final fun asScreen (Ljava/lang/Object;)Lcom/squareup/workflow1/ui/Screen; +} + public abstract interface class com/squareup/workflow1/ui/Compatible { public static final field Companion Lcom/squareup/workflow1/ui/Compatible$Companion; public abstract fun getCompatibilityKey ()Ljava/lang/String; @@ -26,6 +36,23 @@ public final class com/squareup/workflow1/ui/Named : com/squareup/workflow1/ui/C public fun toString ()Ljava/lang/String; } +public final class com/squareup/workflow1/ui/NamedScreen : com/squareup/workflow1/ui/Compatible, com/squareup/workflow1/ui/Screen { + public fun (Lcom/squareup/workflow1/ui/Screen;Ljava/lang/String;)V + public final fun component1 ()Lcom/squareup/workflow1/ui/Screen; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Lcom/squareup/workflow1/ui/Screen;Ljava/lang/String;)Lcom/squareup/workflow1/ui/NamedScreen; + public static synthetic fun copy$default (Lcom/squareup/workflow1/ui/NamedScreen;Lcom/squareup/workflow1/ui/Screen;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/NamedScreen; + public fun equals (Ljava/lang/Object;)Z + public fun getCompatibilityKey ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public final fun getWrapped ()Lcom/squareup/workflow1/ui/Screen; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract interface class com/squareup/workflow1/ui/Screen { +} + public final class com/squareup/workflow1/ui/TextController { public fun ()V public fun (Ljava/lang/String;)V @@ -35,6 +62,86 @@ public final class com/squareup/workflow1/ui/TextController { public final fun setTextValue (Ljava/lang/String;)V } +public final class com/squareup/workflow1/ui/ViewEnvironment { + public fun ()V + public fun (Ljava/util/Map;)V + public synthetic fun (Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun get (Lcom/squareup/workflow1/ui/ViewEnvironmentKey;)Ljava/lang/Object; + public final fun getMap ()Ljava/util/Map; + public fun hashCode ()I + public final fun plus (Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/ViewEnvironment; + public final fun plus (Lkotlin/Pair;)Lcom/squareup/workflow1/ui/ViewEnvironment; + public fun toString ()Ljava/lang/String; +} + +public abstract class com/squareup/workflow1/ui/ViewEnvironmentKey { + public fun (Lkotlin/reflect/KClass;)V + public final fun equals (Ljava/lang/Object;)Z + public abstract fun getDefault ()Ljava/lang/Object; + public final fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/ui/ViewEnvironmentKt { + public static final fun updateFrom (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/ViewEnvironment; +} + +public abstract interface class com/squareup/workflow1/ui/ViewRegistry { + public static final field Companion Lcom/squareup/workflow1/ui/ViewRegistry$Companion; + public abstract fun getEntryFor (Lkotlin/reflect/KClass;)Lcom/squareup/workflow1/ui/ViewRegistry$Entry; + public abstract fun getKeys ()Ljava/util/Set; +} + +public final class com/squareup/workflow1/ui/ViewRegistry$Companion : com/squareup/workflow1/ui/ViewEnvironmentKey { + public fun getDefault ()Lcom/squareup/workflow1/ui/ViewRegistry; + public synthetic fun getDefault ()Ljava/lang/Object; +} + +public abstract interface class com/squareup/workflow1/ui/ViewRegistry$Entry { + public abstract fun getType ()Lkotlin/reflect/KClass; +} + +public final class com/squareup/workflow1/ui/ViewRegistryKt { + public static final fun ViewRegistry ()Lcom/squareup/workflow1/ui/ViewRegistry; + public static final fun ViewRegistry ([Lcom/squareup/workflow1/ui/ViewRegistry$Entry;)Lcom/squareup/workflow1/ui/ViewRegistry; + public static final fun plus (Lcom/squareup/workflow1/ui/ViewRegistry;Lcom/squareup/workflow1/ui/ViewRegistry$Entry;)Lcom/squareup/workflow1/ui/ViewRegistry; + public static final fun plus (Lcom/squareup/workflow1/ui/ViewRegistry;Lcom/squareup/workflow1/ui/ViewRegistry;)Lcom/squareup/workflow1/ui/ViewRegistry; +} + public abstract interface annotation class com/squareup/workflow1/ui/WorkflowUiExperimentalApi : java/lang/annotation/Annotation { } +public final class com/squareup/workflow1/ui/container/BackStackScreen : com/squareup/workflow1/ui/Screen { + public fun (Lcom/squareup/workflow1/ui/Screen;Ljava/util/List;)V + public fun (Lcom/squareup/workflow1/ui/Screen;[Lcom/squareup/workflow1/ui/Screen;)V + public fun equals (Ljava/lang/Object;)Z + public final fun get (I)Lcom/squareup/workflow1/ui/Screen; + public final fun getBackStack ()Ljava/util/List; + public final fun getFrames ()Ljava/util/List; + public final fun getTop ()Lcom/squareup/workflow1/ui/Screen; + public fun hashCode ()I + public final fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/container/BackStackScreen; + public final fun mapIndexed (Lkotlin/jvm/functions/Function2;)Lcom/squareup/workflow1/ui/container/BackStackScreen; + public final fun plus (Lcom/squareup/workflow1/ui/container/BackStackScreen;)Lcom/squareup/workflow1/ui/container/BackStackScreen; + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/ui/container/BackStackScreenKt { + public static final fun toBackStackScreen (Ljava/util/List;)Lcom/squareup/workflow1/ui/container/BackStackScreen; + public static final fun toBackStackScreenOrNull (Ljava/util/List;)Lcom/squareup/workflow1/ui/container/BackStackScreen; +} + +public final class com/squareup/workflow1/ui/container/WithEnvironment : com/squareup/workflow1/ui/Compatible, com/squareup/workflow1/ui/Screen { + public fun getCompatibilityKey ()Ljava/lang/String; + public final fun getScreen ()Lcom/squareup/workflow1/ui/Screen; + public final fun getViewEnvironment ()Lcom/squareup/workflow1/ui/ViewEnvironment; +} + +public final class com/squareup/workflow1/ui/container/WithEnvironmentKt { + public static final fun plus (Lcom/squareup/workflow1/ui/container/WithEnvironment;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/container/WithEnvironment; + public static final fun withEnvironment (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/container/WithEnvironment; + public static synthetic fun withEnvironment$default (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/container/WithEnvironment; + public static final fun withRegistry (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewRegistry;)Lcom/squareup/workflow1/ui/container/WithEnvironment; +} + diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/AsScreen.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/AsScreen.kt new file mode 100644 index 0000000000..6d69d8b637 --- /dev/null +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/AsScreen.kt @@ -0,0 +1,35 @@ +@file:Suppress("DEPRECATION") + +package com.squareup.workflow1.ui + +/** + * Provides backward compatibility for legacy non-[Screen] renderings based on + * `ViewFactory` and `AndroidViewRendering`. Should be used only as a stop gap + * until the wrapped [rendering] can be updated to implement [Screen]. + */ +@Deprecated("Implement Screen directly.") +@WorkflowUiExperimentalApi +public class AsScreen( + public val rendering: W +) : Screen, Compatible { + init { + check(rendering !is Screen) { + "AsScreen is for converting non-Screen renderings, it should not wrap Screen $rendering." + } + } + + override val compatibilityKey: String + get() = Compatible.keyFor(rendering) +} + +/** + * Ensures [rendering] implements [Screen], wrapping it in an [AsScreen] if necessary. + */ +@Deprecated("Implement Screen directly.") +@WorkflowUiExperimentalApi +public fun asScreen(rendering: Any): Screen { + return when (rendering) { + is Screen -> rendering + else -> AsScreen(rendering) + } +} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/CompositeViewRegistry.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/CompositeViewRegistry.kt similarity index 83% rename from workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/CompositeViewRegistry.kt rename to workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/CompositeViewRegistry.kt index e4b1424209..1a387cf51b 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/CompositeViewRegistry.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/CompositeViewRegistry.kt @@ -1,16 +1,18 @@ package com.squareup.workflow1.ui +import com.squareup.workflow1.ui.ViewRegistry.Entry import kotlin.reflect.KClass /** - * A [ViewRegistry] that contains only other registries and delegates to their [buildView] methods. + * A [ViewRegistry] that contains only other registries and delegates to their [getEntryFor] + * methods. * * Whenever any registries are combined using the [ViewRegistry] factory functions or `plus` * operators, an instance of this class is returned. All registries' keys are checked at * construction to ensure that no duplicate keys exist. * - * The implementation of [buildView] consists of a single layer of indirection – the responsible - * [ViewRegistry] is looked up in a map by key, and then that registry's [buildView] is called. + * The implementation of [getEntryFor] consists of a single layer of indirection – the responsible + * [ViewRegistry] is looked up in a map by key, and then that registry's [getEntryFor] is called. * * When multiple [CompositeViewRegistry]s are combined, they are flattened, so that there is never * more than one layer of indirection. In other words, a [CompositeViewRegistry] will never contain @@ -25,9 +27,9 @@ internal class CompositeViewRegistry private constructor( override val keys: Set> get() = registriesByKey.keys - override fun getFactoryFor( + override fun getEntryFor( renderingType: KClass - ): ViewFactory? = registriesByKey[renderingType]?.getFactoryFor(renderingType) + ): Entry? = registriesByKey[renderingType]?.getEntryFor(renderingType) companion object { private fun mergeRegistries(vararg registries: ViewRegistry): Map, ViewRegistry> { diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/NamedScreen.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/NamedScreen.kt new file mode 100644 index 0000000000..c4767072e6 --- /dev/null +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/NamedScreen.kt @@ -0,0 +1,22 @@ +package com.squareup.workflow1.ui + +/** + * Allows [Screen] renderings that do not implement [Compatible] themselves to be distinguished + * by more than just their type. Instances are [compatible] if they have the same name + * and have [compatible] [wrapped] fields. + */ +@WorkflowUiExperimentalApi +public data class NamedScreen( + val wrapped: W, + val name: String +) : Screen, Compatible { + init { + require(name.isNotBlank()) { "name must not be blank." } + } + + override val compatibilityKey: String = Compatible.keyFor(wrapped, name) + + override fun toString(): String { + return "${super.toString()}: $compatibilityKey" + } +} 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 new file mode 100644 index 0000000000..37b586e8aa --- /dev/null +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Screen.kt @@ -0,0 +1,7 @@ +package com.squareup.workflow1.ui + +/** + * Marker interface implemented by renderings that map to a UI system's 2d view class. + */ +@WorkflowUiExperimentalApi +public interface Screen diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/TypedViewRegistry.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/TypedViewRegistry.kt similarity index 66% rename from workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/TypedViewRegistry.kt rename to workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/TypedViewRegistry.kt index cb864873a8..3bddf1a244 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/TypedViewRegistry.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/TypedViewRegistry.kt @@ -1,5 +1,6 @@ package com.squareup.workflow1.ui +import com.squareup.workflow1.ui.ViewRegistry.Entry import kotlin.reflect.KClass /** @@ -8,25 +9,25 @@ import kotlin.reflect.KClass */ @WorkflowUiExperimentalApi internal class TypedViewRegistry private constructor( - private val bindings: Map, ViewFactory<*>> + private val bindings: Map, Entry<*>> ) : ViewRegistry { - constructor(vararg bindings: ViewFactory<*>) : this( + constructor(vararg bindings: Entry<*>) : this( bindings.map { it.type to it } .toMap() .apply { check(keys.size == bindings.size) { "${bindings.map { it.type }} must not have duplicate entries." } - } as Map, ViewFactory<*>> + } as Map, Entry<*>> ) override val keys: Set> get() = bindings.keys - override fun getFactoryFor( + override fun getEntryFor( renderingType: KClass - ): ViewFactory? { + ): Entry? { @Suppress("UNCHECKED_CAST") - return bindings[renderingType] as? ViewFactory + return bindings[renderingType] as? Entry } } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewEnvironment.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/ViewEnvironment.kt similarity index 62% rename from workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewEnvironment.kt rename to workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/ViewEnvironment.kt index a363548e83..77315afa5a 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewEnvironment.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/ViewEnvironment.kt @@ -4,9 +4,11 @@ import kotlin.reflect.KClass /** * Immutable, append-only map of values that a parent view can pass down to - * its children via [View.showRendering][android.view.View.showRendering] et al. - * Allows container views to give descendants information about the context in which - * they're drawing. + * its children. Allows containers to give descendants information about + * the context in which they're drawing. + * + * Calling [Screen.withEnvironment][com.squareup.workflow1.ui.container.withEnvironment] + * is the easiest way to customize its environment. */ @WorkflowUiExperimentalApi public class ViewEnvironment( @@ -51,3 +53,24 @@ public abstract class ViewEnvironmentKey( return "ViewEnvironmentKey($type)-${super.toString()}" } } + +/** + * Combines the receiving [ViewEnvironment] with [other], taking care to merge + * their [ViewRegistry] entries. Duplicate values in [other] replace those + * in the receiver. + */ +@WorkflowUiExperimentalApi +public fun ViewEnvironment.updateFrom(other: ViewEnvironment): ViewEnvironment { + if (other.map.isEmpty()) return this + + val myReg = this[ViewRegistry] + val yourReg = other[ViewRegistry] + + val union = (myReg.keys + yourReg.keys).asSequence() + .map { yourReg.getEntryFor(it) ?: myReg.getEntryFor(it)!! } + .toList() + .toTypedArray() + + val unionRegistry = ViewRegistry(*union) + return this + other + (ViewRegistry to unionRegistry) +} diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/ViewRegistry.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/ViewRegistry.kt new file mode 100644 index 0000000000..cf74263c50 --- /dev/null +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/ViewRegistry.kt @@ -0,0 +1,111 @@ +@file:Suppress("FunctionName") + +package com.squareup.workflow1.ui + +import com.squareup.workflow1.ui.ViewRegistry.Entry +import kotlin.reflect.KClass + +/** + * The [ViewEnvironment] service that can be used to display the stream of renderings + * from a workflow tree as [View] instances. This is the engine behind [AndroidViewRendering], + * [WorkflowViewStub] and [ViewFactory]. Most apps can ignore [ViewRegistry] as an implementation + * detail, by using [AndroidViewRendering] to tie their rendering classes to view code. + * + * To avoid that coupling between workflow code and the Android runtime, registries can + * be loaded with [ViewFactory] instances at runtime, and provided as an optional parameter to + * [WorkflowLayout.start]. + * + * For example: + * + * val AuthViewFactories = ViewRegistry( + * AuthorizingLayoutRunner, LoginLayoutRunner, SecondFactorLayoutRunner + * ) + * + * val TicTacToeViewFactories = ViewRegistry( + * NewGameLayoutRunner, GamePlayLayoutRunner, GameOverLayoutRunner + * ) + * + * val ApplicationViewFactories = ViewRegistry(ApplicationLayoutRunner) + + * AuthViewFactories + TicTacToeViewFactories + * + * override fun onCreate(savedInstanceState: Bundle?) { + * super.onCreate(savedInstanceState) + * + * val model: MyViewModel by viewModels() + * setContentView( + * WorkflowLayout(this).apply { start(model.renderings, ApplicationViewFactories) } + * ) + * } + * + * /** As always, use an androidx ViewModel for state that survives config change. */ + * class MyViewModel(savedState: SavedStateHandle) : ViewModel() { + * val renderings: StateFlow by lazy { + * renderWorkflowIn( + * workflow = rootWorkflow, + * scope = viewModelScope, + * savedStateHandle = savedState + * ) + * } + * } + * + * In the above example, it is assumed that the `companion object`s of the various + * decoupled [LayoutRunner] classes honor a convention of implementing [ViewFactory], in + * aid of this kind of assembly. + * + * class GamePlayLayoutRunner(view: View) : LayoutRunner { + * + * // ... + * + * companion object : ViewFactory by LayoutRunner.bind( + * R.layout.game_layout, ::GameLayoutRunner + * ) + * } + */ +@WorkflowUiExperimentalApi +public interface ViewRegistry { + public interface Entry { + public val type: KClass + } + + /** + * The set of unique keys which this registry can derive from the renderings passed to + * [getEntryFor] and for which it knows how to create views. + * + * Used to ensure that duplicate bindings are never registered. + */ + public val keys: Set> + + /** + * This method is not for general use, use [WorkflowViewStub] instead. + * + * Returns the [ViewFactory] that was registered for the given [renderingType], or null + * if none was found. + */ + public fun getEntryFor( + renderingType: KClass + ): Entry? + + public companion object : ViewEnvironmentKey(ViewRegistry::class) { + override val default: ViewRegistry get() = ViewRegistry() + } +} + +@WorkflowUiExperimentalApi +public fun ViewRegistry(vararg bindings: Entry<*>): ViewRegistry = + TypedViewRegistry(*bindings) + +/** + * Returns a [ViewRegistry] that contains no bindings. + * + * Exists as a separate overload from the other two functions to disambiguate between them. + */ +@WorkflowUiExperimentalApi +public fun ViewRegistry(): ViewRegistry = TypedViewRegistry() + +@WorkflowUiExperimentalApi +public operator fun ViewRegistry.plus(binding: Entry<*>): ViewRegistry = + this + ViewRegistry(binding) + +@WorkflowUiExperimentalApi +public operator fun ViewRegistry.plus(other: ViewRegistry): ViewRegistry = + CompositeViewRegistry(this, other) diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/BackStackScreen.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/BackStackScreen.kt new file mode 100644 index 0000000000..8cd82a1073 --- /dev/null +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/BackStackScreen.kt @@ -0,0 +1,81 @@ +package com.squareup.workflow1.ui.container + +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +/** + * Represents an active screen ([top]), and a set of previously visited screens to which we may + * return ([backStack]). By rendering the entire history we allow the UI to do things like maintain + * cached view state, implement drag-back gestures without waiting for the workflow, etc. + * + * Effectively a list that can never be empty. + * + * @param bottom the bottom-most entry in the stack + * @param rest the rest of the stack, empty by default + */ +@WorkflowUiExperimentalApi +public class BackStackScreen( + bottom: StackedT, + rest: List +) : Screen { + /** + * Creates a screen with elements listed from the [bottom] to the top. + */ + public constructor( + bottom: StackedT, + vararg rest: StackedT + ) : this(bottom, rest.toList()) + + public val frames: List = listOf(bottom) + rest + + /** + * The active screen. + */ + public val top: StackedT = frames.last() + + /** + * Screens to which we may return. + */ + public val backStack: List = frames.subList(0, frames.size - 1) + + public operator fun get(index: Int): StackedT = frames[index] + + public operator fun plus(other: BackStackScreen?): BackStackScreen { + return if (other == null) this + else BackStackScreen(frames[0], frames.subList(1, frames.size) + other.frames) + } + + public fun map(transform: (StackedT) -> R): BackStackScreen { + return frames.map(transform) + .toBackStackScreen() + } + + public fun mapIndexed(transform: (index: Int, StackedT) -> R): BackStackScreen { + return frames.mapIndexed(transform) + .toBackStackScreen() + } + + override fun equals(other: Any?): Boolean { + return (other as? BackStackScreen<*>)?.frames == frames + } + + override fun hashCode(): Int { + return frames.hashCode() + } + + override fun toString(): String { + return "${this::class.java.simpleName}($frames)" + } +} + +@WorkflowUiExperimentalApi +public fun List.toBackStackScreenOrNull(): BackStackScreen? = when { + isEmpty() -> null + else -> toBackStackScreen() +} + +@WorkflowUiExperimentalApi +public fun List.toBackStackScreen(): BackStackScreen { + require(isNotEmpty()) + return BackStackScreen(first(), subList(1, size)) +} diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/WithEnvironment.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/WithEnvironment.kt new file mode 100644 index 0000000000..698bef7a6c --- /dev/null +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/WithEnvironment.kt @@ -0,0 +1,61 @@ +package com.squareup.workflow1.ui.container + +import com.squareup.workflow1.ui.Compatible +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.ViewRegistry +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.updateFrom + +/** + * Pairs a [screen] rendering with a [viewEnvironment] to support its display. + * Typically the rendering type (`RenderingT`) of the root of a UI workflow, + * but can be used at any point to modify the [ViewEnvironment] received from + * a parent view. + * + * Use [withEnvironment] or [withRegistry] to create or update instances. + */ +@WorkflowUiExperimentalApi +public class WithEnvironment internal constructor( + public val screen: V, + public val viewEnvironment: ViewEnvironment = ViewEnvironment() +) : Compatible, Screen { + /** + * Ensures that we make the decision to update or replace the root view based on + * the wrapped [screen]. + */ + override val compatibilityKey: String = Compatible.keyFor(screen, "WithEnvironment") +} + +@WorkflowUiExperimentalApi +public operator fun WithEnvironment.plus( + environment: ViewEnvironment +): WithEnvironment { + return when { + environment.map.isEmpty() -> this + else -> WithEnvironment(screen, viewEnvironment.updateFrom(environment)) + } +} + +/** + * Returns a [WithEnvironment] derived from the receiver, whose [ViewEnvironment] + * includes a [ViewRegistry] updated from the given [viewRegistry]. + */ +@WorkflowUiExperimentalApi +public fun Screen.withRegistry(viewRegistry: ViewRegistry): WithEnvironment<*> { + return withEnvironment(ViewEnvironment(mapOf(ViewRegistry to viewRegistry))) +} + +/** + * Returns a [WithEnvironment] derived from the receiver, whose [ViewEnvironment] + * is [updated][updateFrom] the given [viewEnvironment]. + */ +@WorkflowUiExperimentalApi +public fun Screen.withEnvironment( + viewEnvironment: ViewEnvironment = ViewEnvironment() +): WithEnvironment<*> { + return when (this) { + is WithEnvironment<*> -> this + viewEnvironment + else -> WithEnvironment(this, viewEnvironment) + } +} diff --git a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/CompositeViewRegistryTest.kt b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/CompositeViewRegistryTest.kt similarity index 53% rename from workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/CompositeViewRegistryTest.kt rename to workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/CompositeViewRegistryTest.kt index 2f4a605cfc..026b4245d7 100644 --- a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/CompositeViewRegistryTest.kt +++ b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/CompositeViewRegistryTest.kt @@ -1,78 +1,84 @@ package com.squareup.workflow1.ui import com.google.common.truth.Truth.assertThat +import com.squareup.workflow1.ui.ViewRegistry.Entry import org.junit.Test import kotlin.reflect.KClass import kotlin.test.assertFailsWith @OptIn(WorkflowUiExperimentalApi::class) -class CompositeViewRegistryTest { +internal class CompositeViewRegistryTest { @Test fun `constructor throws on duplicates`() { val fooBarRegistry = TestRegistry(setOf(FooRendering::class, BarRendering::class)) val barBazRegistry = TestRegistry(setOf(BarRendering::class, BazRendering::class)) val error = assertFailsWith { - CompositeViewRegistry(fooBarRegistry, barBazRegistry) + fooBarRegistry + barBazRegistry } assertThat(error).hasMessageThat() - .startsWith("Must not have duplicate entries: ") + .startsWith("Must not have duplicate entries: ") assertThat(error).hasMessageThat() - .contains(BarRendering::class.java.name) + .contains(BarRendering::class.java.name) } @Test fun `getFactoryFor delegates to composite registries`() { - val fooFactory = TestViewFactory(FooRendering::class) - val barFactory = TestViewFactory(BarRendering::class) - val bazFactory = TestViewFactory(BazRendering::class) + val fooFactory = TestEntry(FooRendering::class) + val barFactory = TestEntry(BarRendering::class) + val bazFactory = TestEntry(BazRendering::class) val fooBarRegistry = TestRegistry( - mapOf( - FooRendering::class to fooFactory, - BarRendering::class to barFactory - ) + mapOf( + FooRendering::class to fooFactory, + BarRendering::class to barFactory + ) ) val bazRegistry = TestRegistry(factories = mapOf(BazRendering::class to bazFactory)) - val registry = CompositeViewRegistry(fooBarRegistry, bazRegistry) + val registry = fooBarRegistry + bazRegistry - assertThat(registry.getFactoryFor(FooRendering::class)) - .isSameInstanceAs(fooFactory) - assertThat(registry.getFactoryFor(BarRendering::class)) - .isSameInstanceAs(barFactory) - assertThat(registry.getFactoryFor(BazRendering::class)) - .isSameInstanceAs(bazFactory) + assertThat(registry.getEntryFor(FooRendering::class)) + .isSameInstanceAs(fooFactory) + assertThat(registry.getEntryFor(BarRendering::class)) + .isSameInstanceAs(barFactory) + assertThat(registry.getEntryFor(BazRendering::class)) + .isSameInstanceAs(bazFactory) } @Test fun `getFactoryFor returns null on missing registry`() { val fooRegistry = TestRegistry(setOf(FooRendering::class)) - val registry = CompositeViewRegistry(fooRegistry) + val registry = ViewRegistry() + fooRegistry - assertThat(registry.getFactoryFor(BarRendering::class)).isNull() + assertThat(registry.getEntryFor(BarRendering::class)).isNull() } @Test fun `keys includes all composite registries' keys`() { val fooBarRegistry = TestRegistry(setOf(FooRendering::class, BarRendering::class)) val bazRegistry = TestRegistry(setOf(BazRendering::class)) - val registry = CompositeViewRegistry(fooBarRegistry, bazRegistry) + val registry = fooBarRegistry + bazRegistry assertThat(registry.keys).containsExactly( - FooRendering::class, - BarRendering::class, - BazRendering::class + FooRendering::class, + BarRendering::class, + BazRendering::class ) } + private class TestEntry( + override val type: KClass + ) : Entry + private object FooRendering private object BarRendering private object BazRendering - private class TestRegistry(private val factories: Map, ViewFactory<*>>) : ViewRegistry { - constructor(keys: Set>) : this(keys.associateWith { TestViewFactory(it) }) + @Suppress("DEPRECATION") + private class TestRegistry(private val factories: Map, Entry<*>>) : ViewRegistry { + constructor(keys: Set>) : this(keys.associateWith { TestEntry(it) }) override val keys: Set> get() = factories.keys @Suppress("UNCHECKED_CAST") - override fun getFactoryFor( + override fun getEntryFor( renderingType: KClass - ): ViewFactory = factories.getValue(renderingType) as ViewFactory + ): Entry = factories.getValue(renderingType) as Entry } } diff --git a/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/TypedViewRegistryTest.kt b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/TypedViewRegistryTest.kt new file mode 100644 index 0000000000..1c23bbf077 --- /dev/null +++ b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/TypedViewRegistryTest.kt @@ -0,0 +1,60 @@ +package com.squareup.workflow1.ui + +import com.google.common.truth.Truth.assertThat +import com.squareup.workflow1.ui.ViewRegistry.Entry +import org.junit.Test +import kotlin.reflect.KClass +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +@OptIn(WorkflowUiExperimentalApi::class) +internal class TypedViewRegistryTest { + + @Test fun `keys from bindings`() { + val factory1 = TestEntry(FooRendering::class) + val factory2 = TestEntry(BarRendering::class) + val registry = ViewRegistry(factory1, factory2) + + assertThat(registry.keys).containsExactly(factory1.type, factory2.type) + } + + @Test fun `constructor throws on duplicates`() { + val factory1 = TestEntry(FooRendering::class) + val factory2 = TestEntry(FooRendering::class) + + val error = assertFailsWith { + ViewRegistry(factory1, factory2) + } + assertThat(error).hasMessageThat() + .endsWith("must not have duplicate entries.") + assertThat(error).hasMessageThat() + .contains(FooRendering::class.java.name) + } + + @Test fun `getFactoryFor works`() { + val fooFactory = TestEntry(FooRendering::class) + val registry = ViewRegistry(fooFactory) + + val factory = registry.getEntryFor(FooRendering::class) + assertThat(factory).isSameInstanceAs(fooFactory) + } + + @Test fun `getFactoryFor returns null on missing binding`() { + val fooFactory = TestEntry(FooRendering::class) + val registry = ViewRegistry(fooFactory) + + assertThat(registry.getEntryFor(BarRendering::class)).isNull() + } + + @Test fun `ViewRegistry with no arguments infers type`() { + val registry = ViewRegistry() + assertTrue(registry.keys.isEmpty()) + } + + private class TestEntry( + override val type: KClass + ) : Entry + + private object FooRendering + private object BarRendering +} diff --git a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ViewEnvironmentTest.kt b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/ViewEnvironmentTest.kt similarity index 95% rename from workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ViewEnvironmentTest.kt rename to workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/ViewEnvironmentTest.kt index 1569d0bfac..540931e8fe 100644 --- a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ViewEnvironmentTest.kt +++ b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/ViewEnvironmentTest.kt @@ -4,7 +4,7 @@ import com.google.common.truth.Truth.assertThat import org.junit.Test @OptIn(WorkflowUiExperimentalApi::class) -class ViewEnvironmentTest { +internal class ViewEnvironmentTest { private object StringHint : ViewEnvironmentKey(String::class) { override val default = "" } @@ -22,7 +22,7 @@ class ViewEnvironmentTest { } } - private val emptyEnv = ViewEnvironment(mapOf(ViewRegistry to ViewRegistry())) + private val emptyEnv = ViewEnvironment() @Test fun defaults() { assertThat(emptyEnv[DataHint]).isEqualTo(DataHint()) diff --git a/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/container/BackStackScreenTest.kt b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/container/BackStackScreenTest.kt new file mode 100644 index 0000000000..936f1fb171 --- /dev/null +++ b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/container/BackStackScreenTest.kt @@ -0,0 +1,90 @@ +package com.squareup.workflow1.ui.container + +import com.google.common.truth.Truth.assertThat +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import org.junit.Test +import kotlin.test.assertFailsWith + +@OptIn(WorkflowUiExperimentalApi::class) +internal class BackStackScreenTest { + data class S(val value: T) : Screen + + @Test fun `top is last`() { + assertThat(BackStackScreen(S(1), S(2), S(3), S(4)).top).isEqualTo(S(4)) + } + + @Test fun `backstack is all but top`() { + assertThat(BackStackScreen(S(1), S(2), S(3), S(4)).backStack) + .isEqualTo(listOf(S(1), S(2), S(3))) + } + + @Test fun `get works`() { + assertThat(BackStackScreen(S("able"), S("baker"), S("charlie"))[1]).isEqualTo(S("baker")) + } + + @Test fun `plus another stack`() { + assertThat(BackStackScreen(S(1), S(2), S(3)) + BackStackScreen(S(8), S(9), S(0))) + .isEqualTo(BackStackScreen(S(1), S(2), S(3), S(8), S(9), S(0))) + } + + @Test fun `unequal by order`() { + assertThat(BackStackScreen(S(1), S(2), S(3))) + .isNotEqualTo(BackStackScreen(S(3), S(2), S(1))) + } + + @Test fun `equal have matching hash`() { + assertThat(BackStackScreen(S(1), S(2), S(3)).hashCode()) + .isEqualTo(BackStackScreen(S(1), S(2), S(3)).hashCode()) + } + + @Test fun `unequal have mismatching hash`() { + assertThat(BackStackScreen(S(1), S(2)).hashCode()) + .isNotEqualTo(BackStackScreen(S(1), S(2), S(3)).hashCode()) + } + + @Test fun `bottom and rest`() { + assertThat( + BackStackScreen( + bottom = S(1), + rest = listOf(S(2), S(3), S(4)) + ) + ).isEqualTo(BackStackScreen(S(1), S(2), S(3), S(4))) + } + + @Test fun singleton() { + val stack = BackStackScreen(S("hi")) + assertThat(stack.top).isEqualTo(S("hi")) + assertThat(stack.frames).isEqualTo(listOf(S("hi"))) + assertThat(stack).isEqualTo(BackStackScreen(S("hi"))) + } + + @Test fun map() { + assertThat(BackStackScreen(S(1), S(2), S(3)).map { S(it.value * 2) }) + .isEqualTo(BackStackScreen(S(2), S(4), S(6))) + } + + @Test fun mapIndexed() { + val source = BackStackScreen(S("able"), S("baker"), S("charlie")) + assertThat(source.mapIndexed { index, frame -> S("$index: ${frame.value}") }) + .isEqualTo(BackStackScreen(S("0: able"), S("1: baker"), S("2: charlie"))) + } + + @Test fun nullFromEmptyList() { + assertThat(emptyList>().toBackStackScreenOrNull()).isNull() + } + + @Test fun throwFromEmptyList() { + assertFailsWith { emptyList>().toBackStackScreen() } + } + + @Test fun fromList() { + assertThat(listOf(S(1), S(2), S(3)).toBackStackScreen()) + .isEqualTo(BackStackScreen(S(1), S(2), S(3))) + } + + @Test fun fromListOrNull() { + assertThat(listOf(S(1), S(2), S(3)).toBackStackScreenOrNull()) + .isEqualTo(BackStackScreen(S(1), S(2), S(3))) + } +} diff --git a/workflow-ui/internal-testing-android/api/internal-testing-android.api b/workflow-ui/internal-testing-android/api/internal-testing-android.api index d820aa7654..df8d45249e 100644 --- a/workflow-ui/internal-testing-android/api/internal-testing-android.api +++ b/workflow-ui/internal-testing-android/api/internal-testing-android.api @@ -2,8 +2,8 @@ public abstract class com/squareup/workflow1/ui/internal/test/AbstractLifecycleT public fun ()V public final fun consumeLifecycleEvents ()Ljava/util/List; protected abstract fun getViewRegistry ()Lcom/squareup/workflow1/ui/ViewRegistry; - protected final fun leafViewBinding (Lkotlin/reflect/KClass;Lcom/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity$ViewObserver;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/ViewFactory; - public static synthetic fun leafViewBinding$default (Lcom/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity;Lkotlin/reflect/KClass;Lcom/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity$ViewObserver;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/ViewFactory; + protected final fun leafViewBinding (Lkotlin/reflect/KClass;Lcom/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity$ViewObserver;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/ScreenViewFactory; + public static synthetic fun leafViewBinding$default (Lcom/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity;Lkotlin/reflect/KClass;Lcom/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity$ViewObserver;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/ScreenViewFactory; protected final fun lifecycleLoggingViewObserver (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity$ViewObserver; protected final fun logEvent (Ljava/lang/String;)V protected fun onCreate (Landroid/os/Bundle;)V @@ -63,7 +63,7 @@ public class com/squareup/workflow1/ui/internal/test/WorkflowUiTestActivity : an protected fun onCreate (Landroid/os/Bundle;)V public final fun onRetainCustomNonConfigurationInstance ()Ljava/lang/Object; public final fun recreateViewsOnNextRendering ()V - public final fun setRendering (Ljava/lang/Object;)Landroid/view/View; + public final fun setRendering (Lcom/squareup/workflow1/ui/Screen;)Landroid/view/View; public final fun setRestoreRenderingAfterConfigChange (Z)V public final fun setViewEnvironment (Lcom/squareup/workflow1/ui/ViewEnvironment;)V } diff --git a/workflow-ui/internal-testing-android/src/main/java/com/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity.kt b/workflow-ui/internal-testing-android/src/main/java/com/squareup/workflow1/ui/internal/test/AbstractLifecycleTestActivity.kt index 891ad87b79..5e7115d128 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 @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package com.squareup.workflow1.ui.internal.test import android.content.Context @@ -9,9 +11,10 @@ import androidx.lifecycle.Lifecycle.Event import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewTreeLifecycleOwner -import com.squareup.workflow1.ui.BuilderViewFactory +import com.squareup.workflow1.ui.ManualScreenViewFactory +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.ScreenViewFactory import com.squareup.workflow1.ui.ViewEnvironment -import com.squareup.workflow1.ui.ViewFactory import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub @@ -26,8 +29,8 @@ import kotlin.reflect.KClass * test wants to use. Then call [consumeLifecycleEvents] to get a list of strings back that describe * what lifecycle-related events occurred since the last call. * - * Subclasses must override [viewRegistry] to specify the [ViewFactory]s they require. All views - * will be hosted inside a [WorkflowViewStub]. + * Subclasses must override [viewRegistry] to specify the [ScreenViewFactory]s they require. + * All views will be hosted inside a [WorkflowViewStub]. */ @WorkflowUiExperimentalApi public abstract class AbstractLifecycleTestActivity : WorkflowUiTestActivity() { @@ -86,13 +89,13 @@ public abstract class AbstractLifecycleTestActivity : WorkflowUiTestActivity() { lifecycleEvents += message } - protected fun leafViewBinding( + protected fun leafViewBinding( type: KClass, viewObserver: ViewObserver, viewConstructor: (Context) -> LeafView = ::LeafView - ): ViewFactory = - BuilderViewFactory(type) { initialRendering, initialViewEnvironment, contextForNewView, _ -> - viewConstructor(contextForNewView).apply { + ): ScreenViewFactory = + ManualScreenViewFactory(type) { initialRendering, initialViewEnvironment, context, _ -> + viewConstructor(context).apply { this.viewObserver = viewObserver viewObserver.onViewCreated(this, initialRendering) 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 71bd552c6d..70676137ae 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 @@ -1,9 +1,12 @@ +@file:Suppress("DEPRECATION") + package com.squareup.workflow1.ui.internal.test import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity -import com.squareup.workflow1.ui.Named +import com.squareup.workflow1.ui.NamedScreen +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub @@ -30,7 +33,7 @@ public open class WorkflowUiTestActivity : AppCompatActivity() { private val rootStub by lazy { WorkflowViewStub(this) } private var renderingCounter = 0 - private lateinit var lastRendering: Any + private lateinit var lastRendering: Screen /** * The [ViewEnvironment] used to create views for renderings passed to [setRendering]. @@ -72,7 +75,6 @@ public open class WorkflowUiTestActivity : AppCompatActivity() { super.onCreate(savedInstanceState) setContentView(rootStub) - @Suppress("DEPRECATION") (lastCustomNonConfigurationInstance as NonConfigurationData?)?.let { data -> viewEnvironment = data.viewEnvironment customNonConfigurationData.apply { @@ -96,18 +98,18 @@ public open class WorkflowUiTestActivity : AppCompatActivity() { * If [recreateViewsOnNextRendering] was previously called, the old view tree will be torn down * and re-created from scratch. */ - public fun setRendering(rendering: Any): View { + public fun setRendering(rendering: Screen): View { lastRendering = rendering - val named = Named( + val named = NamedScreen( wrapped = rendering, name = renderingCounter.toString() ) - return rootStub.update(named, viewEnvironment) + return rootStub.show(named, viewEnvironment) } private class NonConfigurationData( val viewEnvironment: ViewEnvironment, - val lastRendering: Any?, + val lastRendering: Screen?, val customData: MutableMap, ) }