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 Mar 1, 2023
1 parent 1a7f4c5 commit ece4923
Show file tree
Hide file tree
Showing 26 changed files with 323 additions and 236 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): PanelOverlay<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
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,8 @@ internal class ComposeViewTreeIntegrationTest {
data class TestModal(
override val content: Screen
) : ScreenOverlay<Screen>, AndroidOverlay<TestModal> {
override fun <ContentU : Screen> map(transform: (Screen) -> ContentU) = error("Not implemented")

override val dialogFactory = object : ScreenOverlayDialogFactory<Screen, TestModal>(
TestModal::class
) {
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 @@ -10,6 +10,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import com.squareup.workflow1.ui.AndroidScreen
import com.squareup.workflow1.ui.Compatible
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ScreenViewFactory
import com.squareup.workflow1.ui.ScreenViewHolder
import com.squareup.workflow1.ui.ViewEnvironment
Expand Down Expand Up @@ -47,7 +48,10 @@ internal class DialogIntegrationTest {
name: String,
override val content: ContentRendering
) :
Compatible, AndroidOverlay<DialogRendering>, ScreenOverlay<ContentRendering> {
AndroidOverlay<DialogRendering>, ScreenOverlay<ContentRendering> {
override fun <ContentU : Screen> map(transform: (ContentRendering) -> ContentU) =
error("Not implemented")

override val compatibilityKey = name
override val dialogFactory =
object : ScreenOverlayDialogFactory<ContentRendering, DialogRendering>(
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
Loading

0 comments on commit ece4923

Please sign in to comment.