experiment: Multi-stack navigation tabs
This commit is contained in:
parent
0586153b7b
commit
2481122cf1
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.artemchep.keyguard.feature.navigation
|
||||||
|
|
||||||
|
import kotlinx.collections.immutable.PersistentList
|
||||||
|
|
||||||
|
interface NavigationIntentScope {
|
||||||
|
val backStack: PersistentList<NavigationEntry>
|
||||||
|
}
|
@ -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(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user