I am learning flutter and currently testing different aspects of it for app development. My eventual plan is to have two tabs, each pulling a list from an API and displaying it in a list view. My concern is that it seems every time you switch tabs, the tabs get fully redrawn as new. How do I save the current state of a tab and all the children so they don't reset when I visit the tab again? Am i thinking about i wrong? Am I supposed to manually save everything to variables and re-populate it every time the tab is drawn?
Below is a quick example I have been testing. It is a simple two tab app with form fields on each page. If I run the app, then type something into the form field, when I switch to tab two and back, the contents of the form field have been removed.
import 'package:flutter/material.dart';
void main() {
print("start");
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Tab Test',
home: new MyTabbedPage(),
);
}
}
class MyTabbedPage extends StatefulWidget {
const MyTabbedPage({Key key}) : super(key: key);
#override
_MyTabbedPageState createState() => new _MyTabbedPageState();
}
class _MyTabbedPageState extends State<MyTabbedPage>
with SingleTickerProviderStateMixin {
final List<Tab> myTabs = <Tab>[
new Tab(text: 'LEFT'),
new Tab(text: 'RIGHT'),
];
TabController _tabController;
var index = 0;
#override
void initState() {
super.initState();
_tabController = new TabController(
vsync: this, length: myTabs.length, initialIndex: index);
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
bottom: new TabBar(
controller: _tabController,
tabs: myTabs,
),
),
body: new TabBarView(
controller: _tabController,
children: myTabs.map((Tab tab) {
return new Center(
child: new TextFormField(
decoration: new InputDecoration(labelText: 'Type Something:'),
));
}).toList(),
),
);
}
}
So, i think I found the answer in setState. I reworked my sample above to have a button that increments a counter on each tab respectively. Using setState to update the vote count allows me to save the state of the field and persist it when I changes tabs. I am adding my sample code below for reference.
import 'package:flutter/material.dart';
void main() {
print("start");
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Tab Test',
home: new MyTabbedPage(),
);
}
}
class MyTabbedPage extends StatefulWidget {
const MyTabbedPage({Key key}) : super(key: key);
#override
_MyTabbedPageState createState() => new _MyTabbedPageState();
}
class _MyTabbedPageState extends State<MyTabbedPage>
with SingleTickerProviderStateMixin {
final List<Tab> myTabs = <Tab>[
new Tab(text: 'LEFT'),
new Tab(text: 'RIGHT'),
];
TabController _tabController;
var index = 0;
#override
void initState() {
super.initState();
_tabController = new TabController(
vsync: this, length: myTabs.length, initialIndex: index);
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
int votesA = 0;
int votesB = 0;
void voteUpA() {
setState(() => votesA++);
}
void voteUpB() {
setState(() => votesB++);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
bottom: new TabBar(
controller: _tabController,
tabs: myTabs,
),
),
body: new TabBarView(
controller: _tabController,
children: <Widget>[
new Column(
children: <Widget>[
Text("Hello Flutter - $votesA"),
new RaisedButton(onPressed: voteUpA, child: new Text("click here"))
],
),
new Column(
children: <Widget>[
Text("Hello Flutter - $votesB"),
new RaisedButton(onPressed: voteUpB, child: new Text("click here"))
],
),
],
),
);
}
}
Related
is there a way to disable a particular tab in the tabbar? so that it cannot be clicked unless it is enabled again? any help is appreciated, thanks!
Edit: Code with Absorb/Ignore Pointer not working:
class MyTabbedPage extends StatefulWidget {
const MyTabbedPage({Key key}) : super(key: key);
#override
_MyTabbedPageState createState() => new _MyTabbedPageState();
}
class _MyTabbedPageState extends State<MyTabbedPage>
with SingleTickerProviderStateMixin {
final List<Tab> myTabs = <Widget>[
Tab(text: 'LEFT'),
AbsorbPointer(
child: Tab(text: 'RIGHT')), //not working
];
TabController _tabController;
#override
void initState() {
super.initState();
_tabController = new TabController(vsync: this, length: myTabs.length);
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
bottom: new TabBar(
controller: _tabController,
tabs: myTabs,
),
),
body: new TabBarView(
controller: _tabController,
children: myTabs.map((Tab tab) {
return new Center(child: new Text(tab.text));
}).toList(),
),
);
}
}
Here you go:
Add list to make which tab is disabled
List<bool> _isDisabled = [false, true];
Add a listener to _tabController
_tabController.addListener(onTap);
onTap() method. If selected tab is disabled we will revert to previous selected tab.
onTap() {
if (_isDisabled[_tabController.index]) {
int index = _tabController.previousIndex;
setState(() {
_tabController.index = index;
});
}
}
Below is the full code:
import 'package:flutter/material.dart';
class MyTabbedPage extends StatefulWidget {
const MyTabbedPage({Key key}) : super(key: key);
#override
_MyTabbedPageState createState() => _MyTabbedPageState();
}
class _MyTabbedPageState extends State<MyTabbedPage>
with SingleTickerProviderStateMixin {
List<bool> _isDisabled = [false, true];
final List<Tab> myTabs = <Tab>[
Tab(text: 'LEFT'),
Tab(text: 'RIGHT'),
];
TabController _tabController;
onTap() {
if (_isDisabled[_tabController.index]) {
int index = _tabController.previousIndex;
setState(() {
_tabController.index = index;
});
}
}
#override
void initState() {
super.initState();
_tabController = TabController(vsync: this, length: myTabs.length);
_tabController.addListener(onTap);
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
bottom: TabBar(
controller: _tabController,
tabs: myTabs,
),
),
body: TabBarView(
controller: _tabController,
children: myTabs.map((Tab tab) {
return Center(child: new Text(tab.text));
}).toList(),
),
);
}
}
TabBar(
tabs: [
Tab(icon: Icon(Icons.directions_car)), //enabled
Tab(icon: Icon(Icons.directions_transit), onTap: null), //disabled
Tab(icon: Icon(Icons.directions_bike)),
],
),
Hi I'm still new to flutter and been studying it for about a week now, I'm currently creating an app that organizes funds for budgeting, I'm attempting to create a multi colored tab bar so users can easily identify their funds, I tried searching over the net and read the tab documentation but still no luck, any tips?
~edit
What I'm trying to achieve is to get the background color of each tab bar same as the background color of scaffold, the default background color of tab bar is currently grey, it should be red on the first tab, blue on second tab, yellow on third tab. is it possible?
here's a screenshot in the emulator:
screenshot
here are sample of my code:
/main.dart
import 'package:flutter/material.dart';
import './FirstTab.dart' as first;
import './SecondTab.dart' as second;
import 'ThirdTab.dart' as third;
void main(){
runApp(new MaterialApp(
home: new MyTabs()
));
}
class MyTabs extends StatefulWidget{
#override
MyTabsState createState() => new MyTabsState();
}
class MyTabsState extends State<MyTabs> with SingleTickerProviderStateMixin {
TabController controller;
#override
void initState(){
super.initState();
controller = new TabController(vsync: this, length: 3);
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Multi Colored Tab Bar'),
backgroundColor: Colors.grey,
bottom: new TabBar(
controller: controller,
tabs: <Tab>[
new Tab(text: 'First Tab'),
new Tab(text: 'Second Tab'),
new Tab(text: 'Third Tab'),
],
),
),
body: new TabBarView(
controller: controller,
children: <Widget>[
new first.First(),
new second.Second(),
new third.Third(),
],
),
);
}
}
/FirstTab.dart
import 'package:flutter/material.dart';
class First extends StatelessWidget{
#override
Widget build(BuildContext context){
return new Scaffold(
backgroundColor: Colors.red
);
}
}
/SecondTab.dart
import 'package:flutter/material.dart';
class Second extends StatelessWidget{
#override
Widget build(BuildContext context){
return new Scaffold(
backgroundColor: Colors.blue
);
}
}
/ThirdTab.dart
import 'package:flutter/material.dart';
class Third extends StatelessWidget{
#override
Widget build(BuildContext context){
return new Scaffold(
backgroundColor: Colors.yellow
);
}
}
Yes it is possible try this
import 'package:flutter/material.dart';
import './FirstTab.dart' as first;
import './SecondTab.dart' as second;
import 'ThirdTab.dart' as third;
void main(){
runApp(new MaterialApp(
home: new MyTabs()
));
}
class MyTabs extends StatefulWidget{
#override
MyTabsState createState() => new MyTabsState();
}
class MyTabsState extends State<MyTabs> with SingleTickerProviderStateMixin {
TabController controller;
int tabIndex =0 ;
#override
void initState(){
super.initState();
controller = new TabController(vsync: this, length: 3);
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Multi Colored Tab Bar'),
backgroundColor: Colors.grey,
bottom: new TabBar(
controller: controller,
indicator: BoxDecoration(color: setColor(tabIndex)),
onTap: (index){
setState(() {
tabIndex = index;
});
},
tabs: <Tab>[
new Tab(text: 'First Tab'),
new Tab(text: 'Second Tab'),
new Tab(text: 'Third Tab'),
],
),
),
body: new TabBarView(
controller: controller,
children: <Widget>[
new first.First(),
new second.Second(),
new third.Third(),
],
),
);
}
setColor(int tabIndex){
if(tabIndex == 0){
return Colors.red;
}else if(tabIndex == 1){
return Colors.blue;
}else if(tabIndex == 2){
return Colors.yellow;
}
}
}
The below will automatically set the Scaffold color on load and the tabs reflect the appropriate default colors.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: TabBarApp(),
);
}
}
class TabBarApp extends StatefulWidget {
createState() => _TabBarAppState();
}
class _TabBarAppState extends State<TabBarApp>
with SingleTickerProviderStateMixin {
TabController _controller;
List<TabData> _tabData;
List<Tab> _tabs = [];
List<Widget> _tabViews = [];
Color _activeColor;
#override
void initState() {
super.initState();
_tabData = [
TabData(title: 'First Tab', color: Colors.red),
TabData(title: 'Second Tab', color: Colors.green),
TabData(title: 'Third Tab', color: Colors.blue),
];
_activeColor = _tabData.first.color;
_tabData.forEach((data) {
final tab = Tab(
child: Container(
constraints: BoxConstraints.expand(),
color: data.color,
child: Center(
child: Text(data.title),
),
),
);
_tabs.add(tab);
final widget = Scaffold(backgroundColor: data.color);
_tabViews.add(widget);
});
_controller = TabController(vsync: this, length: _tabData.length)
..addListener(() {
setState(() {
_activeColor = _tabData[_controller.index].color;
});
});
}
#override
void dispose() {
_controller?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Theme(
data: ThemeData(primaryColor: _activeColor),
child: Scaffold(
backgroundColor: _activeColor,
appBar: AppBar(
title: Text('Multi Colored Tab Bar'),
bottom: TabBar(
indicatorColor: _activeColor,
labelPadding: EdgeInsets.zero,
controller: _controller,
tabs: _tabs,
),
),
body: TabBarView(
controller: _controller,
children: _tabViews,
),
),
);
}
}
class TabData {
TabData({this.title, this.color});
final String title;
final Color color;
}
I got it working by changing primaryColor in theme of MaterialApp, colour of Tab can be changed. So inside build method of MyTabsState return a MaterialApp & set it's theme to a variable currentTabColor.
Declare currentTabColor in MyTabsState & set it to Colors.red
Then set onTap to a callback which returns index of current tab & change state of currentTabColor using if..else
import 'package:flutter/material.dart';
void main() {
runApp(MyTabs());
}
class MyTabs extends StatefulWidget {
#override
MyTabsState createState() => MyTabsState();
}
class MyTabsState extends State<MyTabs> with SingleTickerProviderStateMixin {
TabController controller;
Color currentTabColor;
#override
void initState() {
super.initState();
currentTabColor = Colors.red;
controller = TabController(vsync: this, length: 3);
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: currentTabColor,
),
home: Scaffold(
appBar: AppBar(
title: Text('Multi Colored Tab Bar'),
bottom: TabBar(
onTap: (tabIndex) {
if (tabIndex == 0)
currentTabColor = Colors.red;
else if (tabIndex == 1)
currentTabColor = Colors.blue;
else
currentTabColor = Colors.green;
setState(() {
currentTabColor = currentTabColor;
});
},
controller: controller,
tabs: <Tab>[
Tab(
text: 'First Tab',
),
Tab(text: 'Second Tab'),
Tab(text: 'Third Tab'),
],
),
),
body: TabBarView(
controller: controller,
children: <Widget>[
Scaffold(backgroundColor: Colors.red),
Scaffold(backgroundColor: Colors.blue),
Scaffold(backgroundColor: Colors.green),
],
),
),
);
}
}
Output
I'm using the Flutter UserAccountsDrawerHeader widget to display the user's data but I could not figure out how to implement the onDetailsPressed() function to call the user details. Here is my code:
import 'package:flutter/material.dart';
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
#override
Widget build(BuildContext context) {
return new Scaffold(
drawer: _buildDrawer(context),
appBar: _buildAppBar(),
);
}
}
Widget _buildAppBar() {
return new AppBar();
}
Widget _buildDrawer(BuildContext context) {
return new Drawer(
child: new ListView(
children: <Widget>[
new UserAccountsDrawerHeader(
accountName: new Text("Cleudice Santos"),
accountEmail: new Text("cleudice.ms#gmail.com"),
onDetailsPressed: () {},
),
new ListTile(
title: new Text("Visão geral"),
leading: new Icon(Icons.dashboard),
onTap: () {
print("Visão geral");
},
),
],
),
);
}
I want to click the arrow and show the account details as shown below. That is, overlapping the content of the drawer. As the Gmail app does.
Basically, what you should be doing is replacing the rest of the content with user details rather than the current list. The simplest way to do this is to make your drawer into a stateful widget and have a boolean that keeps track of whether user details or the normal list should be shown.
I've added that to your code (and added a bit to make it self-contained so you can paste it to a new file to test out):
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: UserDetailDrawer(),
appBar: _buildAppBar(),
);
}
}
Widget _buildAppBar() {
return AppBar();
}
class UserDetailDrawer extends StatefulWidget {
#override
_UserDetailDrawerState createState() => _UserDetailDrawerState();
}
class _UserDetailDrawerState extends State<UserDetailDrawer> {
bool showUserDetails = false;
Widget _buildDrawerList() {
return ListView(
children: <Widget>[
ListTile(
title: Text("Visão geral"),
leading: Icon(Icons.dashboard),
onTap: () {
print("Visão geral");
},
),
ListTile(
title: Text("Another tile??"),
leading: Icon(Icons.question_answer),
),
],
);
}
Widget _buildUserDetail() {
return Container(
color: Colors.lightBlue,
child: ListView(
children: [
ListTile(
title: Text("User details"),
leading: Icon(Icons.info_outline),
)
],
),
);
}
#override
Widget build(BuildContext context) {
return Drawer(
child: Column(children: [
UserAccountsDrawerHeader(
accountName: Text("Cleudice Santos"),
accountEmail: Text("cleudice.ms#gmail.com"),
onDetailsPressed: () {
setState(() {
showUserDetails = !showUserDetails;
});
},
),
Expanded(child: showUserDetails ? _buildUserDetail() : _buildDrawerList())
]),
);
}
}
I am learning flutter and I am working with tabBars and I am having an issue with saving the state. I have put a small working example of my issue below. Basically, there is a button and a stateful counter. When I click the button, I see the text field update correctly. But, when I switch to a different tab and come back, the text field is back to zero.
I have found if i move the following line outside of _CounterState so its defined at the top level of the file, then, it works correctly. When I switch tabs, the counter stays at the correct count when I switch back
int _counter = 0;
I don't feel like this is the appropriate way to do this and all of the examples I have seen have the variable inside of the class. Can anyone give me any insights? Why would it reset if it is inside the class? Am I supposed to keep it outside the class? Below is the simplified full example.
import 'package:flutter/material.dart';
void main() {
runApp(new TabBarDemo());
}
class TabBarDemo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new DefaultTabController(
length: 3,
child: new Scaffold(
appBar: new AppBar(
bottom: new TabBar(
tabs: [
new Tab(icon: new Icon(Icons.directions_car)),
new Tab(icon: new Icon(Icons.directions_transit)),
new Tab(icon: new Icon(Icons.directions_bike)),
],
),
title: new Text('Tabs Demo'),
),
body: new TabBarView(
children: [
new Counter(),
new Icon(Icons.directions_transit),
new Icon(Icons.directions_bike),
],
),
),
),
);
}
}
class Counter extends StatefulWidget {
#override
_CounterState createState() => new _CounterState();
}
class _CounterState extends State<Counter> {
int _counter = 0;
void _increment() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return new Row(
children: <Widget>[
new RaisedButton(
onPressed: _increment,
child: new Text('Increment'),
),
new Text('Count: $_counter'),
],
);
}
}
Below is the example with the counter moved outside of the class
import 'package:flutter/material.dart';
void main() {
runApp(new TabBarDemo());
}
class TabBarDemo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new DefaultTabController(
length: 3,
child: new Scaffold(
appBar: new AppBar(
bottom: new TabBar(
tabs: [
new Tab(icon: new Icon(Icons.directions_car)),
new Tab(icon: new Icon(Icons.directions_transit)),
new Tab(icon: new Icon(Icons.directions_bike)),
],
),
title: new Text('Tabs Demo'),
),
body: new TabBarView(
children: [
new Counter(),
new Icon(Icons.directions_transit),
new Icon(Icons.directions_bike),
],
),
),
),
);
}
}
class Counter extends StatefulWidget {
#override
_CounterState createState() => new _CounterState();
}
int _counter = 0; //<-- MOVED OUTSIDE THE _CounterState CLASS
class _CounterState extends State<Counter> {
void _increment() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return new Row(
children: <Widget>[
new RaisedButton(
onPressed: _increment,
child: new Text('Increment'),
),
new Text('Count: $_counter'),
],
);
}
}
As _CounterState widget is built everytime you go to the given TabView you'll need to put _counter variable in the state configuration class (Counter).
class Counter extends StatefulWidget {
int _counter = 0;
#override
_CounterState createState() => new _CounterState();
}
class _CounterState extends State<Counter> {
void _increment() {
setState(() {
widget._counter++;
});
}
#override
Widget build(BuildContext context) {
return new Row(
children: <Widget>[
new RaisedButton(
onPressed: _increment,
child: new Text('Increment'),
),
new Text('Count: ${widget._counter}'),
],
);
}
}
As I used one solution AutomaticKeepAliveClientMixin
You need to use this mixin with your state class of StateFullWidget.
you need to pass true to wantKeepAlive getter method.
class SampleWidget extends StatefulWidget {
#override
_SampleWidgetState createState() => _SampleWidgetState();
}
class _SampleWidgetState extends State<SampleWidget> with AutomaticKeepAliveClientMixin{
#override
Widget build(BuildContext context) {
super.build(context);
return Container();
}
#override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => true;
}
This will save your state and stop your widget to recreate again. I have used it with Tabbar and PageView and it's working fine.
put the variable in that statefulwidget and then call it every time as "widget.variable_name"
Is there a way to render a list then scroll to the bottom.
I understand you can manually scroll to the bottom using ScrollController when a new item is added (like in this question: Programmatically scrolling to the end of a ListView), but how should I automatically scroll to the bottom of the list when the list is built without adding a new item.
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage();
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Column(
children: <Widget>[
new Expanded(
child: new ListView.builder(
itemCount: 200,
itemBuilder: (context, index) {
return new ListTile(
title: new Text("title $index"),
);
},
),
),
],
),
);
}
}
You get this behavior if you use reverse: true
child: new ListView.builder(
reverse: true
https://docs.flutter.io/flutter/widgets/ListView/ListView.builder.html