Skip to content

feat(tab): add persistentTabBar option for keep the tab pinned#2186

Open
usmanvrtx wants to merge 2 commits intomainfrom
feat/tabbar-persistent-header
Open

feat(tab): add persistentTabBar option for keep the tab pinned#2186
usmanvrtx wants to merge 2 commits intomainfrom
feat/tabbar-persistent-header

Conversation

@usmanvrtx
Copy link
Copy Markdown
Contributor

Summary

Add persistentTabBar support so the tab bar stays pinned while only the tab content scrolls.

Key Changes

TabBar Behavior

  • Add persistentTabBar parameter to TabBar.
  • Keep the tab bar fixed at the top.
  • Scroll only the selected tab content.

Usage

  body:
    TabBar:
      styles:
        persistentTabBar: true

@usmanvrtx usmanvrtx requested a review from TheNoumanDev April 7, 2026 17:48
@usmanvrtx usmanvrtx self-assigned this Apr 7, 2026
@usmanvrtx usmanvrtx added the enhancement New feature or request label Apr 7, 2026
@usmanvrtx usmanvrtx linked an issue Apr 7, 2026 that may be closed by this pull request
tabContent

widget._controller.persistentTabBar
? Expanded(
Copy link
Copy Markdown
Member

@TheNoumanDev TheNoumanDev Apr 16, 2026

Choose a reason for hiding this comment

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

We are wrapping it with Expanded widget based on persistentTabBar, and also wrapping it with Expanded when isExpanded is also true, so if we set both properties, it gets wrapped twice in Expanded widget. Can you check if using both properties breaks it or works fine?

Verify with some edge cases:

  • persistentTabBar: true + content has ListView
  • persistentTabBar: true + content has Expanded child
  • Tab content shorter than the screen, verify that it takes space as widget space or it takes extra space.

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.

Property Behavior:

expanded property:

  • expanded: true → Content stretches to fill remaining screen space, even if content is shorter than available height
  • expanded: false → Content only takes the space it needs

persistentTabBar property:

  • persistentTabBar: true → Enables scrolling while keeping tab bar visible; works with or without the expanded property
  • persistentTabBar: false → Default behavior (TabBarView)

Co-authored-by: Copilot <copilot@github.com>
Copilot AI review requested due to automatic review settings April 28, 2026 19:50
Copy link
Copy Markdown

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 a persistentTabBar option intended to keep the TabBar pinned while the selected tab’s content scrolls within the TabBar widget.

Changes:

  • Added persistentTabBar property wiring into BaseTabBar setters and TabBarController.
  • Updated TabBarState.build() to optionally wrap selected tab content in a SingleChildScrollView when persistentTabBar is enabled.

Reviewed changes

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

File Description
modules/ensemble/lib/layout/tab_bar.dart Adds persistentTabBar setter and conditional scroll-wrapping logic for tab content.
modules/ensemble/lib/layout/tab/tab_bar_controller.dart Introduces persistentTabBar boolean flag on the controller.

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

Comment on lines +292 to +300
key: UniqueKey(),
builder: (BuildContext context) =>
SingleChildScrollView(child: buildSelectedTab()),
),
)
: Builder(
key: UniqueKey(),
builder: (BuildContext context) =>
SingleChildScrollView(child: buildSelectedTab()),
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

Using UniqueKey() here guarantees the SingleChildScrollView (and its scroll position) is recreated on every rebuild, so users will lose their scroll offset even for minor state updates. For a persistent/pinned tab bar experience, use a stable key (e.g., PageStorageKey based on selectedIndex) and/or manage a ScrollController per tab to preserve scroll position across rebuilds and tab switches.

Suggested change
key: UniqueKey(),
builder: (BuildContext context) =>
SingleChildScrollView(child: buildSelectedTab()),
),
)
: Builder(
key: UniqueKey(),
builder: (BuildContext context) =>
SingleChildScrollView(child: buildSelectedTab()),
builder: (BuildContext context) =>
SingleChildScrollView(
key: PageStorageKey<int>(
widget._controller.selectedIndex),
child: buildSelectedTab(),
),
),
)
: Builder(
builder: (BuildContext context) =>
SingleChildScrollView(
key: PageStorageKey<int>(
widget._controller.selectedIndex),
child: buildSelectedTab(),
),

Copilot uses AI. Check for mistakes.
Comment on lines +288 to +302
widget._controller.persistentTabBar
? (isExpanded
? Expanded(
child: Builder(
key: UniqueKey(),
builder: (BuildContext context) =>
SingleChildScrollView(child: buildSelectedTab()),
),
)
: Builder(
key: UniqueKey(),
builder: (BuildContext context) =>
SingleChildScrollView(child: buildSelectedTab()),
))
: tabContent,
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

The persistentTabBar branch duplicates the tabContent building logic (Builder + key + buildSelectedTab) instead of reusing the existing tabContent variable, which increases the chance of diverging behavior between modes. Consider factoring this into a single tabBody widget and then conditionally wrapping it (e.g., with Expanded/Scrollable) based on persistentTabBar/isExpanded.

Copilot uses AI. Check for mistakes.
Comment on lines +288 to +301
widget._controller.persistentTabBar
? (isExpanded
? Expanded(
child: Builder(
key: UniqueKey(),
builder: (BuildContext context) =>
SingleChildScrollView(child: buildSelectedTab()),
),
)
: Builder(
key: UniqueKey(),
builder: (BuildContext context) =>
SingleChildScrollView(child: buildSelectedTab()),
))
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

This introduces a new layout/scroll behavior branch for TabBar; given the project has widget tests for other components, it would be good to add a Flutter widget test that verifies (1) the TabBar stays visible while content scrolls and (2) switching tabs doesn’t throw when tab bodies are scrollables (e.g., ListView).

Copilot uses AI. Check for mistakes.
Comment on lines +288 to +301
widget._controller.persistentTabBar
? (isExpanded
? Expanded(
child: Builder(
key: UniqueKey(),
builder: (BuildContext context) =>
SingleChildScrollView(child: buildSelectedTab()),
),
)
: Builder(
key: UniqueKey(),
builder: (BuildContext context) =>
SingleChildScrollView(child: buildSelectedTab()),
))
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

Wrapping buildSelectedTab() in SingleChildScrollView will break when a tab body is already a scrollable (e.g., ListView/GridView/CustomScrollView), typically causing unbounded height exceptions or awkward nested scrolling. Consider implementing the pinned behavior with a NestedScrollView (pinned header for the TabBar) or only wrapping non-scrollable tab bodies (and leaving existing scrollables untouched).

Copilot uses AI. Check for mistakes.
@usmanvrtx usmanvrtx requested a review from TheNoumanDev April 28, 2026 19:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support sticky/pinned TabBar with scrollable tab content

3 participants