我想使用TabBar和BottomNavigationBar来控制同一TabBarView,其主体是自定义小部件RankingTable的列表(仅在演示中放置一个,并将Text小部件用作第二个子级)。 我的问题是点击导航栏时,它不会更新TabBarView主体,例如排名表不会更新;在下面的演示代码中,我将相同的数据放入表格中,但是表格上方的日期下拉菜单应采用不同的格式,因为在轻按导航栏时,我将不同的格式化程序传递到每个数据表中。即,在Nav1的第二个屏幕截图中,它仍显示与Nav0的第一个屏幕截图相同的下拉日期格式。 屏幕截图1:
如果我在TabBarView
正文中放置了诸如Text之类的简单窗口小部件,则在点击导航栏项目时它会按预期更新,不确定是否这是我的自定义RankingTable
小部件。
此外,尽管在导航栏中点击新项目时主体没有更新,但是如果我切换选项卡,例如从Tab1
到Tab2
,然后将其切换回Tab1
,然后正确更新主体,并与相应的导航栏项目匹配。感觉当点击导航时,身体数据确实得到了更新,但只是没有呈现。
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,
);
}
}
答案 0 :(得分:1)
问题出在您的State
。
首先,在您的状态下不需要构造函数。您可以通过调用widget.youFinalField
来访问窗口小部件的变量。
问题是:当您返回new RankingTable
时,基础state
不会被重新创建而是被重建(调用build
和didUpdateWidget
方法)。
因为您在构造中传递了变量(仅在第一次使用),所以格式化程序不会更新。
解决方案非常简单,无需在state
中使用构造函数,只需通过小部件访问变量即可。
工作代码状态代码:
// 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,
);
}
}
答案 1 :(得分:1)
RankingTable
不变的原因是因为代码中的 build 方法使用了存储在状态中的字段(rankingMap,dateFormatter等)。
首次创建StatefulWidget
时,它还会实例化相关的State
对象,然后该对象将通过 build 方法构建您的窗口小部件。每次调用 setState 方法时,Flutter 都会从头开始创建Widget
,而State
对象将被持久保存。
小部件是临时对象,用于在当前状态下构造应用程序的表示形式。另一方面,状态对象在build()的调用之间是持久的,从而使它们能够记住信息。
这意味着,每次创建RankingTable
小部件时,您在_RankingTableState
中的 build 方法将使用在构造函数中传递的相同值/在 initState中分配的值(即使小部件对象包含更新的字段值)。另一方面,当您使用TabBar
来回导航时,状态对象将使用当前的 dateFormatter 重新创建-这就是在这种情况下更新表的原因。
要使其按预期工作,应从 state 对象中删除所有最终字段,并直接引用其 widget 以获取所有必要的值:
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,
);
}
}