table_calendar icon indicating copy to clipboard operation
table_calendar copied to clipboard

Incorrect behavior when firstDay parameter changes

Open kenfoo opened this issue 1 year ago • 3 comments

If firstDay parameter is changed with a state change, the displayed calendar changes to something that is incorrect and behavior becomes erratic.

A simple code which starts with

int firstYear=2023

And in the build method of the widget:

      // Set today's date to 24th Dec 2023
      final now = DateTime.utc(2023, 12, 24);
      return Column(
        children: [
          Expanded(
            flex: 1,
            child: TableCalendar(
              shouldFillViewport: true,
              firstDay: DateTime.utc(firstYear, 1, 1),
              lastDay: DateTime.utc(now.year, 12, 31),
              focusedDay: now,
              calendarBuilders: CalendarBuilders(
                outsideBuilder: (context, day, focusedDay) => Container(),
              ),
            ),
          ),
          FilledButton(
              onPressed: () {
                setState(() => firstYear = 1985);
              },
              child: const Text('Change first year'))
        ],
      );

On first render, calendar works properly. The correct days are shown and if you tap the right button to move to the next month, the calendar doesn't allow you to, since lastDay is 31st Dec 2023.

Once we hit the "Change first year" button which changes the firstDay to 1st Jan 1985, the run of dates in the calendar changes to something that doesn't make sense. And if you tap the right button, the month & year still remains as December 2023, but the run of days shows only 29th, 30th, 31st, which doesn't make sense.

kenfoo avatar Dec 24 '23 01:12 kenfoo

@kenfoo I had a similar problem. It seems like the calendar does not like having its parameters that affect the first/last day and focused day changed during a rebuild.

I fixed it by providing a static key to the calendar, meaning Flutter should reuse the same instance of the calendar on a rebuild from setState and only provide the updated parameters:

  static final _uniqueCalendarKey = UniqueKey();

  /// Then inside `build`:
  TableCalendar(
              key: _uniqueCalendarKey
              ...
  )

Let me know if this helps you, if you see any concerns with this, or if any side effects arise.

niemsie avatar Jan 09 '24 09:01 niemsie

@niemsie your solution didn't work for me, but it got me into the right direction. To force a full reset of the calendar widget, I changed the key when changing the first or last day.

My Code looks like the following:


class LessonsCalendar extends StatefulWidget {
  final DateTime startDate;
  final DateTime endDate;

  LessonsCalendar(
      {required this.startDate, required this.endDate});

  @override
  _LessonsCalendarState createState() => _LessonsCalendarState();
}

class _LessonsCalendarState extends State<LessonsCalendar> {

  static var _calendarKeyCount = 0;

  @mustCallSuper
  @protected
  void didUpdateWidget(oldWidget) {
    super.didUpdateWidget(oldWidget);
    setState(() {
      _selectedDay = _focusedDay = DateTime.now();
      ...
      _calendarKeyCount += 1;
    });
  }


  @override
  Widget build(BuildContext context) {
     ...
     KeyedSubtree(
          key: ValueKey<int>(_calendarKeyCount),
          child: TableCalendar(
            // Configuration
            firstDay: widget.startDate,
            lastDay: widget.endDate,
     ...

Hopefully this works for you too @kenfoo

I'm not sure if there is a better way of doing this, at least it works for now. I got the solution from here: https://stackoverflow.com/a/64183322/13319613

yhlbr avatar Apr 15 '24 15:04 yhlbr

Bro, totally random but, any idea on how to implement day to day swiping?

Somtobro avatar Apr 30 '24 07:04 Somtobro