Skip to content

Commit

Permalink
WIP dual backstack
Browse files Browse the repository at this point in the history
  • Loading branch information
LachlanMcKee committed Sep 25, 2022
1 parent 75fe923 commit be19efe
Show file tree
Hide file tree
Showing 33 changed files with 1,929 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ import com.bumble.appyx.core.state.SavedStateMap
import com.bumble.appyx.navmodel.backstack.BackStack.State.ACTIVE

class BackStack<NavTarget : Any>(
initialElement: NavTarget,
initialElement: NavTarget?,
savedStateMap: SavedStateMap?,
key: String = KEY_NAV_MODEL,
backPressHandler: BackPressHandlerStrategy<NavTarget, State> = PopBackPressHandler(),
val emptyBackStackAllowed: Boolean = false,
backPressHandler: BackPressHandlerStrategy<NavTarget, State> = PopBackPressHandler(
emptyBackStackAllowed
),
operationStrategy: OperationStrategy<NavTarget, State> = ExecuteImmediately(),
screenResolver: OnScreenStateResolver<State> = BackStackOnScreenResolver
) : BaseNavModel<NavTarget, State>(
Expand All @@ -28,18 +31,25 @@ class BackStack<NavTarget : Any>(
savedStateMap = savedStateMap,
key = key,
) {
init {
check(initialElement != null || emptyBackStackAllowed) {
"No initial element and emptyBackStackAllowed=false"
}
}

enum class State {
CREATED, ACTIVE, STASHED, DESTROYED,
}

override val initialElements = listOf(
BackStackElement(
key = NavKey(initialElement),
fromState = ACTIVE,
targetState = ACTIVE,
operation = Noop()
)
override val initialElements = listOfNotNull(
initialElement?.let { element ->
BackStackElement(
key = NavKey(element),
fromState = ACTIVE,
targetState = ACTIVE,
operation = Noop()
)
}
)

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import com.bumble.appyx.navmodel.backstack.operation.Pop
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

class PopBackPressHandler<NavTarget : Any> :
class PopBackPressHandler<NavTarget : Any>(private val emptyBackStackAllowed: Boolean) :
BaseBackPressHandlerStrategy<NavTarget, BackStack.State>() {

override val canHandleBackPressFlow: Flow<Boolean> by lazy {
Expand All @@ -18,6 +18,6 @@ class PopBackPressHandler<NavTarget : Any> :
elements.any { it.targetState == BackStack.State.STASHED }

override fun onBackPressed() {
navModel.accept(Pop())
navModel.accept(Pop(emptyBackStackAllowed))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import kotlinx.parcelize.Parcelize
* [A, B, C] + Pop = [A, B]
*/
@Parcelize
class Pop<T : Any> : BackStackOperation<T> {
class Pop<T : Any>(private val emptyBackStackAllowed: Boolean) : BackStackOperation<T> {

override fun isApplicable(elements: BackStackElements<T>): Boolean =
elements.any { it.targetState == BackStack.State.ACTIVE } &&
elements.any { it.targetState == BackStack.State.STASHED }
emptyBackStackAllowed || elements.any { it.targetState == BackStack.State.STASHED }

override fun invoke(
elements: BackStackElements<T>
Expand All @@ -25,7 +25,9 @@ class Pop<T : Any> : BackStackOperation<T> {
val unStashIndex =
elements.indexOfLast { it.targetState == BackStack.State.STASHED }
require(destroyIndex != -1) { "Nothing to destroy, state=$elements" }
require(unStashIndex != -1) { "Nothing to remove from stash, state=$elements" }
if (!emptyBackStackAllowed) {
require(unStashIndex != -1) { "Nothing to remove from stash, state=$elements" }
}
return elements.mapIndexed { index, element ->
when (index) {
destroyIndex -> element.transitionTo(
Expand All @@ -47,5 +49,5 @@ class Pop<T : Any> : BackStackOperation<T> {
}

fun <T : Any> BackStack<T>.pop() {
accept(Pop())
accept(Pop(emptyBackStackAllowed))
}
9 changes: 9 additions & 0 deletions navmodel-samples/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ android {
composeOptions {
kotlinCompilerExtensionVersion = libs.versions.compose.get()
}
testOptions {
unitTests.all {
it.useJUnitPlatform()
}
}
}

dependencies {
Expand All @@ -39,4 +44,8 @@ dependencies {
implementation(libs.compose.ui.tooling)
implementation(libs.compose.foundation.layout)
implementation(libs.compose.foundation)

testImplementation(libs.junit.api)
testImplementation(libs.junit.params)
testRuntimeOnly(libs.junit.engine)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.bumble.appyx.navmodel.dualbackstack

import android.os.Parcelable
import com.bumble.appyx.core.navigation.BaseNavModel
import com.bumble.appyx.core.navigation.NavKey
import com.bumble.appyx.core.navigation.Operation.Noop
import com.bumble.appyx.core.navigation.onscreen.OnScreenStateResolver
import com.bumble.appyx.core.navigation.operationstrategies.ExecuteImmediately
import com.bumble.appyx.core.navigation.operationstrategies.OperationStrategy
import com.bumble.appyx.core.state.SavedStateMap
import com.bumble.appyx.navmodel.dualbackstack.DualBackStack.State
import com.bumble.appyx.navmodel.dualbackstack.DualBackStack.State.Active1
import com.bumble.appyx.navmodel.dualbackstack.DualBackStack.State.Active2
import com.bumble.appyx.navmodel.dualbackstack.DualBackStack.State.Destroyed1
import com.bumble.appyx.navmodel.dualbackstack.DualBackStack.State.Destroyed2
import com.bumble.appyx.navmodel.dualbackstack.DualBackStack.State.StashedInBackStack1
import com.bumble.appyx.navmodel.dualbackstack.backpresshandler.DualBackPressHandlerStrategy
import com.bumble.appyx.navmodel.dualbackstack.backpresshandler.PopBackPressHandler
import com.bumble.appyx.navmodel.dualbackstack.operation.panelModeChanged
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize

class DualBackStack<NavTarget : Any>(
leftElement: NavTarget,
rightElement: NavTarget? = null,
savedStateMap: SavedStateMap?,
private val showTwoPanelsFlow: StateFlow<Boolean>,
key: String = KEY_NAV_MODEL,
backPressHandler: DualBackPressHandlerStrategy<NavTarget> = PopBackPressHandler(),
operationStrategy: OperationStrategy<NavTarget, State> = ExecuteImmediately(),
screenResolver: OnScreenStateResolver<State> = DualBackStackOnScreenResolver
) : BaseNavModel<NavTarget, State>(
backPressHandler = backPressHandler,
screenResolver = screenResolver,
operationStrategy = operationStrategy,
finalStates = setOf(Destroyed1, Destroyed2),
savedStateMap = savedStateMap,
key = key,
) {
val twoPanelsEnabled: Boolean
get() = showTwoPanelsFlow.value

init {
backPressHandler.setTwoPanelsEnabledFunction { twoPanelsEnabled }

scope.launch {
var initialPanelModeSet = false
showTwoPanelsFlow.collectLatest { twoPanelsEnabled ->
if (initialPanelModeSet) {
panelModeChanged(twoPanelsEnabled)
}
initialPanelModeSet = true
}
}
}

sealed class State(val panel: Panel) : Parcelable {
@Parcelize
object Created1 : State(Panel.PANEL_1)

@Parcelize
object Created2 : State(Panel.PANEL_2)

@Parcelize
object Active1 : State(Panel.PANEL_1)

@Parcelize
object Active2 : State(Panel.PANEL_2)

@Parcelize
object StashedInBackStack1 : State(Panel.PANEL_1)

@Parcelize
object StashedInBackStack2 : State(Panel.PANEL_2)

@Parcelize
object Destroyed1 : State(Panel.PANEL_1)

@Parcelize
object Destroyed2 : State(Panel.PANEL_2)
}

enum class Panel {
PANEL_1, PANEL_2
}

override val initialElements by lazy {
listOfNotNull(
DualBackStackElement(
key = NavKey(leftElement),
fromState = if (rightElement == null || twoPanelsEnabled) Active1 else StashedInBackStack1,
targetState = if (rightElement == null || twoPanelsEnabled) Active1 else StashedInBackStack1,
operation = Noop()
),
rightElement?.let { element ->
DualBackStackElement(
key = NavKey(element),
fromState = Active2,
targetState = Active2,
operation = Noop()
)
}
)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.bumble.appyx.navmodel.dualbackstack

import com.bumble.appyx.core.navigation.NavElement
import com.bumble.appyx.navmodel.dualbackstack.DualBackStack.State

typealias DualBackStackElement<T> = NavElement<T, State>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.bumble.appyx.navmodel.dualbackstack

import com.bumble.appyx.core.navigation.NavElements
import com.bumble.appyx.navmodel.dualbackstack.DualBackStack.State

typealias DualBackStackElements<T> = NavElements<T, State>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.bumble.appyx.navmodel.dualbackstack

import com.bumble.appyx.core.navigation.onscreen.OnScreenStateResolver
import com.bumble.appyx.navmodel.dualbackstack.DualBackStack.State

object DualBackStackOnScreenResolver : OnScreenStateResolver<State> {

override fun isOnScreen(state: State): Boolean =
when (state) {
State.Created1,
State.Created2,
State.StashedInBackStack1,
State.StashedInBackStack2,
State.Destroyed1,
State.Destroyed2 -> false
State.Active1,
State.Active2 -> true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.bumble.appyx.navmodel.dualbackstack.backpresshandler

import com.bumble.appyx.core.navigation.backpresshandlerstrategies.BackPressHandlerStrategy
import com.bumble.appyx.navmodel.dualbackstack.DualBackStack

interface DualBackPressHandlerStrategy<NavTarget : Any> :
BackPressHandlerStrategy<NavTarget, DualBackStack.State> {

fun setTwoPanelsEnabledFunction(enabledFunc: () -> Boolean)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.bumble.appyx.navmodel.dualbackstack.backpresshandler

import com.bumble.appyx.core.navigation.backpresshandlerstrategies.BaseBackPressHandlerStrategy
import com.bumble.appyx.navmodel.dualbackstack.DualBackStack
import com.bumble.appyx.navmodel.dualbackstack.DualBackStackElements
import com.bumble.appyx.navmodel.dualbackstack.operation.Pop
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

class PopBackPressHandler<NavTarget : Any> :
BaseBackPressHandlerStrategy<NavTarget, DualBackStack.State>(),
DualBackPressHandlerStrategy<NavTarget> {

private lateinit var twoPanelsEnabledFunc: () -> Boolean

override val canHandleBackPressFlow: Flow<Boolean> by lazy {
navModel.elements.map(::areThereStashedElements)
}

private fun areThereStashedElements(elements: DualBackStackElements<NavTarget>) =
elements.any {
it.targetState == DualBackStack.State.StashedInBackStack1 ||
it.targetState == DualBackStack.State.StashedInBackStack2
}

override fun onBackPressed() {
navModel.accept(Pop(twoPanelsEnabledFunc()))
}

override fun setTwoPanelsEnabledFunction(enabledFunc: () -> Boolean) {
twoPanelsEnabledFunc = enabledFunc
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.bumble.appyx.navmodel.dualbackstack.operation

import com.bumble.appyx.core.navigation.NavElement
import com.bumble.appyx.core.navigation.Operation
import com.bumble.appyx.navmodel.dualbackstack.DualBackStack

sealed interface BackStackOperation<T> : Operation<T, DualBackStack.State>

fun <NavTarget, State> NavElement<NavTarget, State>.eitherState(state: State): Boolean =
fromState == state || targetState == state
Loading

0 comments on commit be19efe

Please sign in to comment.