Skip to content

Commit 7b502eb

Browse files
Merge pull request #302 from vladcipariu91/backstack-example
Add InsideTheBackStack sample
2 parents 70ed7d3 + 3e9570c commit 7b502eb

14 files changed

Lines changed: 765 additions & 36 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package com.bumble.appyx.app.node.backstack
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Arrangement
5+
import androidx.compose.foundation.layout.Box
6+
import androidx.compose.foundation.layout.Column
7+
import androidx.compose.foundation.layout.Row
8+
import androidx.compose.foundation.layout.Spacer
9+
import androidx.compose.foundation.layout.fillMaxSize
10+
import androidx.compose.foundation.layout.fillMaxWidth
11+
import androidx.compose.foundation.layout.padding
12+
import androidx.compose.material.MaterialTheme
13+
import androidx.compose.material.Text
14+
import androidx.compose.runtime.Composable
15+
import androidx.compose.ui.Alignment
16+
import androidx.compose.ui.Modifier
17+
import androidx.compose.ui.unit.dp
18+
import androidx.lifecycle.coroutineScope
19+
import com.bumble.appyx.app.node.backstack.InsideTheBackStack.NavTarget
20+
import com.bumble.appyx.app.node.backstack.app.ChildNode
21+
import com.bumble.appyx.app.node.backstack.app.composable.CustomButton
22+
import com.bumble.appyx.app.node.backstack.app.composable.PeekInsideBackStack
23+
import com.bumble.appyx.app.node.backstack.app.indexedbackstack.IndexedBackStack
24+
import com.bumble.appyx.app.node.backstack.app.indexedbackstack.operation.pop
25+
import com.bumble.appyx.app.node.backstack.app.indexedbackstack.operation.push
26+
import com.bumble.appyx.app.node.backstack.app.indexedbackstack.transition.rememberRecentsTransitionHandler
27+
import com.bumble.appyx.core.composable.Children
28+
import com.bumble.appyx.core.modality.BuildContext
29+
import com.bumble.appyx.core.node.Node
30+
import com.bumble.appyx.core.node.ParentNode
31+
import kotlinx.coroutines.delay
32+
import kotlinx.coroutines.isActive
33+
34+
class InsideTheBackStack(
35+
buildContext: BuildContext,
36+
autoAdvanceDelayMs: Long? = null,
37+
private val backStack: IndexedBackStack<NavTarget> = IndexedBackStack(
38+
savedState = buildContext.savedStateMap,
39+
initialElement = NavTarget.Child(0)
40+
)
41+
) : ParentNode<NavTarget>(
42+
buildContext = buildContext,
43+
navModel = backStack
44+
) {
45+
46+
init {
47+
autoAdvanceDelayMs?.let { ms ->
48+
lifecycle.coroutineScope.launchWhenStarted {
49+
while (isActive) {
50+
delay(ms)
51+
repeat(4) {
52+
backStack.push(NavTarget.Child(it + 1))
53+
delay(ms)
54+
}
55+
repeat(4) {
56+
backStack.pop()
57+
delay(ms)
58+
}
59+
}
60+
}
61+
}
62+
}
63+
64+
sealed class NavTarget {
65+
data class Child(val index: Int) : NavTarget() {
66+
override fun toString(): String = index.toString()
67+
}
68+
}
69+
70+
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node =
71+
when (navTarget) {
72+
is NavTarget.Child -> ChildNode(buildContext, navTarget.index)
73+
}
74+
75+
@Composable
76+
override fun View(modifier: Modifier) {
77+
Box(
78+
modifier = Modifier
79+
.fillMaxSize()
80+
.background(color = MaterialTheme.colors.background)
81+
) {
82+
Column(
83+
horizontalAlignment = Alignment.CenterHorizontally,
84+
modifier = Modifier.fillMaxSize()
85+
) {
86+
PeekInsideBackStack(backStack, Modifier.weight(0.2f))
87+
Spacer(Modifier.weight(0.1f))
88+
BackStackContent(
89+
modifier = Modifier.weight(0.7f)
90+
)
91+
}
92+
93+
Controls(Modifier.align(Alignment.BottomCenter))
94+
}
95+
}
96+
97+
@Composable
98+
private fun BackStackContent(
99+
modifier: Modifier = Modifier,
100+
) {
101+
Children(
102+
modifier = modifier.padding(16.dp),
103+
navModel = backStack,
104+
transitionHandler = rememberRecentsTransitionHandler()
105+
)
106+
}
107+
108+
@Composable
109+
fun Controls(modifier: Modifier = Modifier) {
110+
Row(
111+
horizontalArrangement = Arrangement.Center,
112+
verticalAlignment = Alignment.CenterVertically,
113+
modifier = modifier
114+
.fillMaxWidth()
115+
.padding(2.dp)
116+
) {
117+
CustomButton(onClick = { backStack.pop() }) {
118+
Text("Pop")
119+
}
120+
CustomButton(onClick = { backStack.push(NavTarget.Child(backStack.elements.value.size)) }) {
121+
Text("Push")
122+
}
123+
}
124+
}
125+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.bumble.appyx.app.node.backstack.app
2+
3+
import androidx.compose.foundation.Image
4+
import androidx.compose.foundation.layout.Box
5+
import androidx.compose.foundation.layout.fillMaxSize
6+
import androidx.compose.foundation.layout.padding
7+
import androidx.compose.foundation.shape.RoundedCornerShape
8+
import androidx.compose.material.Text
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.ui.Modifier
11+
import androidx.compose.ui.draw.clip
12+
import androidx.compose.ui.graphics.Color
13+
import androidx.compose.ui.layout.ContentScale
14+
import androidx.compose.ui.res.painterResource
15+
import androidx.compose.ui.unit.dp
16+
import androidx.compose.ui.unit.sp
17+
import com.bumble.appyx.R
18+
import com.bumble.appyx.core.modality.BuildContext
19+
import com.bumble.appyx.core.node.Node
20+
21+
class ChildNode(
22+
buildContext: BuildContext,
23+
private val index: Int,
24+
) : Node(buildContext = buildContext) {
25+
26+
@Composable
27+
override fun View(modifier: Modifier) {
28+
Box(
29+
modifier = Modifier
30+
.fillMaxSize()
31+
.clip(RoundedCornerShape(10.dp))
32+
) {
33+
Image(
34+
modifier = modifier
35+
.fillMaxSize()
36+
.clip(RoundedCornerShape(10.dp)),
37+
painter = painterResource(id = images[index % images.size]),
38+
contentDescription = "image",
39+
contentScale = ContentScale.Crop,
40+
)
41+
42+
Text(
43+
modifier = Modifier.padding(12.dp),
44+
text = index.toString(),
45+
color = Color.White,
46+
fontSize = 36.sp
47+
)
48+
}
49+
}
50+
51+
companion object {
52+
private val images = listOf(
53+
R.drawable.halloween3,
54+
R.drawable.halloween4,
55+
R.drawable.halloween6,
56+
R.drawable.halloween7,
57+
R.drawable.halloween8,
58+
R.drawable.halloween9,
59+
R.drawable.halloween10,
60+
R.drawable.halloween11,
61+
R.drawable.halloween12,
62+
R.drawable.halloween13,
63+
R.drawable.halloween14,
64+
R.drawable.halloween15,
65+
)
66+
}
67+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.bumble.appyx.app.node.backstack.app.composable
2+
3+
import androidx.compose.foundation.layout.padding
4+
import androidx.compose.material.Button
5+
import androidx.compose.material.ButtonDefaults
6+
import androidx.compose.runtime.Composable
7+
import androidx.compose.ui.Modifier
8+
import androidx.compose.ui.unit.dp
9+
import com.bumble.appyx.app.ui.appyx_dark
10+
import com.bumble.appyx.app.ui.appyx_yellow1
11+
12+
@Composable
13+
fun CustomButton(
14+
onClick: () -> Unit,
15+
modifier: Modifier = Modifier,
16+
content: @Composable () -> Unit
17+
) {
18+
Button(
19+
modifier = modifier.padding(horizontal = 8.dp),
20+
onClick = onClick,
21+
colors = ButtonDefaults.buttonColors(
22+
backgroundColor = appyx_yellow1,
23+
contentColor = appyx_dark
24+
)
25+
) { content() }
26+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package com.bumble.appyx.app.node.backstack.app.composable
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.border
5+
import androidx.compose.foundation.isSystemInDarkTheme
6+
import androidx.compose.foundation.layout.Arrangement
7+
import androidx.compose.foundation.layout.Column
8+
import androidx.compose.foundation.layout.fillMaxWidth
9+
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.foundation.layout.size
11+
import androidx.compose.foundation.lazy.LazyRow
12+
import androidx.compose.foundation.lazy.rememberLazyListState
13+
import androidx.compose.foundation.shape.RoundedCornerShape
14+
import androidx.compose.material.MaterialTheme
15+
import androidx.compose.material.Text
16+
import androidx.compose.runtime.Composable
17+
import androidx.compose.runtime.LaunchedEffect
18+
import androidx.compose.runtime.collectAsState
19+
import androidx.compose.ui.Alignment
20+
import androidx.compose.ui.Modifier
21+
import androidx.compose.ui.draw.clip
22+
import androidx.compose.ui.graphics.Color
23+
import androidx.compose.ui.text.font.FontWeight
24+
import androidx.compose.ui.unit.dp
25+
import androidx.compose.ui.unit.sp
26+
import com.bumble.appyx.app.node.backstack.app.indexedbackstack.IndexedBackStack
27+
import com.bumble.appyx.app.node.backstack.app.indexedbackstack.IndexedBackStack.State.Active
28+
import com.bumble.appyx.app.node.backstack.app.indexedbackstack.IndexedBackStack.State.Created
29+
import com.bumble.appyx.app.node.backstack.app.indexedbackstack.IndexedBackStack.State.Destroyed
30+
import com.bumble.appyx.app.node.backstack.app.indexedbackstack.IndexedBackStack.State.Stashed
31+
import com.bumble.appyx.app.ui.appyx_yellow2
32+
import com.bumble.appyx.app.ui.atomic_tangerine
33+
import com.bumble.appyx.app.ui.imperial_red
34+
import com.bumble.appyx.core.navigation.NavElement
35+
import java.util.Locale
36+
37+
@Composable
38+
fun <T : Any> PeekInsideBackStack(
39+
backStack: IndexedBackStack<T>,
40+
modifier: Modifier = Modifier
41+
) {
42+
val elements = backStack.elements.collectAsState()
43+
44+
val listState = rememberLazyListState()
45+
LaunchedEffect(elements.value.lastIndex) {
46+
listState.animateScrollToItem(index = elements.value.lastIndex)
47+
}
48+
49+
LazyRow(
50+
state = listState,
51+
modifier = modifier
52+
.fillMaxWidth()
53+
.background(if (isSystemInDarkTheme()) Color.DarkGray else Color.LightGray)
54+
.padding(12.dp),
55+
verticalAlignment = Alignment.CenterVertically,
56+
) {
57+
elements.value.forEach { element ->
58+
item {
59+
BackStackElement(element)
60+
}
61+
}
62+
}
63+
}
64+
65+
@Composable
66+
private fun <T> BackStackElement(
67+
element: NavElement<T, IndexedBackStack.State>,
68+
) {
69+
Column(
70+
modifier = Modifier
71+
.size(60.dp)
72+
.padding(4.dp)
73+
.clip(RoundedCornerShape(15))
74+
.background(if (isSystemInDarkTheme()) Color.Transparent else element.targetState.toColor())
75+
.border(2.dp, element.targetState.toColor(), RoundedCornerShape(15))
76+
.padding(6.dp),
77+
verticalArrangement = Arrangement.Center
78+
) {
79+
Text(
80+
text = element.key.navTarget.toString(),
81+
color = MaterialTheme.colors.onSurface,
82+
fontSize = 16.sp,
83+
fontWeight = FontWeight.Bold
84+
)
85+
Text(
86+
text = element.targetState.javaClass.simpleName.toString()
87+
.replace("Destroyed", "Destr")
88+
.uppercase(Locale.getDefault()),
89+
color = MaterialTheme.colors.onSurface,
90+
fontSize = 9.sp,
91+
)
92+
}
93+
}
94+
95+
private fun IndexedBackStack.State.toColor(): Color =
96+
when (this) {
97+
is Created -> appyx_yellow2
98+
is Active -> appyx_yellow2
99+
is Stashed -> atomic_tangerine
100+
is Destroyed -> imperial_red
101+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.bumble.appyx.app.node.backstack.app.indexedbackstack
2+
3+
import com.bumble.appyx.core.navigation.BaseNavModel
4+
import com.bumble.appyx.core.navigation.NavElements
5+
import com.bumble.appyx.core.navigation.NavKey
6+
import com.bumble.appyx.core.navigation.Operation
7+
import com.bumble.appyx.core.state.SavedStateMap
8+
9+
class IndexedBackStack<NavTarget : Any>(
10+
savedState: SavedStateMap?,
11+
initialElement: NavTarget
12+
) : BaseNavModel<NavTarget, IndexedBackStack.State>(
13+
screenResolver = IndexedBackStackOnScreenResolver,
14+
finalStates = setOf(State.Destroyed),
15+
savedStateMap = savedState
16+
) {
17+
18+
sealed interface State {
19+
object Created : State
20+
object Active : State
21+
class Stashed(
22+
val index: Int,
23+
val size: Int
24+
) : State
25+
26+
object Destroyed : State
27+
}
28+
29+
override val initialElements: NavElements<NavTarget, State> =
30+
listOf(
31+
IndexedBackStackElement(
32+
key = NavKey(initialElement),
33+
fromState = State.Active,
34+
targetState = State.Active,
35+
operation = Operation.Noop()
36+
)
37+
)
38+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.bumble.appyx.app.node.backstack.app.indexedbackstack
2+
3+
import com.bumble.appyx.core.navigation.NavElement
4+
5+
typealias IndexedBackStackElement<NavTarget> = NavElement<NavTarget, IndexedBackStack.State>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.bumble.appyx.app.node.backstack.app.indexedbackstack
2+
3+
import com.bumble.appyx.core.navigation.onscreen.OnScreenStateResolver
4+
import com.bumble.appyx.app.node.backstack.app.indexedbackstack.IndexedBackStack.State
5+
import com.bumble.appyx.app.node.backstack.app.indexedbackstack.IndexedBackStack.State.Created
6+
import com.bumble.appyx.app.node.backstack.app.indexedbackstack.IndexedBackStack.State.Active
7+
import com.bumble.appyx.app.node.backstack.app.indexedbackstack.IndexedBackStack.State.Stashed
8+
import com.bumble.appyx.app.node.backstack.app.indexedbackstack.IndexedBackStack.State.Destroyed
9+
10+
object IndexedBackStackOnScreenResolver : OnScreenStateResolver<State> {
11+
12+
override fun isOnScreen(state: State): Boolean =
13+
when (state) {
14+
is Created,
15+
is Active -> true
16+
is Stashed -> {
17+
if (state.size > MAX_ON_SCREEN) {
18+
state.index in state.size - MAX_ON_SCREEN..state.size - 2
19+
} else {
20+
true
21+
}
22+
}
23+
is Destroyed -> false
24+
}
25+
26+
const val MAX_ON_SCREEN = 3
27+
}

0 commit comments

Comments
 (0)