@@ -90,20 +90,29 @@ abstract class BaseTabBar extends StatefulWidget
9090 EnsembleAction .from (action, initiator: this ),
9191 'onTabSelectionHaptic' : (value) =>
9292 _controller.onTabSelectionHaptic = Utils .optionalString (value),
93+ 'useIndexedTab' : (value) =>
94+ _controller.useIndexedTab = Utils .getBool (value, fallback: false ),
9395 };
9496 }
9597}
9698
9799class TabBarState extends BaseTabBarState {
100+ // Cache for indexed tab building mode
101+ late List <Widget ?> _cache;
102+
98103 @override
99104 void initState () {
100105 super .initState ();
101106 _initializeTabController ();
102107 }
103108
104109 void _initializeTabController () {
110+ final safeIndex = _getValidInitialIndex ();
111+ if (widget.controller.useIndexedTab) {
112+ _cache = List <Widget ?>.filled (widget.controller.items.length, null );
113+ }
105114 tabController = TabController (
106- initialIndex: _getValidInitialIndex () ,
115+ initialIndex: safeIndex ,
107116 length: widget.controller.items.length,
108117 vsync: this ,
109118 );
@@ -200,6 +209,12 @@ class TabBarState extends BaseTabBarState {
200209 void _reinitializeTabController () {
201210 tabController.removeListener (notifyListener);
202211 tabController.dispose ();
212+ // If a tab became hidden and the previously-selected index is now out of
213+ // bounds, reset it before recreating the TabController and rebuilding,
214+ // otherwise buildSelectedTab() will throw a RangeError.
215+ if (widget._controller.selectedIndex >= widget._controller.items.length) {
216+ widget._controller.selectedIndex = 0 ;
217+ }
203218 _initializeTabController ();
204219 }
205220
@@ -216,6 +231,20 @@ class TabBarState extends BaseTabBarState {
216231 if (widget.controller.selectedIndex == index) {
217232 return ;
218233 }
234+
235+ // If using indexed tab mode, build the tab on-demand before switching
236+ if (widget.controller.useIndexedTab) {
237+ final scopeManager = DataScopeWidget .getScope (context);
238+ if (scopeManager != null &&
239+ index < _cache.length &&
240+ _cache[index] == null ) {
241+ final items = widget.controller.items;
242+ if (index < items.length) {
243+ _cache[index] = _buildTabAt (scopeManager, items[index]);
244+ }
245+ }
246+ }
247+
219248 setState (() {
220249 widget.controller.selectedIndex = index;
221250 });
@@ -264,13 +293,13 @@ class TabBarState extends BaseTabBarState {
264293 else {
265294 bool isExpanded = widget._controller.expanded;
266295
267- // if Expanded is set, our content needs to stretch to the left-over height
268- // Note we make each Builder unique, as it tends to re-use
269- // the states (down the tree) from the previous Builder
270- // https://stackoverflow.com/questions/55425804/using-builder-instead-of-statelesswidget
271- Widget tabContent = Builder (
272- key : UniqueKey (),
273- builder : ( BuildContext context) => buildSelectedTab ());
296+ // Use indexed tab building if enabled, otherwise use classic single-tab rendering
297+ Widget tabContent = widget._controller.useIndexedTab
298+ ? _buildTabBodies (context)
299+ : Builder (
300+ key : ValueKey (widget._controller.selectedIndex),
301+ builder : ( BuildContext context) => buildSelectedTab ());
302+
274303 if (isExpanded) {
275304 tabContent = Expanded (child: tabContent);
276305 }
@@ -279,16 +308,7 @@ class TabBarState extends BaseTabBarState {
279308 crossAxisAlignment: CrossAxisAlignment .stretch,
280309 children: [
281310 buildTabBar (),
282- // builder gives us dynamic height control vs TabBarView, but
283- // is sub-optimal since it recreates the tab content on each pass.
284- // This means onLoad API may be called multiple times in debug mode
285- tabContent
286-
287- // This cause Expanded child to fail
288- // Padding(
289- // padding: const EdgeInsets.only(left: 0),
290- // child: Builder(builder: (BuildContext context) => buildSelectedTab())
291- // )
311+ tabContent,
292312 ],
293313 );
294314 // if Expanded is set, stretch our column to left-over height
@@ -320,4 +340,54 @@ class TabBarState extends BaseTabBarState {
320340 }
321341 return const Text ("Unknown widget for this Tab" );
322342 }
323- }
343+
344+ /// Builds tabs on-demand with caching (used when useIndexedTab is true).
345+ /// - Non-expanded: Column + Offstage for hidden tabs (zero-height)
346+ /// - Expanded: IndexedStack (bounded height)
347+ Widget _buildTabBodies (BuildContext context) {
348+ final scopeManager = DataScopeWidget .getScope (context);
349+ if (scopeManager == null ) return const Text ('Unknown widget for this Tab' );
350+
351+ final items = widget._controller.items;
352+ final selectedIndex = widget._controller.selectedIndex;
353+
354+ // Ensure cache size matches current item count
355+ if (_cache.length != items.length) {
356+ _cache = List <Widget ?>.filled (items.length, null );
357+ }
358+
359+ // Build the selected tab now if not already cached
360+ _cache[selectedIndex] ?? = _buildTabAt (scopeManager, items[selectedIndex]);
361+
362+ // Non-expanded: lives in unconstrained scroll context
363+ // Use Column + Offstage to zero-out hidden tabs
364+ if (! widget._controller.expanded) {
365+ return Column (
366+ mainAxisSize: MainAxisSize .min,
367+ crossAxisAlignment: CrossAxisAlignment .stretch,
368+ children: List .generate (items.length, (i) {
369+ return Offstage (
370+ offstage: i != selectedIndex,
371+ child: _cache[i] ?? const SizedBox .shrink (),
372+ );
373+ }),
374+ );
375+ }
376+
377+ // Expanded: bounded height is provided by Expanded wrapper
378+ // IndexedStack is safe here and stacks children in bounded space
379+ return IndexedStack (
380+ index: selectedIndex,
381+ children: List .generate (
382+ items.length,
383+ (i) => _cache[i] ?? const SizedBox .shrink (),
384+ ),
385+ );
386+ }
387+
388+ /// Build a single tab at given index
389+ Widget _buildTabAt (ScopeManager scopeManager, TabItem tab) {
390+ if (tab.bodyWidget == null ) return const SizedBox .shrink ();
391+ return scopeManager.buildWidgetFromDefinition (tab.bodyWidget);
392+ }
393+ }
0 commit comments