-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathTabBarIntentResolver.kt
158 lines (143 loc) · 6.87 KB
/
TabBarIntentResolver.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package com.adamkobus.compose.navigation
import com.adamkobus.compose.navigation.TabStateSavingBehaviour.DONT_SAVE
import com.adamkobus.compose.navigation.TabStateSavingBehaviour.SAVE_ALL
import com.adamkobus.compose.navigation.TabStateSavingBehaviour.SAVE_START_DESTINATION
import com.adamkobus.compose.navigation.action.NavAction
import com.adamkobus.compose.navigation.action.navActionOptions
import com.adamkobus.compose.navigation.destination.NavGraph
import com.adamkobus.compose.navigation.destination.NavState
import com.adamkobus.compose.navigation.intent.NavIntent
import com.adamkobus.compose.navigation.intent.ResolveResult
/**
* Use this [NavIntentResolver] to handle edge cases related to navigation using tab bar.
*
* Cases handled by this resolver:
* 1. Back stack doesn't contain tab bar root graph - no action is produced as this is not supported by this resolver
* 2. Current destination already matches the starting destination linked of the graph to which given intent maps.
* No action is taken in such case
* 3. Starting destination of the graph mapped with [tabsMapping] from the processed intent is already present in back stack.
* Back stack will be popped to starting destination.
* 4. Current destination doesn't belong to any of the tab items' graphs. No action will be taken.
* It can happen if someone clicked tab item at the same item as some other interaction happened which opened new flow in the app.
* 5. Current destination belongs to different tab item. In this case navigation will happen.
* Also, the state of the current tab will be saved depending on [tabStateSavingBehaviour]
*
* @param tabsMapping Assumption is that content displayed for each tab item lives in its own [NavGraph].
* This map tells [TabBarIntentResolver] which tab item's graph should be opened for provided intents.
*
* @param tabsRootGraph Assumption is that tab items' graphs are nested in additional graph which is dedicated for tab host content only.
* This is required for this resolved to function properly.
*
* @param popToTabHostIntent optional intent.
* When used, [TabBarIntentResolver] will try to pop back stack to the latest destination in back stack that belongs to tab item content.
*
* @param tabStateSavingBehaviour Configures what parts of the tab item content should
* [TabBarIntentResolver] attempt to preserve during navigation
*
* @see TabStateSavingBehaviour
* @see NavIntentResolver
*/
@Suppress("ReturnCount")
open class TabBarIntentResolver(
private val tabsMapping: Map<String, NavGraph>,
private val tabsRootGraph: NavGraph,
private val popToTabHostIntent: NavIntent? = null,
private val tabStateSavingBehaviour: TabStateSavingBehaviour = SAVE_START_DESTINATION,
) : NavIntentResolver {
private val allGraphs = tabsMapping.values.toSet()
override suspend fun resolve(
intent: NavIntent,
navState: NavState,
): ResolveResult {
if (intent == popToTabHostIntent) {
return handlePopIntent(navState)
}
return resolveInternal(intent, navState)?.let {
ResolveResult.Action(it)
} ?: ResolveResult.None
}
private fun resolveInternal(
intent: NavIntent,
navState: NavState,
): NavAction? {
val mappedGraph = tabsMapping[intent.name] ?: return null
intent.origin?.let {
if (!navState.isCurrent(it)) return null
}
val graphStartDestination = mappedGraph.startDestination()
// no controller with provided tab host was found
val controllerState = navState.find(tabsRootGraph) ?: return null
// this resolver supports only navigation when tab host is already launched (as in, it handles only tab items clicks)
val currentDest = controllerState.currentDestination?.destination ?: return null
// this resolver supports only navigation when tab host is already launched (as in, it handles only tab items clicks)
if (!navState.isInBackStack(tabsRootGraph)) return null
// we're already at the destination that clicking this tab would take us to
if (currentDest == graphStartDestination) return null
val navOptions =
if (currentDest.graph !in allGraphs) {
intent.popOptions?.copy(launchSingleTop = true)
} else {
navActionOptions {
popUpTo(graphStartDestination)
launchSingleTop = true
}
}
// Current destination doesn't belong to tab host and intent didn't provide nav options
if (navOptions == null) return null
// tab item's starting destination is already in back stack and we can pop back to it
if (navState.isInBackStack(graphStartDestination)) {
return currentDest goTo graphStartDestination withOptions
navActionOptions {
popUpTo(graphStartDestination)
launchSingleTop = true
}
}
// we know we're in tab host based on the back stack, but the graph of different tab is being displayed
// in such situation we will pop to the root of the tab host and save state based on [tabStateSavingBehaviour]
return currentDest goTo mappedGraph withOptions
navActionOptions {
popUpTo(tabsRootGraph) {
saveState = tabStateSavingBehaviour == SAVE_ALL ||
(
tabStateSavingBehaviour == SAVE_START_DESTINATION &&
currentDest == currentDest.graph.startDestination()
)
}
restoreState = true
launchSingleTop = true
}
}
private fun handlePopIntent(navState: NavState): ResolveResult {
val controllerState = navState.find(tabsRootGraph) ?: return ResolveResult.None
val currentDest = controllerState.currentDestination ?: return ResolveResult.None
controllerState.backStack.findLast { it.destination.graph in allGraphs }?.let {
val ret =
currentDest.destination goTo it.destination withOptions
navActionOptions {
popUpTo(it.destination)
launchSingleTop = true
}
return ret.asResult()
} ?: return ResolveResult.None
}
}
/**
* @see DONT_SAVE
* @see SAVE_START_DESTINATION
* @see SAVE_ALL
*/
enum class TabStateSavingBehaviour {
/**
* The state of the graph displayed in the current tab will not be saved.
* It will start from scratch when user navigates back to it
*/
DONT_SAVE,
/**
* The state of the graph displayed in the current tab will be saved only if it's displaying starting destination.
*/
SAVE_START_DESTINATION,
/**
* The state of the tabs will always be preserved when switching between them.
*/
SAVE_ALL,
}