experiment: Multi-stack navigation tabs

This commit is contained in:
Artem Chepurnyi 2024-06-29 23:50:58 +03:00
parent 0586153b7b
commit 2481122cf1
5 changed files with 85 additions and 33 deletions

View File

@ -100,7 +100,6 @@ import com.artemchep.keyguard.feature.navigation.NavigationIntent
import com.artemchep.keyguard.feature.navigation.NavigationNode import com.artemchep.keyguard.feature.navigation.NavigationNode
import com.artemchep.keyguard.feature.navigation.NavigationRouter import com.artemchep.keyguard.feature.navigation.NavigationRouter
import com.artemchep.keyguard.feature.navigation.Route import com.artemchep.keyguard.feature.navigation.Route
import com.artemchep.keyguard.feature.navigation.popById
import com.artemchep.keyguard.feature.send.SendRoute import com.artemchep.keyguard.feature.send.SendRoute
import com.artemchep.keyguard.feature.sync.SyncRoute import com.artemchep.keyguard.feature.sync.SyncRoute
import com.artemchep.keyguard.platform.LocalLeContext import com.artemchep.keyguard.platform.LocalLeContext
@ -162,6 +161,16 @@ private val generatorRoute = GeneratorRoute(
private val watchtowerRoute = WatchtowerRoute() private val watchtowerRoute = WatchtowerRoute()
private val settingsRoute = SettingsRoute
val homeRoutes = listOf(
vaultRoute,
sendsRoute,
generatorRoute,
watchtowerRoute,
settingsRoute,
)
@Composable @Composable
fun HomeScreen( fun HomeScreen(
navBarVisible: Boolean = true, navBarVisible: Boolean = true,
@ -213,7 +222,7 @@ fun HomeScreen(
), ),
Rail( Rail(
key = "settings", key = "settings",
route = SettingsRoute, route = settingsRoute,
icon = Icons.Outlined.Settings, icon = Icons.Outlined.Settings,
iconSelected = Icons.Filled.Settings, iconSelected = Icons.Filled.Settings,
label = TextHolder.Res(Res.string.home_settings_label), label = TextHolder.Res(Res.string.home_settings_label),
@ -829,7 +838,10 @@ private fun isSelected(
backStack: List<NavigationEntry>, backStack: List<NavigationEntry>,
route: Route, route: Route,
) = run { ) = run {
val entry = backStack.getOrNull(1) ?: backStack.firstOrNull() val entry = backStack
.lastOrNull { entry ->
homeRoutes.any { it === entry.route }
}
entry?.route === route entry?.route === route
} }
@ -864,18 +876,35 @@ private fun navigateOnClick(
route: Route, route: Route,
) { ) {
val intent = NavigationIntent.Manual { factory -> val intent = NavigationIntent.Manual { factory ->
// If the route exists in the stack, then simply kotlin.run {
// navigate back to it. val index = this.backStack.indexOfFirst { it.route === route }
val indexOfRoute = backStack.indexOfFirst { it.route === route } if (index != -1) {
if (indexOfRoute != -1 && indexOfRoute <= 1) { val end = this.backStack
return@Manual subList(0, indexOfRoute + 1) .drop(index + 1)
.toPersistentList() .indexOfFirst { entry ->
homeRoutes.any { it === entry.route }
}
// Check if that's the end of list
// or a middle part.
.let {
if (it != -1) {
index + it + 1
} else {
this.backStack.size
}
} }
val stack = popById(ROUTE_NAME, exclusive = true) val left = this.backStack.subList(0, index)
.orEmpty() val center = this.backStack.subList(index, end)
.toPersistentList() val right = this.backStack.subList(end, this.backStack.size)
stack.add(factory(route)) return@Manual left.toPersistentList()
.addAll(right)
.addAll(center)
}
}
val entry = factory(route)
this.backStack.add(entry)
} }
controller.queue(intent) controller.queue(intent)
} }

View File

@ -35,6 +35,8 @@ interface NavigationEntry : BackPressInterceptorHost {
val activeBackPressInterceptorsStateFlow: StateFlow<ImmutableMap<String, BackPressInterceptorRegistration>> val activeBackPressInterceptorsStateFlow: StateFlow<ImmutableMap<String, BackPressInterceptorRegistration>>
fun getOrCreate(id: String, create: () -> NavigationEntry): NavigationStack
fun destroy() fun destroy()
} }
@ -96,6 +98,14 @@ data class NavigationEntryImpl(
} }
} }
private val subStacks = mutableMapOf<String, NavigationStack>()
override fun getOrCreate(id: String, create: () -> NavigationEntry): NavigationStack = subStacks
.getOrPut(id) {
val navEntry = create()
NavigationStack(navEntry)
}
override fun destroy() { override fun destroy() {
vm.destroy() vm.destroy()
job.cancel() job.cancel()

View File

@ -97,6 +97,6 @@ sealed interface NavigationIntent {
// manual // manual
data class Manual( data class Manual(
val handle: PersistentList<NavigationEntry>.((Route) -> NavigationEntry) -> PersistentList<NavigationEntry>, val handle: NavigationIntentScope.((Route) -> NavigationEntry) -> PersistentList<NavigationEntry>,
) : NavigationIntent ) : NavigationIntent
} }

View File

@ -0,0 +1,7 @@
package com.artemchep.keyguard.feature.navigation
import kotlinx.collections.immutable.PersistentList
interface NavigationIntentScope {
val backStack: PersistentList<NavigationEntry>
}

View File

@ -22,19 +22,18 @@ fun NavigationRouter(
initial: Route, initial: Route,
content: @Composable (PersistentList<NavigationEntry>) -> Unit, content: @Composable (PersistentList<NavigationEntry>) -> Unit,
) { ) {
// Fid the top-level router and link the entry's lifecycle // Find the top-level router and link the entry's lifecycle
// to it, so if the top level gets destroyed we also get // to it, so if the top level gets destroyed we also get
// destroyed. // destroyed.
val f = LocalNavigationNodeLogicalStack.current.last() val f = LocalNavigationNodeLogicalStack.current.last()
val parentScope = f.scope val parentScope = f.scope
val navStack = remember(id) { val navStack = f.getOrCreate(id) {
val navEntry = NavigationEntryImpl( NavigationEntryImpl(
source = "router root", source = "router root",
id = id, id = id,
parent = parentScope, parent = parentScope,
route = initial, route = initial,
) )
NavigationStack(navEntry)
} }
val canPop = remember(navStack) { val canPop = remember(navStack) {
snapshotFlow { navStack.value } snapshotFlow { navStack.value }
@ -63,7 +62,8 @@ fun NavigationRouter(
val backPressInterceptorRegistration = backStack val backPressInterceptorRegistration = backStack
.asReversed() .asReversed()
.firstNotNullOfOrNull { navEntry -> .firstNotNullOfOrNull { navEntry ->
val backPressInterceptors = navEntry.activeBackPressInterceptorsStateFlow.value val backPressInterceptors =
navEntry.activeBackPressInterceptorsStateFlow.value
backPressInterceptors.values.firstOrNull() backPressInterceptors.values.firstOrNull()
} }
if (backPressInterceptorRegistration != null) { if (backPressInterceptorRegistration != null) {
@ -72,7 +72,10 @@ fun NavigationRouter(
} }
} }
val newBackStack = backStack val scope = object : NavigationIntentScope {
override val backStack: PersistentList<NavigationEntry> = backStack
}
val newBackStack = scope
.exec( .exec(
intent = intent, intent = intent,
scope = parentScope, scope = parentScope,
@ -146,29 +149,32 @@ class NavigationStack(
} }
} }
private fun PersistentList<NavigationEntry>.exec( private fun NavigationIntentScope.exec(
intent: NavigationIntent, intent: NavigationIntent,
scope: CoroutineScope, scope: CoroutineScope,
): PersistentList<NavigationEntry>? = when (intent) { ): PersistentList<NavigationEntry>? = when (intent) {
is NavigationIntent.Composite -> run compose@{ is NavigationIntent.Composite -> run compose@{
var backStack = this var ns = this
for (subIntent in intent.list) { for (subIntent in intent.list) {
val new = backStack.exec( val new = ns.exec(
intent = subIntent, intent = subIntent,
scope = scope, scope = scope,
) ) ?: return@compose null
backStack = new ns = object : NavigationIntentScope {
?: return@compose null override val backStack: PersistentList<NavigationEntry>
get() = new
} }
backStack }
ns.backStack
} }
is NavigationIntent.NavigateToRoute -> kotlin.run { is NavigationIntent.NavigateToRoute -> kotlin.run {
val backStack = when (intent.launchMode) { val backStack = when (intent.launchMode) {
NavigationIntent.NavigateToRoute.LaunchMode.DEFAULT -> this NavigationIntent.NavigateToRoute.LaunchMode.DEFAULT -> backStack
NavigationIntent.NavigateToRoute.LaunchMode.SINGLE -> { NavigationIntent.NavigateToRoute.LaunchMode.SINGLE -> {
val clearedBackStack = removeAll { it.route::class == intent.route::class } val clearedBackStack =
val existingEntry = lastOrNull { it.route == intent.route } backStack.removeAll { it.route::class == intent.route::class }
val existingEntry = backStack.lastOrNull { it.route == intent.route }
if (existingEntry != null) { if (existingEntry != null) {
// Fast path if existing route matches the new route. // Fast path if existing route matches the new route.
return@run clearedBackStack.add(existingEntry) return@run clearedBackStack.add(existingEntry)
@ -201,14 +207,14 @@ private fun PersistentList<NavigationEntry>.exec(
is NavigationIntent.NavigateToStack -> intent.stack.toPersistentList() is NavigationIntent.NavigateToStack -> intent.stack.toPersistentList()
is NavigationIntent.Pop -> { is NavigationIntent.Pop -> {
if (size > 0) { if (backStack.size > 0) {
removeAt(size - 1) backStack.removeAt(backStack.size - 1)
} else { } else {
null null
} }
} }
is NavigationIntent.PopById -> popById(intent.id, intent.exclusive) is NavigationIntent.PopById -> backStack.popById(intent.id, intent.exclusive)
is NavigationIntent.Manual -> { is NavigationIntent.Manual -> {
val factory = fun(route: Route): NavigationEntry = val factory = fun(route: Route): NavigationEntry =
NavigationEntryImpl( NavigationEntryImpl(