feature: Multi-stack navigation tabs
This commit is contained in:
parent
2481122cf1
commit
d53d75550b
|
@ -99,6 +99,7 @@ import com.artemchep.keyguard.feature.navigation.NavigationEntry
|
||||||
import com.artemchep.keyguard.feature.navigation.NavigationIntent
|
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.NavigationStack
|
||||||
import com.artemchep.keyguard.feature.navigation.Route
|
import com.artemchep.keyguard.feature.navigation.Route
|
||||||
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
|
||||||
|
@ -876,35 +877,14 @@ private fun navigateOnClick(
|
||||||
route: Route,
|
route: Route,
|
||||||
) {
|
) {
|
||||||
val intent = NavigationIntent.Manual { factory ->
|
val intent = NavigationIntent.Manual { factory ->
|
||||||
kotlin.run {
|
val navStack = getStack(
|
||||||
val index = this.backStack.indexOfFirst { it.route === route }
|
id = NavigationStack.createIdSuffix(route),
|
||||||
if (index != -1) {
|
)
|
||||||
val end = this.backStack
|
val navEntries = navStack.entries.ifEmpty {
|
||||||
.drop(index + 1)
|
val entry = factory(route)
|
||||||
.indexOfFirst { entry ->
|
persistentListOf(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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
navStack.copy(entries = navEntries)
|
||||||
val entry = factory(route)
|
|
||||||
this.backStack.add(entry)
|
|
||||||
}
|
}
|
||||||
controller.queue(intent)
|
controller.queue(intent)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.artemchep.keyguard.feature.navigation
|
||||||
|
|
||||||
|
import kotlinx.collections.immutable.PersistentList
|
||||||
|
|
||||||
|
data class NavigationBackStack(
|
||||||
|
val id: String,
|
||||||
|
val entries: PersistentList<NavigationEntry>,
|
||||||
|
)
|
|
@ -35,7 +35,7 @@ interface NavigationEntry : BackPressInterceptorHost {
|
||||||
|
|
||||||
val activeBackPressInterceptorsStateFlow: StateFlow<ImmutableMap<String, BackPressInterceptorRegistration>>
|
val activeBackPressInterceptorsStateFlow: StateFlow<ImmutableMap<String, BackPressInterceptorRegistration>>
|
||||||
|
|
||||||
fun getOrCreate(id: String, create: () -> NavigationEntry): NavigationStack
|
fun getOrCreate(id: String, create: () -> NavigationStack): NavigationPile
|
||||||
|
|
||||||
fun destroy()
|
fun destroy()
|
||||||
}
|
}
|
||||||
|
@ -98,12 +98,15 @@ data class NavigationEntryImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val subStacks = mutableMapOf<String, NavigationStack>()
|
private val subStacks = mutableMapOf<String, NavigationPile>()
|
||||||
|
|
||||||
override fun getOrCreate(id: String, create: () -> NavigationEntry): NavigationStack = subStacks
|
override fun getOrCreate(
|
||||||
|
id: String,
|
||||||
|
create: () -> NavigationStack,
|
||||||
|
): NavigationPile = subStacks
|
||||||
.getOrPut(id) {
|
.getOrPut(id) {
|
||||||
val navEntry = create()
|
val navStack = create()
|
||||||
NavigationStack(navEntry)
|
NavigationPile(navStack)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun destroy() {
|
override fun destroy() {
|
||||||
|
|
|
@ -97,6 +97,6 @@ sealed interface NavigationIntent {
|
||||||
// manual
|
// manual
|
||||||
|
|
||||||
data class Manual(
|
data class Manual(
|
||||||
val handle: NavigationIntentScope.((Route) -> NavigationEntry) -> PersistentList<NavigationEntry>,
|
val handle: NavigationIntentScope.((Route) -> NavigationEntry) -> NavigationBackStack,
|
||||||
) : NavigationIntent
|
) : NavigationIntent
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
package com.artemchep.keyguard.feature.navigation
|
package com.artemchep.keyguard.feature.navigation
|
||||||
|
|
||||||
import kotlinx.collections.immutable.PersistentList
|
|
||||||
|
|
||||||
interface NavigationIntentScope {
|
interface NavigationIntentScope {
|
||||||
val backStack: PersistentList<NavigationEntry>
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,39 +27,54 @@ fun NavigationRouter(
|
||||||
// destroyed.
|
// destroyed.
|
||||||
val f = LocalNavigationNodeLogicalStack.current.last()
|
val f = LocalNavigationNodeLogicalStack.current.last()
|
||||||
val parentScope = f.scope
|
val parentScope = f.scope
|
||||||
val navStack = f.getOrCreate(id) {
|
|
||||||
NavigationEntryImpl(
|
val navStackPrefix = id
|
||||||
|
val navPile = f.getOrCreate(id) {
|
||||||
|
val entry = NavigationEntryImpl(
|
||||||
source = "router root",
|
source = "router root",
|
||||||
id = id,
|
id = id,
|
||||||
parent = parentScope,
|
parent = parentScope,
|
||||||
route = initial,
|
route = initial,
|
||||||
)
|
)
|
||||||
|
NavigationStack(
|
||||||
|
id = NavigationStack.createId(
|
||||||
|
prefix = navStackPrefix,
|
||||||
|
suffix = NavigationStack.createIdSuffix(initial),
|
||||||
|
),
|
||||||
|
entry = entry,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val canPop = remember(navStack) {
|
|
||||||
snapshotFlow { navStack.value }
|
val canPop = remember(navPile) {
|
||||||
.flatMapLatest { stack ->
|
snapshotFlow { navPile.value }
|
||||||
val navEntry = stack.lastOrNull()
|
.flatMapLatest { pile ->
|
||||||
if (navEntry != null) {
|
val stack = pile.lastOrNull()
|
||||||
navEntry
|
val entry = stack?.value?.lastOrNull()
|
||||||
|
if (entry != null) {
|
||||||
|
return@flatMapLatest entry
|
||||||
.activeBackPressInterceptorsStateFlow
|
.activeBackPressInterceptorsStateFlow
|
||||||
.map { interceptors ->
|
.map { interceptors ->
|
||||||
interceptors.isNotEmpty() || stack.size > 1
|
interceptors.isNotEmpty() || stack.value.size > 1 || pile.size > 1
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
flowOf(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flowOf(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NavigationController(
|
NavigationController(
|
||||||
canPop = canPop,
|
canPop = canPop,
|
||||||
handle = { intent ->
|
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
|
// If the navigation intent is a simple pop, then give it to
|
||||||
// the back press interceptors first and only then adjust the
|
// the back press interceptors first and only then adjust the
|
||||||
// navigation stack.
|
// navigation stack.
|
||||||
if (intent is NavigationIntent.Pop) {
|
if (intent is NavigationIntent.Pop) {
|
||||||
val backPressInterceptorRegistration = backStack
|
val backPressInterceptorRegistration = primaryNavStack
|
||||||
|
.entries
|
||||||
.asReversed()
|
.asReversed()
|
||||||
.firstNotNullOfOrNull { navEntry ->
|
.firstNotNullOfOrNull { navEntry ->
|
||||||
val backPressInterceptors =
|
val backPressInterceptors =
|
||||||
|
@ -73,32 +88,72 @@ fun NavigationRouter(
|
||||||
}
|
}
|
||||||
|
|
||||||
val scope = object : NavigationIntentScope {
|
val scope = object : NavigationIntentScope {
|
||||||
override val backStack: PersistentList<NavigationEntry> = 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(
|
.exec(
|
||||||
intent = intent,
|
intent = intent,
|
||||||
scope = parentScope,
|
scope = parentScope,
|
||||||
)
|
)
|
||||||
when {
|
if (newNavStack == null) {
|
||||||
newBackStack == null -> {
|
// The intent was not handled, pass it to the next
|
||||||
// The intent was not handled, pass it to the next
|
// navigation controller.
|
||||||
// navigation controller.
|
return@NavigationController intent
|
||||||
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
|
NavigationIntent.Pop
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
navStack.value = newBackStack
|
navPile.value = newNavPile
|
||||||
// We have handled the navigation intent.
|
// We have handled the navigation intent.
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { controller ->
|
) { controller ->
|
||||||
|
val navStack = navPile.value.lastOrNull()
|
||||||
|
?: return@NavigationController
|
||||||
|
|
||||||
val localBackStack = navStack.value
|
val localBackStack = navStack.value
|
||||||
val globalBackStack = LocalNavigationNodeLogicalStack.current.addAll(localBackStack)
|
val globalBackStack = LocalNavigationNodeLogicalStack.current.addAll(localBackStack)
|
||||||
val backHandler = LocalNavigationBackHandler.current
|
val backHandler = LocalNavigationBackHandler.current
|
||||||
|
@ -122,14 +177,50 @@ fun NavigationRouter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NavigationStack(
|
class NavigationPile(
|
||||||
entry: NavigationEntry,
|
stack: NavigationStack,
|
||||||
) {
|
) {
|
||||||
private val _state = kotlin.run {
|
private val _state = kotlin.run {
|
||||||
val initialState = persistentListOf(entry)
|
val initialState = persistentListOf(stack)
|
||||||
mutableStateOf(initialState)
|
mutableStateOf(initialState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var value: PersistentList<NavigationStack>
|
||||||
|
get() = _state.value
|
||||||
|
set(value) {
|
||||||
|
val oldValue = _state.value
|
||||||
|
|
||||||
|
val removedItems = mutableListOf<NavigationStack>()
|
||||||
|
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<NavigationEntry>,
|
||||||
|
) {
|
||||||
|
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<NavigationEntry>
|
var value: PersistentList<NavigationEntry>
|
||||||
get() = _state.value
|
get() = _state.value
|
||||||
set(value) {
|
set(value) {
|
||||||
|
@ -142,17 +233,30 @@ class NavigationStack(
|
||||||
removedItems += e
|
removedItems += e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
removedItems.forEach {
|
removedItems.forEach { entry ->
|
||||||
it.destroy()
|
entry.destroy()
|
||||||
}
|
}
|
||||||
_state.value = value
|
_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(
|
private fun NavigationIntentScope.exec(
|
||||||
intent: NavigationIntent,
|
intent: NavigationIntent,
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
): PersistentList<NavigationEntry>? = when (intent) {
|
): NavigationBackStack? = when (intent) {
|
||||||
is NavigationIntent.Composite -> run compose@{
|
is NavigationIntent.Composite -> run compose@{
|
||||||
var ns = this
|
var ns = this
|
||||||
for (subIntent in intent.list) {
|
for (subIntent in intent.list) {
|
||||||
|
@ -161,23 +265,27 @@ private fun NavigationIntentScope.exec(
|
||||||
scope = scope,
|
scope = scope,
|
||||||
) ?: return@compose null
|
) ?: return@compose null
|
||||||
ns = object : NavigationIntentScope {
|
ns = object : NavigationIntentScope {
|
||||||
override val backStack: PersistentList<NavigationEntry>
|
override val backStack: NavigationBackStack
|
||||||
get() = new
|
get() = new
|
||||||
|
|
||||||
|
override fun getStack(id: String) = this@exec.getStack(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ns.backStack
|
ns.backStack
|
||||||
}
|
}
|
||||||
|
|
||||||
is NavigationIntent.NavigateToRoute -> kotlin.run {
|
is NavigationIntent.NavigateToRoute -> kotlin.run {
|
||||||
val backStack = when (intent.launchMode) {
|
val entries = when (intent.launchMode) {
|
||||||
NavigationIntent.NavigateToRoute.LaunchMode.DEFAULT -> backStack
|
NavigationIntent.NavigateToRoute.LaunchMode.DEFAULT -> backStack.entries
|
||||||
NavigationIntent.NavigateToRoute.LaunchMode.SINGLE -> {
|
NavigationIntent.NavigateToRoute.LaunchMode.SINGLE -> {
|
||||||
val clearedBackStack =
|
val clearedBackStack =
|
||||||
backStack.removeAll { it.route::class == intent.route::class }
|
backStack.entries.removeAll { it.route::class == intent.route::class }
|
||||||
val existingEntry = backStack.lastOrNull { it.route == intent.route }
|
val existingEntry = backStack.entries.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 backStack.copy(
|
||||||
|
entries = clearedBackStack.add(existingEntry),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
clearedBackStack
|
clearedBackStack
|
||||||
|
@ -191,7 +299,9 @@ private fun NavigationIntentScope.exec(
|
||||||
parent = scope,
|
parent = scope,
|
||||||
route = r,
|
route = r,
|
||||||
)
|
)
|
||||||
backStack.add(e)
|
backStack.copy(
|
||||||
|
entries = entries.add(e),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is NavigationIntent.SetRoute -> {
|
is NavigationIntent.SetRoute -> {
|
||||||
|
@ -202,19 +312,33 @@ private fun NavigationIntentScope.exec(
|
||||||
parent = scope,
|
parent = scope,
|
||||||
route = r,
|
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 -> {
|
is NavigationIntent.Pop -> {
|
||||||
if (backStack.size > 0) {
|
val entries = backStack.entries
|
||||||
backStack.removeAt(backStack.size - 1)
|
if (entries.size > 0) {
|
||||||
|
val newEntries = entries.removeAt(entries.size - 1)
|
||||||
|
backStack.copy(entries = newEntries)
|
||||||
} else {
|
} else {
|
||||||
null
|
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 -> {
|
is NavigationIntent.Manual -> {
|
||||||
val factory = fun(route: Route): NavigationEntry =
|
val factory = fun(route: Route): NavigationEntry =
|
||||||
NavigationEntryImpl(
|
NavigationEntryImpl(
|
||||||
|
|
Loading…
Reference in New Issue