📆

table_calendarとFirestoreでアプリを作ってみた

2023/08/30に公開

日付ごとにデータを保存する機能を作る

table_calendarとローカルDBでメモアプリを作ってみたが、FireStoreでもやって見たいと思い作って見ました。

Flutter Webだとこんな感じですね

必要なパッケージを追加する
これだけあればカレンダーアプリが作れます。
https://pub.dev/packages/table_calendar
https://pub.dev/packages/firebase_core
https://pub.dev/packages/cloud_firestore

追加・表示・削除をするページ

複雑なことをしなければ多くのコードを書かなくてもカレンダーアプリを作れました。
このようになっております。

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

class CalendarPage extends StatefulWidget {
  const CalendarPage({Key? key}) : super(key: key);

  
  State<CalendarPage> createState() => _CalendarPageState();
}

class _CalendarPageState extends State<CalendarPage> {
  final CalendarFormat _calendarFormat = CalendarFormat.month;
  DateTime _focusedDay = DateTime.now();
  DateTime? _selectedDay;

  
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: false, // キーボードが出てきても画面が崩れないようにする
      appBar: AppBar(
        title: const Text('Calendar'),
        actions: [
          IconButton(
            onPressed: () async {
              try {
                // ダイアログを出して入力する
                final result = await showDialog<String>(
                  context: context,
                  builder: (context) {
                    final TextEditingController controller =
                        TextEditingController();
                    return AlertDialog(
                      title: const Text('メモを入力してください'),
                      content: TextField(
                        controller: controller,
                      ),
                      actions: [
                        TextButton(
                          onPressed: () {
                            Navigator.pop(context);
                          },
                          child: const Text('キャンセル'),
                        ),
                        TextButton(
                          onPressed: () async {
                            await FirebaseFirestore.instance
                                .collection('calendar')
                                .add({
                              'date': Timestamp.fromDate(_selectedDay!),
                              'memo': controller.text,
                            });
                            if (mounted) {
                              Navigator.pop(context);
                            }
                          },
                          child: const Text('OK'),
                        ),
                      ],
                    );
                  },
                );
              } catch (e) {
                print("Error adding document: $e");
              }
            },
            icon: const Icon(Icons.add),
          ),
        ],
      ),
      body: Column(
        children: [
          TableCalendar(
            focusedDay: _focusedDay, // どの日付を選択したか
            firstDay: DateTime(1990), // 最初に利用可能な日付
            lastDay: DateTime(2050), // 最後に利用可能な日付
            calendarFormat: _calendarFormat,
            // カレンダーウィジェットに以下のコードを追加すると、ユーザーのタップに反応し、
            // タップされた日を選択されたようにマークします
            selectedDayPredicate: (day) =>
                isSameDay(_selectedDay, day), // 選択された日付をマークする
            onDaySelected: (selectedDay, focusedDay) {
              // 日付が選択されたときに呼び出される
              _focusedDay = focusedDay;
              _selectedDay = selectedDay;
              setState(() {});
            },
          ),
          // StreamBuilderを使って、Firestoreのcalendarコレクションのデータを取得する
          StreamBuilder<QuerySnapshot>(
            stream:
                FirebaseFirestore.instance.collection('calendar').snapshots(),
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                // calendarコレクションのデータを取得する
                final List<QueryDocumentSnapshot> documents =
                    snapshot.data!.docs;

                // 選択された日付と一致するドキュメントだけをフィルタリング
                final List<QueryDocumentSnapshot> filteredDocuments =
                    documents.where((doc) {
                  final date = (doc['date'] as Timestamp).toDate();
                  return isSameDay(_selectedDay, date);
                }).toList();

                // calendarコレクションのデータを日付順に並び替える
                filteredDocuments
                    .sort((a, b) => a['date'].compareTo(b['date']));

                return Expanded(
                  child: ListView.builder(
                    itemCount: filteredDocuments.length,
                    itemBuilder: (context, index) {
                      // calendarコレクションのデータを取得する
                      final document = filteredDocuments[index];
                      // calendarコレクションのデータをDateTime型に変換する
                      final date = (document['date'] as Timestamp).toDate();
                      // calendarコレクションのデータを表示する
                      return ListTile(
                        trailing: IconButton(
                          onPressed: () async {
                            await FirebaseFirestore.instance
                                .collection('calendar')
                                .doc(document.id)
                                .delete();
                          },
                          icon: const Icon(Icons.delete),
                        ),
                        title: Text(document['memo']),
                        subtitle:
                            Text('${date.year}/${date.month}/${date.day}'),
                      );
                    },
                  ),
                );
              }
              return const Center(child: CircularProgressIndicator());
            },
          ),
        ],
      ),
    );
  }
}

main.dartでimportして実行する

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:kenty_app/calendar/calender_page.dart';
import 'package:kenty_app/firebase_options.dart';

Future<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(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const CalendarPage()
    );
  }
}

iOSで操作するとこんな感じです
これはまだ削除機能つけてない時に撮った写真です。削除機能は追加してます。

カレンダーのUI

日付をタップして、日にちを選択する。
日付ごとに保存されているデータを表示できます。

AppBar右上の + ボタンを押すとダイアログが出てきて、追加ができます。

データはこんな感じで追加されてます。

最後に

簡単だけど、難しいカレンダーアプリを作って見ました。以前作成した別のコードもご紹介しておきます。
日続けのところにデータの数だけ黒いまるが出てきます。

import 'dart:collection';

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

import 'firebase_options.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late Map<DateTime, List<dynamic>> _eventsList;
  late DateTime _focused;
  DateTime? _selected;
  late CollectionReference _collectionRef;

  
  void initState() {
    super.initState();

    _selected = DateTime.now();
    _focused = DateTime.now();
    _eventsList = {};
    _collectionRef = FirebaseFirestore.instance.collection('events');
    _collectionRef.snapshots().listen((snapshot) {
      setState(() {
        _eventsList = {};
        snapshot.docs.forEach((doc) {
          Map<String, dynamic> data = doc.data() as Map<String, dynamic>;
          DateTime date = (data['date'] as Timestamp).toDate();
          String title = data['title'];
          if (_eventsList[date] == null) {
            _eventsList[date] = [title];
          } else {
            _eventsList[date]!.add(title);
          }
        });
      });
    });
  }

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

  
  Widget build(BuildContext context) {
    final _events = LinkedHashMap<DateTime, List<dynamic>>(
      equals: isSameDay,
      hashCode: getHashCode,
    )..addAll(_eventsList);

    List<dynamic> getEvent(DateTime day) {
      return _events[day] ?? [];
    }

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Column(
        children: [
          TableCalendar(
            firstDay: DateTime.utc(2022, 4, 1),
            lastDay: DateTime.utc(2025, 12, 31),
            eventLoader: getEvent,
            selectedDayPredicate: (day) {
              return isSameDay(_selected!, day);
            },
            onDaySelected: (selected, focused) {
              if (!isSameDay(_selected!, selected)) {
                setState(() {
                  _selected = selected;
                  _focused = focused;
                });
              }
            },
            focusedDay: _focused,
          ),
          Expanded(
            child: ListView.builder(
              shrinkWrap: true,
              itemCount: getEvent(_selected!).length,
              itemBuilder: (BuildContext context, int index) {
                return ListTile(
                  title: Text(getEvent(_selected!)[index]),
                );
              },
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _showAddDialog(context);
        },
        child: Icon(Icons.add),
      ),
    );
  }

  Future<void> _showAddDialog(BuildContext context) async {
    String? title = await showDialog<String>(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text('Add Event'),
          content: TextField(),
          actions: <Widget>[
            TextButton(
              child: Text('CANCEL'),
              onPressed: () => Navigator.pop(context),
            ),
            TextButton(
              child: Text('ADD'),
              onPressed: () {
                Navigator.pop(context, 'New Event');
              },
            ),
          ],
        );
      },
    );
    if (title != null) {
      setState(() {
        if (_eventsList[_selected!] == null) {
          _eventsList[_selected!] = [title];
        } else {
          _eventsList[_selected!]!.add(title);
        }
      });
      await _collectionRef.add({
        'date': _selected,
        'title': title,
        'created_at': Timestamp.now(),
      });
    }
  }
}

Discussion