Reputation: 3669
I want to use a TabBar and a BottomNavigationBar to control the same TabBarView whose body is a list of custom widget RankingTable (only put one in the demo and using a Text widget as the 2nd child).
My problem is when tapping on navigation bar, it doesn't update the TabBarView body e.g. RankingTable doesn't update; in the demo code below I have put the same data in the table but the date drop down above the table should be formatted differently since I pass in a different formatter into each datatable when navigation bar is tapped. Namely, in the 2nd screenshot, on Nav1, it still shows the same dropdown date format as in the 1st screenshot which is on Nav0.
Screenshot1:
If I put a simple widget such as Text in the TabBarView
body, then it updates as expected when tapping on the navigation bar items, not sure whether this implies the problem is my custom RankingTable
widget.
Also, despite the body doesn't update when tapping on a new item in navigation bar, if I switch tab e.g. from Tab1
to Tab2
and switch it back to Tab1
, then the body gets updated correctly, matching with the corresponding navigation bar item. It feels like the body data did get updated when tapping navigation but it just didn't get rendered.
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
void main() => runApp(new Demo());
class Demo extends StatefulWidget {
@override
_DemoState createState() => _DemoState();
}
class _DemoState extends State<Demo> with TickerProviderStateMixin {
int _currentIndex = 0;
Map<DateTime, List<RankingBase>> _playerDateRanking;
TabController controller;
List<_NavigationIconView> _navigationIconViews;
@override
void initState() {
super.initState();
controller = TabController(length: 2, vsync: this);
_navigationIconViews = <_NavigationIconView>[
_NavigationIconView(
icon: Icon(Icons.calendar_view_day),
title: 'Nav0',
color: Colors.deepOrange,
vsync: this,
),
_NavigationIconView(
icon: Icon(Icons.date_range),
title: 'Nav1',
color: Colors.deepOrange,
vsync: this,
),
];
_playerDateRanking = {
DateTime(2018, 9, 10): [
PlayerRanking('Tony', 7, 6, 140, 110, 80),
PlayerRanking('John', 7, 2, 120, 130, 56),
PlayerRanking('Mike', 8, 5, 120, 130, 70),
PlayerRanking('Clar', 6, 2, 100, 134, 63)
],
DateTime(2018, 9, 12): [
PlayerRanking('Tony', 7, 6, 140, 110, 80),
PlayerRanking('John', 7, 2, 120, 130, 56),
PlayerRanking('Mike', 8, 5, 120, 130, 70),
PlayerRanking('Clare', 6, 2, 100, 134, 63),
PlayerRanking('Jo', 5, 1, 100, 134, 63)
]
};
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
bottom: TabBar(
controller: controller,
tabs: <Widget>[Text('Tab1'), Text('Tab2')],
),
),
body: TabBarView(
controller: controller, //TabController(length: 2, vsync: this),
children: <Widget>[
buildRankingTable(_currentIndex),
Text('TEst'),
],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
items: _navigationIconViews.map((x) => x.item).toList(),
onTap: (int index) {
setState(() {
_currentIndex = index;
});
},
),
),
);
}
Widget buildRankingTable(int currentIndex) {
if (currentIndex == 0) {
return RankingTable(_playerDateRanking, dateFormatter: 'yMMMEd');
} else if (currentIndex == 1) {
return RankingTable(_playerDateRanking,
dateFormatter: 'MMMM'); // different date formatter here!
}
return Text('TODO...');
}
}
class _NavigationIconView {
_NavigationIconView({
Widget icon,
//Widget activeIcon,
String title,
Color color,
TickerProvider vsync,
}) : _icon = icon,
_color = color,
_title = title,
item = new BottomNavigationBarItem(
icon: icon,
// activeIcon: activeIcon,
title: new Text(title),
backgroundColor: color,
),
controller = new AnimationController(
duration: kThemeAnimationDuration,
vsync: vsync,
) {
_animation = new CurvedAnimation(
parent: controller,
curve: const Interval(0.5, 1.0, curve: Curves.fastOutSlowIn),
);
}
final Widget _icon;
final Color _color;
final String _title;
final BottomNavigationBarItem item;
final AnimationController controller;
CurvedAnimation _animation;
}
class PlayerRanking extends RankingBase {
String name;
PlayerRanking(this.name, played, won, pointsWon, pointsLost, duration)
: super(played, won, pointsWon, pointsLost, duration);
}
class RankingBase {
DateTime date;
int won;
int played;
int duration;
int pointsWon;
int pointsLost;
double get winRatio => won / played;
RankingBase(
this.played, this.won, this.pointsWon, this.pointsLost, this.duration);
static int performanceSort(RankingBase rb1, RankingBase rb2) {
if (rb1.winRatio > rb2.winRatio) return -1;
if (rb1.winRatio < rb2.winRatio) return 1;
if (rb1.played > rb2.played) return -1;
if (rb2.played == rb2.played) return rb1.pointsWon.compareTo(rb2.pointsWon);
return -1;
}
}
// this puts a scrollable datatable and optionally a header widget into a ListView
class RankingTable extends StatefulWidget {
final Map<DateTime, List<RankingBase>> rankingMap;
final bool hasHeaderWidget;
final String dateFormatter;
//final bool isPlayer;
RankingTable(this.rankingMap,
{this.dateFormatter, this.hasHeaderWidget = true});
@override
_RankingTableState createState() => _RankingTableState(this.rankingMap,
dateFormatter: this.dateFormatter, hasHeaderWidget: this.hasHeaderWidget);
}
class _RankingTableState extends State<RankingTable> {
Map<DateTime, List<RankingBase>> rankingMap;
final bool hasHeaderWidget;
final String dateFormatter;
//final bool isPlayer;
_RankingTableState(this.rankingMap,
{this.dateFormatter, this.hasHeaderWidget = true});
DateTime _selectedDate;
@override
initState() {
super.initState();
_selectedDate = rankingMap.keys.last;
}
DataTable buildRankingTable() {
rankingMap[_selectedDate].sort(RankingBase.performanceSort);
String nameOrPair =
rankingMap[_selectedDate].first is PlayerRanking ? 'Name' : 'Pair';
int rank = 1;
return DataTable(
columns: <DataColumn>[
DataColumn(label: Text('Rank')),
DataColumn(label: Text(nameOrPair)),
DataColumn(label: Text('Played')),
DataColumn(label: Text('Win Ratio')),
DataColumn(label: Text('Points Won-Loss')),
DataColumn(label: Text('Duration')),
],
rows: rankingMap[_selectedDate].map((RankingBase pr) {
DataCell titleCell;
if (pr is PlayerRanking)
titleCell = DataCell(Text('${pr.name}'));
else {
// var pair = pr as PairRanking;
// titleCell = DataCell(Text('${pair.player1Name}\n${pair.player2Name}'));
}
return DataRow(cells: [
DataCell(Text('${rank++}')),
titleCell,
DataCell(Text('${pr.played}')),
DataCell(Text('${NumberFormat("0.##%").format(pr.won / pr.played)}')),
DataCell(Text('${pr.pointsWon} - ${pr.pointsLost}')),
DataCell(Text('${pr.duration}')),
]);
}).toList(),
);
}
@override
Widget build(BuildContext context) {
List<Widget> childrenWidgets = [];
if (hasHeaderWidget) {
var dateDropdown = DropdownButton<DateTime>(
items: rankingMap.keys
.map((date) => DropdownMenuItem(
child: Text(
'${DateFormat(dateFormatter).format(date)}'), //yMMMEd
value: date,
))
.toList(),
value: _selectedDate,
onChanged: (value) {
setState(() {
_selectedDate = value;
});
},
);
childrenWidgets.add(dateDropdown);
}
childrenWidgets.add(SingleChildScrollView(
padding: EdgeInsets.all(20.0),
scrollDirection: Axis.horizontal,
child: buildRankingTable(),
));
return ListView(
padding: EdgeInsets.all(10.0),
children: childrenWidgets,
);
}
}
Upvotes: 3
Views: 4012
Reputation: 1411
The reason why RankingTable
doesn't change is because build method in the code uses fields (rankingMap, dateFormatter, etc.) stored in your state.
When StatefulWidget
is created for the first time, it also instantiates relevant State
object which then builds your widget in build method. Every time you call setState method, Flutter recreates Widget
from scratch, whereas State
object is being persisted.
Widgets are temporary objects, used to construct a presentation of the application in its current state. State objects on the other hand are persistent between calls to build(), allowing them to remember information.
This means, every time RankingTable
widget is created, your build method in _RankingTableState
makes use of the same values passed in the constructor/assigned in initState (even though widget object contains updated fields values). On the other hand, when you naviagte back and forth by using the TabBar
, state object is recreated with current dateFormatter - that's why the table is updated in this scenario.
To make it work as intended, you should remove all final fields from your state object and refer directly to its widget to get all necessary values:
class RankingTable extends StatefulWidget {
final Map<DateTime, List<RankingBase>> rankingMap;
final bool hasHeaderWidget;
final String dateFormatter;
RankingTable(this.rankingMap,
{this.dateFormatter, this.hasHeaderWidget = true});
@override
_RankingTableState createState() => _RankingTableState();
}
class _RankingTableState extends State<RankingTable> {
DateTime _selectedDate;
@override
initState() {
super.initState();
_selectedDate = widget.rankingMap.keys.last;
}
DataTable buildRankingTable() {
widget.rankingMap[_selectedDate].sort(RankingBase.performanceSort);
String nameOrPair =
widget.rankingMap[_selectedDate].first is PlayerRanking ? 'Name' : 'Pair';
int rank = 1;
return DataTable(
columns: <DataColumn>[
DataColumn(label: Text('Rank')),
DataColumn(label: Text(nameOrPair)),
DataColumn(label: Text('Played')),
DataColumn(label: Text('Win Ratio')),
DataColumn(label: Text('Points Won-Loss')),
DataColumn(label: Text('Duration')),
],
rows: widget.rankingMap[_selectedDate].map((RankingBase pr) {
DataCell titleCell;
if (pr is PlayerRanking)
titleCell = DataCell(Text('${pr.name}'));
else {
// var pair = pr as PairRanking;
// titleCell = DataCell(Text('${pair.player1Name}\n${pair.player2Name}'));
}
return DataRow(cells: [
DataCell(Text('${rank++}')),
titleCell,
DataCell(Text('${pr.played}')),
DataCell(Text('${NumberFormat("0.##%").format(pr.won / pr.played)}')),
DataCell(Text('${pr.pointsWon} - ${pr.pointsLost}')),
DataCell(Text('${pr.duration}')),
]);
}).toList(),
);
}
@override
Widget build(BuildContext context) {
List<Widget> childrenWidgets = [];
if (widget.hasHeaderWidget) {
var dateDropdown = DropdownButton<DateTime>(
items: widget.rankingMap.keys
.map((date) => DropdownMenuItem(
child: Text(
'${DateFormat(widget.dateFormatter).format(date)}'), //yMMMEd
value: date,
))
.toList(),
value: _selectedDate,
onChanged: (value) {
setState(() {
_selectedDate = value;
});
},
);
childrenWidgets.add(dateDropdown);
}
childrenWidgets.add(SingleChildScrollView(
padding: EdgeInsets.all(20.0),
scrollDirection: Axis.horizontal,
child: buildRankingTable(),
));
return ListView(
padding: EdgeInsets.all(10.0),
children: childrenWidgets,
);
}
}
Upvotes: 1
Reputation: 1027
The problem lies in your State
.
First, there is no need for a constructor in your state. You can access the variables of your widget by calling widget.youFinalField
.
The problem is: when you return a new RankingTable
the underlying state
is not recreated but rebuilt (the build
and the didUpdateWidget
methods are called).
Because you passed you variables in the construct (which is only used the very first time) your formatter doesn't update.
The solution is pretty simple, instead of using a constructor in your state
just access the variables through the widget.
Working code state code:
// this puts a scrollable datatable and optionally a header widget into a ListView
class RankingTable extends StatefulWidget {
final Map<DateTime, List<RankingBase>> rankingMap;
final bool hasHeaderWidget;
final String dateFormatter;
//final bool isPlayer;
RankingTable(this.rankingMap,
{this.dateFormatter, this.hasHeaderWidget = true});
@override
_RankingTableState createState() => _RankingTableState();
}
class _RankingTableState extends State<RankingTable> {
DateTime _selectedDate;
@override
void initState() {
super.initState();
_selectedDate = widget.rankingMap.keys.last;
}
DataTable buildRankingTable() {
widget.rankingMap[_selectedDate].sort(RankingBase.performanceSort);
String nameOrPair =
widget.rankingMap[_selectedDate].first is PlayerRanking ? 'Name' : 'Pair';
int rank = 1;
return DataTable(
columns: <DataColumn>[
DataColumn(label: Text('Rank')),
DataColumn(label: Text(nameOrPair)),
DataColumn(label: Text('Played')),
DataColumn(label: Text('Win Ratio')),
DataColumn(label: Text('Points Won-Loss')),
DataColumn(label: Text('Duration')),
],
rows: widget.rankingMap[_selectedDate].map((RankingBase pr) {
DataCell titleCell;
if (pr is PlayerRanking)
titleCell = DataCell(Text('${pr.name}'));
else {
// var pair = pr as PairRanking;
// titleCell = DataCell(Text('${pair.player1Name}\n${pair.player2Name}'));
}
return DataRow(cells: [
DataCell(Text('${rank++}')),
titleCell,
DataCell(Text('${pr.played}')),
DataCell(Text('')),
DataCell(Text('${pr.pointsWon} - ${pr.pointsLost}')),
DataCell(Text('${pr.duration}')),
]);
}).toList(),
);
}
@override
Widget build(BuildContext context) {
List<Widget> childrenWidgets = [];
if (widget.hasHeaderWidget) {
var dateDropdown = DropdownButton<DateTime>(
items: widget.rankingMap.keys
.map((date) => DropdownMenuItem(
child: Text(
'$date ${widget.dateFormatter}'), //yMMMEd
value: date,
))
.toList(),
value: _selectedDate,
onChanged: (value) {
setState(() {
_selectedDate = value;
});
},
);
childrenWidgets.add(dateDropdown);
}
childrenWidgets.add(SingleChildScrollView(
padding: EdgeInsets.all(20.0),
scrollDirection: Axis.horizontal,
child: buildRankingTable(),
));
return ListView(
padding: EdgeInsets.all(10.0),
children: childrenWidgets,
);
}
}
Upvotes: 5