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 69c5728c..92388d7e 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 @@ -99,6 +99,7 @@ import com.artemchep.keyguard.feature.navigation.NavigationEntry 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.NavigationStack import com.artemchep.keyguard.feature.navigation.Route import com.artemchep.keyguard.feature.send.SendRoute import com.artemchep.keyguard.feature.sync.SyncRoute @@ -876,35 +877,14 @@ private fun navigateOnClick( route: Route, ) { val intent = NavigationIntent.Manual { factory -> - 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 navStack = getStack( + id = NavigationStack.createIdSuffix(route), + ) + val navEntries = navStack.entries.ifEmpty { + val entry = factory(route) + persistentListOf(entry) } - - val entry = factory(route) - this.backStack.add(entry) + navStack.copy(entries = navEntries) } controller.queue(intent) } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationBackStack.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationBackStack.kt new file mode 100644 index 00000000..8f9ebd6f --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationBackStack.kt @@ -0,0 +1,8 @@ +package com.artemchep.keyguard.feature.navigation + +import kotlinx.collections.immutable.PersistentList + +data class NavigationBackStack( + val id: String, + val entries: PersistentList, +) 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 f4fb8bcc..1356917e 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,7 +35,7 @@ interface NavigationEntry : BackPressInterceptorHost { val activeBackPressInterceptorsStateFlow: StateFlow> - fun getOrCreate(id: String, create: () -> NavigationEntry): NavigationStack + fun getOrCreate(id: String, create: () -> NavigationStack): NavigationPile fun destroy() } @@ -98,12 +98,15 @@ data class NavigationEntryImpl( } } - private val subStacks = mutableMapOf() + private val subStacks = mutableMapOf() - override fun getOrCreate(id: String, create: () -> NavigationEntry): NavigationStack = subStacks + override fun getOrCreate( + id: String, + create: () -> NavigationStack, + ): NavigationPile = subStacks .getOrPut(id) { - val navEntry = create() - NavigationStack(navEntry) + val navStack = create() + NavigationPile(navStack) } override fun destroy() { 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 a4b57b75..0ebfba15 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: NavigationIntentScope.((Route) -> NavigationEntry) -> PersistentList, + val handle: NavigationIntentScope.((Route) -> NavigationEntry) -> NavigationBackStack, ) : 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 index 8f840b02..fab0d62f 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationIntentScope.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationIntentScope.kt @@ -1,7 +1,12 @@ package com.artemchep.keyguard.feature.navigation -import kotlinx.collections.immutable.PersistentList - interface NavigationIntentScope { - val backStack: PersistentList + val backStack: NavigationBackStack + + /** + * If there are multiple stacks, then find and + * return the one that has this ID, otherwise + * create a new navigation stack. + */ + fun getStack(id: String): NavigationBackStack } 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 ad2d1660..f5c0e926 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 @@ -27,39 +27,54 @@ fun NavigationRouter( // destroyed. val f = LocalNavigationNodeLogicalStack.current.last() val parentScope = f.scope - val navStack = f.getOrCreate(id) { - NavigationEntryImpl( + + val navStackPrefix = id + val navPile = f.getOrCreate(id) { + val entry = NavigationEntryImpl( source = "router root", id = id, parent = parentScope, route = initial, ) + NavigationStack( + id = NavigationStack.createId( + prefix = navStackPrefix, + suffix = NavigationStack.createIdSuffix(initial), + ), + entry = entry, + ) } - val canPop = remember(navStack) { - snapshotFlow { navStack.value } - .flatMapLatest { stack -> - val navEntry = stack.lastOrNull() - if (navEntry != null) { - navEntry + + val canPop = remember(navPile) { + snapshotFlow { navPile.value } + .flatMapLatest { pile -> + val stack = pile.lastOrNull() + val entry = stack?.value?.lastOrNull() + if (entry != null) { + return@flatMapLatest entry .activeBackPressInterceptorsStateFlow .map { interceptors -> - interceptors.isNotEmpty() || stack.size > 1 + interceptors.isNotEmpty() || stack.value.size > 1 || pile.size > 1 } - } else { - flowOf(false) } + + flowOf(false) } } NavigationController( canPop = canPop, handle = { intent -> - val backStack = navStack.value + val primaryNavStack = navPile.value.lastOrNull() + ?.toImmutableModel() + // Should never happen + ?: return@NavigationController NavigationIntent.Pop // If the navigation intent is a simple pop, then give it to // the back press interceptors first and only then adjust the // navigation stack. if (intent is NavigationIntent.Pop) { - val backPressInterceptorRegistration = backStack + val backPressInterceptorRegistration = primaryNavStack + .entries .asReversed() .firstNotNullOfOrNull { navEntry -> val backPressInterceptors = @@ -73,32 +88,72 @@ fun NavigationRouter( } val scope = object : NavigationIntentScope { - override val backStack: PersistentList = backStack + override val backStack: NavigationBackStack = primaryNavStack + + override fun getStack(id: String) = kotlin.run { + val navStackId = NavigationStack.createId( + prefix = navStackPrefix, + suffix = id, + ) + val navStack = navPile.value.firstOrNull { it.id == navStackId } + ?: return@run NavigationBackStack( + id = navStackId, + entries = persistentListOf(), + ) + navStack.toImmutableModel() + } } - val newBackStack = scope + + val newNavStack = scope .exec( intent = intent, scope = parentScope, ) - when { - newBackStack == null -> { - // The intent was not handled, pass it to the next - // navigation controller. - return@NavigationController intent + if (newNavStack == null) { + // The intent was not handled, pass it to the next + // navigation controller. + return@NavigationController intent + } + + // Reorder navigation pile + val newNavPile = run { + // Modify existing navigation stack, this is + // needed to maintain the lifecycle of the + // navigation entries. + val existingNavStack = run { + navPile.value.firstOrNull { it.id == newNavStack.id } + ?.apply { + // Change the values of the existing stack + value = newNavStack.entries + } + ?: NavigationStack( + id = newNavStack.id, + entries = newNavStack.entries, + ) } - newBackStack.isEmpty() -> { + navPile.value + .removeAll { it.id == newNavStack.id } + .add(existingNavStack) + .removeAll { it.value.isEmpty() } + } + + when { + newNavPile.isEmpty() -> { NavigationIntent.Pop } else -> { - navStack.value = newBackStack + navPile.value = newNavPile // We have handled the navigation intent. null } } }, ) { controller -> + val navStack = navPile.value.lastOrNull() + ?: return@NavigationController + val localBackStack = navStack.value val globalBackStack = LocalNavigationNodeLogicalStack.current.addAll(localBackStack) val backHandler = LocalNavigationBackHandler.current @@ -122,14 +177,50 @@ fun NavigationRouter( } } -class NavigationStack( - entry: NavigationEntry, +class NavigationPile( + stack: NavigationStack, ) { private val _state = kotlin.run { - val initialState = persistentListOf(entry) + val initialState = persistentListOf(stack) mutableStateOf(initialState) } + var value: PersistentList + get() = _state.value + set(value) { + val oldValue = _state.value + + val removedItems = mutableListOf() + oldValue.forEach { e -> + val exists = value.any { it.id === e.id } + if (!exists) { + removedItems += e + } + } + removedItems.forEach { stack -> + stack.value.forEach { entry -> + entry.destroy() + } + } + _state.value = value + } +} + +class NavigationStack( + val id: String, + entries: PersistentList, +) { + companion object { + fun createId( + prefix: String, + suffix: String, + ) = "$prefix|$suffix" + + fun createIdSuffix(route: Route) = route::class.qualifiedName.orEmpty() + } + + private val _state = mutableStateOf(entries) + var value: PersistentList get() = _state.value set(value) { @@ -142,17 +233,30 @@ class NavigationStack( removedItems += e } } - removedItems.forEach { - it.destroy() + removedItems.forEach { entry -> + entry.destroy() } _state.value = value } + + constructor( + id: String, + entry: NavigationEntry, + ) : this( + id = id, + entries = persistentListOf(entry), + ) + + fun toImmutableModel() = NavigationBackStack( + id = id, + entries = value, + ) } private fun NavigationIntentScope.exec( intent: NavigationIntent, scope: CoroutineScope, -): PersistentList? = when (intent) { +): NavigationBackStack? = when (intent) { is NavigationIntent.Composite -> run compose@{ var ns = this for (subIntent in intent.list) { @@ -161,23 +265,27 @@ private fun NavigationIntentScope.exec( scope = scope, ) ?: return@compose null ns = object : NavigationIntentScope { - override val backStack: PersistentList + override val backStack: NavigationBackStack get() = new + + override fun getStack(id: String) = this@exec.getStack(id) } } ns.backStack } is NavigationIntent.NavigateToRoute -> kotlin.run { - val backStack = when (intent.launchMode) { - NavigationIntent.NavigateToRoute.LaunchMode.DEFAULT -> backStack + val entries = when (intent.launchMode) { + NavigationIntent.NavigateToRoute.LaunchMode.DEFAULT -> backStack.entries NavigationIntent.NavigateToRoute.LaunchMode.SINGLE -> { val clearedBackStack = - backStack.removeAll { it.route::class == intent.route::class } - val existingEntry = backStack.lastOrNull { it.route == intent.route } + backStack.entries.removeAll { it.route::class == intent.route::class } + val existingEntry = backStack.entries.lastOrNull { it.route == intent.route } if (existingEntry != null) { // Fast path if existing route matches the new route. - return@run clearedBackStack.add(existingEntry) + return@run backStack.copy( + entries = clearedBackStack.add(existingEntry), + ) } clearedBackStack @@ -191,7 +299,9 @@ private fun NavigationIntentScope.exec( parent = scope, route = r, ) - backStack.add(e) + backStack.copy( + entries = entries.add(e), + ) } is NavigationIntent.SetRoute -> { @@ -202,19 +312,33 @@ private fun NavigationIntentScope.exec( parent = scope, route = r, ) - persistentListOf(e) + backStack.copy( + entries = persistentListOf(e), + ) + } + + is NavigationIntent.NavigateToStack -> { + backStack.copy( + entries = intent.stack.toPersistentList(), + ) } - is NavigationIntent.NavigateToStack -> intent.stack.toPersistentList() is NavigationIntent.Pop -> { - if (backStack.size > 0) { - backStack.removeAt(backStack.size - 1) + val entries = backStack.entries + if (entries.size > 0) { + val newEntries = entries.removeAt(entries.size - 1) + backStack.copy(entries = newEntries) } else { null } } - is NavigationIntent.PopById -> backStack.popById(intent.id, intent.exclusive) + is NavigationIntent.PopById -> run { + val newEntries = backStack.entries.popById(intent.id, intent.exclusive) + ?: return@run null + backStack.copy(entries = newEntries) + } + is NavigationIntent.Manual -> { val factory = fun(route: Route): NavigationEntry = NavigationEntryImpl(