Skip to content

Commit db1005c

Browse files
committed
Container and Wrapper interfaces, improved forWrapper()
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
1 parent 1a7f4c5 commit db1005c

File tree

24 files changed

+314
-235
lines changed

24 files changed

+314
-235
lines changed

samples/containers/common/src/main/java/com/squareup/sample/container/panel/PanelOverlay.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import com.squareup.workflow1.ui.container.ModalOverlay
66
import com.squareup.workflow1.ui.container.ScreenOverlay
77

88
@OptIn(WorkflowUiExperimentalApi::class)
9-
class PanelOverlay<T : Screen>(
10-
override val content: T
11-
) : ScreenOverlay<T>, ModalOverlay
9+
class PanelOverlay<C : Screen>(
10+
override val content: C
11+
) : ScreenOverlay<C>, ModalOverlay {
12+
override fun <D : Screen> map(transform: (C) -> D): ScreenOverlay<D> =
13+
PanelOverlay(transform(content))
14+
}

samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,30 +29,29 @@ import com.squareup.workflow1.ui.container.AlertOverlay.Button.NEUTRAL
2929
import com.squareup.workflow1.ui.container.AlertOverlay.Button.POSITIVE
3030
import com.squareup.workflow1.ui.container.AlertOverlay.Event.ButtonClicked
3131
import com.squareup.workflow1.ui.container.AlertOverlay.Event.Canceled
32-
import com.squareup.workflow1.ui.container.ScreenOverlay
3332

3433
enum class RunGameResult {
3534
CanceledStart,
3635
FinishedPlaying
3736
}
3837

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

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

@@ -86,14 +85,12 @@ class RealRunGameWorkflow(
8685

8786
RunGameRendering(
8887
gameScreen = emptyGameScreen,
89-
namePrompt = object : ScreenOverlay<Screen> {
90-
override val content = NewGameScreen(
91-
renderState.defaultXName,
92-
renderState.defaultOName,
93-
onCancel = context.eventHandler { setOutput(CanceledStart) },
94-
onStartGame = context.eventHandler { x, o -> state = Playing(PlayerInfo(x, o)) }
95-
)
96-
}
88+
namePrompt = NewGameScreen(
89+
renderState.defaultXName,
90+
renderState.defaultOName,
91+
onCancel = context.eventHandler { setOutput(CanceledStart) },
92+
onStartGame = context.eventHandler { x, o -> state = Playing(PlayerInfo(x, o)) }
93+
)
9794
)
9895
}
9996

@@ -233,6 +230,7 @@ class RealRunGameWorkflow(
233230
NEGATIVE -> continuePlaying()
234231
NEUTRAL -> throw IllegalArgumentException()
235232
}
233+
236234
Canceled -> continuePlaying()
237235
}
238236
}

samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/TicTacToeWorkflow.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,7 @@ class TicTacToeWorkflow(
9191
// Authenticating state was allowed to die, so that this one will start fresh
9292
// in its logged out state.
9393
val stubAuthBackStack = context.renderChild(authWorkflow, "fake") { noAction() }
94-
val fullBackStack = stubAuthBackStack +
95-
BackStackScreen(gameRendering.namePrompt.content)
94+
val fullBackStack = stubAuthBackStack + BackStackScreen(gameRendering.namePrompt)
9695
val allModals = listOf(PanelOverlay(fullBackStack)) + gameRendering.alerts
9796

9897
BodyAndOverlaysScreen(gameRendering.gameScreen, allModals)

workflow-ui/core-android/api/core-android.api

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,13 +313,20 @@ public abstract interface class com/squareup/workflow1/ui/container/AndroidOverl
313313
public abstract fun getDialogFactory ()Lcom/squareup/workflow1/ui/container/OverlayDialogFactory;
314314
}
315315

316-
public final class com/squareup/workflow1/ui/container/BackButtonScreen : com/squareup/workflow1/ui/AndroidScreen {
316+
public final class com/squareup/workflow1/ui/container/BackButtonScreen : com/squareup/workflow1/ui/AndroidScreen, com/squareup/workflow1/ui/Wrapper {
317317
public fun <init> (Lcom/squareup/workflow1/ui/Screen;ZLkotlin/jvm/functions/Function0;)V
318318
public synthetic fun <init> (Lcom/squareup/workflow1/ui/Screen;ZLkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
319+
public fun asSequence ()Lkotlin/sequences/Sequence;
320+
public fun getCompatibilityKey ()Ljava/lang/String;
321+
public fun getContent ()Lcom/squareup/workflow1/ui/Screen;
322+
public synthetic fun getContent ()Ljava/lang/Object;
319323
public final fun getOnBackPressed ()Lkotlin/jvm/functions/Function0;
320324
public final fun getShadow ()Z
321325
public fun getViewFactory ()Lcom/squareup/workflow1/ui/ScreenViewFactory;
322326
public final fun getWrapped ()Lcom/squareup/workflow1/ui/Screen;
327+
public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container;
328+
public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Wrapper;
329+
public fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/container/BackButtonScreen;
323330
}
324331

325332
public final class com/squareup/workflow1/ui/container/BackStackConfig : java/lang/Enum {

workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/BackStackContainerTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ internal class BackStackContainerTest {
159159

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

164164
override fun performTransition(
165165
oldHolderMaybe: ScreenViewHolder<NamedScreen<*>>?,

workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/ViewStateCacheTest.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ internal class ViewStateCacheTest {
3333
private object AScreen : Screen
3434

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

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

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

134134
@Test fun doesnt_restore_state_when_view_id_not_set() {
135135
val cache = ViewStateCache()
136-
val firstRendering = NamedScreen(wrapped = AScreen, name = "first")
137-
val secondRendering = NamedScreen(wrapped = AScreen, name = "second")
136+
val firstRendering = NamedScreen(content = AScreen, name = "first")
137+
val secondRendering = NamedScreen(content = AScreen, name = "second")
138138
val firstView = createTestView(firstRendering)
139139
val secondView = createTestView(secondRendering)
140140

@@ -160,7 +160,7 @@ internal class ViewStateCacheTest {
160160

161161
@Test fun throws_on_duplicate_renderings() {
162162
val cache = ViewStateCache()
163-
val rendering = NamedScreen(wrapped = AScreen, name = "duplicate")
163+
val rendering = NamedScreen(content = AScreen, name = "duplicate")
164164
val view = createTestView(rendering)
165165

166166
try {

workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactory.kt

Lines changed: 71 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -139,107 +139,83 @@ public interface ScreenViewFactory<in ScreenT : Screen> : ViewRegistry.Entry<Scr
139139
}
140140

141141
/**
142-
* Creates a [ScreenViewFactory] for [WrapperT] that finds and delegates to the one for
143-
* [WrappedT]. Allows [WrapperT] to wrap instances of [WrappedT] to add information or behavior,
144-
* without requiring wasteful wrapping in the view system.
145-
*
146-
* One general note: when creating a wrapper rendering, you're very likely to want it to
147-
* implement [Compatible], to ensure that checks made to update or replace a view are based on
148-
* the wrapped item. Each wrapper example below illustrates this.
149-
*
150-
* This a simpler variant of the like named function that takes three arguments, for use when
151-
* there is no need to manipulate the [ScreenViewHolder].
142+
* Creates a [ScreenViewFactory] for type [WrapperT] that finds and delegates to the one for
143+
* [WrappedT]. Allows [WrapperT] to add information or behavior, without requiring wasteful
144+
* parallel wrapping in the view system.
152145
*
153146
* ## Examples
154147
*
155-
* To make one rendering type an "alias" for another -- that is, to use the same
156-
* [ScreenViewFactory] to display it:
157-
*
158-
* class RealScreen(val data: String): AndroidScreen<RealScreen> { override val viewFactory =
159-
* fromLayout<RealScreen>(...) }
160-
*
161-
* class AliasScreen(val similarData: String) : AndroidScreen<AliasScreen> {
162-
* override val viewFactory = forWrapper<AliasScreen, RealScreen> { aliasScreen ->
163-
* RealScreen(aliasScreen.similarData) } }
164-
*
165148
* To make one [Screen] type a wrapper for others:
166149
*
167-
* class Wrapper<W>(val wrapped: W: Screen) : AndroidScreen<Wrapper<W>>, Compatible {
168-
* override val compatibilityKey = Compatible.keyFor(wrapped) override val viewFactory =
169-
* ScreenViewFactory.forWrapper<Wrapper<W>, W> { it.wrapped } }
170-
*
171-
* To make a wrapper that adds information to the [ViewEnvironment]:
150+
* class MyWrapper<W : Screen>(
151+
* override val content: W
152+
* ) : AndroidScreen<Wrapper<W>>, Wrapper<Screen, W> {
153+
* override val viewFactory = forWrapper<MyWrapper<W>, W>()
172154
*
173-
* class ReverseNeutronFlowPolarity : ViewEnvironmentKey<Boolean>(Boolean::class) { override val
174-
* default = false }
155+
* override fun <U : Screen> map(transform: (W) -> U) =
156+
* MyWrapper(transform(content))
157+
* }
175158
*
176-
* class ReversePolarityScreen<W : Screen>( val wrapped: W ) :
177-
* AndroidScreen<ReversePolarityScreen<W>>, Compatible { override val compatibilityKey: String
178-
* = Compatible.keyFor(wrapped) override val viewFactory = forWrapper<OverrideNeutronFlow<W>,
179-
* Screen> { it.wrapped.withEnvironment( Environment.EMPTY + (ReverseNeutronFlowPolarity to
180-
* true) ) } }
159+
* To make a wrapper that customizes [View] initialization:
181160
*
182-
* @param unwrap a function to extract [WrappedT] instances from [WrapperT]s.
183-
*/
184-
@WorkflowUiExperimentalApi
185-
public inline fun <
186-
reified WrapperT : Screen,
187-
WrappedT : Screen
188-
> forWrapper(
189-
crossinline unwrap: (wrapperScreen: WrapperT) -> WrappedT,
190-
): ScreenViewFactory<WrapperT> = forWrapper(
191-
unwrap = unwrap,
192-
beforeShowing = {}
193-
) { _, wrapper, e, showWrapper ->
194-
showWrapper(unwrap(wrapper), e)
195-
}
196-
197-
/**
198-
* Creates a [ScreenViewFactory] for [WrapperT] that finds and delegates to the one for
199-
* [WrappedT]. Allows [WrapperT] to wrap instances of [WrappedT] to add information or behavior,
200-
* without requiring wasteful wrapping in the view system.
161+
* class WithTutorialTips<W : Screen>(
162+
* override val content: W
163+
* ) : AndroidScreen<WithTutorialTips<W>>, Wrapper<Screen, W> {
164+
* override val viewFactory = forWrapper<WithTutorialTips<W>, W>(
165+
* beforeShowing = { TutorialTipRunner.initialize(it.view) }
166+
* )
201167
*
202-
* This fully featured variant of the function is able to initialize the freshly created
203-
* [ScreenViewHolder], and transform the wrapped [ScreenViewHolder.runner].
168+
* override fun <U : Screen> map(transform: (W) -> U) =
169+
* WithTutorialTips(transform(content))
170+
* }
204171
*
205-
* To make a wrapper that customizes [View] initialization:
172+
* @param prepEnvironment a function to process the initial [ViewEnvironment]
173+
* before the [ScreenViewFactory] is fetched. Note that this function is not
174+
* applied on updates. Add a [showWrapperScreen] function if you need that.
206175
*
207-
* class WithTutorialTips<W : Screen>( val wrapped: W ) : AndroidScreen<WithTutorialTips<W>>,
208-
* Compatible { override val compatibilityKey = Compatible.keyFor(wrapped) override
209-
* val viewFactory = forWrapper<WithTutorialTips<W>, W>( unwrap = { it.wrapped },
210-
* beforeShowing = { TutorialTipRunner.initialize(it.view) }, showWrapperScreen = { _,
211-
* wrapper, environment, showWrapper -> showWrapper(unwrap(wrapper), environment) } ) }
176+
* @param prepContext a function to process the [Context] used to create each [View].
177+
* it is passed the product of [prepEnvironment]
212178
*
213179
* @param unwrap a function to extract [WrappedT] instances from [WrapperT]s.
180+
*
214181
* @param beforeShowing a function to be invoked immediately after a new [View] is built.
215-
* @param showWrapperScreen a function to be invoked when an instance of [WrapperT] needs to be
216-
* shown in a [View] built to display instances of [WrappedT]. Allows pre- and
217-
* post-processing of the [View].
182+
*
183+
* @param showWrapperScreen a function to be invoked when an instance of [WrapperT] needs
184+
* to be shown in a [View] built to display instances of [WrappedT]. Allows pre-
185+
* and post-processing of the [View].
218186
*/
219187
@WorkflowUiExperimentalApi
220-
public inline fun <
221-
reified WrapperT : Screen,
222-
WrappedT : Screen
223-
> forWrapper(
224-
crossinline unwrap: (wrapperScreen: WrapperT) -> WrappedT,
188+
public inline fun <reified WrapperT, WrappedT : Screen> forWrapper(
189+
crossinline prepEnvironment: (environment: ViewEnvironment) -> ViewEnvironment = { it },
190+
crossinline prepContext: (
191+
environment: ViewEnvironment,
192+
context: Context
193+
) -> Context = { _, c -> c },
194+
crossinline unwrap: (wrapperScreen: WrapperT) -> WrappedT = { it.content },
225195
crossinline beforeShowing: (viewHolder: ScreenViewHolder<WrapperT>) -> Unit = {},
226196
crossinline showWrapperScreen: (
227197
view: View,
228198
wrapperScreen: WrapperT,
229199
environment: ViewEnvironment,
230200
showUnwrappedScreen: (WrappedT, ViewEnvironment) -> Unit
231-
) -> Unit,
232-
): ScreenViewFactory<WrapperT> =
233-
fromCode { initialRendering, initialEnvironment, context, container ->
234-
val wrappedFactory = unwrap(initialRendering).toViewFactory(initialEnvironment)
235-
val wrapperFactory = wrappedFactory.toUnwrappingViewFactory(unwrap, showWrapperScreen)
236-
wrapperFactory.buildView(
237-
initialRendering,
238-
initialEnvironment,
239-
context,
240-
container
241-
).also { beforeShowing(it) }
201+
) -> Unit = { _, wrapper, e, showWrapper -> showWrapper(wrapper.content, e) },
202+
): ScreenViewFactory<WrapperT> where WrapperT : Screen, WrapperT : Wrapper<Screen, WrappedT> {
203+
return fromCode { initialRendering, initialEnvironment, context, container ->
204+
val preppedEnvironment = prepEnvironment(initialEnvironment)
205+
val wrappedFactory = unwrap(initialRendering).toViewFactory(preppedEnvironment)
206+
val wrapperFactory = wrappedFactory.toUnwrappingViewFactory(
207+
prepEnvironment,
208+
prepContext,
209+
unwrap,
210+
showWrapperScreen
211+
)
212+
213+
// Note that we give the factory the original initialEnvironment.
214+
// It applies prepEnvironment itself.
215+
wrapperFactory.buildView(initialRendering, initialEnvironment, context, container)
216+
.also { beforeShowing(it) }
242217
}
218+
}
243219
}
244220
}
245221

@@ -353,43 +329,34 @@ public fun interface ViewStarter {
353329
* @see [ScreenViewFactory.forWrapper].
354330
*/
355331
@WorkflowUiExperimentalApi
356-
public inline fun <
357-
reified WrapperT : Screen,
358-
WrappedT : Screen
359-
> ScreenViewFactory<WrappedT>.toUnwrappingViewFactory(
360-
crossinline unwrap: (wrapperScreen: WrapperT) -> WrappedT
361-
): ScreenViewFactory<WrapperT> {
362-
return toUnwrappingViewFactory(unwrap) { _, wrapperScreen, environment, showUnwrappedScreen ->
363-
showUnwrappedScreen(unwrap(wrapperScreen), environment)
364-
}
365-
}
366-
367-
/**
368-
* Transforms a [ScreenViewFactory] of [WrappedT] into one that can handle instances of [WrapperT].
369-
*
370-
* @see [ScreenViewFactory.forWrapper].
371-
*/
372-
@WorkflowUiExperimentalApi
373-
public inline fun <
374-
reified WrapperT : Screen,
375-
WrappedT : Screen
376-
> ScreenViewFactory<WrappedT>.toUnwrappingViewFactory(
377-
crossinline unwrap: (wrapperScreen: WrapperT) -> WrappedT,
332+
public inline fun <reified WrapperT, WrappedT> ScreenViewFactory<WrappedT>.toUnwrappingViewFactory(
333+
crossinline prepEnvironment: (environment: ViewEnvironment) -> ViewEnvironment = { e -> e },
334+
crossinline prepContext: (
335+
environment: ViewEnvironment,
336+
context: Context
337+
) -> Context = { _, c -> c },
338+
crossinline unwrap: (wrapperScreen: WrapperT) -> WrappedT = { it.content },
378339
crossinline showWrapperScreen: (
379340
view: View,
380341
wrapperScreen: WrapperT,
381342
environment: ViewEnvironment,
382343
showUnwrappedScreen: (WrappedT, ViewEnvironment) -> Unit
383-
) -> Unit
384-
): ScreenViewFactory<WrapperT> {
344+
) -> Unit = { _, wrapperScreen, environment, showUnwrappedScreen ->
345+
showUnwrappedScreen(wrapperScreen.content, environment)
346+
}
347+
): ScreenViewFactory<WrapperT>
348+
where WrapperT : Screen, WrapperT : Wrapper<Screen, WrappedT>, WrappedT : Screen {
385349
val wrappedFactory = this
386350

387351
return object : ScreenViewFactory<WrapperT> by fromCode(
388352
buildView = { initialRendering, initialEnvironment, context, container ->
353+
val preppedInitialEnvironment = prepEnvironment(initialEnvironment)
354+
val preppedContext = prepContext(preppedInitialEnvironment, context)
355+
389356
val wrappedHolder = wrappedFactory.buildView(
390357
unwrap(initialRendering),
391-
initialEnvironment,
392-
context,
358+
preppedInitialEnvironment,
359+
preppedContext,
393360
container
394361
)
395362

0 commit comments

Comments
 (0)