diff --git a/packages/material_ui/lib/src/tabs.dart b/packages/material_ui/lib/src/tabs.dart index 8a0d0789c06d..97c3bfda74a4 100644 --- a/packages/material_ui/lib/src/tabs.dart +++ b/packages/material_ui/lib/src/tabs.dart @@ -1066,8 +1066,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { this.tabAlignment, this.textScaler, this.indicatorAnimation, - }) : _isPrimary = true, - assert(indicator != null || (indicatorWeight > 0.0)); + }) : _isPrimary = true; /// Creates a Material Design secondary tab bar. /// @@ -1128,8 +1127,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { this.tabAlignment, this.textScaler, this.indicatorAnimation, - }) : _isPrimary = false, - assert(indicator != null || (indicatorWeight > 0.0)); + }) : _isPrimary = false; /// Typically a list of two or more [Tab] widgets. /// @@ -1185,7 +1183,8 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { /// If [ThemeData.useMaterial3] is false, the default value is 2.0. /// /// If [indicator] is specified or provided from [TabBarThemeData], - /// this property is ignored. + /// this property is ignored. Otherwise, when drawing the default underline + /// indicator, the value of this parameter must be greater than zero. final double indicatorWeight; /// The padding for the indicator. @@ -1644,6 +1643,24 @@ class _TabBarState extends State { if (tabBarTheme.indicator != null) { return tabBarTheme.indicator!; } + assert(() { + if (widget.indicatorWeight <= 0.0) { + throw FlutterError.fromParts([ + ErrorSummary('Invalid indicatorWeight for TabBar.'), + ErrorDescription( + 'The indicatorWeight must be greater than zero when the TabBar ' + 'draws its default underline indicator, that is when no indicator ' + 'is provided by the TabBar or the TabBarTheme.', + ), + ErrorHint( + 'To fix this, set indicatorWeight to a value greater than zero, ' + 'or provide an indicator with the TabBar.indicator or ' + 'TabBarThemeData.indicator property.', + ), + ]); + } + return true; + }()); Color color = widget.indicatorColor ?? tabBarTheme.indicatorColor ?? _defaults.indicatorColor!; // ThemeData tries to avoid this by having indicatorColor avoid being the diff --git a/packages/material_ui/pending_changelogs/change_2026_08_30_tab_bar_indicator_weight.yaml b/packages/material_ui/pending_changelogs/change_2026_08_30_tab_bar_indicator_weight.yaml new file mode 100644 index 000000000000..925c0df785ec --- /dev/null +++ b/packages/material_ui/pending_changelogs/change_2026_08_30_tab_bar_indicator_weight.yaml @@ -0,0 +1,3 @@ +changelog: | + - Allows `TabBar.indicatorWeight` of 0 when the indicator is supplied by `TabBarThemeData`. +version: patch \ No newline at end of file diff --git a/packages/material_ui/test/tabs_test.dart b/packages/material_ui/test/tabs_test.dart index d3412668ffb3..fb5d33a17a03 100644 --- a/packages/material_ui/test/tabs_test.dart +++ b/packages/material_ui/test/tabs_test.dart @@ -7,6 +7,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'package:material_ui/material_ui.dart'; import 'feedback_tester.dart'; @@ -3646,6 +3647,105 @@ void main() { ); }); + testWidgets('TabBar accepts indicatorWeight: 0 when the indicator comes from TabBarThemeData', ( + WidgetTester tester, + ) async { + // Regression test for https://github.com/flutter/flutter/issues/188837. + // A themed indicator supplies the decoration, so indicatorWeight: 0 must not + // throw the default-underline assertion. + const indicatorColor = Color(0xFF00FF00); + const Decoration indicator = BoxDecoration(color: indicatorColor); + const tabs = [Tab(text: 'A'), Tab(text: 'B')]; + + Widget buildFrame({required bool secondary}) { + return boilerplate( + tabBarTheme: const TabBarThemeData( + indicator: indicator, + indicatorSize: TabBarIndicatorSize.tab, + ), + child: Container( + alignment: Alignment.topLeft, + child: DefaultTabController( + length: tabs.length, + child: secondary + ? const TabBar.secondary(indicatorWeight: 0.0, tabs: tabs) + : const TabBar(indicatorWeight: 0.0, tabs: tabs), + ), + ), + ); + } + + // Primary TabBar: no assertion is thrown and the themed indicator is painted. + await tester.pumpWidget(buildFrame(secondary: false)); + expect(tester.takeException(), isNull); + + final RenderBox tabBarBox = tester.firstRenderObject(find.byType(TabBar)); + // 46 = _kTabHeight(46) with no weight added by the zero indicatorWeight. + expect(tabBarBox.size.height, 46.0); + expect( + tabBarBox, + paints..rect(rect: const Rect.fromLTRB(0.0, 0.0, 400.0, 46.0), color: indicatorColor), + ); + + // Secondary TabBar: also accepts a zero indicatorWeight with a themed indicator. + await tester.pumpWidget(buildFrame(secondary: true)); + expect(tester.takeException(), isNull); + }); + + testWidgets('TabBar accepts indicatorWeight: 0 when the indicator is provided on the widget', ( + WidgetTester tester, + ) async { + const Decoration indicator = BoxDecoration(color: Color(0xFF00FF00)); + const tabs = [Tab(text: 'A'), Tab(text: 'B')]; + + await tester.pumpWidget( + boilerplate( + child: DefaultTabController( + length: tabs.length, + child: const TabBar(indicator: indicator, indicatorWeight: 0.0, tabs: tabs), + ), + ), + ); + + expect(tester.takeException(), isNull); + }); + + testWidgets( + 'TabBar throws indicatorWeight: 0 with the default underline indicator', + experimentalLeakTesting: LeakTesting.settings + .withIgnoredAll(), // leaking by design because of exception + (WidgetTester tester) async { + // With no indicator on the widget or the theme, the TabBar falls back to the + // default underline indicator, which requires a positive indicatorWeight. + const tabs = [Tab(text: 'A'), Tab(text: 'B')]; + final Matcher throwsInvalidIndicatorWeightError = isFlutterError.having( + (FlutterError error) => error.message, + 'message', + contains('Invalid indicatorWeight for TabBar.'), + ); + + await tester.pumpWidget( + boilerplate( + child: DefaultTabController( + length: tabs.length, + child: const TabBar(indicatorWeight: 0.0, tabs: tabs), + ), + ), + ); + expect(tester.takeException(), throwsInvalidIndicatorWeightError); + + await tester.pumpWidget( + boilerplate( + child: DefaultTabController( + length: tabs.length, + child: const TabBar.secondary(indicatorWeight: 0.0, tabs: tabs), + ), + ), + ); + expect(tester.takeException(), throwsInvalidIndicatorWeightError); + }, + ); + testWidgets('TabBar with custom indicator - directional indicatorPadding (LTR)', ( WidgetTester tester, ) async {