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 8, 2023
1 parent f750b5b commit 62b7c9a
Show file tree
Hide file tree
Showing 20 changed files with 245 additions and 211 deletions.
10 changes: 8 additions & 2 deletions workflow-ui/core-android/api/core-android.api
Original file line number Diff line number Diff line change
Expand Up @@ -312,13 +312,19 @@ 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,81 @@ 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)
) -> 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
initialRendering, initialEnvironment, context, container
).also { beforeShowing(it) }
}
}
}
}

Expand Down Expand Up @@ -353,44 +327,32 @@ 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,
container
unwrap(initialRendering), preppedInitialEnvironment, preppedContext, container
)

object : ScreenViewHolder<WrapperT> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import com.squareup.workflow1.ui.container.BackStackScreenViewFactory
import com.squareup.workflow1.ui.container.BodyAndOverlaysContainer
import com.squareup.workflow1.ui.container.BodyAndOverlaysScreen
import com.squareup.workflow1.ui.container.EnvironmentScreen
import com.squareup.workflow1.ui.container.EnvironmentScreenViewFactory

/**
* [ViewEnvironment] service object used by [Screen.toViewFactory] to find the right
Expand Down Expand Up @@ -71,10 +70,15 @@ public interface ScreenViewFactoryFinder {
BodyAndOverlaysContainer as ScreenViewFactory<ScreenT>
}
?: (rendering as? NamedScreen<*>)?.let {
forWrapper<NamedScreen<ScreenT>, ScreenT> { it.wrapped } as ScreenViewFactory<ScreenT>
forWrapper<NamedScreen<ScreenT>, ScreenT>() as ScreenViewFactory<ScreenT>
}
?: (rendering as? EnvironmentScreen<*>)?.let {
EnvironmentScreenViewFactory<ScreenT>() as ScreenViewFactory<ScreenT>
forWrapper<EnvironmentScreen<ScreenT>, ScreenT>(
prepEnvironment = { e -> e + rendering.environment },
showWrapperScreen = { _, envScreen, environment, showUnwrapped ->
showUnwrapped(envScreen.content, environment + envScreen.environment)
}
) as ScreenViewFactory<ScreenT>
}
?: throw IllegalArgumentException(
"A ScreenViewFactory should have been registered to display $rendering, " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ import com.squareup.workflow1.ui.AndroidScreen
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ScreenViewFactory
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.Wrapper
import com.squareup.workflow1.ui.backPressedHandler

/**
* Adds optional back button handling to a [wrapped] rendering, possibly overriding that
* Adds optional back button handling to a [content] rendering, possibly overriding that
* the wrapped rendering's own back button handler.
*
* @param shadow If `true`, [onBackPressed] is set as the
* [backPressedHandler][android.view.View.backPressedHandler] after
* the [wrapped] rendering's view is built / updated, effectively overriding it.
* the [content] rendering's view is built / updated, effectively overriding it.
* If false (the default), [onBackPressed] is set afterward, to allow the wrapped rendering to
* take precedence if it sets a `backPressedHandler` of its own -- the handler provided
* here serves as a default.
Expand All @@ -22,15 +23,16 @@ import com.squareup.workflow1.ui.backPressedHandler
* Defaults to `null`.
*/
@WorkflowUiExperimentalApi
public class BackButtonScreen<W : Screen>(
public val wrapped: W,
public class BackButtonScreen<C : Screen>(
public override val content: C,
public val shadow: Boolean = false,
public val onBackPressed: (() -> Unit)? = null
) : AndroidScreen<BackButtonScreen<W>> {
) : Wrapper<Screen, C>, AndroidScreen<BackButtonScreen<C>> {
override fun <U : Screen> map(transform: (C) -> U): BackButtonScreen<U> =
BackButtonScreen(transform(content), shadow, onBackPressed)

override val viewFactory: ScreenViewFactory<BackButtonScreen<W>> =
override val viewFactory: ScreenViewFactory<BackButtonScreen<C>> =
ScreenViewFactory.forWrapper(
unwrap = { it.wrapped },
showWrapperScreen = { view, backButtonScreen, env, showUnwrapped ->
if (!backButtonScreen.shadow) {
// Place our handler before invoking innerShowRendering, so that
Expand All @@ -39,8 +41,8 @@ public class BackButtonScreen<W : Screen>(
view.backPressedHandler = backButtonScreen.onBackPressed
}

// Show the wrapped Screen.
showUnwrapped(backButtonScreen.wrapped, env)
// Show the content Screen.
showUnwrapped(backButtonScreen.content, env)

if (backButtonScreen.shadow) {
// Place our handler after invoking innerShowRendering, so that ours wins.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ internal object EnvironmentScreenLegacyViewFactory : ViewFactory<EnvironmentScre
by DecorativeViewFactory(
type = EnvironmentScreen::class,
map = { environmentScreen, inheritedEnvironment ->
Pair(environmentScreen.wrapped, environmentScreen.environment + inheritedEnvironment)
Pair(environmentScreen.content, environmentScreen.environment + inheritedEnvironment)
}
)
Loading

0 comments on commit 62b7c9a

Please sign in to comment.