refactor(Android, Stack v5): separate React and native domains in header implementation#4150
refactor(Android, Stack v5): separate React and native domains in header implementation#4150kligarski wants to merge 17 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Refactors the Android (Stack v5 / “gamma”) header implementation to better separate the “React config” domain (state/props/events) from the “native header view” domain (toolbar/appbar construction and updates), while also improving teardown to avoid leaking Fabric UIManager listeners.
Changes:
- Replaces the monolithic
StackHeaderCoordinatorwith a slimmerStackHeaderCoordinatorLayout+ newStackHeaderApplicator, and introduces an observer/flags-based update pipeline (StackHeaderConfigurationObserver,StackHeaderUpdateFlags). - Reworks header config attachment to pass both a configuration provider and a native delegate (
OnHeaderConfigurationAttachListener,StackHeaderDelegate) and moves layout-to-shadow-state syncing through the delegate. - Adds explicit teardown paths for Fabric UIManager listeners in
StackHostandStackHeaderConfigviaonDropViewInstance.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| android/src/main/java/com/swmansion/rnscreens/gamma/stack/screen/StackScreen.kt | Renames/reshapes header-config attach listener API; refactors shadow-state syncing entrypoints. |
| android/src/main/java/com/swmansion/rnscreens/gamma/stack/host/StackHostViewManager.kt | Calls tearDown() on host drop to unregister UIManager listeners. |
| android/src/main/java/com/swmansion/rnscreens/gamma/stack/host/StackHost.kt | Uses getFabricUIManagerNotNull and adds tearDown() to remove UIManager listener. |
| android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/toolbar/StackHeaderToolbarMenuCoordinator.kt | Deleted; toolbar menu logic moved into applicator. |
| android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/subview/StackHeaderSubviewProviding.kt | Removes origin-offset update API from the provider interface. |
| android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/subview/StackHeaderSubview.kt | Adjusts subview change listener naming and keeps origin shadow-state updates local to the subview. |
| android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/subview/OnStackHeaderSubviewChangeListener.kt | Renames callback method to onStackHeaderSubviewChanged(). |
| android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/StackHeaderCoordinatorLayout.kt | Implements new attach/observe/update flow; delegates shadow-state updates; rebuilds header via applicator. |
| android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/StackHeaderCoordinator.kt | Deleted; responsibilities split into coordinator layout + applicator. |
| android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/StackHeaderApplicator.kt | New; owns header view construction and incremental application of config changes. |
| android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/StackHeaderAppBarLayout.kt | Adds managed title support storage to support applicator-managed title view. |
| android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/config/StackHeaderUpdateFlags.kt | New; introduces bitflags describing which parts of header need updating/rebuild. |
| android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/config/StackHeaderDelegate.kt | New; defines the native delegate contract for frame/subview/menu click callbacks. |
| android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/config/StackHeaderConfigViewManager.kt | Stops calling legacy change notifier; adds tearDown() on drop. |
| android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/config/StackHeaderConfigurationProviding.kt | Renames config provider interface and replaces delegates with a single observer setter. |
| android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/config/StackHeaderConfigurationObserver.kt | Renames/reshapes observer interface to include update flags. |
| android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/config/StackHeaderConfig.kt | Implements provider + delegate; adds flags-based invalidation and flush on Fabric mount completion; adds UIManager listener teardown. |
| android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/config/OnHeaderConfigurationAttachListener.kt | New; attach callback that passes provider + delegate separately. |
| android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/config/OnHeaderConfigAttachListener.kt | Deleted; replaced by the new attach listener contract. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
3178edb to
f054297
Compare
e927adc to
0cd35e1
Compare
| internal val configSubviewsCount: Int | ||
| get() = listOfNotNull(backgroundSubview, leadingSubview, centerSubview, trailingSubview).size | ||
|
|
||
| internal fun getConfigSubviewAt(index: Int): StackHeaderSubview? = | ||
| listOfNotNull(backgroundSubview, leadingSubview, centerSubview, trailingSubview).getOrNull(index) |
There was a problem hiding this comment.
nit: we could have a getter for the listOfNotNull part & use it in both cases
| provider.invalidationFlags.clearing( | ||
| StackHeaderInvalidationFlags.TITLE, | ||
| ) |
There was a problem hiding this comment.
I think having sth like provider.clearInvalidationFlag(x) would be simpler and allow for readonly interface
Description
Refactors the Android gamma stack header architecture to replace the stateful coordinator/delegate pattern with a clean provider + observer + applicator architecture using bitmask invalidation flags.
Closes https://github.com/software-mansion/react-native-screens-labs/issues/1519.
Details
Previous architecture
The header used
StackHeaderCoordinatoras a stateful middleman with 13 cachedlastXxxfields for change detection, andStackHeaderConfigDelegatemixed forward notifications with reverse callbacks on one interface.post()was used for batching but was unreliable sinceStackHeaderConfigis not in the native view hierarchy.New architecture — three separated concerns
Provider (
StackHeaderConfigurationProviding) — read-only props interface with invalidation flags and observer registration. No native-to-Config callbacks on this interface.Observer (
StackHeaderConfigurationObserver) — forward notification channel from Config to CoordinatorLayout. Flag-batchedonConfigChangedfor prop updates, immediateonMenuItemUpdatedfor imperative commands.Delegate (
StackHeaderDelegate) — reverse callback interface for native-to-React communication (header frame changes, menu item clicks, subview origin changes).Applicator (
StackHeaderApplicator) — stateless. Extracted fromStackHeaderCoordinatorwith all mutable state removed. Every method takes parameters and returns results. Absorbs toolbar menu logic fromStackHeaderToolbarMenuCoordinator.Orchestrator (
StackHeaderCoordinatorLayout) — holds all mutable state, connects provider/delegate, dispatches flag-gated updates to the applicator, owns shadow state sync, and owns toolbar menu ID maps.Bitmask invalidation flags
StackHeaderInvalidationFlagsis avalue classbitmask that replaces the 13 cached fields with 6 flags:STRUCTUREtype,hidden,transparentSUBVIEWScollapseModechangeTITLEtitleBACK_BUTTONbackButtonHidden, tint colors, iconSCROLL_FLAGSscrollFlag*propsTOOLBAR_MENUtoolbarMenuItems, item iconsAll config properties use
Delegates.observable— on change they callinvalidate(flag)which ORs the flag intoinvalidationFlags. Flags withSTRUCTUREorSUBVIEWStrigger a full rebuild; the rest trigger granular in-place updates. After processing, each flag is cleared.Config attachment flow
When a screen enters the stack:
StackScreenFragment.onCreateView()createsStackHeaderCoordinatorLayout.OnHeaderConfigurationAttachListeneronStackScreen.StackScreen.attachHeaderConfig(config)callsonHeaderConfigAttached(config, config)— passes the sameStackHeaderConfigobject as both provider and delegate (viewed through two interfaces).configObserveron the provider.processUpdate(provider)runs withALLflags (initialinvalidationFlagsonStackHeaderConfigisALL), performing full rebuild + all in-place updates.React prop batch update flow
willMountItems()→isInsideMountTransaction = true.Delegates.observable→invalidate(flag)accumulates intoinvalidationFlags.onAfterUpdateTransaction→ triggers icon resolution only (no flush).didMountItems()→isInsideMountTransaction = false→flushUpdates()→ observer receivesonConfigChanged(config).invalidationFlags, dispatches to applicator methods, clears each processed flag.Async icon resolution flow
onAfterUpdateTransactionstarts async icon load.didMountItems()flushes already-accumulated flags.invalidate(BACK_BUTTON)→ checks!isInsideMountTransaction→ callsflushUpdates()explicitly.onConfigChangedwith onlyBACK_BUTTON→applicator.applyBackButton().Imperative menu command flow
config.dispatchMenuItemUpdate(id, opts, iconSource).configObserver.onMenuItemUpdated(id, options).applicator.updateToolbarMenuItem(toolbar, forwardIdMap, id, options).Shadow state sync flow (native → React)
delegate.onHeaderFrameChanged(w, h, offset)→ShadowStateProxy.updateStateIfNeeded().delegate.onSubviewOriginChanged(type, x, y).Teardown
StackHeaderConfig.tearDown()— unregisters from UIManager, clears flags and observer.StackHeaderCoordinatorLayout.tearDown()— detaches listeners, resets header, unwraps screen, nulls provider/delegate/appBar.StackHost.tearDown()— unregisters from UIManager event listener.onDropViewInstancein their respective ViewManagers.Interface/callback implementation map
StackHeaderConfigStackHeaderConfigurationProvidingStackHeaderConfigStackHeaderDelegateStackHeaderConfigOnStackHeaderSubviewChangeListenerStackHeaderConfigUIManagerListenerStackHeaderCoordinatorLayoutconfigObserverStackHeaderConfigurationObserverStackHeaderCoordinatorLayoutonHeaderConfigAttachedOnHeaderConfigurationAttachListenerStackHeaderCoordinatorLayoutappBarOffsetListener,appBarLayoutChangeListenerAppBarLayout.OnOffsetChangedListener,View.OnLayoutChangeListenerStackScreenWeakReferencetoOnHeaderConfigurationAttachListenerStackHeaderSubviewWeakReferencetoOnStackHeaderSubviewChangeListenerStackHostUIManagerListenerChanges
StackHeaderApplicator.kt— stateless view-buildingextracted from old
StackHeaderCoordinator, absorbs toolbarmenu logic from
StackHeaderToolbarMenuCoordinatorStackHeaderInvalidationFlags.kt— bitmask valueclass for flag-gated prop updates
StackHeaderConfigurationObserver.kt— forwardnotification interface (Config → CoordinatorLayout)
StackHeaderDelegate.kt— reverse callback interface(CoordinatorLayout → Config)
OnHeaderConfigurationAttachListener.kt— replacesold
OnHeaderConfigAttachListener, passes(provider, delegate)pairStackHeaderCoordinatorLayout.kt— becomesorchestrator, owns all mutable state, flag-gated dispatch,
shadow state sync
StackHeaderConfig.kt— implementsUIManagerListener+StackHeaderDelegate, observable propswith flags,
invalidate()/flushUpdates()patternStackScreen.kt—attachHeaderConfig/detachHeaderConfigpass config as both provider and delegate;register/clear listener API
StackHeaderAppBarLayout.kt—SmallgainsmanagedTitleViewpropertyStackHeaderConfigViewManager.kt— removesnotifyConfigChanged()fromonAfterUpdateTransaction, addsonDropViewInstancefor teardownStackHost.kt— addstearDown(), usesgetFabricUIManagerNotNullhelperStackHostViewManager.kt— addsonDropViewInstancecallingtearDown()StackHeaderSubview.kt— adapted to renamedinterfaces
StackHeaderConfigProviding.kt→StackHeaderConfigurationProviding.kt— removedupdateHeaderFrame,onMenuItemClick,setDelegate/removeDelegate; addedsetConfigurationObserver,invalidationFlagsStackHeaderConfigDelegate.kt→StackHeaderConfigurationObserver.kt— conceptually replaced;old mixed forward+reverse callbacks, new is forward-only
observer with
onConfigChanged+onMenuItemUpdatedStackHeaderCoordinator.kt— replaced byStackHeaderApplicator.kt(stateless) +StackHeaderCoordinatorLayout.kt(orchestrator)StackHeaderToolbarMenuCoordinator.kt— logicabsorbed into
StackHeaderApplicatorOnHeaderConfigAttachListener.kt— replaced byOnHeaderConfigurationAttachListener.ktwith(provider, delegate)pair signatureStackHeaderSubviewProviding.kt— removedupdateContentOriginOffset(moved to delegate)Before & after - visual documentation
N/A
Test plan
Stack v5 single feature tests.
Checklist
prevent-native-dismiss-nested-stackprevent-native-dismiss-single-stacktest-animation-androidtest-stack-back-button-androidtest-stack-simple-navtest-stack-subviews-androidtest-stack-toolbar-menu-commands-androidtest-stack-toolbar-menu-icon-androidtest-stack-toolbar-menu-show-as-action-android