Skip to content

Commit

Permalink
Container and Wrapper interfaces, improved forWrapper()
Browse files Browse the repository at this point in the history
Introduces the `Container` and `Wrapper` types, giving Workflow UI its first
general notion of structure. Their integration with `Compatibile` reduces the
likelihood of making the most common mistake with wrapper types (namely,
forgetting to do that). And they standardize the `map()` function that
gets implemented by wrappers more often than not.

Also updates `forWrapper` and `toUnwrappingViewFactory` to be defined in terms
of `Wrapper`, allowing us to simplify their APIs by relying on
`Wrapper.content` in default lambda implementations.

And while we're in the neighborhood, adds long needed `prepEnvironment` and
`prepContext` function arguments that simplify transforming a
`ScreenViewFactory` to pre-process its `ViewEnvironment` and `Context`. We
use this new capability to simplify the default `ScreenViewFactory`
implementation for `EnvironmentScreen`.

Closes #916
  • Loading branch information
rjrjr committed Feb 28, 2023
1 parent 1a7f4c5 commit db1005c
Show file tree
Hide file tree
Showing 24 changed files with 314 additions and 235 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import com.squareup.workflow1.ui.container.ModalOverlay
import com.squareup.workflow1.ui.container.ScreenOverlay

@OptIn(WorkflowUiExperimentalApi::class)
class PanelOverlay<T : Screen>(
override val content: T
) : ScreenOverlay<T>, ModalOverlay
class PanelOverlay<C : Screen>(
override val content: C
) : ScreenOverlay<C>, ModalOverlay {
override fun <D : Screen> map(transform: (C) -> D): ScreenOverlay<D> =
PanelOverlay(transform(content))
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,30 +29,29 @@ import com.squareup.workflow1.ui.container.AlertOverlay.Button.NEUTRAL
import com.squareup.workflow1.ui.container.AlertOverlay.Button.POSITIVE
import com.squareup.workflow1.ui.container.AlertOverlay.Event.ButtonClicked
import com.squareup.workflow1.ui.container.AlertOverlay.Event.Canceled
import com.squareup.workflow1.ui.container.ScreenOverlay

enum class RunGameResult {
CanceledStart,
FinishedPlaying
}

/**
* This workflow renders up to three layers. There is always a [gameScreen], which
* may be covered by a [namePrompt] and [alerts]. By declaring our rendering shape
* this explicitly, we give parent workflows just enough information to recompose,
* without leaking details about every single type of screen we render.
* This workflow renders in up to three parts, whose display a parent is responsible for
* managing. There is always a [gameScreen], which may be augmented by a [namePrompt]
* and [alerts]. By declaring our rendering shape this explicitly, we give parent workflows
* just enough information to recompose, without leaking details about every single type
* of screen we render.
*/
data class RunGameRendering(
val gameScreen: Screen,
val namePrompt: ScreenOverlay<*>? = null,
val namePrompt: Screen? = null,
val alerts: List<AlertOverlay> = emptyList()
)

/**
* We define this otherwise redundant typealias to keep composite workflows
* that build on [RunGameWorkflow] decoupled from it, for ease of testing.
*/
@OptIn(WorkflowUiExperimentalApi::class)
typealias RunGameWorkflow =
Workflow<Unit, RunGameResult, RunGameRendering>

Expand Down Expand Up @@ -86,14 +85,12 @@ class RealRunGameWorkflow(

RunGameRendering(
gameScreen = emptyGameScreen,
namePrompt = object : ScreenOverlay<Screen> {
override val content = NewGameScreen(
renderState.defaultXName,
renderState.defaultOName,
onCancel = context.eventHandler { setOutput(CanceledStart) },
onStartGame = context.eventHandler { x, o -> state = Playing(PlayerInfo(x, o)) }
)
}
namePrompt = NewGameScreen(
renderState.defaultXName,
renderState.defaultOName,
onCancel = context.eventHandler { setOutput(CanceledStart) },
onStartGame = context.eventHandler { x, o -> state = Playing(PlayerInfo(x, o)) }
)
)
}

Expand Down Expand Up @@ -233,6 +230,7 @@ class RealRunGameWorkflow(
NEGATIVE -> continuePlaying()
NEUTRAL -> throw IllegalArgumentException()
}

Canceled -> continuePlaying()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,7 @@ class TicTacToeWorkflow(
// Authenticating state was allowed to die, so that this one will start fresh
// in its logged out state.
val stubAuthBackStack = context.renderChild(authWorkflow, "fake") { noAction() }
val fullBackStack = stubAuthBackStack +
BackStackScreen(gameRendering.namePrompt.content)
val fullBackStack = stubAuthBackStack + BackStackScreen(gameRendering.namePrompt)
val allModals = listOf(PanelOverlay(fullBackStack)) + gameRendering.alerts

BodyAndOverlaysScreen(gameRendering.gameScreen, allModals)
Expand Down
9 changes: 8 additions & 1 deletion workflow-ui/core-android/api/core-android.api
Original file line number Diff line number Diff line change
Expand Up @@ -313,13 +313,20 @@ public abstract interface class com/squareup/workflow1/ui/container/AndroidOverl
public abstract fun getDialogFactory ()Lcom/squareup/workflow1/ui/container/OverlayDialogFactory;
}

public final class com/squareup/workflow1/ui/container/BackButtonScreen : com/squareup/workflow1/ui/AndroidScreen {
public final class com/squareup/workflow1/ui/container/BackButtonScreen : com/squareup/workflow1/ui/AndroidScreen, com/squareup/workflow1/ui/Wrapper {
public fun <init> (Lcom/squareup/workflow1/ui/Screen;ZLkotlin/jvm/functions/Function0;)V
public synthetic fun <init> (Lcom/squareup/workflow1/ui/Screen;ZLkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun asSequence ()Lkotlin/sequences/Sequence;
public fun getCompatibilityKey ()Ljava/lang/String;
public fun getContent ()Lcom/squareup/workflow1/ui/Screen;
public synthetic fun getContent ()Ljava/lang/Object;
public final fun getOnBackPressed ()Lkotlin/jvm/functions/Function0;
public final fun getShadow ()Z
public fun getViewFactory ()Lcom/squareup/workflow1/ui/ScreenViewFactory;
public final fun getWrapped ()Lcom/squareup/workflow1/ui/Screen;
public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container;
public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Wrapper;
public fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/container/BackButtonScreen;
}

public final class com/squareup/workflow1/ui/container/BackStackConfig : java/lang/Enum {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ internal class BackStackContainerTest {

@Suppress("UNCHECKED_CAST")
val visibleRendering: Screen
get() = (getChildAt(0)?.tag as NamedScreen<*>).wrapped
get() = (getChildAt(0)?.tag as NamedScreen<*>).content

override fun performTransition(
oldHolderMaybe: ScreenViewHolder<NamedScreen<*>>?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ internal class ViewStateCacheTest {
private object AScreen : Screen

@Test fun saves_and_restores_self() {
val rendering = NamedScreen(wrapped = AScreen, name = "rendering")
val rendering = NamedScreen(content = AScreen, name = "rendering")
val childState = SparseArray<Parcelable>().apply {
put(0, TestChildState("hello world"))
}
Expand All @@ -58,8 +58,8 @@ internal class ViewStateCacheTest {

@Test fun saves_and_restores_child_states_on_navigation() {
val cache = ViewStateCache()
val firstRendering = NamedScreen(wrapped = AScreen, name = "first")
val secondRendering = NamedScreen(wrapped = AScreen, name = "second")
val firstRendering = NamedScreen(content = AScreen, name = "first")
val secondRendering = NamedScreen(content = AScreen, name = "second")
// Android requires ID to be set for view hierarchy to be saved or restored.
val firstView = createTestView(firstRendering, id = 1)
val secondView = createTestView(secondRendering)
Expand Down Expand Up @@ -90,8 +90,8 @@ internal class ViewStateCacheTest {

@Test fun doesnt_restore_state_when_restored_view_id_is_different() {
val cache = ViewStateCache()
val firstRendering = NamedScreen(wrapped = AScreen, name = "first")
val secondRendering = NamedScreen(wrapped = AScreen, name = "second")
val firstRendering = NamedScreen(content = AScreen, name = "first")
val secondRendering = NamedScreen(content = AScreen, name = "second")
// Android requires ID to be set for view hierarchy to be saved or restored.
val firstView = createTestView(firstRendering, id = 1)
val secondView = createTestView(secondRendering)
Expand Down Expand Up @@ -133,8 +133,8 @@ internal class ViewStateCacheTest {

@Test fun doesnt_restore_state_when_view_id_not_set() {
val cache = ViewStateCache()
val firstRendering = NamedScreen(wrapped = AScreen, name = "first")
val secondRendering = NamedScreen(wrapped = AScreen, name = "second")
val firstRendering = NamedScreen(content = AScreen, name = "first")
val secondRendering = NamedScreen(content = AScreen, name = "second")
val firstView = createTestView(firstRendering)
val secondView = createTestView(secondRendering)

Expand All @@ -160,7 +160,7 @@ internal class ViewStateCacheTest {

@Test fun throws_on_duplicate_renderings() {
val cache = ViewStateCache()
val rendering = NamedScreen(wrapped = AScreen, name = "duplicate")
val rendering = NamedScreen(content = AScreen, name = "duplicate")
val view = createTestView(rendering)

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,107 +139,83 @@ public interface ScreenViewFactory<in ScreenT : Screen> : ViewRegistry.Entry<Scr
}

/**
* Creates a [ScreenViewFactory] for [WrapperT] that finds and delegates to the one for
* [WrappedT]. Allows [WrapperT] to wrap instances of [WrappedT] to add information or behavior,
* without requiring wasteful wrapping in the view system.
*
* One general note: when creating a wrapper rendering, you're very likely to want it to
* implement [Compatible], to ensure that checks made to update or replace a view are based on
* the wrapped item. Each wrapper example below illustrates this.
*
* This a simpler variant of the like named function that takes three arguments, for use when
* there is no need to manipulate the [ScreenViewHolder].
* Creates a [ScreenViewFactory] for type [WrapperT] that finds and delegates to the one for
* [WrappedT]. Allows [WrapperT] to add information or behavior, without requiring wasteful
* parallel wrapping in the view system.
*
* ## Examples
*
* To make one rendering type an "alias" for another -- that is, to use the same
* [ScreenViewFactory] to display it:
*
* class RealScreen(val data: String): AndroidScreen<RealScreen> { override val viewFactory =
* fromLayout<RealScreen>(...) }
*
* class AliasScreen(val similarData: String) : AndroidScreen<AliasScreen> {
* override val viewFactory = forWrapper<AliasScreen, RealScreen> { aliasScreen ->
* RealScreen(aliasScreen.similarData) } }
*
* To make one [Screen] type a wrapper for others:
*
* class Wrapper<W>(val wrapped: W: Screen) : AndroidScreen<Wrapper<W>>, Compatible {
* override val compatibilityKey = Compatible.keyFor(wrapped) override val viewFactory =
* ScreenViewFactory.forWrapper<Wrapper<W>, W> { it.wrapped } }
*
* To make a wrapper that adds information to the [ViewEnvironment]:
* class MyWrapper<W : Screen>(
* override val content: W
* ) : AndroidScreen<Wrapper<W>>, Wrapper<Screen, W> {
* override val viewFactory = forWrapper<MyWrapper<W>, W>()
*
* class ReverseNeutronFlowPolarity : ViewEnvironmentKey<Boolean>(Boolean::class) { override val
* default = false }
* override fun <U : Screen> map(transform: (W) -> U) =
* MyWrapper(transform(content))
* }
*
* class ReversePolarityScreen<W : Screen>( val wrapped: W ) :
* AndroidScreen<ReversePolarityScreen<W>>, Compatible { override val compatibilityKey: String
* = Compatible.keyFor(wrapped) override val viewFactory = forWrapper<OverrideNeutronFlow<W>,
* Screen> { it.wrapped.withEnvironment( Environment.EMPTY + (ReverseNeutronFlowPolarity to
* true) ) } }
* To make a wrapper that customizes [View] initialization:
*
* @param unwrap a function to extract [WrappedT] instances from [WrapperT]s.
*/
@WorkflowUiExperimentalApi
public inline fun <
reified WrapperT : Screen,
WrappedT : Screen
> forWrapper(
crossinline unwrap: (wrapperScreen: WrapperT) -> WrappedT,
): ScreenViewFactory<WrapperT> = forWrapper(
unwrap = unwrap,
beforeShowing = {}
) { _, wrapper, e, showWrapper ->
showWrapper(unwrap(wrapper), e)
}

/**
* Creates a [ScreenViewFactory] for [WrapperT] that finds and delegates to the one for
* [WrappedT]. Allows [WrapperT] to wrap instances of [WrappedT] to add information or behavior,
* without requiring wasteful wrapping in the view system.
* class WithTutorialTips<W : Screen>(
* override val content: W
* ) : AndroidScreen<WithTutorialTips<W>>, Wrapper<Screen, W> {
* override val viewFactory = forWrapper<WithTutorialTips<W>, W>(
* beforeShowing = { TutorialTipRunner.initialize(it.view) }
* )
*
* This fully featured variant of the function is able to initialize the freshly created
* [ScreenViewHolder], and transform the wrapped [ScreenViewHolder.runner].
* override fun <U : Screen> map(transform: (W) -> U) =
* WithTutorialTips(transform(content))
* }
*
* To make a wrapper that customizes [View] initialization:
* @param prepEnvironment a function to process the initial [ViewEnvironment]
* before the [ScreenViewFactory] is fetched. Note that this function is not
* applied on updates. Add a [showWrapperScreen] function if you need that.
*
* class WithTutorialTips<W : Screen>( val wrapped: W ) : AndroidScreen<WithTutorialTips<W>>,
* Compatible { override val compatibilityKey = Compatible.keyFor(wrapped) override
* val viewFactory = forWrapper<WithTutorialTips<W>, W>( unwrap = { it.wrapped },
* beforeShowing = { TutorialTipRunner.initialize(it.view) }, showWrapperScreen = { _,
* wrapper, environment, showWrapper -> showWrapper(unwrap(wrapper), environment) } ) }
* @param prepContext a function to process the [Context] used to create each [View].
* it is passed the product of [prepEnvironment]
*
* @param unwrap a function to extract [WrappedT] instances from [WrapperT]s.
*
* @param beforeShowing a function to be invoked immediately after a new [View] is built.
* @param showWrapperScreen a function to be invoked when an instance of [WrapperT] needs to be
* shown in a [View] built to display instances of [WrappedT]. Allows pre- and
* post-processing of the [View].
*
* @param showWrapperScreen a function to be invoked when an instance of [WrapperT] needs
* to be shown in a [View] built to display instances of [WrappedT]. Allows pre-
* and post-processing of the [View].
*/
@WorkflowUiExperimentalApi
public inline fun <
reified WrapperT : Screen,
WrappedT : Screen
> forWrapper(
crossinline unwrap: (wrapperScreen: WrapperT) -> WrappedT,
public inline fun <reified WrapperT, WrappedT : Screen> forWrapper(
crossinline prepEnvironment: (environment: ViewEnvironment) -> ViewEnvironment = { it },
crossinline prepContext: (
environment: ViewEnvironment,
context: Context
) -> Context = { _, c -> c },
crossinline unwrap: (wrapperScreen: WrapperT) -> WrappedT = { it.content },
crossinline beforeShowing: (viewHolder: ScreenViewHolder<WrapperT>) -> Unit = {},
crossinline showWrapperScreen: (
view: View,
wrapperScreen: WrapperT,
environment: ViewEnvironment,
showUnwrappedScreen: (WrappedT, ViewEnvironment) -> Unit
) -> Unit,
): ScreenViewFactory<WrapperT> =
fromCode { initialRendering, initialEnvironment, context, container ->
val wrappedFactory = unwrap(initialRendering).toViewFactory(initialEnvironment)
val wrapperFactory = wrappedFactory.toUnwrappingViewFactory(unwrap, showWrapperScreen)
wrapperFactory.buildView(
initialRendering,
initialEnvironment,
context,
container
).also { beforeShowing(it) }
) -> Unit = { _, wrapper, e, showWrapper -> showWrapper(wrapper.content, e) },
): ScreenViewFactory<WrapperT> where WrapperT : Screen, WrapperT : Wrapper<Screen, WrappedT> {
return fromCode { initialRendering, initialEnvironment, context, container ->
val preppedEnvironment = prepEnvironment(initialEnvironment)
val wrappedFactory = unwrap(initialRendering).toViewFactory(preppedEnvironment)
val wrapperFactory = wrappedFactory.toUnwrappingViewFactory(
prepEnvironment,
prepContext,
unwrap,
showWrapperScreen
)

// Note that we give the factory the original initialEnvironment.
// It applies prepEnvironment itself.
wrapperFactory.buildView(initialRendering, initialEnvironment, context, container)
.also { beforeShowing(it) }
}
}
}
}

Expand Down Expand Up @@ -353,43 +329,34 @@ public fun interface ViewStarter {
* @see [ScreenViewFactory.forWrapper].
*/
@WorkflowUiExperimentalApi
public inline fun <
reified WrapperT : Screen,
WrappedT : Screen
> ScreenViewFactory<WrappedT>.toUnwrappingViewFactory(
crossinline unwrap: (wrapperScreen: WrapperT) -> WrappedT
): ScreenViewFactory<WrapperT> {
return toUnwrappingViewFactory(unwrap) { _, wrapperScreen, environment, showUnwrappedScreen ->
showUnwrappedScreen(unwrap(wrapperScreen), environment)
}
}

/**
* Transforms a [ScreenViewFactory] of [WrappedT] into one that can handle instances of [WrapperT].
*
* @see [ScreenViewFactory.forWrapper].
*/
@WorkflowUiExperimentalApi
public inline fun <
reified WrapperT : Screen,
WrappedT : Screen
> ScreenViewFactory<WrappedT>.toUnwrappingViewFactory(
crossinline unwrap: (wrapperScreen: WrapperT) -> WrappedT,
public inline fun <reified WrapperT, WrappedT> ScreenViewFactory<WrappedT>.toUnwrappingViewFactory(
crossinline prepEnvironment: (environment: ViewEnvironment) -> ViewEnvironment = { e -> e },
crossinline prepContext: (
environment: ViewEnvironment,
context: Context
) -> Context = { _, c -> c },
crossinline unwrap: (wrapperScreen: WrapperT) -> WrappedT = { it.content },
crossinline showWrapperScreen: (
view: View,
wrapperScreen: WrapperT,
environment: ViewEnvironment,
showUnwrappedScreen: (WrappedT, ViewEnvironment) -> Unit
) -> Unit
): ScreenViewFactory<WrapperT> {
) -> Unit = { _, wrapperScreen, environment, showUnwrappedScreen ->
showUnwrappedScreen(wrapperScreen.content, environment)
}
): ScreenViewFactory<WrapperT>
where WrapperT : Screen, WrapperT : Wrapper<Screen, WrappedT>, WrappedT : Screen {
val wrappedFactory = this

return object : ScreenViewFactory<WrapperT> by fromCode(
buildView = { initialRendering, initialEnvironment, context, container ->
val preppedInitialEnvironment = prepEnvironment(initialEnvironment)
val preppedContext = prepContext(preppedInitialEnvironment, context)

val wrappedHolder = wrappedFactory.buildView(
unwrap(initialRendering),
initialEnvironment,
context,
preppedInitialEnvironment,
preppedContext,
container
)

Expand Down
Loading

0 comments on commit db1005c

Please sign in to comment.