Skip to content

Commit

Permalink
Makes BackStackScreen<*> practical
Browse files Browse the repository at this point in the history
Adds a few crucial `out` declarations to various `Container` and `Wrapper` types, so that declaring `RenderingT` types like `BackStackScreen<*>` is practical. Also required moving `fun BackStackScreen.plus` out to be an extension function.

Without these changes, this does not compile:

```kotlin
    val foo = BackStackScreen(FooScreen(1))
    val bar = BackStackScreen(BarScreen(1))
    val both = foo + bar
```

Instead we were forced to do things like:

```kotlin
    val foo: BackStackScreen<Screen> = BackStackScreen(FooScreen(1))
    val bar: BackStackScreen<Screen> = BackStackScreen(BarScreen(1))
    val both = foo + bar
```

It gets really old really fast.
  • Loading branch information
rjrjr committed Oct 16, 2023
1 parent ad05052 commit d1c5c4a
Show file tree
Hide file tree
Showing 25 changed files with 254 additions and 169 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ typealias IsLoading = Boolean

@OptIn(WorkflowUiExperimentalApi::class)
class MaybeLoadingGatekeeperWorkflow<T : Any>(
private val childWithLoading: Workflow<T, Any, OverviewDetailScreen>,
private val childWithLoading: Workflow<T, Any, OverviewDetailScreen<*>>,
private val childProps: T,
private val isLoading: Flow<Boolean>
) : StatefulWorkflow<Unit, IsLoading, Unit, MayBeLoadingScreen>() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import com.squareup.workflow1.WorkflowAction
import com.squareup.workflow1.WorkflowAction.Companion.noAction
import com.squareup.workflow1.action
import com.squareup.workflow1.runningWorker
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.container.BackStackScreen
import com.squareup.workflow1.ui.container.toBackStackScreen
Expand Down Expand Up @@ -60,7 +59,7 @@ import kotlinx.coroutines.flow.flow
class PerformancePoemWorkflow(
private val simulatedPerfConfig: SimulatedPerfConfig = SimulatedPerfConfig.NO_SIMULATED_PERF,
private val isLoading: MutableStateFlow<Boolean>,
) : PoemWorkflow, StatefulWorkflow<Poem, State, ClosePoem, OverviewDetailScreen>() {
) : PoemWorkflow, StatefulWorkflow<Poem, State, ClosePoem, OverviewDetailScreen<*>>() {

sealed class State {
val isLoading: Boolean = false
Expand Down Expand Up @@ -100,7 +99,7 @@ class PerformancePoemWorkflow(
renderProps: Poem,
renderState: State,
context: RenderContext
): OverviewDetailScreen {
): OverviewDetailScreen<*> {
if (simulatedPerfConfig.simultaneousActions > 0) {
repeat(simulatedPerfConfig.simultaneousActions) { index ->
context.runningWorker(
Expand Down Expand Up @@ -222,7 +221,7 @@ class PerformancePoemWorkflow(
}

val stackedStanzas = visibleStanza?.let {
(previousStanzas + visibleStanza).toBackStackScreen<Screen>()
(previousStanzas + visibleStanza).toBackStackScreen()
}

val stanzaListOverview =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.squareup.benchmarks.performance.complex.poetry.instrumentation.Tracea
import com.squareup.benchmarks.performance.complex.poetry.instrumentation.asTraceableWorker
import com.squareup.benchmarks.performance.complex.poetry.views.BlankScreen
import com.squareup.sample.container.overviewdetail.OverviewDetailScreen
import com.squareup.sample.container.overviewdetail.plus
import com.squareup.sample.poetry.ConfigAndPoems
import com.squareup.sample.poetry.PoemListScreen.Companion.NO_POEM_SELECTED
import com.squareup.sample.poetry.PoemListWorkflow
Expand Down Expand Up @@ -51,7 +52,7 @@ class PerformancePoemsBrowserWorkflow(
private val isLoading: MutableStateFlow<Boolean>,
) :
PoemsBrowserWorkflow,
StatefulWorkflow<ConfigAndPoems, State, Unit, OverviewDetailScreen>() {
StatefulWorkflow<ConfigAndPoems, State, Unit, OverviewDetailScreen<*>>() {

sealed class State {
object Recurse : State()
Expand Down Expand Up @@ -88,7 +89,7 @@ class PerformancePoemsBrowserWorkflow(
renderProps: ConfigAndPoems,
renderState: State,
context: RenderContext
): OverviewDetailScreen {
): OverviewDetailScreen<*> {
when (renderState) {
is Recurse -> {
val recursiveChild = PerformancePoemsBrowserWorkflow(
Expand Down Expand Up @@ -188,13 +189,13 @@ class PerformancePoemsBrowserWorkflow(
}
}
}
var poems = OverviewDetailScreen(
var poems: OverviewDetailScreen<*> = OverviewDetailScreen(
overviewRendering = BackStackScreen(
poemListRendering.copy(selection = renderState.payload)
)
)
if (renderState.payload != NO_POEM_SELECTED) {
val poem: OverviewDetailScreen = context.renderChild(
val poem = context.renderChild(
poemWorkflow,
renderProps.second[renderState.payload]
) { clearSelection }
Expand All @@ -209,7 +210,7 @@ class PerformancePoemsBrowserWorkflow(
poemListRendering.copy(selection = renderState.poemIndex)
)
)
val poem: OverviewDetailScreen = context.renderChild(
val poem = context.renderChild(
poemWorkflow,
renderProps.second[renderState.poemIndex]
) { clearSelection }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import com.squareup.workflow1.ui.container.FullScreenModal

@OptIn(WorkflowUiExperimentalApi::class)
typealias MayBeLoadingScreen =
BodyAndOverlaysScreen<ScrimScreen<OverviewDetailScreen>, FullScreenModal<LoaderSpinner>>
BodyAndOverlaysScreen<ScrimScreen<OverviewDetailScreen<*>>, FullScreenModal<LoaderSpinner>>

@OptIn(WorkflowUiExperimentalApi::class)
fun MayBeLoadingScreen(
baseScreen: OverviewDetailScreen,
baseScreen: OverviewDetailScreen<*>,
loaders: List<LoaderSpinner> = emptyList()
): MayBeLoadingScreen {
return BodyAndOverlaysScreen(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.WorkflowViewStub
import com.squareup.workflow1.ui.container.BackStackScreen
import com.squareup.workflow1.ui.container.plus

/**
* Displays [OverviewDetailScreen] renderings in either split pane or single pane
Expand All @@ -25,7 +26,7 @@ import com.squareup.workflow1.ui.container.BackStackScreen
* with [OverviewDetailScreen.overviewRendering] as the base of the stack.
*/
@OptIn(WorkflowUiExperimentalApi::class)
class OverviewDetailContainer(view: View) : ScreenViewRunner<OverviewDetailScreen> {
class OverviewDetailContainer(view: View) : ScreenViewRunner<OverviewDetailScreen<*>> {

private val overviewStub: WorkflowViewStub? = view.findViewById(R.id.overview_stub)
private val detailStub: WorkflowViewStub? = view.findViewById(R.id.detail_stub)
Expand All @@ -42,7 +43,7 @@ class OverviewDetailContainer(view: View) : ScreenViewRunner<OverviewDetailScree
}

override fun showRendering(
rendering: OverviewDetailScreen,
rendering: OverviewDetailScreen<*>,
environment: ViewEnvironment
) {
if (singleStub == null) {
Expand All @@ -53,7 +54,7 @@ class OverviewDetailContainer(view: View) : ScreenViewRunner<OverviewDetailScree
}

private fun renderSplitView(
rendering: OverviewDetailScreen,
rendering: OverviewDetailScreen<*>,
viewEnvironment: ViewEnvironment
) {
if (rendering.detailRendering == null && rendering.selectDefault != null) {
Expand Down Expand Up @@ -81,7 +82,7 @@ class OverviewDetailContainer(view: View) : ScreenViewRunner<OverviewDetailScree
}

private fun renderSingleView(
rendering: OverviewDetailScreen,
rendering: OverviewDetailScreen<*>,
viewEnvironment: ViewEnvironment,
stub: WorkflowViewStub
) {
Expand All @@ -92,7 +93,7 @@ class OverviewDetailContainer(view: View) : ScreenViewRunner<OverviewDetailScree
stub.show(combined, viewEnvironment + Single)
}

companion object : ScreenViewFactory<OverviewDetailScreen> by ScreenViewFactory.fromLayout(
companion object : ScreenViewFactory<OverviewDetailScreen<*>> by ScreenViewFactory.fromLayout(
layoutId = R.layout.overview_detail,
constructor = ::OverviewDetailContainer
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import android.content.Context
import android.graphics.Rect
import androidx.appcompat.app.AppCompatDialog
import com.squareup.sample.container.R
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.container.OverlayDialogFactory
Expand All @@ -17,22 +16,22 @@ import kotlin.reflect.KClass
* Android support for [PanelOverlay].
*/
@OptIn(WorkflowUiExperimentalApi::class)
internal object PanelOverlayDialogFactory : OverlayDialogFactory<PanelOverlay<Screen>> {
override val type: KClass<in PanelOverlay<Screen>> = PanelOverlay::class
internal object PanelOverlayDialogFactory : OverlayDialogFactory<PanelOverlay<*>> {
override val type: KClass<in PanelOverlay<*>> = PanelOverlay::class

override fun buildDialog(
initialRendering: PanelOverlay<Screen>,
initialRendering: PanelOverlay<*>,
initialEnvironment: ViewEnvironment,
context: Context
): OverlayDialogHolder<PanelOverlay<Screen>> {
): OverlayDialogHolder<PanelOverlay<*>> {
val dialog = AppCompatDialog(context, R.style.PanelDialog)

val realHolder = dialog.asDialogHolderWithContent(initialRendering, initialEnvironment)

// We replace the default onUpdateBounds function with one that gives the
// panel a square shape on tablets. See OverlayDialogFactory for more details
// on the bounds mechanism.
return object : OverlayDialogHolder<PanelOverlay<Screen>> by realHolder {
return object : OverlayDialogHolder<PanelOverlay<*>> by realHolder {
override val onUpdateBounds: ((Rect) -> Unit) = { bounds ->
val refinedBounds: Rect = if (!dialog.context.isTablet) {
// On a phone, fill the bounds entirely.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.squareup.sample.container.overviewdetail
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.container.BackStackScreen
import com.squareup.workflow1.ui.container.plus

/**
* Rendering type for overview / detail containers, with [BackStackScreen] in both roles.
Expand All @@ -16,51 +17,33 @@ import com.squareup.workflow1.ui.container.BackStackScreen
* or a null [selectDefault]. **[selectDefault] cannot be a no-op.**
*/
@OptIn(WorkflowUiExperimentalApi::class)
class OverviewDetailScreen private constructor(
val overviewRendering: BackStackScreen<Screen>,
val detailRendering: BackStackScreen<Screen>? = null,
class OverviewDetailScreen<out T : Screen> private constructor(
val overviewRendering: BackStackScreen<T>,
val detailRendering: BackStackScreen<T>? = null,
val selectDefault: (() -> Unit)? = null
) : Screen {
constructor(
overviewRendering: BackStackScreen<Screen>,
detailRendering: BackStackScreen<Screen>
overviewRendering: BackStackScreen<T>,
detailRendering: BackStackScreen<T>
) : this(overviewRendering, detailRendering, null)

/**
* @param selectDefault optional function that a split view container may call to request
* that a selection be made to fill a null [detailRendering].
*/
constructor(
overviewRendering: BackStackScreen<Screen>,
overviewRendering: BackStackScreen<T>,
selectDefault: (() -> Unit)? = null
) : this(overviewRendering, null, selectDefault)

operator fun component1(): BackStackScreen<Screen> = overviewRendering
operator fun component2(): BackStackScreen<Screen>? = detailRendering

/**
* Returns a new [OverviewDetailScreen] appending the [overviewRendering] and
* [detailRendering] of [other] to those of the receiver. If the new screen's
* [detailRendering] is `null`, it will have the [selectDefault] function of [other].
*/
operator fun plus(other: OverviewDetailScreen): OverviewDetailScreen {
val newOverview = overviewRendering + other.overviewRendering
val newDetail = detailRendering
?.let { it + other.detailRendering }
?: other.detailRendering

return if (newDetail == null) {
OverviewDetailScreen(newOverview, other.selectDefault)
} else {
OverviewDetailScreen(newOverview, newDetail)
}
}
operator fun component1(): BackStackScreen<T> = overviewRendering
operator fun component2(): BackStackScreen<T>? = detailRendering

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as OverviewDetailScreen
other as OverviewDetailScreen<*>

return overviewRendering == other.overviewRendering &&
detailRendering == other.detailRendering &&
Expand All @@ -80,3 +63,26 @@ class OverviewDetailScreen private constructor(
"selectDefault=$selectDefault)"
}
}

/**
* Returns a new [OverviewDetailScreen] appending the
* [overviewRendering][OverviewDetailScreen.overviewRendering] and
* [detailRendering][OverviewDetailScreen.detailRendering] of [other] to those of the receiver.
* If the new screen's `detailRendering` is `null`, it will have the
* [selectDefault][OverviewDetailScreen.selectDefault] function of [other].
*/
@OptIn(WorkflowUiExperimentalApi::class)
operator fun <T : Screen> OverviewDetailScreen<T>.plus(
other: OverviewDetailScreen<T>
): OverviewDetailScreen<T> {
val newOverview = overviewRendering + other.overviewRendering
val newDetail = detailRendering
?.let { it + other.detailRendering }
?: other.detailRendering

return if (newDetail == null) {
OverviewDetailScreen(newOverview, other.selectDefault)
} else {
OverviewDetailScreen(newOverview, newDetail)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.squareup.workflow1.ui.container.ModalOverlay
import com.squareup.workflow1.ui.container.ScreenOverlay

@OptIn(WorkflowUiExperimentalApi::class)
class PanelOverlay<C : Screen>(
class PanelOverlay<out C : Screen>(
override val content: C
) : ScreenOverlay<C>, ModalOverlay {
override fun <D : Screen> map(transform: (C) -> D): PanelOverlay<D> =
Expand Down
Loading

0 comments on commit d1c5c4a

Please sign in to comment.