feat(tab): add persistentTabBar option for keep the tab pinned#2186
feat(tab): add persistentTabBar option for keep the tab pinned#2186
Conversation
| tabContent | ||
|
|
||
| widget._controller.persistentTabBar | ||
| ? Expanded( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Property Behavior:
expanded property:
expanded: true→ Content stretches to fill remaining screen space, even if content is shorter than available heightexpanded: false→ Content only takes the space it needs
persistentTabBar property:
persistentTabBar: true→ Enables scrolling while keeping tab bar visible; works with or without theexpandedpropertypersistentTabBar: false→ Default behavior (TabBarView)
Co-authored-by: Copilot <copilot@github.com>
There was a problem hiding this comment.
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
persistentTabBarproperty wiring intoBaseTabBarsetters andTabBarController. - Updated
TabBarState.build()to optionally wrap selected tab content in aSingleChildScrollViewwhenpersistentTabBaris 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.
| key: UniqueKey(), | ||
| builder: (BuildContext context) => | ||
| SingleChildScrollView(child: buildSelectedTab()), | ||
| ), | ||
| ) | ||
| : Builder( | ||
| key: UniqueKey(), | ||
| builder: (BuildContext context) => | ||
| SingleChildScrollView(child: buildSelectedTab()), |
There was a problem hiding this comment.
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.
| 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(), | |
| ), |
| 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, |
There was a problem hiding this comment.
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.
| widget._controller.persistentTabBar | ||
| ? (isExpanded | ||
| ? Expanded( | ||
| child: Builder( | ||
| key: UniqueKey(), | ||
| builder: (BuildContext context) => | ||
| SingleChildScrollView(child: buildSelectedTab()), | ||
| ), | ||
| ) | ||
| : Builder( | ||
| key: UniqueKey(), | ||
| builder: (BuildContext context) => | ||
| SingleChildScrollView(child: buildSelectedTab()), | ||
| )) |
There was a problem hiding this comment.
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).
| widget._controller.persistentTabBar | ||
| ? (isExpanded | ||
| ? Expanded( | ||
| child: Builder( | ||
| key: UniqueKey(), | ||
| builder: (BuildContext context) => | ||
| SingleChildScrollView(child: buildSelectedTab()), | ||
| ), | ||
| ) | ||
| : Builder( | ||
| key: UniqueKey(), | ||
| builder: (BuildContext context) => | ||
| SingleChildScrollView(child: buildSelectedTab()), | ||
| )) |
There was a problem hiding this comment.
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).
Summary
Add
persistentTabBarsupport so the tab bar stays pinned while only the tab content scrolls.Key Changes
TabBar Behavior
persistentTabBarparameter toTabBar.Usage