mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-14 22:17:03 +05:30
refactor: add a custom back stack implementation
This commit is contained in:
parent
eef3fa0fc8
commit
e011870986
5 changed files with 102 additions and 26 deletions
|
@ -20,19 +20,18 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation3.runtime.NavKey
|
|
||||||
import dev.chrisbanes.haze.HazeDefaults
|
import dev.chrisbanes.haze.HazeDefaults
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
import dev.chrisbanes.haze.HazeStyle
|
import dev.chrisbanes.haze.HazeStyle
|
||||||
import dev.chrisbanes.haze.hazeEffect
|
import dev.chrisbanes.haze.hazeEffect
|
||||||
import dev.msfjarvis.claw.android.ui.navigation.AppDestinations
|
import dev.msfjarvis.claw.android.ui.navigation.AppDestinations
|
||||||
|
import dev.msfjarvis.claw.android.ui.navigation.ClawBackStack
|
||||||
import dev.msfjarvis.claw.android.ui.navigation.Destination
|
import dev.msfjarvis.claw.android.ui.navigation.Destination
|
||||||
import dev.msfjarvis.claw.common.ui.FloatingNavigationBar
|
import dev.msfjarvis.claw.common.ui.FloatingNavigationBar
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
@ -41,7 +40,7 @@ const val AnimationDuration = 100
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ClawNavigationBar(
|
fun ClawNavigationBar(
|
||||||
backStack: SnapshotStateList<NavKey>,
|
backStack: ClawBackStack<Destination>,
|
||||||
items: ImmutableList<NavigationItem>,
|
items: ImmutableList<NavigationItem>,
|
||||||
isVisible: Boolean,
|
isVisible: Boolean,
|
||||||
hazeState: HazeState,
|
hazeState: HazeState,
|
||||||
|
|
|
@ -19,15 +19,15 @@ import androidx.compose.material3.NavigationRail
|
||||||
import androidx.compose.material3.NavigationRailItem
|
import androidx.compose.material3.NavigationRailItem
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.navigation3.runtime.NavKey
|
import dev.msfjarvis.claw.android.ui.navigation.ClawBackStack
|
||||||
|
import dev.msfjarvis.claw.android.ui.navigation.Destination
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ClawNavigationRail(
|
fun ClawNavigationRail(
|
||||||
backStack: SnapshotStateList<NavKey>,
|
backStack: ClawBackStack<Destination>,
|
||||||
items: ImmutableList<NavigationItem>,
|
items: ImmutableList<NavigationItem>,
|
||||||
isVisible: Boolean,
|
isVisible: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* Copyright © Harsh Shandilya.
|
||||||
|
* Use of this source code is governed by an MIT-style
|
||||||
|
* license that can be found in the LICENSE file or at
|
||||||
|
* https://opensource.org/licenses/MIT.
|
||||||
|
*/
|
||||||
|
package dev.msfjarvis.claw.android.ui.navigation
|
||||||
|
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.navigation3.runtime.NavKey
|
||||||
|
import io.github.aakira.napier.Napier
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Naive implementation of a simple navigation back stack, backed by a
|
||||||
|
* [androidx.compose.runtime.snapshots.SnapshotStateList].
|
||||||
|
*
|
||||||
|
* Using a [java.util.Stack] would've made the API ridiculously simpler, but we would lose all the
|
||||||
|
* cool Compose benefits.
|
||||||
|
*
|
||||||
|
* Stacks function in a completely opposite order of lists, which means when you call
|
||||||
|
* first/firstOrNull on a stack, you expect to receive the item you added *last*, since stacks add
|
||||||
|
* to the front while lists add behind. To counter these expectations with the actual backing data
|
||||||
|
* structure, many APIs in this class inverse of identically named functions on [List].
|
||||||
|
*/
|
||||||
|
class ClawBackStack<T : NavKey>(startRoute: T) {
|
||||||
|
interface TopLevelRoute
|
||||||
|
|
||||||
|
val backStack = mutableStateListOf(startRoute)
|
||||||
|
|
||||||
|
/** Pushes a new destination onto the stack. */
|
||||||
|
fun add(route: T) {
|
||||||
|
logCurrentState("add")
|
||||||
|
backStack.add(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checks if the "top" item in the back stack is an instance of [TopLevelRoute]. */
|
||||||
|
fun isOnTopLevelRoute(): Boolean {
|
||||||
|
logCurrentState("hasTopLevelDestination")
|
||||||
|
val top = firstOrNull()
|
||||||
|
return (top != null && top is TopLevelRoute)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun firstOrNull(): T? {
|
||||||
|
logCurrentState("firstOrNull")
|
||||||
|
return backStack.lastOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun lastOrNull(): T? {
|
||||||
|
logCurrentState("lastOrNull")
|
||||||
|
return backStack.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeLastOrNull(): T? {
|
||||||
|
logCurrentState("removeLastOrNull")
|
||||||
|
return backStack.removeLastOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(msfjarvis): Remove before shipping
|
||||||
|
private fun logCurrentState(methodName: String) {
|
||||||
|
val backStack = this.backStack
|
||||||
|
Napier.d(tag = LOG_TAG) {
|
||||||
|
buildString {
|
||||||
|
appendLine("State of ClawBackStack(${this@ClawBackStack})")
|
||||||
|
appendLine("Caller: $methodName")
|
||||||
|
appendLine("Top: ${backStack.firstOrNull()}")
|
||||||
|
appendLine("Bottom: ${backStack.lastOrNull()}")
|
||||||
|
appendLine("Current entries: ${backStack.joinToString(", ")}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private const val LOG_TAG = "ClawBackStack"
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,11 +21,11 @@ import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
sealed interface Destination : Parcelable, NavKey
|
sealed interface Destination : Parcelable, NavKey
|
||||||
|
|
||||||
@Parcelize @Serializable data object Hottest : Destination
|
@Parcelize @Serializable data object Hottest : Destination, ClawBackStack.TopLevelRoute
|
||||||
|
|
||||||
@Parcelize @Serializable data object Newest : Destination
|
@Parcelize @Serializable data object Newest : Destination, ClawBackStack.TopLevelRoute
|
||||||
|
|
||||||
@Parcelize @Serializable data object Saved : Destination
|
@Parcelize @Serializable data object Saved : Destination, ClawBackStack.TopLevelRoute
|
||||||
|
|
||||||
@Parcelize @Serializable data class Comments(val postId: String) : Destination
|
@Parcelize @Serializable data class Comments(val postId: String) : Destination
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,6 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation3.runtime.entry
|
import androidx.navigation3.runtime.entry
|
||||||
import androidx.navigation3.runtime.entryProvider
|
import androidx.navigation3.runtime.entryProvider
|
||||||
import androidx.navigation3.runtime.rememberNavBackStack
|
|
||||||
import androidx.navigation3.ui.NavDisplay
|
import androidx.navigation3.ui.NavDisplay
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import com.deliveryhero.whetstone.compose.injectedViewModel
|
import com.deliveryhero.whetstone.compose.injectedViewModel
|
||||||
|
@ -63,6 +62,7 @@ import dev.msfjarvis.claw.android.ui.lists.DatabasePosts
|
||||||
import dev.msfjarvis.claw.android.ui.lists.NetworkPosts
|
import dev.msfjarvis.claw.android.ui.lists.NetworkPosts
|
||||||
import dev.msfjarvis.claw.android.ui.navigation.AboutLibraries
|
import dev.msfjarvis.claw.android.ui.navigation.AboutLibraries
|
||||||
import dev.msfjarvis.claw.android.ui.navigation.AppDestinations
|
import dev.msfjarvis.claw.android.ui.navigation.AppDestinations
|
||||||
|
import dev.msfjarvis.claw.android.ui.navigation.ClawBackStack
|
||||||
import dev.msfjarvis.claw.android.ui.navigation.ClawNavigationType
|
import dev.msfjarvis.claw.android.ui.navigation.ClawNavigationType
|
||||||
import dev.msfjarvis.claw.android.ui.navigation.Comments
|
import dev.msfjarvis.claw.android.ui.navigation.Comments
|
||||||
import dev.msfjarvis.claw.android.ui.navigation.Destination
|
import dev.msfjarvis.claw.android.ui.navigation.Destination
|
||||||
|
@ -90,7 +90,7 @@ fun Nav3Screen(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: ClawViewModel = injectedViewModel(),
|
viewModel: ClawViewModel = injectedViewModel(),
|
||||||
) {
|
) {
|
||||||
val backStack = rememberNavBackStack<Destination>(Hottest)
|
val clawBackStack = ClawBackStack<Destination>(Hottest)
|
||||||
|
|
||||||
// region Pain
|
// region Pain
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
@ -111,7 +111,7 @@ fun Nav3Screen(
|
||||||
val postIdOverride = activity?.intent?.extras?.getString(MainActivity.NAVIGATION_KEY)
|
val postIdOverride = activity?.intent?.extras?.getString(MainActivity.NAVIGATION_KEY)
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (postIdOverride != null) {
|
if (postIdOverride != null) {
|
||||||
backStack.add(Comments(postIdOverride))
|
clawBackStack.add(Comments(postIdOverride))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +135,7 @@ fun Nav3Screen(
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
val postActions = remember {
|
val postActions = remember {
|
||||||
PostActions(context, urlLauncher, viewModel) { backStack.add(Comments(it)) }
|
PostActions(context, urlLauncher, viewModel) { clawBackStack.add(Comments(it)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
@ -143,8 +143,10 @@ fun Nav3Screen(
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
modifier = Modifier.shadow(8.dp),
|
modifier = Modifier.shadow(8.dp),
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
if (backStack.firstOrNull() !in navDestinations) {
|
if (!(clawBackStack.isOnTopLevelRoute())) {
|
||||||
IconButton(onClick = { if (backStack.removeLastOrNull() == null) activity?.finish() }) {
|
IconButton(
|
||||||
|
onClick = { if (clawBackStack.removeLastOrNull() == null) activity?.finish() }
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
contentDescription = "Go back to previous screen",
|
contentDescription = "Go back to previous screen",
|
||||||
|
@ -159,16 +161,16 @@ fun Nav3Screen(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
if (backStack.firstOrNull() in navDestinations) {
|
if (clawBackStack.isOnTopLevelRoute()) {
|
||||||
Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold)
|
Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
if (backStack.firstOrNull() in navDestinations) {
|
if (clawBackStack.isOnTopLevelRoute()) {
|
||||||
IconButton(onClick = { backStack.add(Search) }) {
|
IconButton(onClick = { clawBackStack.add(Search) }) {
|
||||||
Icon(imageVector = Icons.Filled.Search, contentDescription = "Search posts")
|
Icon(imageVector = Icons.Filled.Search, contentDescription = "Search posts")
|
||||||
}
|
}
|
||||||
IconButton(onClick = { backStack.add(Settings) }) {
|
IconButton(onClick = { clawBackStack.add(Settings) }) {
|
||||||
Icon(imageVector = Icons.Filled.Tune, contentDescription = "Settings")
|
Icon(imageVector = Icons.Filled.Tune, contentDescription = "Settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -178,9 +180,9 @@ fun Nav3Screen(
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
AnimatedVisibility(visible = navigationType == ClawNavigationType.BOTTOM_NAVIGATION) {
|
AnimatedVisibility(visible = navigationType == ClawNavigationType.BOTTOM_NAVIGATION) {
|
||||||
ClawNavigationBar(
|
ClawNavigationBar(
|
||||||
backStack,
|
clawBackStack,
|
||||||
items = navItems,
|
items = navItems,
|
||||||
isVisible = backStack.firstOrNull() in navDestinations,
|
isVisible = clawBackStack.isOnTopLevelRoute(),
|
||||||
hazeState = hazeState,
|
hazeState = hazeState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -189,9 +191,9 @@ fun Nav3Screen(
|
||||||
modifier = Modifier.semantics { testTagsAsResourceId = true },
|
modifier = Modifier.semantics { testTagsAsResourceId = true },
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
NavDisplay(
|
NavDisplay(
|
||||||
backStack = backStack,
|
backStack = clawBackStack.backStack,
|
||||||
modifier = modifier.hazeSource(hazeState),
|
modifier = modifier.hazeSource(hazeState),
|
||||||
onBack = { backStack.removeLastOrNull() },
|
onBack = { clawBackStack.removeLastOrNull() },
|
||||||
predictivePopTransitionSpec = {
|
predictivePopTransitionSpec = {
|
||||||
slideInHorizontally(initialOffsetX = { -it }, animationSpec = tween(200)) togetherWith
|
slideInHorizontally(initialOffsetX = { -it }, animationSpec = tween(200)) togetherWith
|
||||||
slideOutHorizontally(targetOffsetX = { it }, animationSpec = tween(200))
|
slideOutHorizontally(targetOffsetX = { it }, animationSpec = tween(200))
|
||||||
|
@ -229,7 +231,7 @@ fun Nav3Screen(
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
openInputStream = context.contentResolver::openInputStream,
|
openInputStream = context.contentResolver::openInputStream,
|
||||||
openOutputStream = context.contentResolver::openOutputStream,
|
openOutputStream = context.contentResolver::openOutputStream,
|
||||||
openLibrariesScreen = { backStack.add(AboutLibraries) },
|
openLibrariesScreen = { clawBackStack.add(AboutLibraries) },
|
||||||
importPosts = viewModel::importPosts,
|
importPosts = viewModel::importPosts,
|
||||||
exportPostsAsJson = viewModel::exportPostsAsJson,
|
exportPostsAsJson = viewModel::exportPostsAsJson,
|
||||||
exportPostsAsHtml = viewModel::exportPostsAsHtml,
|
exportPostsAsHtml = viewModel::exportPostsAsHtml,
|
||||||
|
@ -245,7 +247,7 @@ fun Nav3Screen(
|
||||||
getSeenComments = viewModel::getSeenComments,
|
getSeenComments = viewModel::getSeenComments,
|
||||||
markSeenComments = viewModel::markSeenComments,
|
markSeenComments = viewModel::markSeenComments,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
openUserProfile = { backStack.add(User(it)) },
|
openUserProfile = { clawBackStack.add(User(it)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
entry<User>(
|
entry<User>(
|
||||||
|
@ -266,7 +268,7 @@ fun Nav3Screen(
|
||||||
username = dest.username,
|
username = dest.username,
|
||||||
getProfile = viewModel::getUserProfile,
|
getProfile = viewModel::getUserProfile,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
openUserProfile = { backStack.add(User(it)) },
|
openUserProfile = { clawBackStack.add(User(it)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
entry<Search> {
|
entry<Search> {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue