Skip to content

Commit ece4923

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 ece4923

File tree

26 files changed

+323
-236
lines changed

26 files changed

+323
-236
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): PanelOverlay<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/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,8 @@ internal class ComposeViewTreeIntegrationTest {
570570
data class TestModal(
571571
override val content: Screen
572572
) : ScreenOverlay<Screen>, AndroidOverlay<TestModal> {
573+
override fun <ContentU : Screen> map(transform: (Screen) -> ContentU) = error("Not implemented")
574+
573575
override val dialogFactory = object : ScreenOverlayDialogFactory<Screen, TestModal>(
574576
TestModal::class
575577
) {

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/DialogIntegrationTest.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
1010
import com.google.common.truth.Truth.assertThat
1111
import com.squareup.workflow1.ui.AndroidScreen
1212
import com.squareup.workflow1.ui.Compatible
13+
import com.squareup.workflow1.ui.Screen
1314
import com.squareup.workflow1.ui.ScreenViewFactory
1415
import com.squareup.workflow1.ui.ScreenViewHolder
1516
import com.squareup.workflow1.ui.ViewEnvironment
@@ -47,7 +48,10 @@ internal class DialogIntegrationTest {
4748
name: String,
4849
override val content: ContentRendering
4950
) :
50-
Compatible, AndroidOverlay<DialogRendering>, ScreenOverlay<ContentRendering> {
51+
AndroidOverlay<DialogRendering>, ScreenOverlay<ContentRendering> {
52+
override fun <ContentU : Screen> map(transform: (ContentRendering) -> ContentU) =
53+
error("Not implemented")
54+
5155
override val compatibilityKey = name
5256
override val dialogFactory =
5357
object : ScreenOverlayDialogFactory<ContentRendering, DialogRendering>(

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 {

0 commit comments

Comments
 (0)