【Flutter/Dart】Firebase/Firestoreを使ったToDoアプリを作ろう

# 1.はじめに
新たに6つのAndroidStudio&Flutter&Firebase記事を書いている。以下は内容、
- 【Flutter/Dart】Firebaseを使おう
https://qiita.com/my_programming/private/7b3251701183ff59b97c
- 【Flutter/Dart】Firebase Authenticationを使って認証アプリを作ろう
https://qiita.com/my_programming/private/2d81229942bc0d61da6b
- 【Flutter/Dart】Firebase/Firestoreを使ったチャットアプリを作ろう
https://qiita.com/my_programming/private/ad0f7a5f7637a1a37842
- **【Flutter/Dart】Firebase/Firestoreを使ったToDoアプリを作ろう**
- 【Flutter/Dart】Firebase/HostingでWebアプリを外部公開しよう
https://qiita.com/my_programming/private/5950b7e27161dc9ec838
- 【Flutter/Dart】Firebase/Firestoreを使ったBlogアプリを作ろう
https://qiita.com/my_programming/private/6f8515dca825e55d0f07

ToDoアプリをFireStoreを使ってデータを追加、表示、編集、削除できる様にすることでCRUDの実装方法を学ぶ。

# 2.ToDoアプリをCRUD化しよう
以下２つを今回もそのまま利用する。
- 認証アプリのために作ったFirebaseのプロジェクト
- チャットアプリのために作ったCloud Firestoreのデータベース

そのため、classpathをandroid/build.gradleの中にコピーするところから、applicationIdを"com.example.my_auth_app"に書き換えるところまでの手続きはチャットアプリと同じなので説明を省く。

lib/main.dartにチャット用のコードを記述。
``` .dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'ToDo App',
      home: ToDoPage(),
    );
  }
}

class ToDoPage extends StatefulWidget {
  const ToDoPage({Key? key}) : super(key: key);
  @override
  _ToDoPageState createState() => _ToDoPageState();
}

class _ToDoPageState extends State<ToDoPage> {
  final TextEditingController _todoController = TextEditingController();
  final TextEditingController _contentController = TextEditingController();

  final CollectionReference _todos =
  FirebaseFirestore.instance.collection('todo');
  // Functions used for adding and editing
  Future<void> _add_or_update([DocumentSnapshot? documentSnapshot]) async {
    String mode = 'addition';
    if (documentSnapshot != null) {
      mode = 'update';
      _todoController.text = documentSnapshot['todo'];
      _contentController.text = documentSnapshot['content'].toString();
    }

    await showModalBottomSheet(
        isScrollControlled: true,
        context: context,
        builder: (BuildContext ctx) {
          return Padding(
            padding: EdgeInsets.only(
                top: 20,
                left: 20,
                right: 20,
                // prevent the soft keyboard from covering text fields
                bottom: MediaQuery.of(ctx).viewInsets.bottom + 20),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                TextField(
                  controller: _todoController,
                  decoration: const InputDecoration(labelText: 'ToDo'),
                ),
                TextField(
                  keyboardType:
                  const TextInputType.numberWithOptions(decimal: true),
                  controller: _contentController,
                  decoration: const InputDecoration(
                    labelText: 'Content',
                  ),
                ),
                const SizedBox(
                  height: 20,
                ),
                ElevatedButton(
                  child: Text(mode== 'addition' ? 'Add' : 'Update'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.red,
                  ),
                  onPressed: () async {
                    final String? todo = _todoController.text;
                    final String? content = _contentController.text;
                    // Addiing process
                    if (todo != null && content != null) {
                      if (mode == 'addition') {
                        // Persist a new product to Firestore
                        await _todos.add({"todo": todo, "content": content});

                        // Show the snack bar for the add
                        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                            backgroundColor: Colors.red,
                            content: Text(
                              'Added ${todo}!',
                              style: const TextStyle(
                                  fontWeight: FontWeight.bold, fontSize: 15, color: Colors.white),
                            )));
                      }
                      // Editing process
                      if (mode == 'update') {
                        // Update the product
                        await _todos
                            .doc(documentSnapshot!.id)
                            .update({"todo": todo, "content": content});

                        // Show the snack bar for the edit
                        ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                            backgroundColor: Colors.red,
                            content: Text(
                              'Updated Content!',
                              style: TextStyle(
                                  fontWeight: FontWeight.bold, fontSize: 15, color: Colors.white, backgroundColor: Colors.yellowAccent),
                            )));

                      }
                      // Clear the text fields
                      _todoController.text = '';
                      _contentController.text = '';

                      // Hide the bottom sheet
                      Navigator.of(context).pop();
                    }
                  },
                )
              ],
            ),
          );
        });
  }

  // Deletion Processing Functions
  Future<void> _deleteProduct(String productId) async {
    await _todos.doc(productId).delete();

    // Show the snack bar for the exclusion
    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
        backgroundColor: Colors.red,
        content: Text(
          'Deleted Content!',
          style: TextStyle(
              fontWeight: FontWeight.bold, fontSize: 15, color: Colors.black),
        )));
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        appBar: AppBar(
          title: const Text('ToDo App'),
          backgroundColor: Colors.red,
        ),
        // StreamBuilder to pass Firestore values to ListView.builder
        body: StreamBuilder(
          stream: _todos.snapshots(),
          builder: (context, AsyncSnapshot<QuerySnapshot> streamSnapshot) {
            if (streamSnapshot.hasData) {
              // Display FireStore values in list format
              return ListView.builder(
                itemCount: streamSnapshot.data!.docs.length,
                itemBuilder: (context, index) {
                  final DocumentSnapshot documentSnapshot =
                  streamSnapshot.data!.docs[index];
                  // View documents in the card widget
                  return Card(
                    margin: const EdgeInsets.all(10),
                    child: ListTile(
                      title: Text(documentSnapshot['todo']), // ToDo
                      subtitle: Text(documentSnapshot['content'].toString()), // Content
                      trailing: SizedBox(
                        width: 100,
                        child: Row(
                          children: [
                            // Edit Button
                            IconButton(
                                color: Colors.red,
                                icon: const Icon(Icons.edit),
                                onPressed: () =>
                                    _add_or_update(documentSnapshot)),
                            // Delete Button
                            IconButton(
                                color: Colors.red,
                                icon: const Icon(Icons.delete),
                                onPressed: () =>
                                    _deleteProduct(documentSnapshot.id)),
                          ],
                        ),
                      ),
                    ),
                  );
                },
              );
            }

            return const Center(
              child: CircularProgressIndicator(),
            );
          },
        ),
        // Add Button
        floatingActionButton: FloatingActionButton(
          backgroundColor: Colors.red,
          onPressed: () => _add_or_update(),
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}
```

プロジェクトを作る。
<img width="400" alt="step2_1_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/b5d05731-5856-8be8-f820-80717ca1c9be.png">

アプリを実行する。
<img width="200" alt="step2_1_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/8e3d5855-0f86-e970-bce0-08b706369bfd.png">

追加ボタンからToDoアプリなのでタスクを登録する。
<img width="200" alt="step2_1_3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/8b06c698-58ab-0daa-e3bd-fadd63569b4c.png">

タスクが追加される。
<img width="200" alt="step2_1_4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/834b0cb4-d9dc-a565-ad32-891a9175ba94.png">

タスクを編集する。
<img width="200" alt="step2_1_5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/c4382f0a-62dd-c875-6430-49b3dd7da807.png">

編集結果が反映される。
<img width="200" alt="step2_1_6.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/888f5734-987c-9896-9abe-1a0225762273.png">

Cloud Firestoreにもタスクが追加されている。
<img width="600" alt="step2_1_7.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/5cb2fe88-ca0c-2a59-75fb-5cdc45aa7e06.png">

タスクを更に追加する。
<img width="200" alt="step2_1_8.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/13e4f714-5345-b763-0323-8559b9db6109.png">

Cloud Firestoreにもタスクが更に追加されている。
<img width="600" alt="step2_1_9.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/fb2696da-3097-4fd7-88d7-91a9e8e817d4.png">

タスクを１つ削除して、新たにタスクを追加する。
<img width="200" alt="step2_1_10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/7c07a237-0dd1-ef9f-3284-681ef7066610.png">

Cloud Firestoreでも古いタスクが削除され新しいタスクが追加されている。
<img width="600" alt="step2_1_11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/ca87a08e-1720-8e86-3307-ad1c6ee74c55.png">

以上で、FlutterとFirebaseを用いたチャットアプリが完成した。

# 3.まとめ
FlutterとFireStoreを使ってデータを追加、表示、編集、削除（CRUD）する方法を学んだ。