@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.
@@ -88,7 +88,7 @@ data class ComponentInformation(
88
88
  val action: String? = null
89
89
  )
90
90
 
91
- var IsShowBaseLineDebug = true
91
+ var IsShowBaseLineDebug = false
92
92
 
93
93
  val ApplicationContext = staticCompositionLocalOf<MiniAppContext?> {
94
94
  null
@@ -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) DynamicScreenRegistry.setOptions(id, updated)
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 navigator = remember { Navigator(navController = navController, maxApi = maxApi) }
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 { DynamicScreenRegistry.nextId() }
54
- DynamicScreenRegistry.bind(screenId, initialScreenName, initialScreen, options)
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 = DynamicScreenRegistry.getScreen(route.id)
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 = DynamicScreenRegistry.getScreen(route.id)
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(Unit) {
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
- DynamicScreenRegistry.unregisterScreen(screenId)
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 = DynamicScreenRegistry.register(screenName, content, options)
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 latestScreen = DynamicScreenRegistry.getLatestScreen()
40
- latestScreen?.let { DynamicScreenRegistry.unregisterScreen(it.id) }
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 = DynamicScreenRegistry.getLatestScreen()
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
- DynamicScreenRegistry.getLatestScreen()?.let { it1 ->
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 = DynamicScreenRegistry.register(screenName, content, options)
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
- DynamicScreenRegistry.unregisterAll()
142
+ registry.unregisterAll()
114
143
 
115
- val route = DynamicScreenRegistry.register(screenName, content, options)
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 = DynamicScreenRegistry.getLatestScreen()?.id ?: -1
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 = DynamicScreenRegistry.getLatestScreen()?.id ?: -1
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 = DynamicScreenRegistry.getLatestScreen()?.id ?: -1
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
- object DynamicScreenRegistry {
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 = idCounter++
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 = idCounter++
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
- if (id >= idCounter) idCounter = id + 1 // keep counter ahead after process-death restore
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 navigation = remember { Navigation(id = id, bottomTabIndex = bottomTabIndex, initOptions = navigationOptions) }
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
@@ -18,7 +18,7 @@ kotlin.apple.xcodeCompatibility.nowarn=true
18
18
  name="ComposeKits"
19
19
  group=vn.momo.kits
20
20
  artifact.id=kits
21
- version=0.162.1-beta.2
21
+ version=0.162.1-beta.3
22
22
 
23
23
  repo=GitLab
24
24
  url=https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/native-kits",
3
- "version": "0.162.1-beta.2-debug",
3
+ "version": "0.162.1-beta.3-debug",
4
4
  "private": false,
5
5
  "dependencies": {},
6
6
  "devDependencies": {},