From 2481122cf1320f36d47ac030763be72ee35ceb70 Mon Sep 17 00:00:00 2001 From: Artem Chepurnyi Date: Sat, 29 Jun 2024 23:50:58 +0300 Subject: [PATCH] experiment: Multi-stack navigation tabs --- .../keyguard/feature/home/HomeScreen.kt | 55 ++++++++++++++----- .../feature/navigation/NavigationEntry.kt | 10 ++++ .../feature/navigation/NavigationIntent.kt | 2 +- .../navigation/NavigationIntentScope.kt | 7 +++ .../feature/navigation/NavigationRouter.kt | 44 ++++++++------- 5 files changed, 85 insertions(+), 33 deletions(-) create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationIntentScope.kt diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/HomeScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/HomeScreen.kt index a45baaf1..69c5728c 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/HomeScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/HomeScreen.kt @@ -100,7 +100,6 @@ import com.artemchep.keyguard.feature.navigation.NavigationIntent import com.artemchep.keyguard.feature.navigation.NavigationNode import com.artemchep.keyguard.feature.navigation.NavigationRouter 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.sync.SyncRoute import com.artemchep.keyguard.platform.LocalLeContext @@ -162,6 +161,16 @@ private val generatorRoute = GeneratorRoute( private val watchtowerRoute = WatchtowerRoute() +private val settingsRoute = SettingsRoute + +val homeRoutes = listOf( + vaultRoute, + sendsRoute, + generatorRoute, + watchtowerRoute, + settingsRoute, +) + @Composable fun HomeScreen( navBarVisible: Boolean = true, @@ -213,7 +222,7 @@ fun HomeScreen( ), Rail( key = "settings", - route = SettingsRoute, + route = settingsRoute, icon = Icons.Outlined.Settings, iconSelected = Icons.Filled.Settings, label = TextHolder.Res(Res.string.home_settings_label), @@ -829,7 +838,10 @@ private fun isSelected( backStack: List, route: Route, ) = run { - val entry = backStack.getOrNull(1) ?: backStack.firstOrNull() + val entry = backStack + .lastOrNull { entry -> + homeRoutes.any { it === entry.route } + } entry?.route === route } @@ -864,18 +876,35 @@ private fun navigateOnClick( route: Route, ) { val intent = NavigationIntent.Manual { factory -> - // If the route exists in the stack, then simply - // navigate back to it. - val indexOfRoute = backStack.indexOfFirst { it.route === route } - if (indexOfRoute != -1 && indexOfRoute <= 1) { - return@Manual subList(0, indexOfRoute + 1) - .toPersistentList() + kotlin.run { + val index = this.backStack.indexOfFirst { it.route === route } + if (index != -1) { + val end = this.backStack + .drop(index + 1) + .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 left = this.backStack.subList(0, index) + val center = this.backStack.subList(index, end) + val right = this.backStack.subList(end, this.backStack.size) + return@Manual left.toPersistentList() + .addAll(right) + .addAll(center) + } } - val stack = popById(ROUTE_NAME, exclusive = true) - .orEmpty() - .toPersistentList() - stack.add(factory(route)) + val entry = factory(route) + this.backStack.add(entry) } controller.queue(intent) } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationEntry.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationEntry.kt index 017407a5..f4fb8bcc 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationEntry.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationEntry.kt @@ -35,6 +35,8 @@ interface NavigationEntry : BackPressInterceptorHost { val activeBackPressInterceptorsStateFlow: StateFlow> + fun getOrCreate(id: String, create: () -> NavigationEntry): NavigationStack + fun destroy() } @@ -96,6 +98,14 @@ data class NavigationEntryImpl( } } + private val subStacks = mutableMapOf() + + override fun getOrCreate(id: String, create: () -> NavigationEntry): NavigationStack = subStacks + .getOrPut(id) { + val navEntry = create() + NavigationStack(navEntry) + } + override fun destroy() { vm.destroy() job.cancel() diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationIntent.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationIntent.kt index 9b04c8dc..a4b57b75 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationIntent.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationIntent.kt @@ -97,6 +97,6 @@ sealed interface NavigationIntent { // manual data class Manual( - val handle: PersistentList.((Route) -> NavigationEntry) -> PersistentList, + val handle: NavigationIntentScope.((Route) -> NavigationEntry) -> PersistentList, ) : NavigationIntent } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationIntentScope.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationIntentScope.kt new file mode 100644 index 00000000..8f840b02 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationIntentScope.kt @@ -0,0 +1,7 @@ +package com.artemchep.keyguard.feature.navigation + +import kotlinx.collections.immutable.PersistentList + +interface NavigationIntentScope { + val backStack: PersistentList +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationRouter.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationRouter.kt index ca91a67a..ad2d1660 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationRouter.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationRouter.kt @@ -22,19 +22,18 @@ fun NavigationRouter( initial: Route, content: @Composable (PersistentList) -> 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 // destroyed. val f = LocalNavigationNodeLogicalStack.current.last() val parentScope = f.scope - val navStack = remember(id) { - val navEntry = NavigationEntryImpl( + val navStack = f.getOrCreate(id) { + NavigationEntryImpl( source = "router root", id = id, parent = parentScope, route = initial, ) - NavigationStack(navEntry) } val canPop = remember(navStack) { snapshotFlow { navStack.value } @@ -63,7 +62,8 @@ fun NavigationRouter( val backPressInterceptorRegistration = backStack .asReversed() .firstNotNullOfOrNull { navEntry -> - val backPressInterceptors = navEntry.activeBackPressInterceptorsStateFlow.value + val backPressInterceptors = + navEntry.activeBackPressInterceptorsStateFlow.value backPressInterceptors.values.firstOrNull() } if (backPressInterceptorRegistration != null) { @@ -72,7 +72,10 @@ fun NavigationRouter( } } - val newBackStack = backStack + val scope = object : NavigationIntentScope { + override val backStack: PersistentList = backStack + } + val newBackStack = scope .exec( intent = intent, scope = parentScope, @@ -146,29 +149,32 @@ class NavigationStack( } } -private fun PersistentList.exec( +private fun NavigationIntentScope.exec( intent: NavigationIntent, scope: CoroutineScope, ): PersistentList? = when (intent) { is NavigationIntent.Composite -> run compose@{ - var backStack = this + var ns = this for (subIntent in intent.list) { - val new = backStack.exec( + val new = ns.exec( intent = subIntent, scope = scope, - ) - backStack = new - ?: return@compose null + ) ?: return@compose null + ns = object : NavigationIntentScope { + override val backStack: PersistentList + get() = new + } } - backStack + ns.backStack } is NavigationIntent.NavigateToRoute -> kotlin.run { val backStack = when (intent.launchMode) { - NavigationIntent.NavigateToRoute.LaunchMode.DEFAULT -> this + NavigationIntent.NavigateToRoute.LaunchMode.DEFAULT -> backStack NavigationIntent.NavigateToRoute.LaunchMode.SINGLE -> { - val clearedBackStack = removeAll { it.route::class == intent.route::class } - val existingEntry = lastOrNull { it.route == intent.route } + val clearedBackStack = + backStack.removeAll { it.route::class == intent.route::class } + val existingEntry = backStack.lastOrNull { it.route == intent.route } if (existingEntry != null) { // Fast path if existing route matches the new route. return@run clearedBackStack.add(existingEntry) @@ -201,14 +207,14 @@ private fun PersistentList.exec( is NavigationIntent.NavigateToStack -> intent.stack.toPersistentList() is NavigationIntent.Pop -> { - if (size > 0) { - removeAt(size - 1) + if (backStack.size > 0) { + backStack.removeAt(backStack.size - 1) } else { null } } - is NavigationIntent.PopById -> popById(intent.id, intent.exclusive) + is NavigationIntent.PopById -> backStack.popById(intent.id, intent.exclusive) is NavigationIntent.Manual -> { val factory = fun(route: Route): NavigationEntry = NavigationEntryImpl(