Flutter tabs - a tab is created even though it's not displayed - flutter

I have a DefaultTabController containing a TabBarView with a TabController, so far nothing special.
I have 1 tab which needs to make a request to the server as soon as it's initialized, let's call it the explore tab.
I'm making the request in didChangeDependencies if it wasn't already initialized (I have a boolean flag for initialized).
It's indeed making a request everytime the tab's widget is created, however there's one problem.
Let's say there are 3 tabs, while the explore tab is the second tab in between 1 and 3.
When going from tab 1 to tab 3, or from tab 3 to tab 1, it seems that the explore tab is created during the transition (even though it's not displayed at all), and a request to the server is consequently made.
I'd like to avoid that and only make that request in the explore tab's didChangeDependencies when the user is specifically going to the explore tab.
Any ideas?

Ok, so, you have 3 tabs
[tab1],[tab2],[tab3]
Where you would like to triger an event (request to the server) only when [tab2] was pressed.
To achieve this, create a tabController isolated to add an event listener in a StateFull widget, example:
TabController _tabController;
#override
void initState() {
super.initState();
_tabController = TabController(
length: 2,
vsync: this,
);
_tabController.addListener(() {
if (_tabController.indexIsChanging == false) {
if(_tabController.index == 1) {
//Trigger your request
}
}
});
}
Note: indexIsChanging == false is to ensure that tab already finished the job of change currentIndex of that controller, full example below:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: BodyWidget(),
);
}
}
class BodyWidget extends StatefulWidget {
#override
BodyWidgetState createState() {
return new BodyWidgetState();
}
}
class BodyWidgetState extends State<BodyWidget>
with SingleTickerProviderStateMixin {
TabController _tabController;
#override
void initState() {
super.initState();
_tabController = TabController(
length: 3,
vsync: this,
);
_tabController.addListener(() {
if (_tabController.indexIsChanging == false) {
if (_tabController.index == 1) {
//Trigger your request
print('Triger 2 selected, do the http call now.');
}
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('test'),
bottom: TabBar(
controller: _tabController,
tabs: <Widget>[
Tab(text: 'Tab 1'),
Tab(text: 'Tab 2'),
Tab(text: 'Tab 3'),
],
),
),
body: TabBarView(
controller: _tabController,
children: <Widget>[
Container(child: Center(child: Text('tab1'))),
Container(child: Center(child: Text('tab2'))),
Container(child: Center(child: Text('tab3'))),
],
),
);
}
}

Related

How to set state between tabbar tabs in flutter?

I have tackeled without any success with problem...
My main page is stateful class with tabbar with two tabs. First tab has some text from global variables and couple of textfields that also are prefilled with global variables.
Second tab has a button and ontap it calls setstate that changes variables, that are used on first tab and then animates to first tab.
My problem is that first tabs text doesnt change to new value. At the same time textfields will have new values. If i add print command before returning text on first tab, code will print out new values, but state for text is not set, at the same time textfields state will be set.
Its not possible at moment to add code, but i hope i described mu problem good enough.
Thank You!
I tryed many things and now i got strange working solution that makes what i want.
If i just set new variables and after that let tabcontroller to animate dirst page, pages state will not be set, but if i add small delay, then it works like i want. If anyone could explain why, i would be really thankful.
onPressed: () {
setProduct();
Timer(Duration(milliseconds: 100), animateToFirstPage);
}
There is a really elaborate explanation in this answer.
Bottom line, there is a race condition between setState and animateTo, and he suggests breaking it so:
onPressed: () {
setProduct();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
animateToFirstPage;
})
}
Verified it worked for me, and without creepy .sleep solutions
Use a simple state management solution. Where both tabs can listen and modify the values you want. Without code is hard to demonstrate. But you can't simply change the state of a widget from another widget, using provider would be easier.
To update and listen to the change, use StreamController and StreamBuilder. You can put the first tab in a widget combined with SingleTickerProviderStateMixin to prevent it from reloading as well. I created a simple app for demonstration:
Full example:
main.dart
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
final StreamController _streamController = StreamController<String>();
TabController _tabController;
#override
void initState() {
_tabController = TabController(length: 2, vsync: this);
super.initState();
}
#override
void dispose() {
_streamController.close();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sample App'),
),
bottomNavigationBar: TabBar(
controller: _tabController,
labelColor: Colors.red,
tabs: [Tab(text: 'Tab 1'), Tab(text: 'Tab 2')],
),
body: TabBarView(
controller: _tabController,
children: [
FirstTab(_streamController),
Container(
child: Center(
child: TextButton(
child: Text('Press me'),
onPressed: () {
final _someText =
'Random number: ' + Random().nextInt(100).toString();
_streamController.add(_someText);
_tabController.animateTo(0);
},
),
),
),
],
),
);
}
}
class FirstTab extends StatefulWidget {
final StreamController _streamController;
FirstTab(this._streamController);
#override
_FirstTabState createState() => _FirstTabState();
}
class _FirstTabState extends State<FirstTab>
with AutomaticKeepAliveClientMixin {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
return Container(
child: Center(
child: StreamBuilder<String>(
initialData: 'Empty text',
stream: widget._streamController.stream,
builder: (context, snapshot) {
return Text(snapshot.data);
}),
),
);
}
}

How to "merge" scrolls on a TabBarView inside a PageView?

I have an app that uses a PageView on its main page. Today, I got assigned to insert a TabBarView in one of these pages. The problem is that when I scroll the between the tabs when in the last tab, scrolling to the left won't scroll the PageView.
I need a way to make the scroll of page view scroll when at the start or end of the tabbarview.
I found a question with the inverted problem: flutter PageView inside TabBarView: scrolling to next tab at the end of page
However, the method stated there is not suitable to my issue.
I made a minimal example:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) => MaterialApp(
title: 'TabBarView inside PageView',
home: MyHomePage(),
);
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final PageController _pageController = PageController();
#override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text('TabBarView inside PageView'),
),
body: PageView(
controller: _pageController,
children: <Widget>[
Container(color: Colors.red),
GreenShades(),
Container(color: Colors.yellow),
],
),
);
}
class GreenShades extends StatefulWidget {
#override
_GreenShadesState createState() => _GreenShadesState();
}
class _GreenShadesState extends State<GreenShades>
with SingleTickerProviderStateMixin {
TabController _tabController;
#override
void initState() {
this._tabController = TabController(length: 3, vsync: this);
super.initState();
}
#override
Widget build(BuildContext context) => Column(
children: <Widget>[
TabBar(
labelColor: Colors.green,
indicatorColor: Colors.green,
controller: _tabController,
tabs: <Tab>[
const Tab(text: "Dark"),
const Tab(text: "Normal"),
const Tab(text: "Light"),
],
),
Expanded(
child: TabBarView(
controller: _tabController,
children: <Widget>[
Container(color: Colors.green[800]),
Container(color: Colors.green),
Container(color: Colors.green[200]),
],
),
)
],
);
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
}
Note that, in this MRE, it's possible to reach the 3rd page if you drag the TabBar, but not if you drag the TabBarView.
How may I achieve this behavior?
Edit:
As stated by #Fethi, there's a similar question:
Is it possible to swipe from an TabBarView content area to an adjacent PageView page?
However, the question was not answered satisfactorily, as the solution given does not really "blend" the scroll, although the behavior is similar to what was described. It doesn't scroll naturally.
This is possible by using the PageController.postion attribute's drag method, which internally drags the ScrollPosition of the screen. This way, user can intuitively drag the pages like drag halfway and then leave or continue fully.
The idea is inspired from the other post to use the OverScrollNotification but add rather more step to continue intuitive dragging.
Collect the DragstartDetail when user starts scrolling.
Listen for OverScrollNotification and start the draging and at the same time update the drag using the drag.update with the DragUpdateDetails from OverscrollNotification method.
On ScrollEndNotification cancel the the drag.
To keep the idea simple I am pasting only build method of the Tabs page.
A fully working example is available in this dart pad.
#override
Widget build(BuildContext context) {
// Local dragStartDetail.
DragStartDetails dragStartDetails;
// Current drag instance - should be instantiated on overscroll and updated alongside.
Drag drag;
return Column(
children: <Widget>[
TabBar(
labelColor: Colors.green,
indicatorColor: Colors.green,
controller: _tabController,
tabs: <Tab>[
const Tab(text: "Dark"),
const Tab(text: "Normal"),
const Tab(text: "Light"),
],
),
Expanded(
child: NotificationListener(
onNotification: (notification) {
if (notification is ScrollStartNotification) {
dragStartDetails = notification.dragDetails;
}
if (notification is OverscrollNotification) {
drag = _pageController.position.drag(dragStartDetails, () {});
drag.update(notification.dragDetails);
}
if (notification is ScrollEndNotification) {
drag?.cancel();
}
return true;
},
child: TabBarView(
controller: _tabController,
children: <Widget>[
Container(color: Colors.green[800]),
Container(color: Colors.green),
Container(color: Colors.green[200]),
],
),
),
),
],
);
}
Old Answer
The above might not handle some edge cases. If you need more control below code provides the same result but you can handle UserScrollNotification. I am pasting this because, it might be useful for others who would like to know which direction the use is scrolling w.r.t the Axis of the ScrollView.
if (notification is ScrollStartNotification) {
dragStartDetails = notification.dragDetails;
}
if (notification is UserScrollNotification &&
notification.direction == ScrollDirection.forward &&
!_tabController.indexIsChanging &&
dragStartDetails != null &&
_tabController.index == 0) {
_pageController.position.drag(dragStartDetails, () {});
}
// Simialrly Handle the last tab.
if (notification is UserScrollNotification &&
notification.direction == ScrollDirection.reverse &&
!_tabController.indexIsChanging &&
dragStartDetails != null &&
_tabController.index == _tabController.length - 1) {
_pageController.position.drag(dragStartDetails, () {});
}
so you want to scroll the page view to the left when you reach the end of tabs and the same goes to scrolling to the right when on the first tab, what i have been thinking about is manually swipe the page view when in those cases as follow:
index value should the index of page that comes before the tab bar page and after it.
pageController.animateToPage(index,
duration: Duration(milliseconds: 500), curve: Curves.ease);
here is a complete code of what you are looking for, hopefully this helps!
I have a different approach using Listener Widget and TabView physics as show below:
//PageView Widget
#override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
children: [
Widge1()
TabBarWidget(),
Widget2()
]
)
)
}
//TabBar Widget
final _physycsNotifier = ValueNotifier<bool>(false);
....
....
#override
Widget build(BuildContext context) {
return Column(
children: [
TabBar(
controller: _tabController,
//... other properties
)
Expanded(
child: Listener(
onPointerMove: (event) {
final offset = event.delta.dx;
final index = _tabController.index;
//Check if we are in the first or last page of TabView and the notifier is false
if(((offset > 0 && index == 0) || (offset < 0 && index == _categories.length - 1)) && !_physycsNotifier.value){
_physycsNotifier.value = true;
}
},
onPointerUp: (_) => _physycsNotifier.value = false;
child: ValueListenableBuilder<bool>(
valueListenable: _physycsNotifier,
builder: (_, value, __) {
return TabBarView(
controller: _tabController,
physics: value ? NeverScrollableScrollPhysics() : null,
children: List.generate(_categories.length, (index) {
return _CategoryTab(index: index);
})
);
},
),
)
)
]
)
}
this works fine if you set default physics for PageView and TabView (it means null) if you set other physisc like BouncingScrollPhsysisc there will be some bugs, but i think this is good workaround.

Change selected tab in TabView given state change in ChangeNotifier

I have the following class for holding my state:
import 'package:flutter/foundation.dart';
enum ActiveProduct {
HOME,
BURGUNDY,
VIRTUAL
}
class ActiveProductModel extends ChangeNotifier {
ActiveProduct _activeProduct = ActiveProduct.HOME;
ActiveProduct get value => _activeProduct;
void set(ActiveProduct newValue) {
_activeProduct = newValue;
notifyListeners();
}
}
Whenever the "ActiveProduct" changes, I want to change the selected tab in a TabView.
Currently I have setup the application like this:
class MainScaffold extends StatelessWidget {
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
body: TabBarView(
children: [
Text("hello! abc"),
Text("hello! sdsadsa"),
Text("hello! 231321"),
],
),
),
);
}
}
How can I change the selected tab in the TabBarView when the ActiveProduct changes?
I have tried wrapping the MainScaffold in a Consumer and set the initialIndex value of DefaultTabController. This didn't work however.
I am using the Provider package for state management.
What I would do is transforming your MainScaffold into a StatelessWidget and then, I would not use a DefaultTabController but instead I would use a TabController and pass it as an argument to the TabBarView.
Then in the initState of the MainScaffoldState, I would listen on the ActiveProductModel and use tabController.animateTo(yourpage) whenever something is fired.
I'm not sure that I was very clear in my explanation so here is an example:
class MainScaffold extends StatefulWidget {
#override
_MainScaffoldState createState() => _MainScaffoldState();
}
class _MainScaffoldState extends State<MainScaffold> with SingleTickerProviderStateMixin {
TabController tabController;
/// Just for the example
ActiveProductModel activeProductModel = ActiveProductModel();
#override
void initState() {
// Initiate the tabController
tabController = TabController(vsync: this, length: 3);
activeProductModel.addListener(() {
if (mounted)
/// Here you can do whatever you want, just going to page 2 for the example.
tabController.animateTo(2);
});
super.initState();
}
#override
void dispose() {
tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: TabBarView(
controller: tabController,
children: <Widget>[
Text("hello! abc"),
Text("hello! sdsadsa"),
Text("hello! 231321"),
],
),
);
}
}

Force TabController to create all tabs Flutter

I have a default page with two tabs, using TabBar and TabController. All is working fine, except when I enter the page, the second tab's build method is only called when I click to change to the second tab. The problem is that when I enter the page I want both tabs to be already created (a.k.a their build methods executed). Any thoughts?
Illustration code:
//This widget is used inside a Scaffold
class TabsPage extends StatefulWidget {
#override
State<StatefulWidget> createState() => new TabsPageState();
}
class TabsPageState extends State<TabsPage> with TickerProviderStateMixin {
List<Tab> _tabs;
List<Widget> _pages;
TabController _controller;
#override
void initState() {
super.initState();
_tabs = [
new Tab(text: 'TabOne'),
new Tab(text: 'TabTwo')
];
_pages = [
//Just normal stateful widgets
new TabOne(),
new TabTwo()
];
_controller = new TabController(length: _tabs.length, vsync: this);
}
#override
Widget build(BuildContext context) {
return new Padding(
padding: EdgeInsets.all(10.0),
child: new Column(
children: <Widget>[
new TabBar(
controller: _controller,
tabs: _tabs
),
new SizedBox.fromSize(
size: const Size.fromHeight(540.0),
child: new TabBarView(
controller: _controller,
children: _pages
),
)
],
),
);
}
}
I had a similar issue, I'm using Tabs with Forms and I wanted to validate all of them, my solution was to switch to the second tab programmatically, It doesn't make sense to not visit a tab with a form that requires validation.
So given a global key for each form:
final _formKey = GlobalKey<FormState>();
The reason I wanted all tabs to be built was to use:
_formKey.currentState.validate()
So my fix is
if (_formKey.currentState == null) { //The tab was never visited, force it
_tabController.animateTo(1);
return;
}
I was able to achieve this.
In my case, I had 2 tabs and I wanted to preload the other tab.
So what I did was, I initialised the tab controller in initState like this:
tabController = TabController(
length: 2,
vsync: this,
initialIndex: 1,
);
I set the initialIndex to the other tabs index, and then set the index back in after the build was done like this:
WidgetsBinding.instance.addPostFrameCallback((_) {
tabController.index = 0;
});
But this may only work if you have 2 tabs

Preserving state between tab view pages

issue
I have two ListViews rendering inside of a TabBarView using a TabController.
How do I preserve state (for lack of a better word) between each ListView so that: 1.) the Widgets don't rebuild and 2.) the ListView position is remembered between tabs.
relevant code
class AppState extends State<App> with SingleTickerProviderStateMixin {
TabController _tabController;
#override
void initState() {
super.initState();
_tabController = new TabController(
vsync: this,
length: _allPages.length,
);
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
Widget _buildScaffold(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('headlines'),
bottom: new TabBar(
controller: _tabController,
isScrollable: true,
tabs: _allPages
.map((_Page page) => new Tab(text: page.country))
.toList()),
),
body: new TabBarView(
controller: _tabController,
children: _allPages.map((_Page page) {
return new SafeArea(
top: false,
bottom: false,
child: new Container(
key: new ObjectKey(page.country),
child: new Newsfeed(country: page.country),
),
);
}).toList()),
);
}
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'news app',
home: _buildScaffold(context),
);
}
}
illustrating gif
https://media.giphy.com/media/2ysWhzqHVqL1xcBlBE/giphy.gif
In case you want to keep the state of your screen in your TabBarView, you can use the mixin class called AutomaticKeepAliveClientMixin in your State class.
After that you have to override the wantKeepAlive method and return true. It will looks like something that :
class _SearchScreenState extends State<SearchScreen> with AutomaticKeepAliveClientMixin<SearchScreen>{
...
#override
Widget build(BuildContext context) {
// call this method
super.build(context);
/// your widget here
}
#override
bool get wantKeepAlive => true;
}
I wrote a post about that here: https://medium.com/#diegoveloper/flutter-persistent-tab-bars-a26220d322bc
Long story short, use a PageStorageKey() for your ListView or one of it's ancestors, the Container widget in your case:
child: Container(
key: PageStorageKey(page.country),
child: Newsfeed(country: page.country),
),
See details here:
https://api.flutter.dev/flutter/widgets/PageStorageKey-class.html
https://api.flutter.dev/flutter/widgets/PageStorage-class.html
https://api.flutter.dev/flutter/widgets/ScrollView/controller.html
Try containing your children views in a stack with an offset. It helped me save the state of my bottom bar navigation.