Skip to content

Commit

Permalink
Merge pull request #1121 from square/ray/tighten-up-backstack-types
Browse files Browse the repository at this point in the history
Makes `BackStackScreen<*>` practical
  • Loading branch information
rjrjr authored Oct 17, 2023
2 parents ad05052 + b1c9a12 commit d352bb1
Show file tree
Hide file tree
Showing 26 changed files with 256 additions and 171 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 d352bb1

Please sign in to comment.