Skip to content

refactor(Android, Stack v5): separate React and native domains in header implementation#4150

Open
kligarski wants to merge 17 commits into
mainfrom
@kligarski/stack-v5-android-refactor
Open

refactor(Android, Stack v5): separate React and native domains in header implementation#4150
kligarski wants to merge 17 commits into
mainfrom
@kligarski/stack-v5-android-refactor

Conversation

@kligarski

@kligarski kligarski commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

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 StackHeaderCoordinator as a stateful middleman with 13 cached lastXxx fields for change detection, and StackHeaderConfigDelegate mixed forward notifications with reverse callbacks on one interface. post() was used for batching but was unreliable since StackHeaderConfig is not in the native view hierarchy.

New architecture — three separated concerns

  1. Provider (StackHeaderConfigurationProviding) — read-only props interface with invalidation flags and observer registration. No native-to-Config callbacks on this interface.

  2. Observer (StackHeaderConfigurationObserver) — forward notification channel from Config to CoordinatorLayout. Flag-batched onConfigChanged for prop updates, immediate onMenuItemUpdated for imperative commands.

  3. Delegate (StackHeaderDelegate) — reverse callback interface for native-to-React communication (header frame changes, menu item clicks, subview origin changes).

  4. Applicator (StackHeaderApplicator) — stateless. Extracted from StackHeaderCoordinator with all mutable state removed. Every method takes parameters and returns results. Absorbs toolbar menu logic from StackHeaderToolbarMenuCoordinator.

  5. 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

StackHeaderInvalidationFlags is a value class bitmask that replaces the 13 cached fields with 6 flags:

Flag Properties
STRUCTURE type, hidden, transparent
SUBVIEWS subview add/remove, collapseMode change
TITLE title
BACK_BUTTON backButtonHidden, tint colors, icon
SCROLL_FLAGS all scrollFlag* props
TOOLBAR_MENU toolbarMenuItems, item icons

All config properties use Delegates.observable — on change they call invalidate(flag) which ORs the flag into
invalidationFlags. Flags with STRUCTURE or SUBVIEWS trigger 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:

  1. StackScreenFragment.onCreateView() creates StackHeaderCoordinatorLayout.
  2. CoordinatorLayout registers as OnHeaderConfigurationAttachListener on StackScreen.
  3. StackScreen.attachHeaderConfig(config) calls onHeaderConfigAttached(config, config) — passes the same StackHeaderConfig object as both provider and delegate (viewed through two interfaces).
  4. CoordinatorLayout sets its configObserver on the provider.
  5. processUpdate(provider) runs with ALL flags (initial invalidationFlags on StackHeaderConfig is ALL), performing full rebuild + all in-place updates.

React prop batch update flow

  1. Mount transaction opens → willMountItems()isInsideMountTransaction = true.
  2. ViewManager setters fire → Delegates.observableinvalidate(flag) accumulates into invalidationFlags.
  3. onAfterUpdateTransaction → triggers icon resolution only (no flush).
  4. didMountItems()isInsideMountTransaction = falseflushUpdates() → observer receives onConfigChanged(config).
  5. CoordinatorLayout reads invalidationFlags, dispatches to applicator methods, clears each processed flag.

Async icon resolution flow

  1. onAfterUpdateTransaction starts async icon load.
  2. didMountItems() flushes already-accumulated flags.
  3. Later, icon loads on main thread → callback fires → invalidate(BACK_BUTTON) → checks !isInsideMountTransaction → calls flushUpdates() explicitly.
  4. CoordinatorLayout receives onConfigChanged with only BACK_BUTTONapplicator.applyBackButton().

Imperative menu command flow

  1. ViewManager calls config.dispatchMenuItemUpdate(id, opts, iconSource).
  2. If no icon to resolve: directly calls configObserver.onMenuItemUpdated(id, options).
  3. CoordinatorLayout receives callback → applicator.updateToolbarMenuItem(toolbar, forwardIdMap, id, options).
  4. Bypasses flag system entirely — applied immediately.

Shadow state sync flow (native → React)

  1. User scrolls / layout changes → AppBar offset/layout listeners fire.
  2. CoordinatorLayout calls delegate.onHeaderFrameChanged(w, h, offset)ShadowStateProxy.updateStateIfNeeded().
  3. Subview offsets updated via 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.
  • All teardowns triggered from onDropViewInstance in their respective ViewManagers.

Interface/callback implementation map

Class Implements / has callback Interface
StackHeaderConfig implements StackHeaderConfigurationProviding
StackHeaderConfig implements StackHeaderDelegate
StackHeaderConfig implements OnStackHeaderSubviewChangeListener
StackHeaderConfig implements UIManagerListener
StackHeaderCoordinatorLayout has anonymous object field configObserver StackHeaderConfigurationObserver
StackHeaderCoordinatorLayout has lambda field onHeaderConfigAttached OnHeaderConfigurationAttachListener
StackHeaderCoordinatorLayout has listener fields appBarOffsetListener, appBarLayoutChangeListener AppBarLayout.OnOffsetChangedListener, View.OnLayoutChangeListener
StackScreen stores WeakReference to OnHeaderConfigurationAttachListener
StackHeaderSubview stores WeakReference to OnStackHeaderSubviewChangeListener
StackHost implements UIManagerListener

Changes

  • New: StackHeaderApplicator.kt — stateless view-building
    extracted from old StackHeaderCoordinator, absorbs toolbar
    menu logic from StackHeaderToolbarMenuCoordinator
  • New: StackHeaderInvalidationFlags.kt — bitmask value
    class for flag-gated prop updates
  • New: StackHeaderConfigurationObserver.kt — forward
    notification interface (Config → CoordinatorLayout)
  • New: StackHeaderDelegate.kt — reverse callback interface
    (CoordinatorLayout → Config)
  • New: OnHeaderConfigurationAttachListener.kt — replaces
    old OnHeaderConfigAttachListener, passes (provider, delegate) pair
  • Rewritten: StackHeaderCoordinatorLayout.kt — becomes
    orchestrator, owns all mutable state, flag-gated dispatch,
    shadow state sync
  • Rewritten: StackHeaderConfig.kt — implements
    UIManagerListener + StackHeaderDelegate, observable props
    with flags, invalidate()/flushUpdates() pattern
  • Modified: StackScreen.ktattachHeaderConfig /
    detachHeaderConfig pass config as both provider and delegate;
    register/clear listener API
  • Modified: StackHeaderAppBarLayout.ktSmall gains
    managedTitleView property
  • Modified: StackHeaderConfigViewManager.kt — removes
    notifyConfigChanged() from onAfterUpdateTransaction, adds
    onDropViewInstance for teardown
  • Modified: StackHost.kt — adds tearDown(), uses
    getFabricUIManagerNotNull helper
  • Modified: StackHostViewManager.kt — adds
    onDropViewInstance calling tearDown()
  • Modified: StackHeaderSubview.kt — adapted to renamed
    interfaces
  • Renamed: StackHeaderConfigProviding.kt
    StackHeaderConfigurationProviding.kt — removed
    updateHeaderFrame, onMenuItemClick,
    setDelegate/removeDelegate; added
    setConfigurationObserver, invalidationFlags
  • Renamed: StackHeaderConfigDelegate.kt
    StackHeaderConfigurationObserver.kt — conceptually replaced;
    old mixed forward+reverse callbacks, new is forward-only
    observer with onConfigChanged + onMenuItemUpdated
  • Deleted: StackHeaderCoordinator.kt — replaced by
    StackHeaderApplicator.kt (stateless) +
    StackHeaderCoordinatorLayout.kt (orchestrator)
  • Deleted: StackHeaderToolbarMenuCoordinator.kt — logic
    absorbed into StackHeaderApplicator
  • Deleted: OnHeaderConfigAttachListener.kt — replaced by
    OnHeaderConfigurationAttachListener.kt with
    (provider, delegate) pair signature
  • Modified: StackHeaderSubviewProviding.kt — removed
    updateContentOriginOffset (moved to delegate)

Before & after - visual documentation

N/A

Test plan

Stack v5 single feature tests.

Checklist

  • Tested for regressions:
    • prevent-native-dismiss-nested-stack
    • prevent-native-dismiss-single-stack
    • test-animation-android
    • test-stack-back-button-android
    • test-stack-simple-nav
    • test-stack-subviews-android
    • test-stack-toolbar-menu-commands-android
    • test-stack-toolbar-menu-icon-android
    • test-stack-toolbar-menu-show-as-action-android
  • Ensured that CI passes

@kligarski kligarski changed the base branch from main to @kligarski/stack-v5-android-update-back-button June 10, 2026 17:18
@kligarski kligarski requested a review from Copilot June 10, 2026 17:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 StackHeaderCoordinator with a slimmer StackHeaderCoordinatorLayout + new StackHeaderApplicator, 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 StackHost and StackHeaderConfig via onDropViewInstance.

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.

@kligarski kligarski force-pushed the @kligarski/stack-v5-android-update-back-button branch from 3178edb to f054297 Compare June 19, 2026 12:09
Base automatically changed from @kligarski/stack-v5-android-update-back-button to main June 19, 2026 13:15
@kligarski kligarski force-pushed the @kligarski/stack-v5-android-refactor branch from e927adc to 0cd35e1 Compare June 22, 2026 08:03
@kligarski kligarski requested a review from Copilot June 22, 2026 08:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

@kligarski kligarski marked this pull request as ready for review June 22, 2026 08:56
Comment on lines +298 to +302
internal val configSubviewsCount: Int
get() = listOfNotNull(backgroundSubview, leadingSubview, centerSubview, trailingSubview).size

internal fun getConfigSubviewAt(index: Int): StackHeaderSubview? =
listOfNotNull(backgroundSubview, leadingSubview, centerSubview, trailingSubview).getOrNull(index)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we could have a getter for the listOfNotNull part & use it in both cases

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +197 to +199
provider.invalidationFlags.clearing(
StackHeaderInvalidationFlags.TITLE,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having sth like provider.clearInvalidationFlag(x) would be simpler and allow for readonly interface

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kligarski kligarski requested a review from kmichalikk June 23, 2026 06:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants