Skip to content

feat(Android, Stack v5): add basic support for header#3753

Merged
kligarski merged 21 commits intomainfrom
@kligarski/stack-v5-android-header-skeleton
Apr 9, 2026
Merged

feat(Android, Stack v5): add basic support for header#3753
kligarski merged 21 commits intomainfrom
@kligarski/stack-v5-android-header-skeleton

Conversation

@kligarski
Copy link
Copy Markdown
Contributor

@kligarski kligarski commented Mar 11, 2026

Description

Adds basic support for the header on Android in Stack v5 using Material 3 App Bar.

This PR focuses on creating the class structure and handling layout synchronization between native side and Yoga.

There is no configuration exposed to JS, on the native side StackScreenHeaderConfigurationProviding exposes basic properties:

  • headerMode: can be one of small, medium, large
  • title
  • isHidden
  • isTransparent - please note here that this does not change the color of the header to transparent but only the layout. Changing appearance will be handled in separate PRs (then setting this or similar prop will override background color). Also note that this option currently might work incorrectly with liftOnScroll enabled in AppBarLayout. This will be handled in the future PRs.

Closes https://github.com/software-mansion/react-native-screens-labs/issues/892.
Closes https://github.com/software-mansion/react-native-screens-labs/issues/903.

Changes

  • create necessary classes for setting up the header
  • create custom ShadowNode logic for StackScreen
  • add WIP Single Feature Test - it is currently static as there are no props exposed to JS. In the future PRs, it will be modified to allow changing header modes in runtime.

Before & after - visual documentation

Small + no scroll Large + scroll
header_small_no_scroll.mp4
header_large_scroll.mp4
"Transparent" (liftOnScroll disabled) Dynamic hide
header_small_no_scroll_transparent.mp4
header_large_scroll_dynamic_hide.mp4

Test plan

Run single-feature-tests/test-stack-header-modes.tsx. For testing, you can change properties, currently hard-coded in StackScreenCoordinatorLayout and scroll flags/liftOnScroll in StackScreenAppBarLayout.

Checklist

  • Included code example that can be used to test this change.
  • For visual changes, included screenshots / GIFs / recordings documenting the change.
  • Ensured that CI passes

@kligarski kligarski changed the title [WIP] feat(Android, Stack v5): add basic support for header feat(Android, Stack v5): add basic support for header Mar 12, 2026
@kligarski kligarski marked this pull request as ready for review March 12, 2026 17:33
@kligarski kligarski requested a review from Copilot March 12, 2026 17:33
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds foundational Android support for Stack v5 headers using Material 3 App Bar, primarily by introducing native header view structure and synchronizing native layout with Yoga via Fabric state.

Changes:

  • Adds new Fabric ShadowNode/State/ComponentDescriptor for RNSStackScreen to drive Yoga sizing and content origin offset from native state.
  • Introduces Android StackScreen header infrastructure (CoordinatorLayout + AppBarLayout variants) and a state proxy to push layout/offset updates into Fabric state.
  • Adds an Android-only single-feature test scenario entry for validating header modes (currently static/WIP).

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/fabric/gamma/stack/StackScreenNativeComponent.ts Marks StackScreen codegen component as interface-only for Fabric.
react-native.config.js Registers RNSStackScreenComponentDescriptor for Android codegen.
ios/gamma/stack/screen/RNSStackScreenComponentView.mm Includes the new StackScreen component descriptor header.
common/cpp/react/renderer/components/rnscreens/RNSStackScreenState.h Introduces StackScreen Fabric state container (Android: frame + content offset).
common/cpp/react/renderer/components/rnscreens/RNSStackScreenState.cpp Implements Android getDynamic() for StackScreen state.
common/cpp/react/renderer/components/rnscreens/RNSStackScreenShadowNode.h Adds StackScreen ShadowNode (Android override for getContentOriginOffset).
common/cpp/react/renderer/components/rnscreens/RNSStackScreenShadowNode.cpp Defines component name and returns content offset from state (Android).
common/cpp/react/renderer/components/rnscreens/RNSStackScreenComponentDescriptor.h Adds descriptor adopt logic to push state-derived size into Yoga (Android).
apps/src/tests/single-feature-tests/stack-v5/test-stack-header-modes.tsx Adds Android-only WIP scenario to exercise header modes/scrolling.
apps/src/tests/single-feature-tests/stack-v5/index.ts Registers the new Stack v5 header modes scenario.
android/src/main/jni/rnscreens.h Exposes RNSStackScreenComponentDescriptor to Android JNI compilation.
android/src/main/java/com/swmansion/rnscreens/utils/DimensionUtils.kt Adds helper to resolve theme dimension attributes for Material sizing.
android/src/main/java/com/swmansion/rnscreens/gamma/stack/screen/header/configuration/StackScreenHeaderType.kt Adds enum for basic header modes (small/medium/large).
android/src/main/java/com/swmansion/rnscreens/gamma/stack/screen/header/configuration/StackScreenHeaderConfigurationProviding.kt Adds interface describing header configuration inputs on native side.
android/src/main/java/com/swmansion/rnscreens/gamma/stack/screen/header/StackScreenScrollingViewBehavior.kt Adds Coordinator behavior to observe scrolling dependency changes and report header height.
android/src/main/java/com/swmansion/rnscreens/gamma/stack/screen/header/StackScreenHeaderCoordinator.kt Adds coordinator that builds/removes app bar structure and wires behavior + title updates.
android/src/main/java/com/swmansion/rnscreens/gamma/stack/screen/header/StackScreenCoordinatorLayout.kt Adds CoordinatorLayout wrapper to host header + screen wrapper and trigger relayout sync.
android/src/main/java/com/swmansion/rnscreens/gamma/stack/screen/header/StackScreenAppBarLayout.kt Adds Material3 AppBarLayout implementations for small + collapsing (medium/large).
android/src/main/java/com/swmansion/rnscreens/gamma/stack/screen/StackScreenViewManager.kt Stores Fabric StateWrapper on the view so native can push state updates.
android/src/main/java/com/swmansion/rnscreens/gamma/stack/screen/StackScreenShadowStateProxy.kt Adds proxy that diffs and pushes frame/offset updates into Fabric state.
android/src/main/java/com/swmansion/rnscreens/gamma/stack/screen/StackScreenFragment.kt Wraps StackScreen inside the new CoordinatorLayout root to enable native header layout.
android/src/main/java/com/swmansion/rnscreens/gamma/stack/screen/StackScreen.kt Pushes measured size into Fabric state during layout and exposes state wrapper hook.
android/src/main/java/com/swmansion/rnscreens/gamma/stack/host/StackContainer.kt Switches container base class to FrameLayout, passes context into fragments, adds forced measure/layout pass helper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +11 to +12
require(context.theme.resolveAttribute(attrId, typedValue, true)) {
"[RNScreens] Unable to resolve Material theme dimension."
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

The require(...) message is too generic to debug theme issues. Including which attribute failed to resolve (e.g. attrId as hex) and/or the current theme would make failures actionable, especially since this will throw at runtime.

Suggested change
require(context.theme.resolveAttribute(attrId, typedValue, true)) {
"[RNScreens] Unable to resolve Material theme dimension."
val theme = context.theme
require(theme.resolveAttribute(attrId, typedValue, true)) {
"[RNScreens] Unable to resolve Material theme dimension (attrId=0x%08x, theme=%s)".format(
attrId,
theme,
)

Copilot uses AI. Check for mistakes.
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.

I don't think that this is necessary, we need those dimensions from Material, we don't have a reasonable fallback. If something breaks, you can add a breakpoint to investigate.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 23 out of 23 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

android/src/main/java/com/swmansion/rnscreens/gamma/stack/screen/StackScreenFragment.kt:4

  • Unused import android.content.Context is left in this file after switching to requireContext(). This will be flagged by Spotless/ktlint (and can fail CI). Remove the unused import.
import android.os.Bundle
import android.view.Gravity

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Copy link
Copy Markdown
Contributor

@t0maboro t0maboro left a comment

Choose a reason for hiding this comment

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

just leaving a note that I haven't checked the cpp part yet

shadowNode.getState());
auto stateData = state->getData();

if (stateData.frameSize.width != 0 && stateData.frameSize.height != 0) {
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.

maybe > 0 here?

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.

I'm not sure here. We want to setSize when we get data from native. If native supplies incorrect values, I think it's better that we apply them and we discover a bug rather than rely on previous value being valid.

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'm wondering whether we should apply them or throw, because of an incorrect state

Copy link
Copy Markdown
Contributor Author

@kligarski kligarski Apr 2, 2026

Choose a reason for hiding this comment

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

We would need to also update other components (Stack v4, Tabs Bottom Accessory, Split Screen) as they all use the same pattern I guess.

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.

so maybe just let's make the ticket for that

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.

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.

btw I wouldn't use = for floats

@kligarski kligarski force-pushed the @kligarski/stack-v5-android-header-skeleton branch from 1536e9c to 8b00e5b Compare April 7, 2026 12:34
Copy link
Copy Markdown
Contributor

@kmichalikk kmichalikk left a comment

Choose a reason for hiding this comment

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

Few nitpicks, could be done in the followup

@@ -0,0 +1,8 @@
package com.swmansion.rnscreens.gamma.stack.screen.header.configuration

internal interface StackScreenHeaderConfigurationProviding {
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.

what is the benefit of using interface here instead of, for instance, a simple dataclass? I can see later objects artificially created that implement the interface inline. I'm not sure I like it, though I don't know, maybe this is an idiomatic way?

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.

Arguments of such dataclass should not have -Providing then (some cases below in this PR)

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.

StackHeaderConfig (native React component) will implement this interface. The object is for debugging in this PR.

*/
private fun stackContainerOrNull(): StackContainer? = this.parent as StackContainer?

// TODO: do we need to rely on parent here?
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.

Let's settle this before merging

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.

appBarLayout: StackScreenAppBarLayout,
title: String,
) {
// TODO: diffing mechanism?
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.

Let's settle on something before merging

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.

) {
val stackScreenWrapper = coordinatorLayout.stackScreenWrapper
val params = stackScreenWrapper.layoutParams as CoordinatorLayout.LayoutParams
val needsBehavior = appBarLayout != null && !config.isTransparent && !config.isHidden
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: needsBehavior is vague to me, maybe something more descriptive? hasBehavior is okay if it implies "has any behavior set"

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.

Won't be needed after follow-up PR: #3796.

}

companion object {
private const val DELTA = 0.9f
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.

delta could be less

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.

Will be fixed in follow-up: #3796.

shadowNode.getState());
auto stateData = state->getData();

if (stateData.frameSize.width != 0 && stateData.frameSize.height != 0) {
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.

btw I wouldn't use = for floats

@kligarski kligarski merged commit a821ff0 into main Apr 9, 2026
7 of 8 checks passed
@kligarski kligarski deleted the @kligarski/stack-v5-android-header-skeleton branch April 9, 2026 17:05
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.

4 participants