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 d1439ac commit f523346
Show file tree
Hide file tree
Showing 21 changed files with 293 additions and 217 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))
}
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,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 <D : Screen> map(transform: (C) -> D): BackButtonScreen<D> =
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,13 +41,16 @@ 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.
view.backPressedHandler = backButtonScreen.onBackPressed
}
}
)

@Deprecated("Use content", ReplaceWith("content"))
public val wrapped: C = content
}
Loading

0 comments on commit f523346

Please sign in to comment.