📆

詳解 TableCalendar

2022/08/20に公開


前回のTextFieldの引き続き、今回はTableCalendarパッケージを深掘りしていきます。
初学者の方も経験者の方も、TableCalendarの知見を深められると思いますので、気楽に、楽しく読んでいただければ幸いです。また、今回もFlutter for webで実際に触れるようにしました。コードも公開していますので、ご自由にお使いください。
https://flutter-dictionary-release.web.app/tableCalendarTop
https://github.com/mafreud/flutter_dictionary/tree/develop/lib/src/features/table_calendar

対応プラットフォーム

主要なプラットフォームには全て対応しています。

  • Android
  • iOS
  • Linux
  • macOS
  • web
  • Windows

インストール

以下のコマンドを実行し、table_calendarをインストールします。こちらはflutter pub getも実行してくれます。

flutter pub add table_calendar

もしくは、pubspec.yamlに以下を追加してください。

dependencies:
  # 適宜、最新版を指定してください
  table_calendar: ^3.0.6 

入門 TableCalendar


https://flutter-dictionary-release.web.app/tableCalendarTop/tableCalendarBasics

TableCalendarの使用はシンプルです。
名前付き必須引数である、firstDaylastDayfocusedDayにDateTimeオブジェクトを指定すれば、カレンダーが完成します。

TableCalendar(
  firstDay: DateTime.utc(2010, 1, 1),
  lastDay: DateTime.utc(2030, 1, 1),
  focusedDay: DateTime.now(),
);

firstDay

カレンダーの初日を表します。ユーザーはこの日にち以前の日にちを参照することはできません。

lastDay

カレンダーの最終日を表します。ユーザーはこの日にち以後を参照することができません。

focusedDay

フォーカスを当てる日にちです。上記の例では2022年8月10日を指定しました。

ローカライズ(日本語化)

デフォルトでは英語が使用されていますが、もちろん日本語化も可能です。

intlパッケージ

日本語化するためには、intlパッケージを利用します。

flutter pub add intl

上記コマンドを実行し、intlパッケージを導入しましょう

その後、main関数を以下のように変更します。

import 'package:intl/date_symbol_data_local.dart';

void main() {
  initializeDateFormatting().then((_) => runApp(MyApp()));
}

これで、ローカライズの準備は完了です。

日本語化


https://flutter-dictionary-release.web.app/tableCalendarTop/tableCalendarBasics

TableCalendar(
  firstDay: DateTime.utc(2010, 1, 1),
  lastDay: DateTime.utc(2030, 1, 1),
  focusedDay: DateTime.now(),
+ locale: 'ja_JP'
);

ロケール(ja_JP)を指定すれば、日本語化は完了です。もちろん、他のロケールを選択すれば、その言語に対応します。

しかしこのままでは、カレンダーをタップしフォーカスを変更することができません。

カレンダーを操作

様々なコールバックを利用し、カレンダーの操作を可能にします。

日付のフォーカス変更

フォーカス対象の日付を格納する、focusedDayおよび、選択した日にちを格納するselectedDayを変数として置きます。focusedDayは本日(DateTime.now())にしましたが、どの日付でも構いません。

DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay;

selectedDayPredicateおよびisSameDay関数

selectedDayPredicateは、どの日にちが選択されているか確認するために利用します。
実際には、table_calendarが提供する isSameDay関数を返す実装にします。
isSameDayは2つの引数(DateTime)を取り、2つの日付が同じか否かを判定します。

onDaySelected

onDaySelected はカレンダーがタップされるたびに呼ばれるコールバック関数です。以下実装では、_selectedDayおよび_focusedDayを最新の値に更新しています。
これで、isSameDayがtrueを返せば、フォーカスが更新されます。

TableCalendar(
  firstDay: DateTime.utc(2010, 1, 1),
  lastDay: DateTime.utc(2030, 1, 1),
  focusedDay: _focusedDay,
+ selectedDayPredicate: (day) {
+   return isSameDay(selectedDay, day);
+ },
+ onDaySelected: (selectedDay, focusedDay) {
+   setState(() {
+     _selectedDay = selectedDay;
+     _focusedDay = focusedDay;
+   });
+ }
);

カレンダーの長さを変更

デフォルトでは1ヶ月が表示されますが、2週間、1週間と変更可能です。

calendarFormatCalendarFormatを渡せば、カレンダーの長さを変更できます。

CalendarFormatはenumで、

  • month
  • twoWeeks
  • week

があります。

+ CalendarFormat _calendarFormat = CalendarFormat.month;

TableCalendar(
  firstDay: DateTime.utc(2010, 1, 1),
  lastDay: DateTime.utc(2030, 1, 1),
  focusedDay: _focusedDay,
+ calendarFormat: _calendarFormat
);

onFormatChanged

また、フォーマット変更時に発火するonFormatChangedというコールバックも用意されています。

CalendarFormat _calendarFormat = CalendarFormat.month;

TableCalendar(
  firstDay: DateTime.utc(2010, 1, 1),
  lastDay: DateTime.utc(2030, 1, 1),
  focusedDay: _focusedDay,
  calendarFormat: _calendarFormat
+ onFormatChanged: (format) {
+ // do something
+ },
);

例えば、フォーマットの変更時にダイアログを出すことも可能ですね。


https://flutter-dictionary-release.web.app/tableCalendarTop/tableCalendarFormat

TableCalendar 応用

イベント

カレンダーにイベントを設定することが可能です。
以下サンプルでは、2022年8月3日および2022年8月5日に2件ずつイベントを登録しています。

https://flutter-dictionary-release.web.app/tableCalendarTop/tableCalendarEvent

イベントの設定

イベントを設定するためには、eventLoaderコールバック を使用します。

eventLoader

イベントはMap形式で保持します。Mapオブジェクトのkeyはイベントの対象日(DateTime, UTC)、valueはイベント内容(List<String>) です。

final sampleMap = {
  DateTime.utc(2022, 8, 3): ['firstEvent', 'secodnEvent'],
  DateTime.utc(2022, 8, 5): ['thirdEvent', 'fourthEvent']
};

eventLoaderは表示されている日付全てを返しますので、KeyのDateTimeが合致した値が返され、カレンダーに反映されます。

TableCalendar(
  firstDay: DateTime.utc(2010, 1, 1),
  lastDay: DateTime.utc(2030, 1, 1),
  focusedDay: _focusedDay,
+ eventLoader: (date) {
+   return sampleMap[date] ?? [];
+ }
);

https://github.com/aleksanderwozniak/table_calendar#events

イベントの表示

イベントをタップし、イベントを表示することが可能です。

https://flutter-dictionary-release.web.app/tableCalendarTop/tableCalendarEventSelection

イベントデータ

表示するイベントは以下のように置きました。8/3および8/5の2件に対して、イベント2件ずつです。

final sampleEvents = {
  DateTime.utc(2022, 8, 3): ['firstEvent', 'secondEvent'],
  DateTime.utc(2022, 8, 5): ['thirdEvent', 'fourthEvent']
};

表示イベントの格納

表示するイベントを格納するために、以下の変数を設置します。

List<String> _selectedEvents = [];

日付をタップ後、イベントを_selectedEventsに代入します。タップの検知はonDaySelected で行えます。従って、イベントがある日にちがタップされた時に、イベントが_selectedEventsに格納されます。

onDaySelected: (selectedDay, focusedDay) {
  setState(() {
    _selectedEvents = sampleEvents[selectedDay] ?? [];
  });
}

イベントの表示

イベントは_selectedEventsに格納されましたので、あとは表示するだけです。
今回はCard+ListTileを使用しました。もちろんお好きなWidgetをお使いください。

Expanded(
  child: ListView.builder(
    itemCount: _selectedEvents.length,
    itemBuilder: (context, index) {
      final event = _selectedEvents[index];
      return Card(
        child: ListTile(
	  title: Text(event),
	),
      );
    },
  ),
),

以上をまとめると、以下のようになります。
https://github.com/mafreud/flutter_dictionary/blob/develop/lib/src/features/table_calendar/table_calendar_event_selection.dart

定期イベント

例えば毎週月曜日のイベントを設定する場合、以下のように実装できます。

TableCalendar(
  firstDay: DateTime.utc(2010, 1, 1),
  lastDay: DateTime.utc(2030, 1, 1),
  focusedDay: _focusedDay,
+ eventLoader: (date) {
+   if (day.weekday == DateTime.monday) {
+     return [('cyclic event')];
+   }  
+   return []'
+ }
);


https://flutter-dictionary-release.web.app/tableCalendarTop/tableCalendarCyclicEvent

期間の選択

例えば13日から25日のように、期間を選択することが可能です。

期間設定、開始日、終了日

まず、期間の選択を有効にする必要があります。今回は1日だけの選択も可能である、toggeOnを選択しました。

RangeSelectionMode _rangeSelectionMode = RangeSelectionMode.toggledOn;

また、開始日・終了日を格納する変数も用意しておきましょう。

RangeSelectionMode _rangeSelectionMode = RangeSelectionMode.toggledOn;
+ DateTime? _rangeStart;
+ DateTime? _rangeEnd;

TableCalendarで、今までの値を追加します。そして、onRangeSelectedを使用し、開始日、終了日を設定します。

TableCalendar(
  rangeSelectionMode: _rangeSelectionMode,
  rangeStartDay: _rangeStart,
  rangeEndDay: _rangeEnd,
  onRangeSelected: (start, end, focusedDay) {
    setState(() {
      _rangeStart = start;
      _rangeEnd = end;
    });
  }
)

これでカレンダーで期間設定が可能になりました。
コードは以下になります。
https://github.com/mafreud/flutter_dictionary/blob/develop/lib/src/features/table_calendar/table_calendar_range_selection.dart

1日だけ選択

しかしこのままでは、期間設定後、1日だけの選択をすると、両方の状態が残ってしまいます。
(toggleOnに設定している場合、長押しをすると、1日だけ選択が可能です。)

そのため、日付を選択する度に、startとendにnullを代入し、リセットする必要があります。

onDaySelected: (selectedDay, focusedDay) {
  setState(() {
    _selectedDay = selectedDay;
    _focusedDay = focusedDay;
+   _rangeStart = null;
+   _rangeEnd = null;
  }
}

問題なく、単一日および期間の設定ができるようになりました。

https://flutter-dictionary-release.web.app/tableCalendarTop/tableCalendarRangeSelection
コードはこちらです。
https://github.com/mafreud/flutter_dictionary/blob/develop/lib/src/features/table_calendar/table_calendar_range_selection.dart

複数日の選択

カレンダー上で複数日を選択することが可能です。

選択した日にちを格納する変数を用意します。同日を複数選択することはありませんので、要素の重複のない、Setクラスを活用します。

final Set<DateTime> _selectedDays = <DateTime>{};

日付を選択した際に、Setオブジェクトに要素が既に存在するか確認するロジックを追加し、_selectedDaysに値を追加します。

selectedDayPredicate: (day) => _selectedDays.contains(day),
onDaySelected: (selectedDay, focusedDay) {
  setState(() {
    _focusedDay = focusedDay;
    if (_selectedDays.contains(selectedDay)) {
      _selectedDays.remove(selectedDay);
    } else {
      _selectedDays.add(selectedDay);
    }
  });
}

選択した日にちをクリア

選択した日にちをクリアするためには、_selectedDaysの要素をクリアしてください。

setState(() {
  _selectedDays.clear();
});

コードはこちらです。
https://github.com/mafreud/flutter_dictionary/blob/develop/lib/src/features/table_calendar/table_calendar_multi_selection.dart

UIのカスタム

calendarBuildersを使用し、カレンダーのUIを柔軟に変更することが可能です。
日曜日(Sun)を灰色から赤色へ変更しました。

*
https://flutter-dictionary-release.web.app/tableCalendarTop/tableCalendarCustomBuilder
*

カレンダーの曜日ラベルを変更するには、CalendarBuildersのdowBuilderを使用します。
以下のように、日曜日の場合、'Sun'を返すロジックを書きます。

calendarBuilders: CalendarBuilders(
  dowBuilder: (_, day) {
    if (day.weekday == DateTime.sunday) {
      final text = DateFormat.E().format(day);
      return Center(
        child: Text(
	  text,
	  style: const TextStyle(color: Colors.red),
	),
      );
    }
  return null;
),

またnullを返すことにより、デフォルトのスタイルを適用します。
https://github.com/mafreud/flutter_dictionary/blob/develop/lib/src/features/table_calendar/table_calendar_custom_builder.dart

最後に

長文でしたが、最後まで読んで頂きましてありがとうございました。
不明点、間違い等ございましたら、お気軽にコメントで教えていただけるととても嬉しいです!

参考資料

https://github.com/aleksanderwozniak/table_calendar
https://pub.dev/packages/table_calendar
https://qiita.com/Ryota-Nakamura-317/items/56a022a37c9dd8e2d020
https://medium.flutterdevs.com/display-dynamic-events-at-calendar-in-flutter-22b69b29daf6
https://zenn.dev/iwatos/articles/cfae571c9fc6b61397ad

Discussion