@momo-kits/native-kits 0.162.1-beta.2-debug → 0.162.1-beta.3-debug
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +3 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +26 -8
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationEvent.kt +53 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +72 -19
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +0 -10
- package/gradle.properties +1 -1
- package/package.json +1 -1
|
@@ -9,7 +9,8 @@ import vn.momo.kits.navigation.component.*
|
|
|
9
9
|
class Navigation(
|
|
10
10
|
val id: Int = -1,
|
|
11
11
|
val bottomTabIndex: Int = -1,
|
|
12
|
-
val initOptions: NavigationOptions? = null
|
|
12
|
+
val initOptions: NavigationOptions? = null,
|
|
13
|
+
private val registry: DynamicScreenRegistry? = null
|
|
13
14
|
) {
|
|
14
15
|
private val _options = mutableStateOf(initOptions ?: NavigationOptions())
|
|
15
16
|
val currentOptions: State<NavigationOptions> get() = _options
|
|
@@ -56,7 +57,7 @@ class Navigation(
|
|
|
56
57
|
private fun updateOptions(updated: NavigationOptions) {
|
|
57
58
|
_options.value = updated
|
|
58
59
|
if (bottomTabIndex != -1) setBottomTabOption(bottomTabIndex, updated)
|
|
59
|
-
if (id != -1)
|
|
60
|
+
if (id != -1) registry?.setOptions(id, updated)
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
@@ -5,6 +5,7 @@ import androidx.compose.animation.core.tween
|
|
|
5
5
|
import androidx.compose.runtime.*
|
|
6
6
|
import androidx.compose.runtime.saveable.rememberSaveable
|
|
7
7
|
import androidx.compose.ui.unit.Dp
|
|
8
|
+
import androidx.navigation.NavController
|
|
8
9
|
import androidx.navigation.compose.NavHost
|
|
9
10
|
import androidx.navigation.compose.composable
|
|
10
11
|
import androidx.navigation.compose.rememberNavController
|
|
@@ -27,10 +28,18 @@ fun NavigationContainer(
|
|
|
27
28
|
setNavigator: ((Navigator) -> Unit)? = null,
|
|
28
29
|
statusBarHeight: Dp? = null,
|
|
29
30
|
config: KitConfig? = null,
|
|
30
|
-
language: String? = null
|
|
31
|
+
language: String? = null,
|
|
32
|
+
onNavigationEvent: ((NavigationEvent) -> Unit)? = null
|
|
31
33
|
){
|
|
32
34
|
val navController = rememberNavController()
|
|
33
|
-
val
|
|
35
|
+
val registry = remember { DynamicScreenRegistry() }
|
|
36
|
+
val navigator = remember { Navigator(navController = navController, maxApi = maxApi, registry = registry, onNavigationEvent = onNavigationEvent) }
|
|
37
|
+
|
|
38
|
+
val recomposeCount = remember { mutableStateOf(0) }
|
|
39
|
+
SideEffect {
|
|
40
|
+
recomposeCount.value += 1
|
|
41
|
+
onNavigationEvent?.invoke(NavigationEvent.ContainerRecompose(recomposeCount.value))
|
|
42
|
+
}
|
|
34
43
|
val statusBarHeight = statusBarHeight ?: getAppStatusBarHeight()
|
|
35
44
|
val navigationBarHeight = getNavigationBarHeight()
|
|
36
45
|
|
|
@@ -50,13 +59,14 @@ fun NavigationContainer(
|
|
|
50
59
|
}
|
|
51
60
|
}
|
|
52
61
|
|
|
53
|
-
val screenId = rememberSaveable {
|
|
54
|
-
|
|
62
|
+
val screenId = rememberSaveable { registry.nextId() }
|
|
63
|
+
registry.bind(screenId, initialScreenName, initialScreen, options)
|
|
55
64
|
val startDestination = remember(screenId) { DynamicScreenRoute(screenId) }
|
|
56
65
|
|
|
57
66
|
ProvideNavigationEventDispatcherOwner {
|
|
58
67
|
CompositionLocalProvider(
|
|
59
68
|
LocalNavigator provides navigator,
|
|
69
|
+
LocalDynamicScreenRegistry provides registry,
|
|
60
70
|
LocalMaxApi provides maxApi,
|
|
61
71
|
AppTheme provides theme.value,
|
|
62
72
|
AppStatusBar provides statusBarHeight,
|
|
@@ -88,7 +98,7 @@ fun NavigationContainer(
|
|
|
88
98
|
}
|
|
89
99
|
) { backStackEntry ->
|
|
90
100
|
val route = backStackEntry.toRoute<DynamicScreenRoute>()
|
|
91
|
-
val screen =
|
|
101
|
+
val screen = registry.getScreen(route.id)
|
|
92
102
|
|
|
93
103
|
if (screen != null) {
|
|
94
104
|
StackScreen(
|
|
@@ -117,7 +127,7 @@ fun NavigationContainer(
|
|
|
117
127
|
}
|
|
118
128
|
) { backStackEntry ->
|
|
119
129
|
val route = backStackEntry.toRoute<DynamicDialogRoute>()
|
|
120
|
-
val screen =
|
|
130
|
+
val screen = registry.getScreen(route.id)
|
|
121
131
|
|
|
122
132
|
if (screen != null) {
|
|
123
133
|
StackScreen(
|
|
@@ -132,10 +142,18 @@ fun NavigationContainer(
|
|
|
132
142
|
}
|
|
133
143
|
}
|
|
134
144
|
|
|
135
|
-
DisposableEffect(
|
|
145
|
+
DisposableEffect(navController) {
|
|
146
|
+
val listener = NavController.OnDestinationChangedListener { _, destination, _ ->
|
|
147
|
+
val current = registry.getLatestScreen()
|
|
148
|
+
onNavigationEvent?.invoke(
|
|
149
|
+
NavigationEvent.DestinationChanged(destination.route, current?.id, current?.name)
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
navController.addOnDestinationChangedListener(listener)
|
|
136
153
|
onDispose {
|
|
154
|
+
navController.removeOnDestinationChangedListener(listener)
|
|
137
155
|
navigator.dispose()
|
|
138
|
-
|
|
156
|
+
registry.unregisterScreen(screenId)
|
|
139
157
|
}
|
|
140
158
|
}
|
|
141
159
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
package vn.momo.kits.navigation
|
|
2
|
+
|
|
3
|
+
sealed class NavigationEvent {
|
|
4
|
+
abstract val label: String
|
|
5
|
+
|
|
6
|
+
data class Push(val screenName: String, val id: Int) : NavigationEvent() {
|
|
7
|
+
override val label get() = "Push(screen=$screenName, id=$id)"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
data class Replace(val screenName: String, val id: Int) : NavigationEvent() {
|
|
11
|
+
override val label get() = "Replace(screen=$screenName, id=$id)"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
data class Pop(val count: Int, val overlayType: OverplayComponentType?) : NavigationEvent() {
|
|
15
|
+
override val label get() = "Pop(count=$count, overlay=$overlayType)"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
data class Present(val screenName: String, val id: Int) : NavigationEvent() {
|
|
19
|
+
override val label get() = "Present(screen=$screenName, id=$id)"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
data class Reset(val screenName: String, val id: Int) : NavigationEvent() {
|
|
23
|
+
override val label get() = "Reset(screen=$screenName, id=$id)"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
data class ShowModal(val parentId: Int, val barrierDismissible: Boolean) : NavigationEvent() {
|
|
27
|
+
override val label get() = "ShowModal(parentId=$parentId, barrierDismissible=$barrierDismissible)"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
data class ShowBottomSheet(val parentId: Int, val barrierDismissible: Boolean) : NavigationEvent() {
|
|
31
|
+
override val label get() = "ShowBottomSheet(parentId=$parentId, barrierDismissible=$barrierDismissible)"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
data class ShowSnackBar(val parentId: Int) : NavigationEvent() {
|
|
35
|
+
override val label get() = "ShowSnackBar(parentId=$parentId)"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
data class DestinationChanged(
|
|
39
|
+
val route: String?,
|
|
40
|
+
val id: Int?,
|
|
41
|
+
val screenName: String?
|
|
42
|
+
) : NavigationEvent() {
|
|
43
|
+
override val label get() = "DestinationChanged(route=$route, id=$id, screen=$screenName)"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
data class ContainerRecompose(val recomposeCount: Int) : NavigationEvent() {
|
|
47
|
+
override val label get() = "ContainerRecompose(count=$recomposeCount)"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
object Dispose : NavigationEvent() {
|
|
51
|
+
override val label get() = "Dispose"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -9,6 +9,7 @@ import androidx.compose.runtime.staticCompositionLocalOf
|
|
|
9
9
|
import androidx.compose.ui.Alignment
|
|
10
10
|
import androidx.compose.ui.Modifier
|
|
11
11
|
import androidx.navigation.NavController
|
|
12
|
+
import androidx.navigation.toRoute
|
|
12
13
|
import kotlinx.coroutines.CoroutineScope
|
|
13
14
|
import kotlinx.coroutines.Dispatchers
|
|
14
15
|
import kotlinx.coroutines.SupervisorJob
|
|
@@ -25,26 +26,51 @@ import kotlin.time.Duration.Companion.milliseconds
|
|
|
25
26
|
|
|
26
27
|
class Navigator(
|
|
27
28
|
private val navController: NavController,
|
|
28
|
-
private val maxApi: IMaxApi
|
|
29
|
+
private val maxApi: IMaxApi?,
|
|
30
|
+
private val registry: DynamicScreenRegistry,
|
|
31
|
+
private val onNavigationEvent: ((NavigationEvent) -> Unit)? = null
|
|
29
32
|
) {
|
|
30
33
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
|
31
34
|
|
|
35
|
+
private fun emit(event: NavigationEvent) {
|
|
36
|
+
onNavigationEvent?.invoke(event)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The screen currently on top of THIS container's [navController] back stack.
|
|
41
|
+
*
|
|
42
|
+
* Use this instead of [DynamicScreenRegistry.getLatestScreen], which returns the globally
|
|
43
|
+
* last-registered screen across every NavigationContainer. With multiple containers sharing
|
|
44
|
+
* the single [DynamicScreenRegistry], the global "latest" is not guaranteed to belong to this
|
|
45
|
+
* navController, so back/dismiss/overlay logic could read the wrong options or unregister the
|
|
46
|
+
* wrong screen.
|
|
47
|
+
*/
|
|
48
|
+
private fun currentScreen(): DynamicScreen? {
|
|
49
|
+
val entry = navController.currentBackStackEntry ?: return null
|
|
50
|
+
val id = runCatching { entry.toRoute<DynamicScreenRoute>().id }.getOrNull()
|
|
51
|
+
?: runCatching { entry.toRoute<DynamicDialogRoute>().id }.getOrNull()
|
|
52
|
+
?: return null
|
|
53
|
+
return registry.getScreen(id)
|
|
54
|
+
}
|
|
55
|
+
|
|
32
56
|
fun push(screenName: String, content: @Composable () -> Unit, options: NavigationOptions? = null) {
|
|
33
|
-
val route =
|
|
57
|
+
val route = registry.register(screenName, content, options)
|
|
58
|
+
emit(NavigationEvent.Push(screenName, route.id))
|
|
34
59
|
navController.navigate(DynamicScreenRoute(route.id))
|
|
35
60
|
}
|
|
36
61
|
|
|
37
62
|
fun replace(screenName: String, content: @Composable () -> Unit, options: NavigationOptions? = null) {
|
|
38
63
|
if (navController.previousBackStackEntry != null){
|
|
39
|
-
val
|
|
40
|
-
|
|
64
|
+
val replacedScreen = currentScreen()
|
|
65
|
+
emit(NavigationEvent.Replace(screenName, replacedScreen?.id ?: -1))
|
|
66
|
+
replacedScreen?.let { registry.unregisterScreen(it.id) }
|
|
41
67
|
navController.popBackStack()
|
|
42
68
|
push(screenName, content, options)
|
|
43
69
|
}
|
|
44
70
|
}
|
|
45
71
|
|
|
46
72
|
fun onBackSafe(callBack: (() -> Unit)? = null) {
|
|
47
|
-
val latestScreen =
|
|
73
|
+
val latestScreen = currentScreen()
|
|
48
74
|
val options = latestScreen?.options
|
|
49
75
|
if (options?.onBackHandler != null) {
|
|
50
76
|
options.onBackHandler.invoke()
|
|
@@ -71,6 +97,7 @@ class Navigator(
|
|
|
71
97
|
}
|
|
72
98
|
|
|
73
99
|
fun pop(count: Int = 1, callBack: (() -> Unit)? = null) {
|
|
100
|
+
emit(NavigationEvent.Pop(count, OverplayComponentRegistry.getOverplayType()))
|
|
74
101
|
scope.launch {
|
|
75
102
|
repeat(count) {
|
|
76
103
|
if (OverplayComponentRegistry.getOverplayType() == OverplayComponentType.SNACK_BAR){
|
|
@@ -94,25 +121,28 @@ class Navigator(
|
|
|
94
121
|
|
|
95
122
|
private suspend fun dismissScreen() {
|
|
96
123
|
if (navController.previousBackStackEntry != null){
|
|
124
|
+
// Capture the screen being dismissed BEFORE popping; after popBackStack the current
|
|
125
|
+
// entry is the revealed screen, so reading it post-pop would unregister the wrong one.
|
|
126
|
+
val dismissing = currentScreen()
|
|
97
127
|
navController.popBackStack()
|
|
98
128
|
delay(300L.milliseconds)
|
|
99
|
-
|
|
100
|
-
DynamicScreenRegistry.unregisterScreen(it1.id)
|
|
101
|
-
}
|
|
129
|
+
dismissing?.let { registry.unregisterScreen(it.id) }
|
|
102
130
|
} else {
|
|
103
131
|
maxApi?.dismiss { }
|
|
104
132
|
}
|
|
105
133
|
}
|
|
106
134
|
|
|
107
135
|
fun present(screenName: String, content: @Composable () -> Unit, options: NavigationOptions? = null) {
|
|
108
|
-
val route =
|
|
136
|
+
val route = registry.register(screenName, content, options)
|
|
137
|
+
emit(NavigationEvent.Present(screenName, route.id))
|
|
109
138
|
navController.navigate(DynamicDialogRoute(route.id))
|
|
110
139
|
}
|
|
111
140
|
|
|
112
141
|
fun reset(screenName: String, content: @Composable () -> Unit, options: NavigationOptions? = null) {
|
|
113
|
-
|
|
142
|
+
registry.unregisterAll()
|
|
114
143
|
|
|
115
|
-
val route =
|
|
144
|
+
val route = registry.register(screenName, content, options)
|
|
145
|
+
emit(NavigationEvent.Reset(screenName, route.id))
|
|
116
146
|
navController.navigate(DynamicScreenRoute(route.id)) {
|
|
117
147
|
popUpTo(0) { inclusive = true }
|
|
118
148
|
}
|
|
@@ -123,7 +153,8 @@ class Navigator(
|
|
|
123
153
|
barrierDismissible: Boolean = true,
|
|
124
154
|
onDismiss: (() -> Unit)? = null
|
|
125
155
|
){
|
|
126
|
-
val id =
|
|
156
|
+
val id = currentScreen()?.id ?: -1
|
|
157
|
+
emit(NavigationEvent.ShowModal(id, barrierDismissible))
|
|
127
158
|
OverplayComponentRegistry.registerOverplay(id, content, OverplayComponentType.MODAL, false, barrierDismissible, onDismiss)
|
|
128
159
|
}
|
|
129
160
|
|
|
@@ -134,12 +165,14 @@ class Navigator(
|
|
|
134
165
|
onDismiss: (() -> Unit)? = null,
|
|
135
166
|
bottomSheetHeader: BottomHeader? = null
|
|
136
167
|
){
|
|
137
|
-
val id =
|
|
168
|
+
val id = currentScreen()?.id ?: -1
|
|
169
|
+
emit(NavigationEvent.ShowBottomSheet(id, barrierDismissible))
|
|
138
170
|
OverplayComponentRegistry.registerOverplay(id, content, OverplayComponentType.BOTTOM_SHEET, isSurface, barrierDismissible, onDismiss, bottomSheetHeader)
|
|
139
171
|
}
|
|
140
172
|
|
|
141
173
|
fun showSnackBar(snackBar: SnackBar, onDismiss: (() -> Unit)? = null) {
|
|
142
|
-
val id =
|
|
174
|
+
val id = currentScreen()?.id ?: -1
|
|
175
|
+
emit(NavigationEvent.ShowSnackBar(id))
|
|
143
176
|
scope.launch {
|
|
144
177
|
OverplayComponentRegistry.registerOverplay(
|
|
145
178
|
id = id,
|
|
@@ -161,6 +194,7 @@ class Navigator(
|
|
|
161
194
|
}
|
|
162
195
|
|
|
163
196
|
fun dispose(){
|
|
197
|
+
emit(NavigationEvent.Dispose)
|
|
164
198
|
scope.cancel()
|
|
165
199
|
}
|
|
166
200
|
}
|
|
@@ -182,12 +216,19 @@ data class DynamicScreen(
|
|
|
182
216
|
var options: NavigationOptions? = null
|
|
183
217
|
)
|
|
184
218
|
|
|
185
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Per-container store of dynamic screens. Each [NavigationContainer] owns its own instance, so the
|
|
221
|
+
* number of live containers no longer matters: `getLatestScreen`/`unregisterAll` only ever see the
|
|
222
|
+
* screens of their own container.
|
|
223
|
+
*
|
|
224
|
+
* Ids are still drawn from a process-wide counter so they stay globally unique — this keeps the
|
|
225
|
+
* still-global [OverplayComponentRegistry] (which keys overlays by screen id) collision-free.
|
|
226
|
+
*/
|
|
227
|
+
class DynamicScreenRegistry {
|
|
186
228
|
private val screens = mutableMapOf<Int, DynamicScreen>()
|
|
187
|
-
private var idCounter = 1
|
|
188
229
|
|
|
189
230
|
fun register(screenName: String, content: @Composable () -> Unit, options: NavigationOptions?): DynamicScreenRoute {
|
|
190
|
-
val id =
|
|
231
|
+
val id = nextGlobalId()
|
|
191
232
|
screens[id] = DynamicScreen(
|
|
192
233
|
id = id,
|
|
193
234
|
name = screenName,
|
|
@@ -197,7 +238,7 @@ object DynamicScreenRegistry {
|
|
|
197
238
|
return DynamicScreenRoute(id)
|
|
198
239
|
}
|
|
199
240
|
|
|
200
|
-
fun nextId(): Int =
|
|
241
|
+
fun nextId(): Int = nextGlobalId()
|
|
201
242
|
|
|
202
243
|
fun bind(id: Int, screenName: String, content: @Composable () -> Unit, options: NavigationOptions?) {
|
|
203
244
|
screens[id] = DynamicScreen(
|
|
@@ -206,7 +247,7 @@ object DynamicScreenRegistry {
|
|
|
206
247
|
content = content,
|
|
207
248
|
options = options
|
|
208
249
|
)
|
|
209
|
-
|
|
250
|
+
advanceGlobalIdPast(id) // keep counter ahead after process-death restore
|
|
210
251
|
}
|
|
211
252
|
|
|
212
253
|
fun unregisterScreen(id: Int) {
|
|
@@ -229,8 +270,20 @@ object DynamicScreenRegistry {
|
|
|
229
270
|
fun setOptions(id: Int, options: NavigationOptions){
|
|
230
271
|
screens[id]?.options = options
|
|
231
272
|
}
|
|
273
|
+
|
|
274
|
+
companion object {
|
|
275
|
+
private var globalIdCounter = 1
|
|
276
|
+
|
|
277
|
+
private fun nextGlobalId(): Int = globalIdCounter++
|
|
278
|
+
|
|
279
|
+
private fun advanceGlobalIdPast(id: Int) {
|
|
280
|
+
if (id >= globalIdCounter) globalIdCounter = id + 1
|
|
281
|
+
}
|
|
282
|
+
}
|
|
232
283
|
}
|
|
233
284
|
|
|
285
|
+
val LocalDynamicScreenRegistry = staticCompositionLocalOf<DynamicScreenRegistry?> { null }
|
|
286
|
+
|
|
234
287
|
|
|
235
288
|
sealed class OverplayComponentParams {
|
|
236
289
|
class Modal(
|
|
@@ -92,7 +92,8 @@ internal fun StackScreen(
|
|
|
92
92
|
val context = ApplicationContext.current
|
|
93
93
|
val statusBar = AppStatusBar.current
|
|
94
94
|
val density = LocalDensity.current
|
|
95
|
-
val
|
|
95
|
+
val screenRegistry = LocalDynamicScreenRegistry.current
|
|
96
|
+
val navigation = remember { Navigation(id = id, bottomTabIndex = bottomTabIndex, initOptions = navigationOptions, registry = screenRegistry) }
|
|
96
97
|
|
|
97
98
|
val options by navigation.currentOptions
|
|
98
99
|
|
|
@@ -5,16 +5,6 @@ import org.jetbrains.compose.resources.DrawableResource
|
|
|
5
5
|
import org.jetbrains.compose.resources.InternalResourceApi
|
|
6
6
|
import org.jetbrains.compose.resources.ResourceItem
|
|
7
7
|
import org.jetbrains.compose.resources.readResourceBytes
|
|
8
|
-
|
|
9
|
-
@OptIn(InternalResourceApi::class)
|
|
10
|
-
@Composable
|
|
11
|
-
internal fun getResource(name: String): DrawableResource {
|
|
12
|
-
return DrawableResource(
|
|
13
|
-
"drawable:$name",
|
|
14
|
-
setOf(ResourceItem(setOf(), "drawable/$name", -1, -1))
|
|
15
|
-
)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
8
|
@OptIn(InternalResourceApi::class)
|
|
19
9
|
@Composable
|
|
20
10
|
internal fun readJson(name: String): String {
|
package/gradle.properties
CHANGED