Skip to content

Commit 0d7d7fb

Browse files
committed
refactor(tabs): split TabsNavState into state and update request
Mirrors the iOS split landed in PR #3950. `TabsNavState` previously served as both the authoritative current state and the JS-driven update request, with `provenance` doing double duty as both "current generation" and "base provenance the request was derived from". Splits out `TabsNavStateUpdateRequest(selectedScreenKey, baseProvenance, actionOrigin)` as a sibling type. Origin assignment moves from the container to the request-construction site (`TabsHostViewManager.setNavStateRequest`). The container becomes origin-agnostic on the JS-driven path: `onMenuItemSelected` reads `actionOrigin` off the buffered `TabSelectOp.request` instead of hardcoding `PROGRAMMATIC_JS`. Today only `PROGRAMMATIC_JS` flows through, so runtime behavior is unchanged — but future native-programmatic origins can land here without touching the container. Also bundles a `TabsNavState.selectedKey -> selectedScreenKey` rename for consistency with the JS contract and the new request type. JS-facing WritableMap keys (`selectedScreenKey`, `rejectedScreenKey`) were already named correctly; this only aligns the Kotlin-internal field name.
1 parent 92ac80f commit 0d7d7fb

9 files changed

Lines changed: 67 additions & 35 deletions

File tree

android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainer.kt

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ internal class TabsContainer(
7777

7878
internal val selectedTab: TabsScreenFragment
7979
get() =
80-
checkNotNull(getFragmentForScreenKey(navState.selectedKey)) { "[RNScreens] No selected tab present" }
80+
checkNotNull(getFragmentForScreenKey(navState.selectedScreenKey)) { "[RNScreens] No selected tab present" }
8181

8282
internal val invalidationFlags = TabsContainerInvalidationFlags()
8383

@@ -259,14 +259,14 @@ internal class TabsContainer(
259259
val tabSelectOp = pendingOperation as TabSelectOp
260260

261261
val nextSelectedMenuItemId =
262-
checkNotNull(getMenuItemIdForFragment(requireFragmentForScreenKey(tabSelectOp.navState.selectedKey))) {
263-
"[RNScreens] Failed to find Menu Item for screenKey: ${tabSelectOp.navState.selectedKey}"
262+
checkNotNull(getMenuItemIdForFragment(requireFragmentForScreenKey(tabSelectOp.request.selectedScreenKey))) {
263+
"[RNScreens] Failed to find Menu Item for screenKey: ${tabSelectOp.request.selectedScreenKey}"
264264
}
265265

266-
if (rejectOpsWithStaleNavState && isNavStateStale(tabSelectOp.navState)) {
266+
if (rejectOpsWithStaleNavState && isNavStateStale(tabSelectOp.request)) {
267267
delegate.onNavStateUpdateRejected(
268268
navState,
269-
tabSelectOp.navState,
269+
tabSelectOp.request,
270270
TabsNavStateUpdateRejectionReason.STALE,
271271
)
272272
pendingOperation = null
@@ -281,7 +281,7 @@ internal class TabsContainer(
281281
} else {
282282
delegate.onNavStateUpdateRejected(
283283
navState,
284-
tabSelectOp.navState,
284+
tabSelectOp.request,
285285
TabsNavStateUpdateRejectionReason.REPEATED,
286286
)
287287
}
@@ -328,7 +328,7 @@ internal class TabsContainer(
328328
val currentSelectedFragment = selectedTab
329329

330330
if (nextSelectedFragment === currentSelectedFragment) {
331-
progressNavigationState(navState.selectedKey)
331+
progressNavigationState(navState.selectedScreenKey)
332332
return true
333333
}
334334

@@ -343,8 +343,8 @@ internal class TabsContainer(
343343
return true
344344
}
345345

346-
private fun progressNavigationState(selectedKey: String) {
347-
navState = TabsNavState(selectedKey, navState.provenance + 1)
346+
private fun progressNavigationState(selectedScreenKey: String) {
347+
navState = TabsNavState(selectedScreenKey, navState.provenance + 1)
348348
if (!isInExternalOperationContext) {
349349
lastUINavState = navState
350350
}
@@ -377,7 +377,12 @@ internal class TabsContainer(
377377
navState,
378378
isRepeated = isRepeated,
379379
hasTriggeredSpecialEffect = hasTriggeredSpecialEffect,
380-
actionOrigin = if (isInExternalOperationContext) TabsActionOrigin.PROGRAMMATIC_JS else TabsActionOrigin.USER,
380+
actionOrigin =
381+
if (isInExternalOperationContext) {
382+
(pendingOperation as TabSelectOp).request.actionOrigin
383+
} else {
384+
TabsActionOrigin.USER
385+
},
381386
)
382387
}
383388

@@ -413,7 +418,7 @@ internal class TabsContainer(
413418

414419
private fun getSelectedTabsScreenFragmentId(): Int? =
415420
tabsModel
416-
.indexOfFirst { it.requireScreenKey == navState.selectedKey }
421+
.indexOfFirst { it.requireScreenKey == navState.selectedScreenKey }
417422
.takeIf { it != -1 }
418423

419424
private fun getMenuItemForTabsScreen(tabsScreen: TabsScreen): MenuItem? =
@@ -544,9 +549,9 @@ internal class TabsContainer(
544549
fragmentManager = null
545550
}
546551

547-
private fun isNavStateStale(state: TabsNavState): Boolean {
552+
private fun isNavStateStale(request: TabsNavStateUpdateRequest): Boolean {
548553
if (navState.isEmpty() || lastUINavState.isEmpty()) return false
549-
return state.provenance < lastUINavState.provenance
554+
return request.baseProvenance < lastUINavState.provenance
550555
}
551556

552557
companion object {

android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainerDelegate.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ internal interface TabsContainerDelegate {
2424
* Called when the container rejects a navigation state update.
2525
*
2626
* @param currentNavState The currently active navigation state that was kept.
27-
* @param rejectedNavState The navigation state update that was rejected.
27+
* @param rejectedRequest The navigation state update request that was rejected.
2828
* @param reason Why the update was rejected.
2929
*/
3030
fun onNavStateUpdateRejected(
3131
currentNavState: TabsNavState,
32-
rejectedNavState: TabsNavState,
32+
rejectedRequest: TabsNavStateUpdateRequest,
3333
reason: TabsNavStateUpdateRejectionReason,
3434
)
3535

android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainerOps.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ package com.swmansion.rnscreens.gamma.tabs.container
33
internal sealed class TabsContainerOp
44

55
internal data class TabSelectOp(
6-
val navState: TabsNavState,
6+
val request: TabsNavStateUpdateRequest,
77
) : TabsContainerOp()

android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsNavState.kt

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import com.swmansion.rnscreens.gamma.tabs.container.TabsNavStateUpdateRejectionR
66
/**
77
* Describes navigation state of a tabs container.
88
*
9-
* @property selectedKey Screen key of the currently selected tab.
9+
* @property selectedScreenKey Screen key of the currently selected tab.
1010
* @property provenance Monotonically increasing number describing the history (generation) of the state.
1111
* State with provenance `N + 1` is derived from state with provenance `N`.
1212
* This allows detecting stale updates.
1313
*/
1414
data class TabsNavState(
15-
val selectedKey: String,
15+
val selectedScreenKey: String,
1616
val provenance: Int,
1717
) {
1818
internal fun isEmpty(): Boolean = this === EMPTY
@@ -24,6 +24,23 @@ data class TabsNavState(
2424
}
2525
}
2626

27+
/**
28+
* A request to change navigation state.
29+
*
30+
* Carries the target [selectedScreenKey], the [baseProvenance] of the state the request was derived from,
31+
* and the [actionOrigin] (actor) that initiated it. Mirrors the public `TabsHostNavStateRequest` TS type
32+
* plus an [actionOrigin] carried internally.
33+
*
34+
* @property selectedScreenKey Screen key of the requested tab.
35+
* @property baseProvenance Provenance of the state this request was derived from. Used for staleness detection.
36+
* @property actionOrigin Origin (actor) that initiated this request.
37+
*/
38+
data class TabsNavStateUpdateRequest(
39+
val selectedScreenKey: String,
40+
val baseProvenance: Int,
41+
val actionOrigin: TabsActionOrigin,
42+
)
43+
2744
/**
2845
* Reason why a navigation state update was rejected by the container.
2946
*

android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHost.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.swmansion.rnscreens.gamma.tabs.container.TabsContainer
1818
import com.swmansion.rnscreens.gamma.tabs.container.TabsContainerDelegate
1919
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavState
2020
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavStateUpdateRejectionReason
21+
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavStateUpdateRequest
2122
import com.swmansion.rnscreens.gamma.tabs.screen.TabsScreen
2223
import com.swmansion.rnscreens.utils.RNSLog
2324
import kotlin.properties.Delegates
@@ -30,7 +31,7 @@ class TabsHost(
3031
TabsContainerDelegate,
3132
UIManagerListener {
3233
private val renderedScreens: ArrayList<TabsScreen> = arrayListOf()
33-
private var jsNavStateRequest: TabsNavState = TabsNavState.EMPTY
34+
private var jsNavStateRequest: TabsNavStateUpdateRequest? = null
3435

3536
private val container: TabsContainer =
3637
TabsContainer(reactContext, this).apply {
@@ -110,9 +111,9 @@ class TabsHost(
110111
container.removeAllTabsScreens()
111112
}
112113

113-
internal fun updateJSNavStateRequest(navStateRequest: TabsNavState) {
114+
internal fun updateJSNavStateRequest(navStateRequest: TabsNavStateUpdateRequest) {
114115
jsNavStateRequest = navStateRequest
115-
container.setContainerOperation(TabSelectOp(jsNavStateRequest.copy()))
116+
container.setContainerOperation(TabSelectOp(navStateRequest.copy()))
116117
}
117118

118119
private val layoutCallback =
@@ -163,7 +164,7 @@ class TabsHost(
163164
actionOrigin: TabsActionOrigin,
164165
) {
165166
eventEmitter.emitOnTabSelectedEvent(
166-
navState.selectedKey,
167+
navState.selectedScreenKey,
167168
navState.provenance,
168169
isRepeated,
169170
hasTriggeredSpecialEffect,
@@ -173,12 +174,12 @@ class TabsHost(
173174

174175
override fun onNavStateUpdateRejected(
175176
currentNavState: TabsNavState,
176-
rejectedNavState: TabsNavState,
177+
rejectedRequest: TabsNavStateUpdateRequest,
177178
reason: TabsNavStateUpdateRejectionReason,
178179
) {
179180
eventEmitter.emitOnTabSelectionRejectedEvent(
180181
currentNavState,
181-
rejectedNavState,
182+
rejectedRequest,
182183
reason,
183184
)
184185
}

android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHostEventEmitter.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.swmansion.rnscreens.gamma.common.event.BaseEventEmitter
55
import com.swmansion.rnscreens.gamma.tabs.container.TabsActionOrigin
66
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavState
77
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavStateUpdateRejectionReason
8+
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavStateUpdateRequest
89
import com.swmansion.rnscreens.gamma.tabs.host.event.TabsHostTabSelectedEvent
910
import com.swmansion.rnscreens.gamma.tabs.host.event.TabsHostTabSelectionPreventedEvent
1011
import com.swmansion.rnscreens.gamma.tabs.host.event.TabsHostTabSelectionRejectedEvent
@@ -42,15 +43,15 @@ internal class TabsHostEventEmitter(
4243
*/
4344
fun emitOnTabSelectionRejectedEvent(
4445
currentNavState: TabsNavState,
45-
rejectedNavState: TabsNavState,
46+
rejectedRequest: TabsNavStateUpdateRequest,
4647
rejectionReason: TabsNavStateUpdateRejectionReason,
4748
) {
4849
reactEventDispatcher.dispatchEvent(
4950
TabsHostTabSelectionRejectedEvent(
5051
surfaceId,
5152
viewTag,
5253
currentNavState,
53-
rejectedNavState,
54+
rejectedRequest,
5455
rejectionReason,
5556
),
5657
)

android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHostViewManager.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import com.facebook.react.viewmanagers.RNSTabsHostAndroidManagerDelegate
1111
import com.facebook.react.viewmanagers.RNSTabsHostAndroidManagerInterface
1212
import com.swmansion.rnscreens.gamma.common.colorscheme.ColorScheme
1313
import com.swmansion.rnscreens.gamma.helpers.makeEventRegistrationInfo
14-
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavState
14+
import com.swmansion.rnscreens.gamma.tabs.container.TabsActionOrigin
15+
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavStateUpdateRequest
1516
import com.swmansion.rnscreens.gamma.tabs.host.event.TabsHostTabSelectedEvent
1617
import com.swmansion.rnscreens.gamma.tabs.host.event.TabsHostTabSelectionPreventedEvent
1718
import com.swmansion.rnscreens.gamma.tabs.host.event.TabsHostTabSelectionRejectedEvent
@@ -79,7 +80,13 @@ class TabsHostViewManager :
7980
val navStateRequestMap = requireNotNull(value) { "[RNScreens] navStateRequest must not be nullish" }
8081
val selectedScreenKey = requireNotNull(navStateRequestMap.getString("selectedScreenKey"))
8182
val baseProvenance = requireNotNull(navStateRequestMap.getInt("baseProvenance"))
82-
view.updateJSNavStateRequest(TabsNavState(selectedScreenKey, baseProvenance))
83+
view.updateJSNavStateRequest(
84+
TabsNavStateUpdateRequest(
85+
selectedScreenKey = selectedScreenKey,
86+
baseProvenance = baseProvenance,
87+
actionOrigin = TabsActionOrigin.PROGRAMMATIC_JS,
88+
),
89+
)
8390
}
8491

8592
override fun setRejectStaleNavStateUpdates(

android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/event/TabsHostTabSelectionPreventedEvent.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class TabsHostTabSelectionPreventedEvent(
2727

2828
override fun getEventData(): WritableMap? =
2929
Arguments.createMap().apply {
30-
putString(EK_SELECTED_KEY, currentNavState.selectedKey)
30+
putString(EK_SELECTED_KEY, currentNavState.selectedScreenKey)
3131
putInt(EK_PROVENANCE, currentNavState.provenance)
3232
putString(EK_PREVENTED_KEY, preventedScreenKey)
3333
}

android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/event/TabsHostTabSelectionRejectedEvent.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,20 @@ import com.facebook.react.uimanager.events.Event
66
import com.swmansion.rnscreens.gamma.common.event.NamingAwareEventType
77
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavState
88
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavStateUpdateRejectionReason
9+
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavStateUpdateRequest
910

1011
/**
1112
* React Native event dispatched to JS when a tab selection request is rejected by the container.
1213
*
13-
* Carries the currently active navigation state ([currentNavState]), the rejected update
14-
* ([rejectedNavState]), and the [rejectionReason]. This event is never coalesced — every
14+
* Carries the currently active navigation state ([currentNavState]), the rejected request
15+
* ([rejectedRequest]), and the [rejectionReason]. This event is never coalesced — every
1516
* rejection is delivered individually so the JS side has a complete picture of state transitions.
1617
*/
1718
class TabsHostTabSelectionRejectedEvent(
1819
surfaceId: Int,
1920
viewId: Int,
2021
val currentNavState: TabsNavState,
21-
val rejectedNavState: TabsNavState,
22+
val rejectedRequest: TabsNavStateUpdateRequest,
2223
val rejectionReason: TabsNavStateUpdateRejectionReason,
2324
) : Event<TabsHostTabSelectionRejectedEvent>(surfaceId, viewId),
2425
NamingAwareEventType {
@@ -31,10 +32,10 @@ class TabsHostTabSelectionRejectedEvent(
3132

3233
override fun getEventData(): WritableMap? =
3334
Arguments.createMap().apply {
34-
putString(EK_SELECTED_KEY, currentNavState.selectedKey)
35+
putString(EK_SELECTED_KEY, currentNavState.selectedScreenKey)
3536
putInt(EK_PROVENANCE, currentNavState.provenance)
36-
putString(EK_REJECTED_KEY, rejectedNavState.selectedKey)
37-
putInt(EK_REJECTED_PROVENANCE, rejectedNavState.provenance)
37+
putString(EK_REJECTED_KEY, rejectedRequest.selectedScreenKey)
38+
putInt(EK_REJECTED_PROVENANCE, rejectedRequest.baseProvenance)
3839
putString(EK_REJECTION_REASON, rejectionReason.toString())
3940
}
4041

0 commit comments

Comments
 (0)