The TabBarView is a layout component provided by the Material widget library, often used in conjunction with the TabBar.
45.1 TabBarView
TabBarView encapsulates PageView, and its constructor is straightforward:
TabBarView({ Key? key, required this.children, // The tab pages this.controller, // TabController this.physics, this.dragStartBehavior = DragStartBehavior.start, })
The TabController is used to monitor and control page switching in TabBarView, often in coordination with TabBar. If no controller is specified, it will search up the widget tree and use the nearest DefaultTabController.
45.2 TabBar
The TabBar serves as the navigation header for TabBarView, as shown in Figure .
TabBar has numerous configuration parameters that allow you to define its style, including many properties for customizing the indicator and label. For example, in the figure above, the label represents the text of each tab, while the indicator is the white underline beneath "History."
const TabBar({ Key? key, required this.tabs, // The tabs to be created this.controller, this.isScrollable = false, // Whether the tabs are scrollable this.padding, this.indicatorColor, // Color of the indicator, default is a 2px underline this.automaticIndicatorColorAdjustment = true, this.indicatorWeight = 2.0, // Height of the indicator this.indicatorPadding = EdgeInsets.zero, // Padding of the indicator this.indicator, // Custom indicator this.indicatorSize, // Indicator size: can be tab length or label length this.labelColor, this.labelStyle, this.labelPadding, this.unselectedLabelColor, this.unselectedLabelStyle, this.mouseCursor, this.onTap, ... })
TabBar is usually positioned at the bottom of the AppBar. It can also receive a TabController. If you want TabBar to synchronize with TabBarView, both must use the same TabController. Be aware that the number of children in TabBar and TabBarView must match for proper linkage. If no controller is specified, it will look for the nearest DefaultTabController in the widget tree. Additionally, we need to create the tabs and pass them through the tabs
parameter. Tabs can be any widget, though the Material widget library includes a built-in Tab component, which we typically use:
const Tab({ Key? key, this.text, // Text of the tab this.icon, // Icon of the tab this.iconMargin = const EdgeInsets.only(bottom: 10.0), this.height, this.child, // Custom widget })
Note that the text
and child
properties are mutually exclusive and cannot be used simultaneously.
45.3 Example
Let’s look at an example:
class TabViewRoute1 extends StatefulWidget { @override _TabViewRoute1State createState() => _TabViewRoute1State(); } class _TabViewRoute1State extends State<TabViewRoute1> with SingleTickerProviderStateMixin { late TabController _tabController; List tabs = ["News", "History", "Images"]; @override void initState() { super.initState(); _tabController = TabController(length: tabs.length, vsync: this); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("App Name"), bottom: TabBar( controller: _tabController, tabs: tabs.map((e) => Tab(text: e)).toList(), ), ), body: TabBarView( // Construct the TabBarView controller: _tabController, children: tabs.map((e) { return KeepAliveWrapper( child: Container( alignment: Alignment.center, child: Text(e, textScaleFactor: 5), ), ); }).toList(), ), ); } @override void dispose() { // Dispose of resources _tabController.dispose(); super.dispose(); } }
When run, the output looks like Figure :
When you swipe through the pages, the top tabs move along, and when you tap on a tab, the page switches accordingly. To enable the interaction between TabBar and TabBarView, we explicitly created a TabController. Since TabController requires a TickerProvider (via the vsync
parameter), we mixed in SingleTickerProviderStateMixin. Because TabController performs animations and holds resources, we must dispose of it when the page is destroyed.
Overall, creating a TabController can be complex. In practice, when TabBar and TabBarView need to interact, it’s common to create a DefaultTabController as their parent component. This allows them to search up the widget tree at runtime and use the specified DefaultTabController. Here’s a revised version:
class TabViewRoute2 extends StatelessWidget { @override Widget build(BuildContext context) { List tabs = ["News", "History", "Images"]; return DefaultTabController( length: tabs.length, child: Scaffold( appBar: AppBar( title: Text("App Name"), bottom: TabBar( tabs: tabs.map((e) => Tab(text: e)).toList(), ), ), body: TabBarView( // Construct the TabBarView children: tabs.map((e) { return KeepAliveWrapper( child: Container( alignment: Alignment.center, child: Text(e, textScaleFactor: 5), ), ); }).toList(), ), ), ); } }
As you can see, we no longer need to manually manage the controller’s lifecycle, provide a SingleTickerProviderStateMixin, or manage any other state, so a StatefulWidget is no longer necessary, making the implementation much simpler.
Page Caching
Since TabBarView encapsulates PageView, you can refer to the PageView section for details on caching pages.