Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions packages/material_ui/lib/src/tabs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -1173,7 +1171,8 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {

/// The thickness of the line that appears below the selected tab.
///
/// The value of this parameter must be greater than zero.
/// The value of this parameter must be greater than zero, unless an indicator
/// is provided by [indicator] or [TabBarThemeData.indicator].
///
/// If [ThemeData.useMaterial3] is true and [TabBar] is used to create a
/// primary tab bar, the default value is 3.0. If the provided value is less
Expand Down Expand Up @@ -1645,6 +1644,13 @@ class _TabBarState extends State<TabBar> {
return tabBarTheme.indicator!;
}

assert(
widget.indicatorWeight > 0.0,
'The indicatorWeight must be greater than zero when the TabBar draws its '
'default underline indicator, i.e. when no indicator is provided by the '
'TabBar or by TabBarThemeData.indicator.',
);

Color color = widget.indicatorColor ?? tabBarTheme.indicatorColor ?? _defaults.indicatorColor!;
// ThemeData tries to avoid this by having indicatorColor avoid being the
// primaryColor. However, it's possible that the tab bar is on a
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
changelog: |
- Fixes `TabBar` asserting on construction when `indicatorWeight` is zero and the indicator is provided by `TabBarThemeData.indicator`.
version: patch
69 changes: 69 additions & 0 deletions packages/material_ui/test/tabs_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3160,6 +3160,75 @@ void main() {
);
});

// Regression test for https://github.com/flutter/flutter/issues/188837.
testWidgets('TabBar.indicatorWeight can be zero when the indicator comes from the theme', (
WidgetTester tester,
) async {
const indicatorColor = Color(0xFF00FF00);
const tabBarTheme = TabBarThemeData(
indicator: BoxDecoration(color: indicatorColor),
indicatorSize: TabBarIndicatorSize.tab,
);

final tabs = List<Widget>.generate(2, (int index) => Tab(text: 'Tab $index'));

Widget buildTabBar({bool secondaryTabBar = false}) {
final TabController controller = createTabController(
vsync: const TestVSync(),
length: tabs.length,
);
return boilerplate(
useMaterial3: false,
tabBarTheme: tabBarTheme,
child: Container(
alignment: Alignment.topLeft,
child: secondaryTabBar
? TabBar.secondary(indicatorWeight: 0.0, controller: controller, tabs: tabs)
: TabBar(indicatorWeight: 0.0, controller: controller, tabs: tabs),
),
);
}
Comment on lines +3175 to +3190

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using DefaultTabController is more idiomatic and safer here, as it automatically manages the lifecycle of the TabController and avoids potential memory or ticker leaks in tests.

    Widget buildTabBar({bool secondaryTabBar = false}) {
      return boilerplate(
        useMaterial3: false,
        tabBarTheme: tabBarTheme,
        child: DefaultTabController(
          length: tabs.length,
          child: Container(
            alignment: Alignment.topLeft,
            child: secondaryTabBar
                ? TabBar.secondary(indicatorWeight: 0.0, tabs: tabs)
                : TabBar(indicatorWeight: 0.0, tabs: tabs),
          ),
        ),
      );
    }


await tester.pumpWidget(buildTabBar());
expect(tester.takeException(), isNull);

RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
// 46 = _kTabHeight(46) + indicatorWeight(0.0)
expect(tabBarBox.size.height, 46.0);
expect(
tabBarBox,
paints..rect(rect: const Rect.fromLTRB(0.0, 0.0, 400.0, 46.0), color: indicatorColor),
);

await tester.pumpWidget(buildTabBar(secondaryTabBar: true));
expect(tester.takeException(), isNull);

tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect(tabBarBox.size.height, 46.0);
expect(
tabBarBox,
paints..rect(rect: const Rect.fromLTRB(0.0, 0.0, 400.0, 46.0), color: indicatorColor),
);
});

testWidgets('TabBar asserts when indicatorWeight is zero and no indicator is provided', (
WidgetTester tester,
) async {
final tabs = List<Widget>.generate(2, (int index) => Tab(text: 'Tab $index'));
final TabController controller = createTabController(
vsync: const TestVSync(),
length: tabs.length,
);

await tester.pumpWidget(
boilerplate(
child: TabBar(indicatorWeight: 0.0, controller: controller, tabs: tabs),
),
);
Comment on lines +3217 to +3227

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using DefaultTabController here avoids manually creating a TabController and ensures proper disposal, preventing potential ticker leaks in the test.

Suggested change
final tabs = List<Widget>.generate(2, (int index) => Tab(text: 'Tab $index'));
final TabController controller = createTabController(
vsync: const TestVSync(),
length: tabs.length,
);
await tester.pumpWidget(
boilerplate(
child: TabBar(indicatorWeight: 0.0, controller: controller, tabs: tabs),
),
);
final tabs = List<Widget>.generate(2, (int index) => Tab(text: 'Tab $index'));
await tester.pumpWidget(
boilerplate(
child: DefaultTabController(
length: tabs.length,
child: TabBar(indicatorWeight: 0.0, tabs: tabs),
),
),
);


expect(tester.takeException(), isAssertionError);
});

testWidgets('TabBar with indicatorWeight, indicatorPadding (LTR)', (WidgetTester tester) async {
const indicatorColor = Color(0xFF00FF00);
const indicatorWeight = 8.0;
Expand Down