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
Related
I'm trying to insert an initial value to the flutter_quill flutter text editor with no success. Link to package https://pub.dev/packages/flutter_quill . From a normal TextField I would have done something like below using the controller.
class _FooState extends State<Foo> {
TextEditingController _controller;
#override
void initState() {
super.initState();
_controller = new TextEditingController(text: 'Initial value');
}
#override
Widget build(BuildContext context) {
return new Column(
children: <Widget>[
new TextField(
// The TextField is first built, the controller has some initial text,
// which the TextField shows. As the user edits, the text property of
// the controller is updated.
controller: _controller,
),
],
);
}
}
I appreciate your help.
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);
}),
),
);
}
}
I have a tabbar with three tabs with a different form for each one. I have a save button on the bottom to save all of them.
Problem is, the three form's globalkeys currentstate are accessible only when the relative tab is active.
So the formkey1.currentstate is null when I'm in tab 2 and 3, and so on.
Note that I'm managing state with provider.
Any clues on how to solve this?
Here is a short version of the code:
class Myclass extends StatelessWidget{
final _formKey1 = GlobalKey<FormState>();
final _formKey2 = GlobalKey<FormState>();
final _formKey3 = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return DefaultTabController(length: 3, child:
Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(text: "TAB ONE"),
Tab(text: "TAB TWO"),
Tab(text: "TAB THREE"),
],
),
),
bottomNavigationBar:
child: Row(
children: [
FlatButton(
onPressed: (){
_save(context);
},
child: Text("Save"),
),
],
)
),
body: TabBarView(children: [
Form(key: _formKey1, ...),
Form(key: _formKey2, ...),
Form(key: _formKey3, ...),
])
),);
}
void _save(BuildContext context) async {
print(_formKey1.currentState); // NULL ON TAB 2 AND 3
print(_formKey2.currentState); // NULL ON TAB 1 AND 3
print(_formKey3.currentState); // NULL ON TAB 1 AND 2
//my save procedure
}}
You should use AutomaticKeepAliveClientMixin for each TabView page if you want to keep their states. Also you can you use a KeepAliveWrapper to wrap each page.
Here is the code:
class KeepAliveWrapper extends StatefulWidget {
final Widget child;
const KeepAliveWrapper({Key key, this.child}) : super(key: key);
#override
__KeepAliveWrapperState createState() => __KeepAliveWrapperState();
}
class __KeepAliveWrapperState extends State<KeepAliveWrapper>
with AutomaticKeepAliveClientMixin {
#override
Widget build(BuildContext context) {
super.build(context);
return widget.child;
}
#override
bool get wantKeepAlive => true;
}
Use it like this:
KeepAliveWrapper(
child: Form(
key: _formKey1,
child: Container(),
),
)
Note that before visiting each tab, your sub pages are still not built.
you also can use instances of class (Model) to store data while navigating each page
You can navigate to a new page (new class) which contains a form. For these 3 forms you can create 3 classes and at the tab bar you can navigate into those 3 classes. Inside a class you can nicely handle your form. No difference in the view! just try it!
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'))),
],
),
);
}
}
I would like to be able to change the navigation bar tab programmatically. I have a button inside Page1 that navigates to Page2. When I perform this, the navigation bar disappears because I didn't select page2 using the navigation bar.
I have 4 dart files along the lines of navigationbar.dart, page1.dart, page2.dart, page3.dart
The Navigation page is the home page of the application with the children:
final List<Widget> _children = [
Page1(),
Page2(),
Page3(),
];
return Scaffold(
backgroundColor: Colors.black,
body: _children[_selectedPage],
bottomNavigationBar: _bottomNavigationBar(context),
resizeToAvoidBottomPadding: true,
);
You have to change a TabControlller like this
1* Create TabController instance
TabController _tabController;
2* in initState methode use this
#override
void initState() {
super.initState();
_tabController = TabController(vsync: this, length: 3);
}
3* add a Mixin to _HomeState
class _HomeState extends State<Home> with SingleTickerProviderStateMixin {....}
4* assign the TabController to your TabBar
TabBar(
controller: _tabController,
tabs: _yourTabsHere,
),
5* Pass controller to your Pages
TabBarView(
controller: _tabController,
children:<Widget> [
Page1(tabController:_tabController),
Page2(tabController:_tabController),
Page3(tabController:_tabController),
];
6* call tabController.animateTo() from Page1
class Page1 extends StatefulWidget {
final TabController tabController
Page1({this.tabController});
....}
class _Page1State extends State<Page1>{
....
onButtonClick(){
widget._tabController.animateTo(index); //index is the index of the page your are intending to (open. 1 for page2)
}
}
Hope it helps
You can do that using a GlobalKey.
var bottomNavigatorKey = GlobalKey<State<BottomNavigationBar>>();
put that key in widget,
new BottomNavigationBar(
key: bottomNavigatorKey,
items: [...],
onTap: (int index) {...},
),
put below lines in change tab method,
BottomNavigationBar navigationBar = bottomNavigatorKey.currentWidget as BottomNavigationBar;
navigationBar.onTap(1);
in the button :
onPressed: (){
_currentIndex = 1;
//or which value you want
setState((){});
}
in the bottom Navigation bar :
currentIndex = _currentIndex
You can Use IndexStack.
Demostration.