Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simple api for backStackNode and spotlightNode #679

Merged
merged 10 commits into from
Feb 11, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import com.bumble.appyx.components.backstack.ui.stack3d.BackStack3D
import com.bumble.appyx.interactions.core.model.transition.Operation
import com.bumble.appyx.interactions.core.ui.Visualisation
import com.bumble.appyx.interactions.core.ui.context.UiContext
import com.bumble.appyx.interactions.testing.InteractionTarget
import com.bumble.appyx.interactions.testing.TestTarget
import com.bumble.appyx.interactions.testing.setupAppyxComponent
import com.bumble.appyx.interactions.testing.waitUntilAnimationEnded
import com.bumble.appyx.interactions.testing.waitUntilAnimationStarted
Expand All @@ -32,11 +32,11 @@ class BackStackTest(private val testParam: TestParam) {
@get:Rule
val composeTestRule = createComposeRule()

private lateinit var backStack: BackStack<InteractionTarget>
private lateinit var backStack: BackStack<TestTarget>

companion object {
data class TestParam(
val visualisation: (UiContext) -> Visualisation<InteractionTarget, BackStackModel.State<InteractionTarget>>
val visualisation: (UiContext) -> Visualisation<TestTarget, BackStackModel.State<TestTarget>>
)

@JvmStatic
Expand All @@ -55,9 +55,9 @@ class BackStackTest(private val testParam: TestParam) {
composeTestRule.setupAppyxComponent(backStack)

val tweenTwoSec = tween<Float>(durationMillis = 2000)
backStack.push(interactionTarget = InteractionTarget.Child2)
backStack.push(interactionTarget = InteractionTarget.Child3)
backStack.push(interactionTarget = InteractionTarget.Child4)
backStack.push(navTarget = TestTarget.Child2)
backStack.push(navTarget = TestTarget.Child3)
backStack.push(navTarget = TestTarget.Child4)
backStack.pop(animationSpec = tweenTwoSec)

// all operations finished
Expand All @@ -73,9 +73,9 @@ class BackStackTest(private val testParam: TestParam) {
composeTestRule.setupAppyxComponent(backStack)

val tweenTwoSec = tween<Float>(durationMillis = 2000)
backStack.push(interactionTarget = InteractionTarget.Child2)
backStack.push(interactionTarget = InteractionTarget.Child3)
backStack.push(interactionTarget = InteractionTarget.Child4)
backStack.push(navTarget = TestTarget.Child2)
backStack.push(navTarget = TestTarget.Child3)
backStack.push(navTarget = TestTarget.Child4)
backStack.pop(animationSpec = tweenTwoSec)

// last operation is not finished. advanced time < 2000 (last operation animation spec)
Expand All @@ -89,9 +89,9 @@ class BackStackTest(private val testParam: TestParam) {
createBackStack(disableAnimations = true, testParam.visualisation)
composeTestRule.setupAppyxComponent(backStack)

backStack.push(interactionTarget = InteractionTarget.Child2)
backStack.push(interactionTarget = InteractionTarget.Child3)
backStack.push(interactionTarget = InteractionTarget.Child4)
backStack.push(navTarget = TestTarget.Child2)
backStack.push(navTarget = TestTarget.Child3)
backStack.push(navTarget = TestTarget.Child4)
backStack.pop()

Assert.assertEquals(3, backStack.elements.value.all.size)
Expand All @@ -106,17 +106,17 @@ class BackStackTest(private val testParam: TestParam) {
val popSpringSpec = spring<Float>(stiffness = 10f)

backStack.push(
interactionTarget = InteractionTarget.Child2,
navTarget = TestTarget.Child2,
mode = Operation.Mode.IMMEDIATE,
animationSpec = pushSpringSpec
)
// all subsequent operations will be in IMMEDIATE mode until settled
backStack.push(
interactionTarget = InteractionTarget.Child3,
navTarget = TestTarget.Child3,
animationSpec = pushSpringSpec
)
backStack.push(
interactionTarget = InteractionTarget.Child4,
navTarget = TestTarget.Child4,
animationSpec = pushSpringSpec
)
backStack.pop(animationSpec = popSpringSpec)
Expand All @@ -133,11 +133,11 @@ class BackStackTest(private val testParam: TestParam) {

private fun createBackStack(
disableAnimations: Boolean,
visualisation: (UiContext) -> Visualisation<InteractionTarget, BackStackModel.State<InteractionTarget>>
visualisation: (UiContext) -> Visualisation<TestTarget, BackStackModel.State<TestTarget>>
) {
backStack = BackStack(
model = BackStackModel(
initialTargets = listOf(InteractionTarget.Child1),
initialTargets = listOf(TestTarget.Child1),
savedStateMap = null
),
visualisation = visualisation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import com.bumble.appyx.components.backstack.BackStack
import com.bumble.appyx.components.backstack.BackStackModel
import com.bumble.appyx.components.backstack.operation.push
import com.bumble.appyx.interactions.core.model.transition.Update
import com.bumble.appyx.interactions.testing.InteractionTarget
import com.bumble.appyx.interactions.testing.TestTarget
import com.bumble.appyx.interactions.testing.setupAppyxComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand All @@ -18,22 +18,22 @@ class BackStackParallaxTest {
@get:Rule
val composeTestRule = createComposeRule()

private lateinit var backStack: BackStack<InteractionTarget>
private lateinit var backStackModel: BackStackModel<InteractionTarget>
private lateinit var visualisation: BackStackParallax<InteractionTarget>
private lateinit var backStack: BackStack<TestTarget>
private lateinit var backStackModel: BackStackModel<TestTarget>
private lateinit var visualisation: BackStackParallax<TestTarget>

@Test
fun backStackParallax_resolves_visibility_to_false_when_element_is_not_top_most_stashed_one() {
createBackStack()
composeTestRule.setupAppyxComponent(backStack)

backStack.push(interactionTarget = InteractionTarget.Child2)
backStack.push(interactionTarget = InteractionTarget.Child3)
backStack.push(interactionTarget = InteractionTarget.Child4)
backStack.push(navTarget = TestTarget.Child2)
backStack.push(navTarget = TestTarget.Child3)
backStack.push(navTarget = TestTarget.Child4)

composeTestRule.waitForIdle()

with(visualisation.mapUpdate(backStackModel.output.value as Update<BackStackModel.State<InteractionTarget>>)) {
with(visualisation.mapUpdate(backStackModel.output.value as Update<BackStackModel.State<TestTarget>>)) {
Assert.assertFalse(get(0).visibleState.value) // Child #1 should be false
Assert.assertFalse(get(1).visibleState.value) // Child #2 should be false
Assert.assertFalse(get(2).visibleState.value) // Child #3 should be false
Expand All @@ -43,13 +43,13 @@ class BackStackParallaxTest {

private fun createBackStack() {
backStackModel = BackStackModel(
initialTargets = listOf(InteractionTarget.Child1),
initialTargets = listOf(TestTarget.Child1),
savedStateMap = null
)
backStack = BackStack(
model = backStackModel,
visualisation = { uiContext ->
BackStackParallax<InteractionTarget>(uiContext).also {
BackStackParallax<TestTarget>(uiContext).also {
visualisation = it
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation(project(":appyx-interactions:appyx-interactions"))
implementation(project(":appyx-navigation:appyx-navigation"))
api(compose.runtime)
api(compose.foundation)
api(compose.material)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob

class BackStack<InteractionTarget : Any>(
class BackStack<NavTarget : Any>(
scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main),
val model: BackStackModel<InteractionTarget>,
visualisation: (UiContext) -> Visualisation<InteractionTarget, BackStackModel.State<InteractionTarget>>,
val model: BackStackModel<NavTarget>,
visualisation: (UiContext) -> Visualisation<NavTarget, BackStackModel.State<NavTarget>>,
animationSpec: AnimationSpec<Float> = spring(),
gestureFactory: (TransitionBounds) -> GestureFactory<InteractionTarget, BackStackModel.State<InteractionTarget>> = {
gestureFactory: (TransitionBounds) -> GestureFactory<NavTarget, BackStackModel.State<NavTarget>> = {
GestureFactory.Noop()
},
gestureSettleConfig: GestureSettleConfig = GestureSettleConfig(),
backPressStrategy: BackPressHandlerStrategy<InteractionTarget, BackStackModel.State<InteractionTarget>> =
backPressStrategy: BackPressHandlerStrategy<NavTarget, BackStackModel.State<NavTarget>> =
PopBackstackStrategy(scope),
disableAnimations: Boolean = false,
) : BaseAppyxComponent<InteractionTarget, BackStackModel.State<InteractionTarget>>(
) : BaseAppyxComponent<NavTarget, BackStackModel.State<NavTarget>>(
scope = scope,
model = model,
visualisation = visualisation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,56 +9,56 @@ import com.bumble.appyx.utils.multiplatform.Parcelable
import com.bumble.appyx.utils.multiplatform.Parcelize
import com.bumble.appyx.utils.multiplatform.SavedStateMap

class BackStackModel<InteractionTarget : Any>(
initialTargets: List<InteractionTarget>,
class BackStackModel<NavTarget : Any>(
initialTargets: List<NavTarget>,
savedStateMap: SavedStateMap?,
) : BaseTransitionModel<InteractionTarget, State<InteractionTarget>>(
) : BaseTransitionModel<NavTarget, State<NavTarget>>(
savedStateMap = savedStateMap,
) {
@Parcelize
data class State<InteractionTarget>(
data class State<NavTarget>(
/**
* Elements that have been created, but not yet moved to an active state
*/
val created: Elements<InteractionTarget> = listOf(),
val created: Elements<NavTarget> = listOf(),

/**
* The currently active element.
* There should be only one such element in the stack.
*/
val active: Element<InteractionTarget>,
val active: Element<NavTarget>,

/**
* Elements stashed in the back stack (history).
*/
val stashed: Elements<InteractionTarget> = listOf(),
val stashed: Elements<NavTarget> = listOf(),

/**
* Elements that will be destroyed after reaching this state.
*/
val destroyed: Elements<InteractionTarget> = listOf(),
val destroyed: Elements<NavTarget> = listOf(),
) : Parcelable

constructor(
initialTarget: InteractionTarget,
initialTarget: NavTarget,
savedStateMap: SavedStateMap?,
) : this(
initialTargets = listOf(initialTarget),
savedStateMap = savedStateMap
)

override fun State<InteractionTarget>.availableElements(): Set<Element<InteractionTarget>> =
override fun State<NavTarget>.availableElements(): Set<Element<NavTarget>> =
(created + active + stashed + destroyed).toSet()

override fun State<InteractionTarget>.destroyedElements(): Set<Element<InteractionTarget>> =
override fun State<NavTarget>.destroyedElements(): Set<Element<NavTarget>> =
destroyed.toSet()

override fun State<InteractionTarget>.removeDestroyedElement(
element: Element<InteractionTarget>
): State<InteractionTarget> =
override fun State<NavTarget>.removeDestroyedElement(
element: Element<NavTarget>
): State<NavTarget> =
copy(destroyed = destroyed.filterNot { it == element })

override fun State<InteractionTarget>.removeDestroyedElements(): State<InteractionTarget> =
override fun State<NavTarget>.removeDestroyedElements(): State<NavTarget> =
copy(destroyed = emptyList())

override val initialState = State(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ import com.bumble.appyx.interactions.core.Element
val <T : Any> BackStackModel<T>.activeElement: Element<T>
get() = output.value.currentTargetState.active

val <T : Any> BackStackModel<T>.activeInteractionTarget: T
val <T : Any> BackStackModel<T>.activeTarget: T
get() = activeElement.interactionTarget
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import com.bumble.appyx.mapState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow

class PopBackstackStrategy<InteractionTarget : Any>(
class PopBackstackStrategy<NavTarget : Any>(
val scope: CoroutineScope,
val animationSpec: AnimationSpec<Float>? = null
) :
BaseBackPressHandlerStrategy<InteractionTarget, BackStackModel.State<InteractionTarget>>() {
BaseBackPressHandlerStrategy<NavTarget, BackStackModel.State<NavTarget>>() {

override val canHandleBackPress: StateFlow<Boolean> by lazy {
transitionModel.output.mapState(scope) { output ->
Expand All @@ -21,7 +21,7 @@ class PopBackstackStrategy<InteractionTarget : Any>(
}

override fun handleBackPress(): Boolean {
val pop = Pop<InteractionTarget>()
val pop = Pop<NavTarget>()
//todo find a better way to check if operation is applicable
return if (pop.isApplicable(transitionModel.output.value.currentTargetState)) {
appyxComponent.operation(operation = Pop(), animationSpec)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.bumble.appyx.components.backstack.node

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.components.backstack.BackStack
import com.bumble.appyx.components.backstack.BackStackModel
import com.bumble.appyx.components.backstack.BackStackModel.State
import com.bumble.appyx.components.backstack.ui.fader.BackStackFader
import com.bumble.appyx.interactions.core.ui.Visualisation
import com.bumble.appyx.interactions.core.ui.context.TransitionBounds
import com.bumble.appyx.interactions.core.ui.context.UiContext
import com.bumble.appyx.interactions.core.ui.gesture.GestureFactory
import com.bumble.appyx.navigation.composable.AppyxNavigationContainer
import com.bumble.appyx.navigation.modality.NodeContext
import com.bumble.appyx.navigation.node.ComponentNode
import com.bumble.appyx.navigation.node.Node

/**
* A simplified way of creating a Node with back stack navigation, allowing you to
* quickly sketch out your navigation.
*
* In more complex scenarios you'll probably want to create your own Node class, but in many
* cases this should offer a simple api to reduce the amount of code required.
*/
fun <T : Any> backStackNode(
nodeContext: NodeContext,
initialTarget: T,
mappings: (BackStack<T>, T, NodeContext) -> Node<*>,
visualisation: (UiContext) -> Visualisation<T, State<T>> = { BackStackFader(it) },
gestureFactory: (TransitionBounds) -> GestureFactory<T, State<T>> = {
GestureFactory.Noop()
},
content: @Composable (BackStack<T>, Modifier) -> Unit = { backStack, modifier ->
AppyxNavigationContainer(
appyxComponent = backStack,
modifier = modifier
)
}
) = ComponentNode(
nodeContext = nodeContext,
component = BackStack(
model = BackStackModel(
initialTarget = initialTarget,
savedStateMap = nodeContext.savedStateMap
),
visualisation = visualisation,
gestureFactory = gestureFactory
),
mappings = mappings,
content = content
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,23 @@ import com.bumble.appyx.utils.multiplatform.RawValue
* [A, B, C] + NewRoot(D) = [ D ]
*/
@Parcelize
data class NewRoot<InteractionTarget>(
private val interactionTarget: @RawValue InteractionTarget,
data class NewRoot<NavTarget>(
private val navTarget: @RawValue NavTarget,
override var mode: Operation.Mode = Operation.Mode.KEYFRAME
) : BaseOperation<BackStackModel.State<InteractionTarget>>() {
override fun isApplicable(state: BackStackModel.State<InteractionTarget>): Boolean =
) : BaseOperation<BackStackModel.State<NavTarget>>() {
override fun isApplicable(state: BackStackModel.State<NavTarget>): Boolean =
true

override fun createFromState(
baseLineState: BackStackModel.State<InteractionTarget>
): BackStackModel.State<InteractionTarget> =
baseLineState: BackStackModel.State<NavTarget>
): BackStackModel.State<NavTarget> =
baseLineState.copy(
created = baseLineState.created + interactionTarget.asElement()
created = baseLineState.created + navTarget.asElement()
)

override fun createTargetState(
fromState: BackStackModel.State<InteractionTarget>
): BackStackModel.State<InteractionTarget> =
fromState: BackStackModel.State<NavTarget>
): BackStackModel.State<NavTarget> =
fromState.copy(
active = fromState.created.last(),
created = fromState.created.dropLast(1),
Expand All @@ -40,10 +40,10 @@ data class NewRoot<InteractionTarget>(
)
}

fun <InteractionTarget : Any> BackStack<InteractionTarget>.newRoot(
interactionTarget: InteractionTarget,
fun <NavTarget : Any> BackStack<NavTarget>.newRoot(
navTarget: NavTarget,
mode: Operation.Mode = Operation.Mode.KEYFRAME,
animationSpec: AnimationSpec<Float>? = null
) {
operation(operation = NewRoot(interactionTarget, mode), animationSpec = animationSpec)
operation(operation = NewRoot(navTarget, mode), animationSpec = animationSpec)
}
Loading
Loading