Repository navigation
refactor(Android, Tabs): apply tab title, badge and accessibility only when invalidated - #4975
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
bc4e472 to
efb203d
Compare
efb203d to
afa6e07
Compare
afa6e07 to
d84266a
Compare
d84266a to
7f97149
Compare
7f97149 to
aa774ad
Compare
eba3f82 to
aa44f2b
Compare
aa44f2b to
8e1385a
Compare
6d5c987 to
fb9dde8
Compare
fb9dde8 to
0bdd0c3
Compare
0bdd0c3 to
7c01082
Compare
7c01082 to
3f9f5bd
Compare
…y when invalidated The `TITLE`, `BADGE` and `ACCESSIBILITY` flags only routed a change to the per-item menu update, which then re-applied all three. Now, mirroring the Stack header's `needsRebuild || flag`: - the title is applied only for `TITLE` (no more manual diffing), - the badge only for `BADGE`, except in the whole-bar appearance update, which re-applies every badge because badge colours come from the selected tab's appearance, - accessibility only for `ACCESSIBILITY` in the per-item update; the whole-bar update still re-applies it to every item. `TabsAppearanceApplicator.updateMenuItemAppearance` returns the flags it consumed so the coordinator and the container can gate their parts. A fresh menu item state is fully dirty, so new and rebuilt menu items get everything applied. Closes software-mansion/react-native-screens-labs#1956. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
3f9f5bd to
c210c80
Compare
| @@ -283,6 +289,8 @@ internal class TabsAppearanceApplicator( | |||
| // The menu item's view doesn't exist yet; retry on the next update. | |||
| state.pendingFlags = state.pendingFlags or TabsItemInvalidationFlags.ACTIVE_INDICATOR_SIZE | |||
| } | |||
|
|
|||
| return flags | |||
There was a problem hiding this comment.
This method now does something peculiar. It zeros out the state.pendingFlags - so it takes responsibility for all the updates, but then it returns flags - so a set of still invalidated flags, that need to be applied somewhere else.
Suggest architecture here that actually does make sense.
There was a problem hiding this comment.
Agreed, the ownership was wrong: one function cleared every flag, applied three parts, and relied on two other classes to finish the job from its return value. e3c6bde restructures it as one consumer, apply-only primitives:
TabsMenuItemState.consumePendingFlags()reads and clears the flags. Its only caller isTabsContainer.applyPendingMenuItemChanges, which hands the flags toTabsAppearanceCoordinator.updateMenuItemAppearance(title, icon, active indicator, badge) and applies accessibility throughTabsHostA11yCoordinator. Whoever consumes the flags applies everything they name.- The applicator only applies (
applyTitle,applyIcon,applyActiveIndicatorSize,updateBadgeAppearance) and knows nothing about flags. A part that can't be applied yet (no item view) is re-invalidated throughTabsBottomNavigationView.invalidateMenuItem, like any other change: the active indicator as before, and accessibility too since ce494b0 (it used to be dropped until the next a11y change or bar pass). - No
isWholeBarUpdate: the bar pass (TabsContainer.updateTabBarAppearance: bar-wide appearance, then the menu items, then fonts) invalidates what it re-applies to every item and runs the same per-item path:BADGE, because badge colours come from the selected tab, andACCESSIBILITYon appearance updates, which replacessetA11yPropertiesToAllTabItemsand its index-based pairing. A fresh menu item state is still fully dirty, so rebuilt items get everything.
Side effect worth knowing: the colour-scheme path (applyDayNightUiMode) used to clear pending ACCESSIBILITY without applying it (safe only by call order). It now applies whatever it consumes. TabsHostA11yCoordinator loses its unused tabsScreenFragments constructor parameter.
#4976 and #4982 are rebased; everything compiles and passes yarn lint-android. Re-running the Android tabs Detox suites and a manual title / badge / a11y check on this PR alone (without #4976) now; I'll update the test plan with the results.
Generated by Claude Code
…s pending flags Review follow-up: `TabsAppearanceApplicator.updateMenuItemAppearance` cleared all of a menu item's pending flags, applied only some of them (title, icon, active indicator) and returned them so the coordinator and the container could apply the rest (badge, accessibility). Who owned the flags depended on which caller looked at the return value. Now there is one consumer and the parts are applied by their owners: - `TabsContainer.applyPendingMenuItemChanges` is the only caller of `TabsMenuItemState.consumePendingFlags`. It hands the flags to `TabsAppearanceCoordinator.updateMenuItemAppearance` (title, icon, active indicator, badge) and applies accessibility through `TabsHostA11yCoordinator`. - The applicator only applies (`applyTitle`, `applyIcon`, `applyActiveIndicatorSize`, `updateBadgeAppearance`) and knows nothing about flags. An active indicator that can't be applied yet is re-invalidated through `TabsBottomNavigationView.invalidateMenuItem`, like any other change. - The whole-bar update no longer needs `isWholeBarUpdate`: it invalidates what it re-applies to every menu item (`BADGE`, as badge colours come from the selected tab; `ACCESSIBILITY` on appearance changes, replacing `setA11yPropertiesToAllTabItems`), then runs the same per-item path. `TabsContainer.updateTabBarAppearance` orders it: bar-wide appearance, menu items, fonts. No behavioural change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s missing Review follow-up. `setA11yPropertiesToTabItem` sets the test ID only when the menu item's view exists, but its `ACCESSIBILITY` flag was consumed either way, so a missing view dropped the update until the next accessibility change or bar pass (before the flag gating, any per-item update retried it). It now reports whether it found the view, and the container re-invalidates `ACCESSIBILITY` when it didn't, like the active indicator. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| @@ -246,43 +248,32 @@ internal class TabsAppearanceApplicator( | |||
| } | |||
| } | |||
|
|
|||
| fun updateMenuItemAppearance( | |||
| fun applyTitle( | |||
There was a problem hiding this comment.
Should these methods be public? It seems that internal is enough here. They are used only from inside the core, right?
There was a problem hiding this comment.
fb1e94a: yes, only core's tabs code calls them. They are now explicitly internal: applyIconBox, applyActiveIndicatorSize, updateSharedAppearance, updateFontStyles, applyTitle and applyIcon in TabsAppearanceApplicator, plus the same for updateBarAppearance and updateFontStyles in TabsAppearanceCoordinator. Both classes were already internal, so the effective visibility doesn't change, but the declarations now say it.
Generated by Claude Code
| if (flags.containsAny(TabsItemInvalidationFlags.ICON_SIZE or TabsItemInvalidationFlags.ACTIVE_INDICATOR_SIZE) && | ||
| !appearanceApplicator.applyActiveIndicatorSize(menuItem, item, bottomNavigationView.menuItemState(menuItem.itemId)) | ||
| ) { | ||
| // The menu item's view doesn't exist yet; retry on the next update. | ||
| bottomNavigationView.invalidateMenuItem(menuItem.itemId, TabsItemInvalidationFlags.ACTIVE_INDICATOR_SIZE) |
There was a problem hiding this comment.
Nest an if block here with the second part of the condition. It'll be easier to read IMO.
There was a problem hiding this comment.
fb1e94a: nested. The same "apply, and re-invalidate if the item view is missing" check for accessibility in TabsContainer.applyPendingMenuItemChanges got the same shape.
Generated by Claude Code
| val menuItemId = fragment.menuItemId | ||
| val menuItem = | ||
| checkNotNull(bottomNavigationView.menu.findItem(menuItemId)) { | ||
| "[RNScreens] Missing MenuItem for id: $menuItemId" | ||
| } |
There was a problem hiding this comment.
Check whether there isn't a helper method already for this. If yes - use it. If not - create one. Look for other places that might use the helper method.
There was a problem hiding this comment.
fb1e94a: there was none for a fragment → menu item lookup; MenuHelpers.kt only had getOrCreateMenuItemForFragment. Added next to it:
Menu.findMenuItemForFragment(fragment): MenuItem?Menu.requireMenuItemForFragment(fragment): MenuItem(thecheckNotNullwith the "Missing MenuItem" message)
Used here, in TabsContainer.getMenuItemForItem and in getOrCreateMenuItemForFragment. Those were all the findItem(fragment.menuItemId) call sites in tabs; the only other menu.findItem calls in core are the Stack header toolbar's, which look items up by their own id map.
Generated by Claude Code
There was a problem hiding this comment.
1f0d8bd: done your way. TabsBottomNavigationView now has
internal fun findMenuItem(menuItemId: Int): MenuItem? = menu.findItem(menuItemId)
internal fun requireMenuItem(menuItemId: Int): MenuItem =
checkNotNull(findMenuItem(menuItemId)) { "[RNScreens] Missing MenuItem for id: $menuItemId" }and the call sites are bottomNavigationView.requireMenuItem(fragment.menuItemId) (here) and bottomNavigationView.findMenuItem(fragment.menuItemId) (getMenuItemForItem). The Menu extensions from fb1e94a are gone, so MenuHelpers.kt is back to its state on main.
Generated by Claude Code
…item lookup Review follow-ups: - `TabsAppearanceApplicator` and `TabsAppearanceCoordinator` methods are explicitly `internal`: only core's tabs code calls them. - The "apply, and re-invalidate if the item view is missing" checks for the active indicator and accessibility are nested ifs. - New `Menu.findMenuItemForFragment` / `Menu.requireMenuItemForFragment` helpers in `MenuHelpers.kt`, used by the whole-bar update, `getMenuItemForItem` and `getOrCreateMenuItemForFragment` instead of repeating `findItem(fragment.menuItemId)`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review follow-up: instead of `Menu` extensions taking a fragment, `TabsBottomNavigationView` has `findMenuItem(menuItemId)` and `requireMenuItem(menuItemId)`, so call sites pass `fragment.menuItemId` directly. `MenuHelpers.kt` is back to its state on `main`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review follow-up (adversarial review): `onItemInvalidated` still said the applicator re-applies the invalidated parts, `consumePendingFlags` promised every flag is applied although `APPEARANCE` is bar-wide, and "retry on the next update" suggested a scheduled retry where the flag is only kept pending. Comments only. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review decision: only core's `TabsContainer` uses it, and this PR changes its signature anyway (no `tabsScreenFragments` parameter, a Boolean from `setA11yPropertiesToTabItem`), so it should not be part of the public surface. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Requested by Kacper
Description
Follow-up to #4920, raised in its review. The tab item invalidation flags
TITLE,BADGEandACCESSIBILITYonly routed a change to the per-item menu update, which then re-applied title (diffed), badge and accessibility every time. They now gate what is applied, mirroring the Stack header'sneedsRebuild || flags.containsAny(X)inStackHeaderCoordinatorLayout.processUpdate. Nothing visible changes; unchanged parts of a menu item are no longer re-applied.Closes https://github.com/software-mansion/react-native-screens-labs/issues/1956.
Targets
main: #4946, which it was stacked on, merged on 2026-10-21.Changes
TabsContainer.applyPendingMenuItemChangesis the only caller ofTabsMenuItemState.consumePendingFlags(). It hands the flags toTabsAppearanceCoordinator.updateMenuItemAppearance, which applies title (TITLE), icon (ICON/ICON_SIZE), active indicator (ICON_SIZE/ACTIVE_INDICATOR_SIZE) and badge (BADGE), and applies accessibility (ACCESSIBILITY) throughTabsHostA11yCoordinator. The title is no longer diffed manually.TabsAppearanceApplicatorexposesapplyTitle,applyIcon,applyActiveIndicatorSizeandupdateBadgeAppearanceand knows nothing about flags. A part whose menu item view doesn't exist yet is re-invalidated throughTabsBottomNavigationView.invalidateMenuItem: the active indicator as before, and accessibility too (ce494b0;setA11yPropertiesToTabItemnow reports whether it found the view).TabsContainer.updateTabBarAppearanceapplies the bar-wide appearance, then every menu item through the same per-item path, then fonts. It invalidatesBADGEon every item first (badge colours come from the selected tab's appearance); appearance updates also invalidateACCESSIBILITY, replacingsetA11yPropertiesToAllTabItems(removed, withTabsHostA11yCoordinator's unusedtabsScreenFragmentsparameter).TabsBottomNavigationView.findMenuItem(menuItemId)/requireMenuItem(menuItemId), and the appearance applicator / coordinator methods are explicitlyinternal(review, fb1e94a and 1f0d8bd; comment fixes in c339610). No behavioural change, so the device runs below on ce494b0 still apply.Test plan
Run locally:
yarn lint-android(layer check +spotlessCheck) passes.:react-native-screens:compileDebugKotlinsucceeds.On device, after the review changes, this PR alone (ce494b0, without #4976; Android, FabricExample, release build, Pixel 3a API 34 emulator):
FabricExample/e2e/single-feature-tests/tabs/plusexample-tests/bottomTabs: 11 suites, 66 tests pass. Crash buffer empty.title,badgeValue,tabBarItemTestIDandtabBarItemAccessibilityLabel(temporary buttons ontest-tabs-general-appearance-android, not committed), read withuiautomator dump: the item's title, badge,resource-idand content description update immediately; the other tabs stay unchanged; the values survive switching tabs and whole-bar updates (label visibilitylabeled/unlabeled); a reset restores the original values. Crash buffer empty.On device, before the review restructuring (Android, FabricExample, release build with this PR and #4976 on top, Pixel 3a API 34 emulator):
FabricExample/e2e/single-feature-tests/tabs/plusexample-tests/bottomTabs: 11 suites, 66 tests pass (test-tabs-item-badgeandtest-tabs-navigation-state-search-tabare iOS-gated). Crash buffer empty.single-feature-tests/tabs/test-tabs-item-icon-tint-and-size: runtime icon size (32 / 56 / default), active indicator (96x64 / auto), tinting modes and the badge toggle apply immediately and revert; other tabs stay unchanged.test-tabs-item-icon: asynchronously loaded image icons appear (they arrive outside a React transaction); the selected icon swaps.test-tabs-item-badge: badge colours follow the selected tab on every switch (whole-bar update re-applies all badges).test-tabs-general-appearance-androidandissue-tests/TestBottomTabs: tab items expose their testIDs (viewIdResourceName) and content descriptions, unchanged across tab switches and label modes.test-tabs-item-title: titles, colours and per-tab fonts follow selection.Checklist
Generated by Claude Code