Flutter table_calendarとRiverpod　カレンダーでタップした日を取得し色を付ける実装　

# はじめに
とりあえずできる画面はこんな感じです。

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">Flutterのtable_calendarを使ってみる <a href="https://t.co/scbyeu3XPE">pic.twitter.com/scbyeu3XPE</a></p>&mdash; kokogento (@gento34165638) <a href="https://twitter.com/gento34165638/status/1830976682716733646?ref_src=twsrc%5Etfw">September 3, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


**Riverpodの最初の設定部分は省いています**

# 必要パッケージを入れる
まずは必要なパッケージを入れます。
riverpodはいくつか種類ありますが`flutter_riverpod`を選びます。
日本語化のために`intl`も入れます。

[flutter_riverpod](https://pub.dev/packages/flutter_riverpod)
[table_calendar](https://pub.dev/packages/table_calendar)
[intl](https://pub.dev/packages/intl)


```yaml:pubspec.yaml
dev_dependencies:
  flutter_test:
    sdk: flutter

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^3.0.0
  flutter_riverpod: ^2.5.1
  table_calendar: ^3.1.2
  intl: ^0.19.0
  ```
  

# まずはカレンダーを表示させる

```dart:lib/views/pages/home_page.dart
import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class HomePage extends ConsumerWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {

    return Scaffold(
      body: TableCalendar(
        firstDay: DateTime.utc(2010, 10, 16),
        lastDay: DateTime.utc(2030, 3, 14),
        focusedDay: DateTime.now(),
      ),
    );
  }
}
```

とりあえずこれだけでカレンダー自体は表示されます。簡単ですね！

![Simulator Screenshot - iPhone SE (3rd generation) - 2024-09-03 at 22.47.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167220/89c83c0f-719d-06b8-2f4b-cf992600d33b.png)

`firstDay`と`lastDay`と`focusedDay`が必須プロパティなので、それだけあれば一旦OKです。

# Riverpodを使ってタップした日を選択する
さて、ここからが本題です。
実現したい動作は「**選択した日付を取得しアクティブにする**」です。

そのために、選択した日付をRiverpodで状態管理します。

## Providerを作る

```dart:lib/models/calendar.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

final selectedDateProvider = StateProvider<DateTime>((ref) => DateTime.now());

final focusedDateProvider = StateProvider<DateTime>((ref) => DateTime.now());
```
外部から変更可能な`StateProvider`を作ります。

カレンダーにはイベント関数`OnDaySelected`があり、それで2つの`DateTime`（selectedDayとfocusedDay）を受け取ります。

https://pub.dev/documentation/table_calendar/latest/table_calendar/OnDaySelected.html

## TableCalendarにonDaySelectedを追加

```dart:lib/views/pages/home_page.dart
class HomePage extends ConsumerWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final selectedDate = ref.watch(selectedDateProvider);
    final focusedDate = ref.watch(focusedDateProvider);

    return Scaffold(
      body: TableCalendar(
        firstDay: DateTime.utc(2010, 10, 16),
        lastDay: DateTime.utc(2030, 3, 14),
        focusedDay: focusedDate,
        currentDay: DateTime.now(),
        selectedDayPredicate: (day) {
          return isSameDay(selectedDate, day);
        },
        onDaySelected: (selectedDay, focusedDay) {
          print(selectedDay); // 選択した日がコンソールで表示
          ref.read(selectedDateProvider.notifier).state = selectedDay; // 選択した値に書き換え
          ref.read(focusedDateProvider.notifier).state = focusedDay; // 選択した値に書き換え
        },
      ),
    );
  }
}
```

これで日付を選択できるようになりました！:relieved:

## カレンダーのデザインを変更
最後にカレンダーのデザインを少し変更します。
色を管理する`ThemeData`の作成と、カレンダーを日本語化します。

### ThemeDataを作成

```dart:lib/theme.dart
import 'package:flutter/material.dart';

class AppTheme {
  static const Color lightGray = Color(0xFFF0F0F0);
  static const Color darkGray = Color(0xFFCCCCCC);

  static final ThemeData lightTheme = ThemeData(
    colorScheme: ColorScheme.fromSeed(
      brightness: Brightness.light,
      seedColor: const Color(0xFFF5BFC0),
      primary: const Color(0xFFF5BFC0),
    ),
    useMaterial3: true,
  );
}
```

### main.dartでThemeDataを設定 & 日本語化
私はgo_routerを使ってるので少しデフォルト違いますが、大体こんな感じです。

```dart:lib/main.dart
import 'package:flutter/material.dart';
import 'package:h_calender/router.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:h_calender/theme.dart';
import 'package:intl/date_symbol_data_local.dart';

void main() {
  MyApp.run();
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'カレンダー',
      routerConfig: router,
      theme: AppTheme.lightTheme, // AppThemeクラスを使用
    );
  }

  static Future<void> run() async {
    await initializeDateFormatting('ja_JP').then((_) {
      WidgetsFlutterBinding.ensureInitialized();
      runApp(const ProviderScope(
        child: MyApp(),
      ));
    });
  }
}
```

最終的なカレンダーのコードはこちら！

```dart:lib/views/pages/home_page.dart
import 'package:flutter/material.dart';
import 'package:h_calender/models/calendar.dart';
import 'package:h_calender/theme.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class HomePage extends ConsumerWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final selectedDate = ref.watch(selectedDateProvider);
    final focusedDate = ref.watch(focusedDateProvider);
    final colorScheme = Theme.of(context).colorScheme;

    return Scaffold(
      body: TableCalendar(
        firstDay: DateTime.utc(2010, 10, 16),
        lastDay: DateTime.utc(2030, 3, 14),
        focusedDay: focusedDate,
        currentDay: DateTime.now(),
        selectedDayPredicate: (day) {
          return isSameDay(selectedDate, day);
        },
        locale: 'ja_JP',
        headerStyle: const HeaderStyle(
          formatButtonVisible: false,
          titleCentered: true,
        ),
        calendarStyle: CalendarStyle(
          todayDecoration: const BoxDecoration(
            color: AppTheme.darkGray, // 今日の日付の背景色
            shape: BoxShape.circle, // 形を丸く
          ),
          selectedDecoration: BoxDecoration(
              shape: BoxShape.circle, color: colorScheme.primary // 選択した日付の背景色
              ),
          todayTextStyle: const TextStyle(
            color: Colors.white, // 今日の日付の文字色
          ),
          selectedTextStyle: const TextStyle(
            color: Colors.white, // 選択した日付の文字色
          ),
        ),
        onDaySelected: (selectedDay, focusedDay) {
          ref.read(selectedDateProvider.notifier).state = selectedDay;
          ref.read(focusedDateProvider.notifier).state = focusedDay;
        },
      ),
    );
  }
}
```

カレンダーのデザイン部分は肥大化するので別クラスで切り分けると良さそうですね！