Switching between tabs initstate() called multiple times - flutter

Switching between tabs initstate() called multiple times.
i have 4 tabs in my tab barA,B,C and D.
case (1) if i switch in tab like from tab A to B it's working fine.
case (2) but if i'm go to tab A to C then initstate() of tab 'B' called two times
results of case (1)
flutter: A
flutter: B
results of case (2)
flutter: A
flutter: B
flutter: C
flutter: B
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin{
TabController _controller;
void initState() {
super.initState();
_controller = TabController(length: 4, vsync: this);
_controller.addListener(_handleSelected);
}
bool alarm = false;
// Function for handle tap event of tab
void _handleSelected() async {
}
Widget build(BuildContext context) {
return DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
controller: _controller,
tabs: [
Tab(text: "A"),
Tab(text: "B"),
Tab(text: "C"),
Tab(text: "D"),
],
),
actions: [
Switch(
value: alarm,
onChanged: (value) {
},
activeTrackColor: Color(0xffff6b6b),
activeColor: Color(0xffff0000),
),
],
),
body: TabBarView(
controller: _controller,
children: [
A(),
B(),
C(),
D(),
],
),
),
);
}
}

You can use IndexedStack widget to solve this kind of problem.
In _MyHomePageState use one variable to manage index of selected page;
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin{
int _selectedPage;
/////
Your code
/////
}
In the body of your scaffold implement IndexedStack
body: IndexedStack(
index:_selectedPage,
children: [
A(),
B(),
C(),
D(),
],
),
Now in _handleSelected () method handle take the latest page index from controller and using setState update the tab bar
void _handleSelected () async {
int index = _controller.page ;// get index from controller (I am not sure about exact parameter name for selected index) ;
setState((){
_selectedPage = index;
});
}

To keep a stateful widget alive (not rebuild or re-render), you can use AutomaticKeepAliveClientMixin. By this way, you can easily decide which widget need to rebuild by changing ``wantKeepAlive'' parameter.
Here is a example for Class A:
class A extends StatefulWidget {
#override
_AState createState() => _AState();
}
class _AState extends State<A> with AutomaticKeepAliveClientMixin{
bool _isLoading;
#override
void initState() {
super.initState();
Future.delayed(Duration(seconds: 3)).then((_){
setState(() {
_isLoading = false;
});
});
}
#override
Widget build(BuildContext context) {
return Center(
child: _isLoading == false ?
Text("A")
: CircularProgressIndicator(),
);
}
#override
bool get wantKeepAlive => true;
}

Related

While learning flutter using (https://github.com/afitz0/exploration_planner). How to implement the action on the LinearProgressIndicator()?

This code is part of online training of flutter by Google team. The original code can be accessed in https://github.com/afitz0/exploration_planner. I am new on flutter and I´ve got some dificulties to use statefull widget. I still do not have enough confidence. I made some modification on original code to add action to the indicator bar, it works fine but I dont think my solution is ideal...
My question is related to the right way to make a change in the state of the taskitem give an
update on the linearProgressIndicator ? Thanks in advance..
import 'package:flutter/material.dart';
double _percentual = 0; //variable to hold progress bar values from zero to 1 step 0.2
// first comes root run appp
void main() => runApp(MyApp()
//MaterialApp
//Scaffold
//AppBar
//Text
//body: Column
//text, text, text
//image
//Row
//text, text, bttom
//....
);
// second comes materialapp
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Exploration!',
theme: ThemeData(primarySwatch: Colors.blueGrey),
home: MyHomePage(),
);
}
}
//third comes home page describes visual of app
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
late AnimationController controller;
#override
void initState() {
controller = AnimationController(
vsync: this,
)..addListener(() {
setState(() {
controller.value = _percentual;
});
});
super.initState();
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Space Exploration planner'),
),
body: Column(
children: [
Progress(),
TaskList(),
],
),
);
}
}
class Progress extends StatefulWidget {
const Progress({super.key});
#override
State<Progress> createState() => _ProgressState();
}
class _ProgressState extends State<Progress> {
#override
Widget build(BuildContext context) {
return Column(
children: [
Text('You are this far away from exploring the whole universe'),
LinearProgressIndicator(
value: _percentual,
)
],
);
}
}
class TaskList extends StatelessWidget {
const TaskList({super.key});
#override
Widget build(BuildContext context) {
return Column(
children: [
TaskItem(label: "Load rocket with supplies"),
TaskItem(label: "Launch rocket"),
TaskItem(label: "Circle the home planet"),
TaskItem(label: "Head out to de first moon"),
TaskItem(label: "Launch moon lander #1"),
],
);
}
}
class TaskItem extends StatefulWidget {
final String label;
const TaskItem({Key? key, required this.label}) : super(key: key);
#override
State<TaskItem> createState() => _TaskItemState();
}
class _TaskItemState extends State<TaskItem> {
bool? _value = false;
#override
Widget build(BuildContext context) {
return Row(
children: [
Checkbox(
onChanged: (newValue) => setState(() => {
_value = newValue,
if (_value == true)
{
_percentual = double.parse(
(_percentual + 0.2).toStringAsPrecision(1)),
_ProgressState(),
}
else if (_value == false)
{
_percentual = double.parse(
(_percentual - 0.2).toStringAsPrecision(1)),
_ProgressState(),
},
main(), *//<-- worked like hot-reload but I dont think is the right way to do it.*
}),
value: _value,
),
Text(widget.label),
],
);
}
}

TabBarView page not rebuilding correctly

I am trying to display the tab number on each page of a TabBarView, by reading the index of its TabController. For some reason though, the value does not seem to update correctly visually, even though the correct value is printed in the logs.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
TabController? _tabController;
#override
void initState() {
super.initState();
_tabController = TabController(
length: 3,
vsync: this,
);
}
_back() {
if (_tabController!.index > 0) {
_tabController!.animateTo(_tabController!.index - 1);
setState(() {});
}
}
_next() {
if (_tabController!.index < _tabController!.length - 1) {
_tabController!.animateTo(_tabController!.index + 1);
setState(() {});
}
}
Widget _tab(int index) {
var value = "Page $index: ${_tabController!.index + 1} / ${_tabController!.length}";
print(value);
return Row(
children: [
TextButton(
onPressed: _back,
child: const Text("Back"),
),
Text(value,
style: const TextStyle(
),
),
TextButton(
onPressed: _next,
child: const Text("Next"),
),
],
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: TabBarView(
controller: _tabController,
children: [
_tab(1),
_tab(2),
_tab(3),
],
)
);
}
}
When navigating from index 0 to index 1, the following is printed in the logs, as expected:
I/flutter (25730): Page 1: 2 / 3
I/flutter (25730): Page 2: 2 / 3
I/flutter (25730): Page 3: 2 / 3
However, what is actually displayed is Page 2: 1 / 3
I have tried using UniqueKey as well as calling setState on the next frame, but it doesn't make a difference. Calling setState with a hardcoded delay seems to work, but it also seems wrong.
Why is what's printed in the logs different to what's being displayed, considering that all tabs are rebuilt when setState is called? Assuming it has something to do with the PageView/Scrollable/Viewport widgets that make up the TabBarView, but what exactly is going on? Notice how even when going from page 1 to page 2 and then to page 3, none of the values on any of the pages are being updated, so even the on-screen widgets aren't rebuilding correctly.
I am finally able to answer my own question. This odd behaviour is explained by the internal logic of the _TabBarViewState. The TabBarView uses a PageView internally, which it animates based on changes to the TabController index. Here is a snippet of that logic:
final int previousIndex = _controller!.previousIndex;
if ((_currentIndex! - previousIndex).abs() == 1) {
_warpUnderwayCount += 1;
await _pageController.animateToPage(_currentIndex!, duration: kTabScrollDuration, curve: Curves.ease);
_warpUnderwayCount -= 1;
return Future<void>.value();
}
Note that it keeps track of whether an animation is in progress with the _warpUnderwayCount variable, which will get a value of 1 as soon as we call animateTo() on the TabController.
Additionally, the _TabBarViewState maintains a _children list of widgets representing each page, which is first created when the TabBarView is initialized, and can later be updated only by the _TabBarViewState itself by calling its _updateChildren() function:
void _updateChildren() {
_children = widget.children;
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(widget.children);
}
The _TabBarViewState also overrides the default behaviour of the didUpdateWidget function:
#override
void didUpdateWidget(TabBarView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller)
_updateTabController();
if (widget.children != oldWidget.children && _warpUnderwayCount == 0)
_updateChildren();
}
Note that even though we provide a new list of children from our parent stateful widget by calling setState() just after animateTo(), that list of children will be ignored by the TabBarView because _warpUnderwayCount will have a value of 1 at the point that didUpdateWidget is called, and therefore _updateChildren() will not be called as per the internal logic shown above.
I believe this is a constraint of the TabBarView widget that has to do with its complexity in terms of coordinating with its internal PageView as well as with an optional TabBar widget with which it shares a TabController.
In terms of a solution, given that rebuilding the whole TabBarView by updating its Key would cancel the animation, and that setting new children by calling setState() after calling animateTo() is ignored if done while the page change animation is still running, I can only think of calling setState() after saving all the variables required for rebuilding the children and before animateTo() is called on the next frame. If it is called within the same frame, the children will still not update because didUpdateWidget will still be called after the animation starts. Here is the code from my question, updated with the proposed solution:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
TabController? _tabController;
int _newIndex = 0;
#override
void initState() {
super.initState();
_tabController = TabController(
length: 3,
vsync: this,
);
}
_back() {
if (_tabController!.index > 0) {
_newIndex = _tabController!.index - 1;
setState(() {});
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
_tabController!.animateTo(_newIndex);
});
}
}
_next() {
if (_tabController!.index < _tabController!.length - 1) {
_newIndex = _tabController!.index + 1;
setState(() {});
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
_tabController!.animateTo(_newIndex);
});
}
}
Widget _tab(int index) {
var value = "Page $index: ${_newIndex + 1} / ${_tabController!.length}";
print(value);
return Row(
children: [
TextButton(
onPressed: _back,
child: const Text("Back"),
),
Text(value,
style: const TextStyle(
),
),
TextButton(
onPressed: _next,
child: const Text("Next"),
),
],
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: TabBarView(
controller: _tabController,
children: [
_tab(1),
_tab(2),
_tab(3),
],
)
);
}
}
You can use Stream to listen for tab index change when switching pages. Update the index when changing page.
final _tabPageIndicator = StreamController<int>.broadcast();
Stream<int> get getTabPage => _tabPageIndicator.stream;
...
// Update tab index on Stream
_tabPageIndicator.sink.add(_tabController!.index + 1);
Then using StreamBuilder, this gets rebuild when there's a change on the Stream it's listening to. There's no need to use setState() to rebuild the Widgets inside StreamBuilder.
StreamBuilder<int>(
stream: getTabPage,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
if (snapshot.hasData && snapshot.data != null) {
tabIndex = snapshot.data!;
}
return Text(
'Page $index: [$tabIndex / ${_tabController!.length}]',
style: const TextStyle(),
);
}
),
Complete Sample
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
TabController? _tabController;
int tabIndex = 1;
final _tabPageIndicator = StreamController<int>.broadcast();
Stream<int> get getTabPage => _tabPageIndicator.stream;
#override
void initState() {
super.initState();
_tabController = TabController(
length: 3,
vsync: this,
);
// Update tab index on Stream
_tabPageIndicator.sink.add(_tabController!.index + 1);
}
#override
void dispose() {
super.dispose();
// Close Stream when not in use
_tabPageIndicator.close();
}
_back() {
if (_tabController!.index > 0) {
_tabController!.animateTo(_tabController!.index - 1);
// setState(() {
// });
// Update tab index on Stream
_tabPageIndicator.sink.add(_tabController!.index + 1);
}
}
_next() {
if (_tabController!.index < _tabController!.length - 1) {
_tabController!.animateTo(_tabController!.index + 1);
// setState(() {
// });
// Update tab index on Stream
_tabPageIndicator.sink.add(_tabController!.index + 1);
}
}
Widget _tab(int index) {
var value =
"Page $index: ${_tabController!.index + 1} / ${_tabController!.length}";
debugPrint(value);
return Row(
children: [
TextButton(
onPressed: _back,
child: const Text("Back"),
),
// StreamBuilder rebuilds every time there's a change on Stream
StreamBuilder<int>(
stream: getTabPage,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
if (snapshot.hasData && snapshot.data != null) {
tabIndex = snapshot.data!;
}
return Text(
'Page $index: [$tabIndex / ${_tabController!.length}]',
style: const TextStyle(),
);
}),
TextButton(
onPressed: _next,
child: const Text("Next"),
),
],
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: TabBarView(
controller: _tabController,
children: [
_tab(1),
_tab(2),
_tab(3),
],
));
}
}
From documentation of flutter animatedTo method description: animatedTo immediately sets index and previous index and then plays the animation from its current value to index.
Once the _tab method is called, the widget that returns from it is now in the widget tree.
Whenever the build method is run again then its appearance will change.
Every time the _tab method is called, the part of the code that does not return the widget runs again, and return widget which is already in the widget tree.
But it is necessary to run the build method again for the widget to change.
The build is called when the widget is built for the first time. But after that, it is necessary to re-run the build method with setState.
I convert your tab navigation buttons to Widget class. We can more easily understand the comparison with the _tab method and Widget class.
When navigating from index 0 to index 1,2,3 the following is printed in the logs:
flutter: Page 0: 1 / 3
flutter: Page 0: 1 / 3
import 'package:flutter/material.dart';
void main() {
runApp(const TestMyApp());
}
class TestMyApp extends StatelessWidget {
const TestMyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
TabController? _tabController;
#override
void initState() {
super.initState();
_tabController = TabController(
length: 3,
vsync: this,
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: [
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_tab(1),
_tab(2),
_tab(3),
],
),
),
Expanded(
child: TabNavigationWidget(tabController: _tabController!)),
],
));
}
Widget _tab(int index) {
return Text('$index');
}
}
class TabNavigationWidget extends StatelessWidget {
TabController tabController;
TabNavigationWidget({Key? key, required this.tabController})
: super(key: key);
#override
Widget build(BuildContext context) {
var value = "Page : ${tabController.index + 1} / ${tabController.length}";
print(value);
return Row(
children: [
TextButton(
onPressed: _back,
child: Text("Back ${tabController.index}"),
),
Text(
value,
style: const TextStyle(),
),
TextButton(
onPressed: _next,
child: const Text("Next"),
),
],
);
}
_back() {
if (tabController.index > 0) {
tabController.animateTo(tabController.index - 1);
}
}
_next() {
if (tabController.index < tabController.length - 1) {
tabController.animateTo(tabController.index + 1);
}
}
}
I recommend to you use widgets classes instead of _tab() methods. Your _tab methods build 3 times when the setState method is called.
I know too little about widget tree feel free to correct and update the answer.
All tabs are building initially, while the _tabController!.index is 0. _next method does to wait for animateTo to finish the animation, then call setState. Using setState rebuild the UI under build but the TabBarView is not rebuilding until we are telling it that it is having changes.
widget tree is smart enough while updating the UI. -🔎
While creating a widget, without providing key it generates objectRuntimeType key, and doesn't change(same for providing key) on calling setState.
While here, update is depending on key, and widget tree(key) is not different for TabBarView and TabBarView is thinking nothing happen to me, we can't see any update on UI.
Then next comes by adding listener
Register a closure to be called when the object changes.
We can add listener on TabController to listen changes and inside setState to update the UI. You can also remove setState from _back and _next methods.
_tabController = TabController(
length: 3,
vsync: this,
)..addListener(() {
setState(() {});
});
Or just
Use index instead of _tabController!.index while both responsibility is same inside Row.

Flutter: DefaulltTabController with single child TabBarView

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.
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:
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 to TabController.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.

Flutter: How to reset TabController index upon bottom navigation

I have a Flutter app with a Cupertino bottom navigation bar. The first page has tabbed views like this.
What I'm trying to achieve
Upon navigating away from that first page (or when tapping on any of the bottom navigation items/icons), I want the index of the tab controller on that first page to reset to 0 so that when I return to that first page, I see the initial tab by default (i.e. the car tab). The current default behaviour is that it will display whichever tab I left the page on.
How do I achieve the above? I've pasted in sample code below to somewhat replicate my scenario. I created the _resetTabIndex function that calls tabController.previousIndex and then tried to call that function whenever user navigates away from the page, but I couldn't get that to work. Thanks in advance for any help with this!
(NOTE: I have to stick with Cupertino bottom navigation because of other requirements in the real app)
main.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'page1.dart';
void main() {
runApp(TabBarDemo());
}
class TabBarDemo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
enum TabItem { page1, page2 }
class TabItemData {
const TabItemData({#required this.title, #required this.icon});
final String title;
final IconData icon;
static const Map<TabItem, TabItemData> allTabs = {
TabItem.page1: TabItemData(title: 'Page 1', icon: Icons.shopping_cart_outlined),
TabItem.page2: TabItemData(title: 'Page 2', icon: Icons.person_outline_rounded),
};
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
TabItem _currentTab = TabItem.page1;
Map<TabItem, WidgetBuilder> get widgetBuilders {
return {
TabItem.page1: (_) => Page1(),
TabItem.page2: (_) => Scaffold(
appBar: AppBar(
title: Center(child: Text('Page 2')),
),
body: Center(child: Text('Page 2'))),
};
}
void _resetTabIndex(TabItem tabItem) {
setState(() => _currentTab = tabItem); // Ignore this. Set up for a behaviour in the complete app.
// How can I amend this function to trigger the resetTabIndex method in page1.dart (which one alternative I thought might work)?
}
#override
Widget build(BuildContext context) {
return CupertinoHomeScaffold(
currentTab: _currentTab,
onSelectTab: _resetTabIndex,
widgetBuilders: widgetBuilders,
);
}
}
class CupertinoHomeScaffold extends StatelessWidget {
CupertinoHomeScaffold({
Key key,
#required this.currentTab,
#required this.onSelectTab,
#required this.widgetBuilders,
}) : super(key: key);
final TabItem currentTab;
final ValueChanged<TabItem> onSelectTab;
final Map<TabItem, WidgetBuilder> widgetBuilders;
#override
Widget build(BuildContext context) {
return CupertinoTabScaffold(
tabBar: CupertinoTabBar(
items: [
_buildItem(TabItem.page1),
_buildItem(TabItem.page2),
],
onTap: (index) => onSelectTab(TabItem.values[index]),
),
tabBuilder: (context, index) {
final item = TabItem.values[index];
return CupertinoTabView(
builder: (context) => widgetBuilders[item](context),
);
},
);
}
BottomNavigationBarItem _buildItem(TabItem tabItem) {
final itemData = TabItemData.allTabs[tabItem];
final color = currentTab == tabItem ? Colors.indigo : Colors.grey;
return BottomNavigationBarItem(
icon: Icon(itemData.icon, color: color),
title: Text(
itemData.title,
style: TextStyle(color: color),
),
);
}
}
page1.dart
import 'package:flutter/material.dart';
class Page1 extends StatefulWidget {
#override
_Page1State createState() => _Page1State();
}
class _Page1State extends State<Page1> with SingleTickerProviderStateMixin {
TabController _tabController;
#override
void initState() {
super.initState();
_tabController = TabController(vsync: this, length: 2);
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
resetTabIndex() {
setState(() {
_tabController.previousIndex;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
bottom: TabBar(
controller: _tabController,
tabs: [
Tab(icon: Icon(Icons.directions_car)),
Tab(icon: Icon(Icons.directions_bike)),
],
),
title: Center(child: Text('Page 1')),
),
body: TabBarView(
controller: _tabController,
children: [
Icon(Icons.directions_car),
Icon(Icons.directions_bike),
],
),
);
}
}
When navigating away from that first page do this
_tabController.animateTo(0,duration: Duration(milliseconds: 200),curve:Curves.easeIn);

Skip a tab while switching among tabs in TabBarView

In my flutter app, I use a simple tab-bar. I used the code from the flutter website and updated to make sure that I can keep the state of each tab using AutomaticKeepAliveClientMixin.
I have 3 tabs and each tab is fetching a list of data (why I need to use AutomaticKeepAliveClientMixin) from my backend API.
The problem is that when I switch between first and 3rd tabs (Page1 and Page3), the middle tab keeps rebuilding over and over again until I switch to that tab (Page2) and only at that point it doesn't get rebuilt anymore.
Every rebuild results in fetching data from API and that's not desirable.
Below, i have included a simplified code to reproduce this issue.
You can see in the debug console once switching between 1st and 3rd tab (without switching to 2nd tab) that it keeps printing "p2" (in my real app, it keeps fetching data for the 2nd tab).
Is there a way to switch between tabs without other tabs in between being built/rebuilt?
This is my code.
import 'package:flutter/material.dart';
void main() {
runApp(TabBarDemo());
}
class TabBarDemo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.directions_car)),
Tab(icon: Icon(Icons.directions_transit)),
Tab(icon: Icon(Icons.directions_bike)),
],
),
title: Text('Tabs Demo'),
),
body: TabBarView(
children: [
Page1(),
Page2(),
Page3(),
],
),
),
),
);
}
}
class Page1 extends StatefulWidget {
#override
_Page1State createState() => _Page1State();
}
class _Page1State extends State<Page1>
with AutomaticKeepAliveClientMixin<Page1> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
print('p1');
return Container(
child: Center(
child: Icon(Icons.directions_car),
),
);
}
}
class Page2 extends StatefulWidget {
#override
_Page2State createState() => _Page2State();
}
class _Page2State extends State<Page2>
with AutomaticKeepAliveClientMixin<Page2> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
print('p2');
return Container(
child: Center(
child: Icon(Icons.directions_transit),
),
);
}
}
class Page3 extends StatefulWidget {
#override
_Page3State createState() => _Page3State();
}
class _Page3State extends State<Page3>
with AutomaticKeepAliveClientMixin<Page3> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
print('p3');
return Container(
child: Center(
child: Icon(Icons.directions_bike),
),
);
}
}
I believe this isn't a bug with flutter, but ultimately comes down to your implementation.
Please take a look at the code I wrote for you.
import 'package:flutter/material.dart';
import 'dart:async';
class FakeApi {
Future<List<int>> call() async {
print('calling api');
await Future.delayed(const Duration(seconds: 3));
return <int>[for (var i = 0; i < 100; ++i) i];
}
}
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp() : super(key: const Key('MyApp'));
#override
Widget build(BuildContext context) => const MaterialApp(home: MyHomePage());
}
class MyHomePage extends StatelessWidget {
const MyHomePage() : super(key: const Key('MyHomePage'));
static const _icons = [
Icon(Icons.directions_car),
Icon(Icons.directions_transit),
Icon(Icons.directions_bike),
];
#override
Widget build(BuildContext context) => DefaultTabController(
length: _icons.length,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [for (final icon in _icons) Tab(icon: icon)],
),
title: Text('Tabs Demo'),
),
body: TabBarView(
children: [
Center(child: _icons[0]),
StaggeredWidget(_icons[1]),
Center(child: _icons[2]),
],
),
),
);
}
class StaggeredWidget extends StatefulWidget {
const StaggeredWidget(this.icon)
: super(key: const ValueKey('StaggeredWidget'));
final Icon icon;
#override
_StaggeredWidgetState createState() => _StaggeredWidgetState();
}
class _StaggeredWidgetState extends State<StaggeredWidget> {
Widget _child;
Timer _timer;
#override
void initState() {
super.initState();
_timer = Timer(const Duration(milliseconds: 150), () {
if (mounted) {
setState(() => _child = MyApiWidget(widget.icon));
}
});
}
#override
void dispose() {
_timer.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) => _child ?? widget.icon;
}
class MyApiWidget extends StatefulWidget {
const MyApiWidget(this.icon, [Key key]) : super(key: key);
final Icon icon;
#override
_MyApiWidgetState createState() => _MyApiWidgetState();
}
class _MyApiWidgetState extends State<MyApiWidget>
with AutomaticKeepAliveClientMixin {
final _api = FakeApi();
#override
Widget build(BuildContext context) {
print('building `MyApiWidget`');
super.build(context);
return FutureBuilder<List<int>>(
future: _api(),
builder: (context, snapshot) => !snapshot.hasData
? const Center(child: CircularProgressIndicator())
: snapshot.hasError
? const Center(child: Icon(Icons.error))
: ListView.builder(
itemBuilder: (context, index) => ListTile(
title: Text('item $index'),
),
),
);
}
#override
bool get wantKeepAlive => true;
}