From 77dc95b171ba07d7a5d82a3e502a4ac5dbae0f12 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 28 May 2026 11:10:02 +0000 Subject: [PATCH] fix(tab): avoid TabController length 0 when all conditional tabs hide When every tab has a conditional visible binding that evaluates to false, handleConditionalTabs() can leave zero visible items. TabController asserts length >= 1, which crashed the app during _reinitializeTabController. Use a placeholder length of 1 when no tabs are visible (build already returns shrink) and skip tab selection listeners until tabs return. Align ScrollableTabBar with the same guard and selectedIndex clamp. Co-authored-by: Sharjeel Yunus --- .../lib/layout/tab/scrollable_tab_bar.dart | 7 ++++++- .../lib/layout/tab/tab_bar_controller.dart | 9 +++++++++ modules/ensemble/lib/layout/tab_bar.dart | 11 ++++++++--- .../effective_tab_controller_length_test.dart | 15 +++++++++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 modules/ensemble/test/effective_tab_controller_length_test.dart diff --git a/modules/ensemble/lib/layout/tab/scrollable_tab_bar.dart b/modules/ensemble/lib/layout/tab/scrollable_tab_bar.dart index 471c11238..953536d53 100644 --- a/modules/ensemble/lib/layout/tab/scrollable_tab_bar.dart +++ b/modules/ensemble/lib/layout/tab/scrollable_tab_bar.dart @@ -114,14 +114,19 @@ class ScrollableTabBarState extends BaseTabBarState { } void _initializeTabController() { + final tabLength = + effectiveTabControllerLength(widget.controller.items.length); tabController = TabController( - length: widget.controller.items.length, + length: tabLength, vsync: this, ); } void _reinitializeTabController() { tabController.dispose(); + if (widget._controller.selectedIndex >= widget._controller.items.length) { + widget._controller.selectedIndex = 0; + } _initializeTabController(); } diff --git a/modules/ensemble/lib/layout/tab/tab_bar_controller.dart b/modules/ensemble/lib/layout/tab/tab_bar_controller.dart index 6c7e7a4a7..70860f632 100644 --- a/modules/ensemble/lib/layout/tab/tab_bar_controller.dart +++ b/modules/ensemble/lib/layout/tab/tab_bar_controller.dart @@ -6,8 +6,17 @@ import 'package:ensemble/layout/tab_bar.dart'; import 'package:ensemble/util/utils.dart'; import 'package:ensemble/widget/helpers/controllers.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:yaml/yaml.dart'; +/// [TabController] requires `length >= 1`. When conditional `visible` bindings +/// hide every tab, the UI shows nothing but the controller still needs a valid length. +@visibleForTesting +int effectiveTabControllerLength(int visibleTabCount) { + assert(visibleTabCount >= 0); + return visibleTabCount == 0 ? 1 : visibleTabCount; +} + // the Controller for the TabBar class TabBarController extends BoxController { String? tabPosition; diff --git a/modules/ensemble/lib/layout/tab_bar.dart b/modules/ensemble/lib/layout/tab_bar.dart index 648ab60c4..cdf7980db 100644 --- a/modules/ensemble/lib/layout/tab_bar.dart +++ b/modules/ensemble/lib/layout/tab_bar.dart @@ -113,12 +113,17 @@ class TabBarState extends BaseTabBarState { if (widget.controller.useIndexedTab) { _cache = List.filled(widget.controller.items.length, null); } + final tabLength = + effectiveTabControllerLength(widget.controller.items.length); tabController = TabController( - initialIndex: safeIndex, - length: widget.controller.items.length, + initialIndex: + widget.controller.items.isEmpty ? 0 : safeIndex.clamp(0, tabLength - 1), + length: tabLength, vsync: this, ); - tabController.addListener(notifyListener); + if (widget.controller.items.isNotEmpty) { + tabController.addListener(notifyListener); + } } int _getValidInitialIndex() { diff --git a/modules/ensemble/test/effective_tab_controller_length_test.dart b/modules/ensemble/test/effective_tab_controller_length_test.dart new file mode 100644 index 000000000..caedb1505 --- /dev/null +++ b/modules/ensemble/test/effective_tab_controller_length_test.dart @@ -0,0 +1,15 @@ +import 'package:ensemble/layout/tab/tab_bar_controller.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('effectiveTabControllerLength', () { + test('uses placeholder length 1 when no tabs are visible', () { + expect(effectiveTabControllerLength(0), 1); + }); + + test('matches visible tab count when at least one tab is visible', () { + expect(effectiveTabControllerLength(1), 1); + expect(effectiveTabControllerLength(4), 4); + }); + }); +}