I use the following code snippet to create a tab bar with 20 tabs along with their views (you can copy-paste the code to try it out, it complies with no problems):
import 'package:flutter/material.dart';
class TabBody extends StatefulWidget {
final int tabNumber;
const TabBody({required this.tabNumber, Key? key}) : super(key: key);
@override
State<TabBody> createState() => _TabBodyState();
}
class _TabBodyState extends State<TabBody> {
@override
void initState() {
print(
'inside init state for ${widget.tabNumber}'); //<--- I want this line to execute only once
super.initState();
}
getDataForTab() {
//getting data for widget.tabNumber
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
color: Colors.grey,
child: Text('This is tab #${widget.tabNumber} body')),
);
}
}
class MainPage extends StatefulWidget {
const MainPage({Key? key}) : super(key: key);
@override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
List<Text> get _tabs {
var list = [for (var i = 0; i < 20; i = 1) i];
List<Text> tabs = list.map((i) => Text('Tab Title $i')).toList();
return tabs;
}
List<TabBody> get _tabsBodies {
var list = [for (var i = 0; i < 20; i = 1) i];
List<TabBody> bodies = list.map((i) => TabBody(tabNumber: i)).toList();
return bodies;
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: _tabs.length,
child: Column(
children: <Widget>[
Container(
width: double.infinity,
height: 50,
color: Colors.black,
child: TabBar(
isScrollable: true,
tabs: _tabs,
),
),
Expanded(
child: TabBarView(
children: _tabsBodies, //<--- i want this to be one child only
),
)
],
),
);
}
}
I need to do the following but couldn't find a way for that:
I want to let the TabBarView to have only one child of type
TabBody
not a list of_tabsBodies
, i.e. the print statement ininitState
should execute once.I want to execute the function
getDataForTab
every time the tab is changed to another tab.
so in general I need to refresh the tab body page for each tab selection, in contrast to the default implementation of the DefaultTabController
widget which requires to have n
number of tab bodies for n
number of tabs.
CodePudding user response:
You'll need to do three things:
Remove the
TabBarView
. You don't need it if you want to have a single widget. (Having aTabBar
does not require you to have aTabBarView
)Create your own
TabController
so you can pass the current index to theTabBody
.Listen to the
TabController
and update the state to pass the new index toTabBody
.
Here's a fully runnable example and that you can copy and paste to DartPad
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatelessWidget {
final String title;
const MyHomePage({
Key? key,
required this.title,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: MainPage(),
);
}
}
class MainPage extends StatefulWidget {
const MainPage({Key? key}) : super(key: key);
@override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage>
with SingleTickerProviderStateMixin {
late final tabController =
TabController(length: 20, vsync: this, initialIndex: 0);
@override
void initState() {
super.initState();
tabController.addListener(() {
if (tabController.previousIndex != tabController.index && !tabController.indexIsChanging) {
print('setting state'); // <~~ will print one time now
setState(() {});
}
});
}
List<Text> get _tabs {
var list = [for (var i = 0; i < 20; i = 1) i];
List<Text> tabs = list.map((i) => Text('Tab Title $i')).toList();
return tabs;
}
@override
void dispose() {
tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Container(
width: double.infinity,
height: 50,
color: Colors.black,
child: TabBar(
isScrollable: true, tabs: _tabs, controller: tabController),
),
Expanded(
child: TabBody(tabNumber: tabController.index),
)
],
);
}
}
class TabBody extends StatefulWidget {
final int tabNumber;
const TabBody({required this.tabNumber, Key? key}) : super(key: key);
@override
State<TabBody> createState() => _TabBodyState();
}
class _TabBodyState extends State<TabBody> {
@override
void initState() {
print(
'inside init state for ${widget.tabNumber}'); //<--- I want this line to execute only once
super.initState();
}
getDataForTab() {
//getting data for widget.tabNumber
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
color: Colors.grey,
child: Text('This is tab #${widget.tabNumber} body')),
);
}
}
Few notes about the example:
- when you create a TabController, you'll need a ticker. You can use
SingleTickerProviderStateMixin
to make the class itself a ticker (hence:vsync: this
). Alternatively, you can create your own and pass it toTabController.vsync
parameter.
class _MainPageState extends State<MainPage> with SingleTickerProviderStateMixin {
late final tabController = TabController(length: 20, vsync: this, initialIndex: 0);
- Here we are listening to the tab controller whenever the tabs changes:
@override
void initState() {
super.initState();
tabController.addListener(() {
if (tabController.previousIndex != tabController.index && !tabController.indexIsChanging) {
print('setting state'); // <~~ will print one time now
setState(() {});
}
});
}
edit: you'll also need to dispose the tabController
. I updated the code above.