📆

table_calendar + cloud_firestore

2024/06/10に公開

対象者

  • Flutterでカレンダーアプリを作りたい
  • Cloud Firestoreと組み合わせたい
  • 動くもの見てみたい

公式のコードを参考に作ってみました。
https://github.com/aleksanderwozniak/table_calendar/blob/master/example/lib/utils.dart
https://github.com/aleksanderwozniak/table_calendar/blob/master/example/lib/pages/events_example.dart

こんなものを作りました。データが保存されている日には、黒丸がつきます。日付をタップすると保存されているデータを確認することができます。


プロジェクトの説明

とある人が、健康管理アプリを作ってみたいと言っていた。カレンダーアプリを作るときは、Map<Key, Value>、List<T>の知識が必要だったりします。チュートリアルを何度かやりましたが難しかったです😅

まずはこんな感じで、Cloud Firestoreにダミーデータを作っておいてください。

コード綺麗ではないですが💦

モデルクラスと、Firestoreからデータを全て取得するメソッド、時間を扱うロジックがあるコードを作成します。

  • やること
  • データを全て取得
  • for in でループする
  • Mapに、.addAllで追加する。
import 'dart:collection';

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:table_calendar/table_calendar.dart';

class Event {
  final String? uuid;
  final String? memo;
  final Timestamp? createdAt;

  // toJson
  Map<String, dynamic> toJson() => {
        'uuid': uuid,
        'memo': memo,
        'createdAt': createdAt,
      };

  // fromJson
  Event.fromJson(Map<String, dynamic> json)
      : uuid = json['uuid'] as String?,
        memo = json['memo'] as String?,
        createdAt = json['createdAt'] as Timestamp?;
}

class EventUtils {
  final db = FirebaseFirestore.instance;

  // event collectionから全データを取得
  Future<List<Event>> getEvents() async {
    final snapshot = await db.collection('event').get();
    return snapshot.docs.map((doc) => Event.fromJson(doc.data())).toList();
  }

  // event collectionから全データを取得し、日付ごとにグループ化
  Future<LinkedHashMap<DateTime, List<Event>>> fetchEvents() async {
    // event collectionから全データを取得
    final events = await getEvents();
    // eventMapに日付ごとにグループ化
    final eventMap = <DateTime, List<Event>>{};
    // for文でeventsを回して、eventMapに日付ごとにグループ化
    for (final event in events) {
      // event.createdAtをDateTime型に変換
      final eventDate = event.createdAt!.toDate();
      // eventMapに日付ごとにグループ化
      if (eventMap[eventDate] == null) {
        // eventMapにeventDateがない場合は、[event]をリストで追加
        eventMap[eventDate] = [event];
      } else {
        // eventMapにeventDateがある場合は、.add(event)でリストに追加
        eventMap[eventDate]!.add(event);
      }
    }
    // LinkedHashMap<DateTime, List<Event>>を返す
    return LinkedHashMap<DateTime, List<Event>>(
      equals: isSameDay,
      hashCode: getHashCode,
    )..addAll(eventMap); // eventMapを返す
  }
}

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

List<DateTime> daysInRange(DateTime first, DateTime last) {
  final dayCount = last.difference(first).inDays + 1;
  return List.generate(
    dayCount,
    (index) => DateTime.utc(first.year, first.month, first.day + index),
  );
}

final kToday = DateTime.now();
final kFirstDay = DateTime(kToday.year, kToday.month - 3, kToday.day);
final kLastDay = DateTime(kToday.year, kToday.month + 3, kToday.day);

カレンダーのUIのロジック

日付をタップするイベントがあるので、日付が青く点灯したり、リストに値が表示されます。複雑なことをしているので、これはおそらくStatefulWidgetでないと難しいそう。

import 'package:flutter/material.dart';
import 'package:flutter_table_calendart/utils/event_utils.dart';
import 'package:table_calendar/table_calendar.dart';

class CalendarScreen extends StatefulWidget {
  const CalendarScreen({super.key});

  
  _CalendarScreenState createState() => _CalendarScreenState();
}

class _CalendarScreenState extends State<CalendarScreen> {
  // カレンダーの最初の日付
  Map<DateTime, List<Event>> _events = {};
  // カレンダーの最後の日付
  List<Event> _selectedEvents = [];
  // 選択された日付
  DateTime _selectedDay = DateTime.now();

  
  void initState() {
    super.initState();
    // _fetchEventsを呼び出す
    _fetchEvents();
  }

  // イベントを取得する
  Future<void> _fetchEvents() async {
    try {
      // EventUtilsをインスタンス化
      final eventUtils = EventUtils();
      // fetchEventsを呼び出し、取得したデータをfetchedEventsに代入
      final fetchedEvents = await eventUtils.fetchEvents();
      setState(() {
        // _eventsにfetchedEventsを代入
        _events = fetchedEvents;
        // _selectedEventsに_events[_selectedDay]を代入
        // _selectedDayが_eventsにない場合は、[]を代入
        // _events[] のデータ型はList<Event>なので、List<Event>を代入
        _selectedEvents = _events[_selectedDay] ?? [];
      });
    } catch (e) {
      debugPrint('Failed to fetch events: $e');
    }
  }

  void _onDaySelected(DateTime selectedDay, DateTime focusedDay) {
    setState(() {
      // _selectedDayにselectedDayを代入
      _selectedDay = selectedDay;
      // _selectedEventsに_events[_selectedDay]を代入
      // _selectedDayが_eventsにない場合は、[]を代入
      // _events[] のデータ型はList<Event>なので、List<Event>を代入
      _selectedEvents = _events[_selectedDay] ?? [];
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Calendar'),
      ),
      body: Column(
        children: [
          TableCalendar(
            // _eventsのキーを取得
            selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
            firstDay: kFirstDay, // カレンダーの最初の日付
            lastDay: kLastDay, // カレンダーの最後の日付
            focusedDay: DateTime.now(), // フォーカスされている日付
            eventLoader: (day) => _events[day] ?? [], // イベントを取得
            onDaySelected: _onDaySelected, // 日付が選択されたときの処理
          ),
          // 選択された日付のイベントを表示
          Expanded(
            child: ListView.builder(
              itemCount: _selectedEvents.length, // _selectedEventsの数だけリストを表示
              itemBuilder: (context, index) {
                final event =
                    _selectedEvents[index]; // _selectedEventsのindex番目のデータを取得
                return ListTile(
                  title: Text(event.memo ?? ''),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

main.dartでインポートして実行すればデモアプリを動かせます。

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_table_calendart/event_example/calendar_screen.dart';

import 'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  runApp(const MyApp());
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const CalendarScreen(),
    );
  }
}

感想

カレンダーアプリを作ってみましたが、ロジックを考えるのは結構難しいです。ダミーのデータを使うだけでもDartのロジックを考えるので、詰まりました。カレンダーを使うとなると、HashMapだとか文法の知識がいるわけですが、パッケージのコードを使用する前提でやるときは、独特な文法を使うので、もっとハードル上がりました💦

カレンダー作るときは、Map, List, forを使う場面が多かったので、アルゴリズムへの深い理解が求められそうと思いました。

参考にしたもの
https://zenn.dev/joo_hashi/articles/cc7496e60d9e58
https://api.dart.dev/stable/3.4.3/dart-collection/LinkedHashMap-class.html

Discussion