Reputation: 7932
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 in initState
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.
Upvotes: 0
Views: 921
Reputation: 3014
You'll need to do three things:
Remove the TabBarView
. You don't need it if you want to have a single widget. (Having a TabBar
does not require you to have a TabBarView
)
Create your own TabController
so you can pass the current index to the TabBody
.
Listen to the TabController
and update the state to pass the new index to TabBody
.
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:
SingleTickerProviderStateMixin
to make the class itself a ticker (hence: vsync: this
). Alternatively, you can create your own and pass it to TabController.vsync
parameter.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(() {});
}
});
}
edit: you'll also need to dispose the tabController
. I updated the code above.
Upvotes: 1