导航不会使用自定义小部件来更新TabBarView正文

时间:2018-09-20 19:16:27

标签: flutter flutter-layout

我想使用TabBar和BottomNavigationBar来控制同一TabBarView,其主体是自定义小部件RankingTable的列表(仅在演示中放置一个,并将Text小部件用作第二个子级)。 我的问题是点击导航栏时,它不会更新TabBarView主体,例如排名表不会更新;在下面的演示代码中,我将相同的数据放入表格中,但是表格上方的日期下拉菜单应采用不同的格式,因为在轻按导航栏时,我将不同的格式化程序传递到每个数据表中。即,在Nav1的第二个屏幕截图中,它仍显示与Nav0的第一个屏幕截图相同的下拉日期格式。 屏幕截图1: enter image description here

截屏2: enter image description here

如果我在TabBarView正文中放置了诸如Text之类的简单窗口小部件,则在点击导航栏项目时它会按预期更新,不确定是否这是我的自定义RankingTable小部件。 此外,尽管在导航栏中点击新项目时主体没有更新,但是如果我切换选项卡,例如从Tab1Tab2,然后将其切换回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,
    );
  }
}

2 个答案:

答案 0 :(得分:1)

问题出在您的State

首先,在您的状态下不需要构造函数。您可以通过调用widget.youFinalField来访问窗口小部件的变量。

问题是:当您返回new RankingTable时,基础state不会被重新创建而是被重建(调用builddidUpdateWidget方法)。 因为您在构造中传递了变量(仅在第一次使用),所以格式化程序不会更新。

解决方案非常简单,无需在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,
    );
  }
}