dragonfly02
dragonfly02

Reputation: 3669

Navigation doesn't update TabBarView body with a custom widget

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: enter image description here

Screenshot2: enter image description here

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

Answers (2)

tomwyr
tomwyr

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

Norbert
Norbert

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

Related Questions