Skip to content

[material_ui] Let TabBarThemeData.indicator satisfy the indicatorWeight assert - #12665

Open
m1roxx wants to merge 1 commit into
flutter:mainfrom
m1roxx:tabbar-theme-indicator-assert
Open

[material_ui] Let TabBarThemeData.indicator satisfy the indicatorWeight assert#12665
m1roxx wants to merge 1 commit into
flutter:mainfrom
m1roxx:tabbar-theme-indicator-assert

Conversation

@m1roxx

@m1roxx m1roxx commented Aug 28, 2026

Copy link
Copy Markdown

TabBar and TabBar.secondary asserted indicator != null || (indicatorWeight > 0.0) in their
constructors. A constructor has no BuildContext, so that check can only see the widget-level
indicator and is blind to TabBarThemeData.indicator. As a result, an app that supplies its
indicator app-wide through the theme cannot also pass indicatorWeight: 0 to suppress the default
underline thickness — the only workarounds are an epsilon weight, or moving the indicator to the
widget, which then overrides the themed one.

This contradicts the documented behavior of indicatorWeight, which already states:

If indicator is specified or provided from TabBarThemeData, this property is ignored.

This PR moves the check to _TabBarState._getIndicator, which runs after both the widget-level and
the theme-level indicator have been ruled out, so it now fires only when the TabBar actually draws
its default underline indicator. This matches the direction suggested by @dkwingsmt in
flutter/flutter#188837 (comment).

The indicatorWeight doc comment is updated to describe the relaxed constraint.

Fixes flutter/flutter#188837

Tests

Two tests are added to packages/material_ui/test/tabs_test.dart; both fail before this change:

  • TabBar.indicatorWeight can be zero when the indicator comes from the theme — a regression test
    covering both TabBar and TabBar.secondary, verifying that the widget builds and paints the
    themed indicator.
  • TabBar asserts when indicatorWeight is zero and no indicator is provided — verifies the check is
    still enforced when the TabBar falls back to its default underline indicator.

material_ui is a batch-release package, so the changelog entry is a new file under
pending_changelogs/ with version: patch rather than a direct CHANGELOG.md/pubspec.yaml edit.

Pre-Review Checklist

Footnotes

  1. Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. 2

…ht assert

TabBar asserted `indicator != null || indicatorWeight > 0.0` in its
constructors, where the resolved TabBarThemeData is not available. An
app-wide indicator supplied via TabBarThemeData.indicator therefore could
not be combined with `indicatorWeight: 0`, even though indicatorWeight is
documented to be ignored whenever an indicator is provided by the widget
or the theme.

Moves the check to _getIndicator, which runs after both the widget-level
and theme-level indicators have been ruled out, so it now fires only when
the TabBar actually draws its default underline indicator.

Fixes flutter/flutter#188837
@github-actions github-actions Bot added p: material_ui triage-design Should be looked at in design triage labels Aug 28, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request removes the constructor assertion requiring indicatorWeight to be greater than zero when an indicator is provided by the theme, deferring the assertion to the state build phase when the default underline indicator is actually used. The review feedback suggests using DefaultTabController in the newly added tests to manage the controller lifecycle more safely and prevent potential ticker leaks.

Comment on lines +3175 to +3190
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),
),
);
}

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),
          ),
        ),
      );
    }

Comment on lines +3217 to +3227
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),
),
);

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),
),
),
);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

p: material_ui triage-design Should be looked at in design triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TabBar throws assertion indicator != null || indicatorWeight > 0.0 despite a TabBarThemeData.indicator

1 participant