【Flutter】table_calendarを使ってみる。【table_calendar 3.0.0対応】

#はじめに
Flutterのパッケージにtable_calendarといういい感じでカレンダーを実装できるものがあるのですが、
日本語で紹介された記事があまりなかったので実際に使ってみて実装するまでの記録をまとめたいと思います。

以下のサイトを参考に実装しました。

table_calendar
https://pub.dev/packages/table_calendar
https://github.com/aleksanderwozniak/table_calendar

　
今回使用するバージョンは以下の通りです。

Flutter 2.0.6
Dart 2.12.3

table_calendar 3.0.0
intl 0.17.0 　(言語対応に使用。)




#達成目標
<img width="350" alt="Simulator Screen Shot - iPhone 12 Pro Max - 2021-05-06 at 11.08.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1263604/6667555a-de0b-f2cb-c814-466039ef6f4f.png">



・カレンダーの表示（日本語）
・イベントマーカーの表示
・カレンダーのしたにイベントをリスト表示
の3点を達成目標として進めて行きます。



#インストール
まずはパッケージのインストールをします。

```:pubspec.yaml
environment:
  sdk: ">=2.12.3 <3.0.0" 　　//dartのsdkを手動で2.12.3に変更します。

dependencies:
  flutter:
    sdk: flutter
  
  table_calendar: ^3.0.0
  intl: ^0.17.0

```

追加できたら`pub get`を実行。


#基本設定
まず、TableCalendarでは`firstDay`、`lastDay`、`focusedDay`がrequiresとして設定されてます。

`firstDay`と`lastDay`はカレンダーを表示する最初と最後の月の指定ができ、
`focusedDay`はデフォルトでフォーカスされて日付の部分に色が付きます。

```dart:calendar.dart

DateTime _focusedDay = DateTime.now();

TableCalendar(
            // 以下必ず設定が必要
            firstDay: DateTime.utc(2020, 1, 1),
            lastDay: DateTime.utc(2030, 12, 31),
            focusedDay: _focusedDay,
             );
```
ひとまずこれだけでカレンダーの表示はできます。
![スクリーンショット 2021-05-06 午後16.03.04 午後.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1263604/9021dfc7-dd07-e2de-fd07-ab8390a40f9e.png)

#カレンダーフォーマット
ここからtable_calendarをより実践的なものにしていきます。
まずはカレンダーのフォーマット設定です。
赤く囲ったボタンを押すと3パターンのフォーマットに変更するように設定します。

```dart:calendar.dart

DateTime _focusedDay = DateTime.now();
CalendarFormat _calendarFormat = CalendarFormat.month;

TableCalendar(
            firstDay: DateTime.utc(2020, 1, 1),
            lastDay: DateTime.utc(2030, 12, 31),
            focusedDay: _focusedDay,
            calendarFormat: _calendarFormat,          //以下、追記部分。
            // フォーマット変更のボタン押下時の処理
            onFormatChanged: (format) {
              if (_calendarFormat != format) {
                setState(() {
                  _calendarFormat = format;
                });
              }
            },
          ),
```
`CalendarFormat.month`←デフォルト設定時の表示
<img width="350" alt="Simulator Screen Shot - iPhone 12 Pro Max - 2021-05-06 at 17.20.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1263604/1df9f784-1dda-9808-227d-8c3dfeaccf02.png">

`CalendarFormat.twoWeeks`
<img width="350" alt="Simulator Screen Shot - iPhone 12 Pro Max - 2021-05-06 at 17.24.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1263604/4fc6f96a-9492-dda7-62cd-67b3216ee6ee.png">

`CalendarFormat.week`
<img width="350" alt="Simulator Screen Shot - iPhone 12 Pro Max - 2021-05-06 at 17.24.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1263604/9864b87d-58ef-5c8f-68c6-725ca0b0b626.png">



ボタンを押すか、カレンダー部分を上下にスワイプさせることでフォーマットを任意で変えることができます。
フォーマットのデフォルトを設定し、ボタン表示をなしにすることも可能です。

```dart:
headerStyle: HeaderStyle(
        formatButtonVisible: false,
      ),
```


#日付選択
`focusedDay`によって今日の日付に印がついていますが、
今度はタップした日付が選択され、印が付くように設定します。

```dart:calendar.dart

DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay;   //追記
CalendarFormat _calendarFormat = CalendarFormat.month;

TableCalendar(
            firstDay: DateTime.utc(2020, 1, 1),
            lastDay: DateTime.utc(2030, 12, 31),
            focusedDay: _focusedDay,
            calendarFormat: _calendarFormat,
            onFormatChanged: (format) {
              if (_calendarFormat != format) {
                setState(() {
                  _calendarFormat = format;
                });
              }
            },
            selectedDayPredicate: (day) {      //以下追記部分
              return isSameDay(_selectedDay, day);
            },
            onDaySelected: (selectedDay, focusedDay) {
              if (!isSameDay(_selectedDay, selectedDay)) {
                setState(() {
                  _selectedDay = selectedDay;
                  _focusedDay = focusedDay;
                });
              }
            },
          ),
```

どの日が現在選択されているかを判断するには、`selectedDayPredicate`を使用します。
これが`true`を返した場合、`day`が選択されたとみなされます。
`isSameDay`は、比較されたDateTimeオブジェクトの時間部分を無視するために公式より推奨されています。

`onDaySelected`と合わせて使用することで、タップした日付に印が付きます。
（21日をタップした時の画面↓）
<img width="350" alt="Simulator Screen Shot - iPhone 12 Pro Max - 2021-05-06 at 17.38.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1263604/9edbfa31-a205-8b6a-67ac-db1751979020.png">
#イベントの読み込み
TableCalendarの特徴の一つでもあるイベントの読み込みとカレンダーへの反映部分を実装していきます。
ここからコードが少し見にくいかもしれません。。。

```dart:calendar.dart
class CalendarScreen extends StatefulWidget {
  @override
  _CalendarScreenState createState() => _CalendarScreenState();
}

class _CalendarScreenState extends State<CalendarScreen> {
  CalendarFormat _calendarFormat = CalendarFormat.month;
  DateTime _focusedDay = DateTime.now();
  DateTime? _selectedDay;
  Map<DateTime, List> _eventsList = {};

  int getHashCode(DateTime key) {
    return key.day * 1000000 + key.month * 10000 + key.year;
  }

  @override
  void initState() {
    super.initState();
    _selectedDay = _focusedDay;
    //サンプルのイベントリスト
    _eventsList = {
      DateTime.now().subtract(Duration(days: 2)): ['Event A6', 'Event B6'],
      DateTime.now(): ['Event A7', 'Event B7', 'Event C7', 'Event D7'],
      DateTime.now().add(Duration(days: 1)): [
        'Event A8',
        'Event B8',
        'Event C8',
        'Event D8'
      ],
      DateTime.now().add(Duration(days: 3)):
          Set.from(['Event A9', 'Event A9', 'Event B9']).toList(),
      DateTime.now().add(Duration(days: 7)): [
        'Event A10',
        'Event B10',
        'Event C10'
      ],
      DateTime.now().add(Duration(days: 11)): ['Event A11', 'Event B11'],
      DateTime.now().add(Duration(days: 17)): [
        'Event A12',
        'Event B12',
        'Event C12',
        'Event D12'
      ],
      DateTime.now().add(Duration(days: 22)): ['Event A13', 'Event B13'],
      DateTime.now().add(Duration(days: 26)): [
        'Event A14',
        'Event B14',
        'Event C14'
      ],
    };
  }

  @override
  Widget build(BuildContext context) {

    final _events = LinkedHashMap<DateTime, List>(
      equals: isSameDay,
      hashCode: getHashCode,
    )..addAll(_eventsList);

    List getEventForDay(DateTime day) {
      return _events[day] ?? [];
    }   

    return Scaffold(
      appBar: AppBar(
        title: Text('calendar sample'),
      ),
      body: Column(
        children: [
          TableCalendar(
            //locale: 'ja_JP',
            firstDay: DateTime.utc(2020, 1, 1),
            lastDay: DateTime.utc(2030, 12, 31),
            focusedDay: _focusedDay,
            eventLoader: getEventForDay,   //追記
            calendarFormat: _calendarFormat,
            onFormatChanged: (format) {
              if (_calendarFormat != format) {
                setState(() {
                  _calendarFormat = format;
                });
              }
            },
            selectedDayPredicate: (day) {
              return isSameDay(_selectedDay, day);
            },
            onDaySelected: (selectedDay, focusedDay) {
              if (!isSameDay(_selectedDay, selectedDay)) {
                setState(() {
                  _selectedDay = selectedDay;
                  _focusedDay = focusedDay;
                });
              }
            },
            onPageChanged: (focusedDay) {
              _focusedDay = focusedDay;
            },
          ),
        ],
      ),
    );
  }
}

```
できるだけ細かく解説していきます。
今回表示させるイベントサンプルは`_eventsList`で定義しました。

まずイベントを読み込むために`eventLoader`を使用します。
`eventLoader`に関わってくる部分は以下の部分です。

```dart:
// DateTime型から20210930の8桁のint型へ変換
int getHashCode(DateTime key) {
    return key.day * 1000000 + key.month * 10000 + key.year;
  }

//...中略

final _events = LinkedHashMap<DateTime, List>(
      equals: isSameDay,
      hashCode: getHashCode,
    )..addAll(_eventsList);

List _getEventForDay(DateTime day) {
      return _events[day] ?? [];
    }   

//...中略

eventLoader: _getEventForDay, 
```

TableCalendarでは、カレンダーに読み込むイベントをMapで定義した場合、`LinkedHashMap`を使用することを推奨されています。
純粋に日付だけを比較することができるそうです。
`getHashCode`、`LinkedHashMap`の部分に関しては公式のサンプルを参考にしています。

これらを設定し終えると、カレンダー部分にマーカーが表示されます。
<img width="350" alt="Simulator Screen Shot - iPhone 12 Pro Max - 2021-05-06 at 19.02.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1263604/71d2fc5d-4dc9-7943-4727-af8d6156cc29.png">

このマーカーの数は今回で言うと、`_eventsList`の各日付のイベントの数に対応しています。
このマーカーは`CalendarBuilders`クラスの`markerBuilder`を使用してカスタマイズすることも可能です。
後ほど例を紹介します。


#イベントのリスト表示
次に実践することとして、タップした日付のイベントの詳細をカレンダーの下にリスト表示したいと思います。

```dart:calendar.dart
//省略
return Scaffold(
      appBar: AppBar(
        title: Text('calendar sample'),
      ),
      body: Column(
        children: [
          TableCalendar(
            firstDay: DateTime.utc(2020, 1, 1),
            lastDay: DateTime.utc(2030, 12, 31),
            focusedDay: _focusedDay,
            eventLoader: _getEventForDay,
            calendarFormat: _calendarFormat,
            onFormatChanged: (format) {
              if (_calendarFormat != format) {
                setState(() {
                  _calendarFormat = format;
                });
              }
            },
            selectedDayPredicate: (day) {
              return isSameDay(_selectedDay, day);
            },
            onDaySelected: (selectedDay, focusedDay) {
              if (!isSameDay(_selectedDay, selectedDay)) {
                setState(() {
                  _selectedDay = selectedDay;
                  _focusedDay = focusedDay;
                });
                _getEventForDay(selectedDay);   //追記部分
              }
            },
            onPageChanged: (focusedDay) {
              _focusedDay = focusedDay;
            },
          ),
          //以下追記
          ListView(
            shrinkWrap: true,
            children: _getEventForDay(_selectedDay!)
                .map((event) => ListTile(
                      title: Text(event.toString()),
                    ))
                .toList(),
          )
        ],
      ),
    );
```

`onDaySelected`内で`eventLoader`で定義したものと同じメソッドを定義します。
さらに同じメソッドを`ListView`を使ってリスト表示させます。
すると、タップした日付のイベントがカレンダーの下にリスト表示されました。

![Videotogif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1263604/9487dece-b673-917b-ec34-e8bbb671618b.gif)

#言語対応
table_calendarでは言語対応もされており、日本語もあります。
設定方法も簡単でした。

はじめに`pubspec.yaml`で追加した

```
intl: ^0.17.0
```
が別途必要になります。

`main()`を以下のように書き換えます。

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

`locale`で設定できます。


```dart:
TableCalendar(
            locale: 'ja_JP',   //追記部分
            firstDay: DateTime.utc(2020, 1, 1),
            lastDay: DateTime.utc(2030, 12, 31),
            focusedDay: _focusedDay,
```



年月の部分と曜日が日本語になりました。
<img width="350" alt="Simulator Screen Shot - iPhone 12 Pro Max - 2021-05-06 at 11.08.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1263604/498bddb0-f1d7-add5-c935-653eba6287b3.png">

#カスタマイズ例
table_calendarパッケージでは好みに合わせてカレンダーのデザインを細かくカスタマイズすることができます。
今回はその一例を紹介します。

カレンダーのマーカーを数字で表示してみました。

```dart:

//TableCalendar内
calendarBuilders: CalendarBuilders(
        markerBuilder: (context, date, events) {
          if (events.isNotEmpty) {
            return _buildEventsMarker(date, events);
          }
        },
       

Widget _buildEventsMarker(DateTime date, List events) {
  return Positioned(
    right: 5,
    bottom: 5,
    child: AnimatedContainer(
      duration: const Duration(milliseconds: 300),
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        color: Colors.red[300],
      ),
      width: 16.0,
      height: 16.0,
      child: Center(
        child: Text(
          '${events.length}',
          style: TextStyle().copyWith(
            color: Colors.white,
            fontSize: 12.0,
          ),
        ),
      ),
    ),
  );
}
```
上記コードでイベントの数を数字で表示することができました。

![スクリーンショット 2021-05-07 午後18.08.42 午後.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1263604/bdca355e-7729-0a57-08e0-660cfb157939.png)

こちらの写真では`calendarBuilders`以外に、
`daysOfWeekStyle`、`calendarStyle`、`headerStyle`、を使ってカスタマイズしています。

table_calendar libraryを参考にしました。
https://pub.dev/documentation/table_calendar/latest/table_calendar/table_calendar-library.html#classes

#終わりに
記事の中で間違った情報などあればコメントで指摘していただけますと幸いです。

今回のコードです。
https://github.com/Ryota-Nakamura-317/table_calendar_sample