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 theTabBar.tabsandTabBarView.childrencounts.index- the source of truth for the active tab, as aMutableValue<int>. AuseState(0)works directly.builder- receives the managedTabController; pass it to yourTabBarandTabBarView.onTransition- how to move whenindexchanges programmatically. Defaults tocontroller.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 setsindex.value- which animates theTabBarViewvia the defaultanimateTo.
For a swipeable PageView without the TabBar chrome, use PageControllerWrapper.
Caveats
-
lengthmust match your tabs and views, and changing it recreates the controller. The controller is keyed onlength, so a change rebuilds it from scratch (resetting any in-progress animation) - fine for a changing tab set, but don't pass alengththat disagrees with the number oftabs/children. -
The default transition is
animateTo, so a programmatic index change animates. This differs fromPageControllerWrapper, whose default is an instantjumpToPage. PassonTransitionto override. -
controllerProvideris accepted but ignored. The widget builds itsTabControllerdirectly with the internal ticker rather than callingcontrollerProvider, so a custom factory passed here has no effect today. Treat the controller as always being a stockTabController; do not rely on this parameter. -
StatelessTabControllerWrapperis deprecated - it is a typedef alias for this class. UseTabControllerWrapperdirectly.
See also
- PageControllerWrapper - the
PageViewcounterpart, with an instant default transition - TextEditingControllerWrapper - the same controller-binding pattern for text input
- useState - the usual source of the
indexvalue