Skip to main content

TabControllerWrapper

Owns a TabController and keeps it in two-way sync with a MutableValue<int> index. It provides the TickerProvider, creates and disposes the controller, writes the index when the tab changes, and drives the controller when the index changes from elsewhere - the TabBar / TabBarView counterpart of PageControllerWrapper.

class TabsScreen extends HookWidget {
const TabsScreen({super.key});


Widget build(BuildContext context) {
final index = useState(0);

return TabControllerWrapper(
length: 3, // <- must match the number of tabs and views
index: index,
builder: (controller) => Column(
children: [
TabBar(
controller: controller,
tabs: const [Tab(text: 'One'), Tab(text: 'Two'), Tab(text: 'Three')],
),
Expanded(
child: TabBarView(
controller: controller,
children: const [Text('One'), Text('Two'), Text('Three')],
),
),
],
),
);
}
}

Constructor

TabControllerWrapper({
Key? key,
required int length,
required MutableValue<int> index,
required Widget Function(TabController controller) builder,
void Function(TabController controller, int index)? onTransition,
// NOTE: accepted but ignored - see the caveat below
TabController Function({required TickerProvider vsync, required int length}) controllerProvider = TabController.new,
});
  • length - the number of tabs. Must match the TabBar.tabs and TabBarView.children counts.
  • index - the source of truth for the active tab, as a MutableValue<int>. A useState(0) works directly.
  • builder - receives the managed TabController; pass it to your TabBar and TabBarView.
  • onTransition - how to move when index changes programmatically. Defaults to controller.animateTo.
  • controllerProvider - see the caveat below; this parameter is currently not used by the widget.

The wrapper obtains a single-ticker provider internally, creates the controller keyed on length (recreating it if length changes), and disposes it automatically. A listener writes index from controller.index on every change; an effect runs onTransition (or animateTo) whenever index changes and differs from the controller.

Use cases

  • A Material tabbed screen where you want TabBar's indicator animation and swipe handling, with the active tab living in your state. The shell state owns the index; the controller follows it.
  • Driving the tab from outside the TabBar - a button or deep link that sets index.value - which animates the TabBarView via the default animateTo.

For a swipeable PageView without the TabBar chrome, use PageControllerWrapper.

Caveats

  • length must match your tabs and views, and changing it recreates the controller. The controller is keyed on length, so a change rebuilds it from scratch (resetting any in-progress animation) - fine for a changing tab set, but don't pass a length that disagrees with the number of tabs / children.

  • The default transition is animateTo, so a programmatic index change animates. This differs from PageControllerWrapper, whose default is an instant jumpToPage. Pass onTransition to override.

  • controllerProvider is accepted but ignored. The widget builds its TabController directly with the internal ticker rather than calling controllerProvider, so a custom factory passed here has no effect today. Treat the controller as always being a stock TabController; do not rely on this parameter.

  • StatelessTabControllerWrapper is deprecated - it is a typedef alias for this class. Use TabControllerWrapper directly.

See also