mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-14 18:47: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.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import dev.chrisbanes.haze.HazeDefaults
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.HazeStyle
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
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.common.ui.FloatingNavigationBar
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
@ -41,7 +40,7 @@ const val AnimationDuration = 100
|
|||
|
||||
@Composable
|
||||
fun ClawNavigationBar(
|
||||
backStack: SnapshotStateList<NavKey>,
|
||||
backStack: ClawBackStack<Destination>,
|
||||
items: ImmutableList<NavigationItem>,
|
||||
isVisible: Boolean,
|
||||
hazeState: HazeState,
|
||||
|
|
|
@ -19,15 +19,15 @@ import androidx.compose.material3.NavigationRail
|
|||
import androidx.compose.material3.NavigationRailItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Modifier
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun ClawNavigationRail(
|
||||
backStack: SnapshotStateList<NavKey>,
|
||||
backStack: ClawBackStack<Destination>,
|
||||
items: ImmutableList<NavigationItem>,
|
||||
isVisible: Boolean,
|
||||
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
|
||||
|
||||
@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
|
||||
|
||||
|
|
|
@ -47,7 +47,6 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation3.runtime.entry
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
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.navigation.AboutLibraries
|
||||
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.Comments
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Destination
|
||||
|
@ -90,7 +90,7 @@ fun Nav3Screen(
|
|||
modifier: Modifier = Modifier,
|
||||
viewModel: ClawViewModel = injectedViewModel(),
|
||||
) {
|
||||
val backStack = rememberNavBackStack<Destination>(Hottest)
|
||||
val clawBackStack = ClawBackStack<Destination>(Hottest)
|
||||
|
||||
// region Pain
|
||||
val context = LocalContext.current
|
||||
|
@ -111,7 +111,7 @@ fun Nav3Screen(
|
|||
val postIdOverride = activity?.intent?.extras?.getString(MainActivity.NAVIGATION_KEY)
|
||||
LaunchedEffect(Unit) {
|
||||
if (postIdOverride != null) {
|
||||
backStack.add(Comments(postIdOverride))
|
||||
clawBackStack.add(Comments(postIdOverride))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,7 +135,7 @@ fun Nav3Screen(
|
|||
// endregion
|
||||
|
||||
val postActions = remember {
|
||||
PostActions(context, urlLauncher, viewModel) { backStack.add(Comments(it)) }
|
||||
PostActions(context, urlLauncher, viewModel) { clawBackStack.add(Comments(it)) }
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
|
@ -143,8 +143,10 @@ fun Nav3Screen(
|
|||
TopAppBar(
|
||||
modifier = Modifier.shadow(8.dp),
|
||||
navigationIcon = {
|
||||
if (backStack.firstOrNull() !in navDestinations) {
|
||||
IconButton(onClick = { if (backStack.removeLastOrNull() == null) activity?.finish() }) {
|
||||
if (!(clawBackStack.isOnTopLevelRoute())) {
|
||||
IconButton(
|
||||
onClick = { if (clawBackStack.removeLastOrNull() == null) activity?.finish() }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Go back to previous screen",
|
||||
|
@ -159,16 +161,16 @@ fun Nav3Screen(
|
|||
}
|
||||
},
|
||||
title = {
|
||||
if (backStack.firstOrNull() in navDestinations) {
|
||||
if (clawBackStack.isOnTopLevelRoute()) {
|
||||
Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (backStack.firstOrNull() in navDestinations) {
|
||||
IconButton(onClick = { backStack.add(Search) }) {
|
||||
if (clawBackStack.isOnTopLevelRoute()) {
|
||||
IconButton(onClick = { clawBackStack.add(Search) }) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
@ -178,9 +180,9 @@ fun Nav3Screen(
|
|||
bottomBar = {
|
||||
AnimatedVisibility(visible = navigationType == ClawNavigationType.BOTTOM_NAVIGATION) {
|
||||
ClawNavigationBar(
|
||||
backStack,
|
||||
clawBackStack,
|
||||
items = navItems,
|
||||
isVisible = backStack.firstOrNull() in navDestinations,
|
||||
isVisible = clawBackStack.isOnTopLevelRoute(),
|
||||
hazeState = hazeState,
|
||||
)
|
||||
}
|
||||
|
@ -189,9 +191,9 @@ fun Nav3Screen(
|
|||
modifier = Modifier.semantics { testTagsAsResourceId = true },
|
||||
) { contentPadding ->
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
backStack = clawBackStack.backStack,
|
||||
modifier = modifier.hazeSource(hazeState),
|
||||
onBack = { backStack.removeLastOrNull() },
|
||||
onBack = { clawBackStack.removeLastOrNull() },
|
||||
predictivePopTransitionSpec = {
|
||||
slideInHorizontally(initialOffsetX = { -it }, animationSpec = tween(200)) togetherWith
|
||||
slideOutHorizontally(targetOffsetX = { it }, animationSpec = tween(200))
|
||||
|
@ -229,7 +231,7 @@ fun Nav3Screen(
|
|||
SettingsScreen(
|
||||
openInputStream = context.contentResolver::openInputStream,
|
||||
openOutputStream = context.contentResolver::openOutputStream,
|
||||
openLibrariesScreen = { backStack.add(AboutLibraries) },
|
||||
openLibrariesScreen = { clawBackStack.add(AboutLibraries) },
|
||||
importPosts = viewModel::importPosts,
|
||||
exportPostsAsJson = viewModel::exportPostsAsJson,
|
||||
exportPostsAsHtml = viewModel::exportPostsAsHtml,
|
||||
|
@ -245,7 +247,7 @@ fun Nav3Screen(
|
|||
getSeenComments = viewModel::getSeenComments,
|
||||
markSeenComments = viewModel::markSeenComments,
|
||||
contentPadding = contentPadding,
|
||||
openUserProfile = { backStack.add(User(it)) },
|
||||
openUserProfile = { clawBackStack.add(User(it)) },
|
||||
)
|
||||
}
|
||||
entry<User>(
|
||||
|
@ -266,7 +268,7 @@ fun Nav3Screen(
|
|||
username = dest.username,
|
||||
getProfile = viewModel::getUserProfile,
|
||||
contentPadding = contentPadding,
|
||||
openUserProfile = { backStack.add(User(it)) },
|
||||
openUserProfile = { clawBackStack.add(User(it)) },
|
||||
)
|
||||
}
|
||||
entry<Search> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue