FlutterでFirebaseを使用するサンプル - Cloud Firesotre編

試した全機能を一つの記事にすると長くなりそうなので、機能ごとに分けたいと思います。
本記事は、`Cloud Firesotre`についての記事です。
他の記事は以下に掲載しています
* [Firebase Authentication](https://qiita.com/MLLB/items/490c343d07901c55c086)
* Cloud Functions(Comming soon)
* Firebase Cloud Messaging(Comming soon)
* Cloud Storage(Comming soon)
* Cloud Analytics(Comming soon)

# はじめに
<!-- 発端や概要を記載 -->
`Flutter`で`Firebase`を使用する勉強をしたので、、
サンプルアプリ的な感じで試せるコードを書いていきたいと思います。
目次の上から順にサンプルコードをコピペ実装していただければ動作すると思います。

`Firebase`の環境構築と`Firebase`の各機能の環境構築方法については、この記事では書きません。
環境構築はして、ドキュメントを読みながら試そうと思ったけど、
サンプルアプリ的な感じで動作を試すのに、コード書くのめんどくさいなって方や、
ドキュメントで使い方の説明書いてあるけど、実際どう使えばいいかよくわからないと挫折した方向けになります。
UIとセットで簡単に`Firebase`の機能を試せるようにするというのが本記事の目的ですので、
勉強がてらさくっと試してみたい方に、コードのコピペで試せるように書いていきたいと思います。
またサンプルコードのポイントも解説していますので、ドキュメントで挫折した方の助けにもなるかと思います。

本記事では、`Cloud Firesotre`の機能のうち以下を試せるサンプルコードを掲載します。
* データの取得
* データの追加
* データの更新
* データの削除

# 目次
<!-- タイトルとアンカー名を編集 -->
1. [データとAndorid側のエラーについて](#データとAndorid側のエラーについて)
1. [データの取得](#データの取得)
1. [データの追加](#データの追加)
1. [データの更新](#データの更新)
1. [データの削除](#データの削除)

<!-- 各チャプター -->
<a id="#データとAndorid側のエラーについて"></a>
# データとAndorid側のエラーについて
## ○使用するデータの構造
下記のデータをもとにコードを書いておりますので、
コレクション名やフィールド名が異なる場合は、コードの対象となる箇所を読み替えてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/2620e518-c34b-495a-1537-0e8706a0db90.png)

## ○Android側のエラーについて
サンプルでは、cloud_firestore: ^3.5.1を使用していますが、
こちらのバージョンでAndoridで動かした場合、エラーとなる可能性があります。
対処方法は、minSdkVersionを19以上に設定することなのですが、
`minSdkVersion`を19にして動かすと、`dex file cannot exceed 64K`というエラーが出ました。(これは私の環境のみかもしれません)
エラーと対処法については以下の記事がわかりやすかったです
https://majintools.com/2019/08/06/dexfile/

上記を踏まえて、ビルド時にエラーとなる場合
`android/app/build.gradle`を以下のように編集してみて下さい。
```gradle
・・・
android {
    ・・・
    defaultConfig {
        // ここを19以上にする
        // 21にしているのは21以降の場合は「multiDexEnabled true」の設定を追加するだけで済むため
        // 詳しくは上記のエラーと対処法の記事を参照
        minSdkVersion 21
        // 以下を追加する
        multiDexEnabled true
        ・・・
    }
    
    ・・・
}

・・・

```
<a id="#データの取得"></a>
# データの取得

データを一覧で取得し表示するサンプルコードは下記です。
後述するデータの追加、データの編集ページのコードがないとエラーになるのでご注意ください。
```dart
// ToDoのデータモデル
class ToDoData {
  final String id;
  final String name;

  ToDoData(this.id, this.name);
}

// ToDo一覧を表示するページ
class ToDoListPage extends StatelessWidget {
  const ToDoListPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("ToDo List"),
      ),
      body: SafeArea(
        child: StreamBuilder(
            stream: FirebaseFirestore.instance.collection('todos').snapshots(),
            builder:
                (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
              if (snapshot.hasError) {
                return Text(snapshot.error.toString());
              }

              if (snapshot.connectionState == ConnectionState.waiting) {
                return const CircularProgressIndicator();
              }

              final docs = snapshot.data!.docs;
              return ListView.builder(
                itemBuilder: (context, index){
                  final doc = docs[index];
                  final data = doc.data()! as Map<String, dynamic>;
                  return Dismissible(
                    key: Key(doc.id),
                    onDismissed: (DismissDirection direction) async {
                      // remove
                      await FirebaseFirestore.instance
                          .collection('todos')
                          .doc(doc.id)
                          .delete();
                    },
                    background: Container(
                      color: Colors.red,
                    ),
                    child: ListTile(
                      onTap: () {
                        Navigator.push(
                          context,
                          MaterialPageRoute(
                            builder: (context) => EditToDoPage(toDoData: ToDoData(doc.id, data['name'])),
                          ),
                        );
                      },
                      title: Text(data['name']),
                    ),
                  );
                },
                itemCount: docs.length,
              );
            }),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () {
          Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) => const AddToDoPage(),
            ),
          );
        },
      ),
    );
  }
}
```

## ポイント解説
### データ一覧をStreamで購読する
データの追加・編集・削除に合わせて、UIを自動更新させるため、
StreamBuilderを使ってデータの変更に合わせてUIを描画するようにしています。
Streamは`FirebaseFirestore.instance.collection('todos').snapshots()`で取得できます。
'todos'には、対象のコレクション名をしてしてください。

### 取得したデータを読み取る
StreamBuilderの中では、取得した`snapshot`の状態を確認し、データがあれば表示します。
データは、`final docs = snapshot.data!.docs;`で取得できます。
公式の名称を使うと、上記のコードでドキュメント一覧が取得できます。
MySQLなどのデータベースで例えると、コレクションがテーブルで、ドキュメントがRow(行)に相当するものだと思います。
(違ってたらご指摘お願いします)

ドキュメントの中身を取得する方法は下記になります。
(サンプルコードでいうと、ListViewBuilderのitemBuilderの冒頭）
```dart
final doc = docs[index];                  
final data = doc.data()! as Map<String, dynamic>;
```

ここで取得したデータはキーとバリューのペアで情報が格納されているので、
`data['name']`とすれば中身が取得できます。
(キーの内容は、Firebaseで作成したデータによって異なりますので適宜変更してください)
加えて、**ドキュメントのIDを取得したい場合は`doc.id`で取得できます。**

<a id="#データの追加"></a>
# データの追加

データを追加するサンプルコードは以下です。
```dart
class AddToDoPage extends StatefulWidget {
  const AddToDoPage({Key? key}) : super(key: key);

  @override
  State<AddToDoPage> createState() => _AddToDoPageState();
}

class _AddToDoPageState extends State<AddToDoPage> {
  final TextEditingController _nameController = TextEditingController();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Add ToDo"),
      ),
      body: SafeArea(
        child: Column(
          children: [
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: TextField(
                controller: _nameController,
                decoration: const InputDecoration(
                  hintText: "name",
                  border: OutlineInputBorder(),
                ),
              ),
            ),
            ElevatedButton(
              onPressed: () async {
                try {
                  await FirebaseFirestore.instance
                      .collection('todos')
                      .add({
                    'name': _nameController.text,
                  });
                  if(!mounted) return;
                  Navigator.of(context).pop();
                } catch(e){
                  print(e.toString());
                }
              },
              child: const Text("Save"),
            ),
          ],
        ),
      ),
    );
  }
}
```

ポイントはボタン押下時のメソッド内です。
下記でデータを追加できます。
'todos'と`add`の中身については、適宜ご自分の作成したコレクションに合わせて変更してください。
`await FirebaseFirestore.instance.collection('todos').add({'name': _nameController.text,});`


<a id="#データの更新"></a>
# データの更新
データを更新するサンプルコードは以下です。
```dart
class EditToDoPage extends StatefulWidget {
  final ToDoData toDoData;
  const EditToDoPage({Key? key, required this.toDoData}) : super(key: key);

  @override
  State<EditToDoPage> createState() => _EditToDoPageState();
}

class _EditToDoPageState extends State<EditToDoPage> {
  late TextEditingController _nameController;

  @override
  void initState() {
    _nameController = TextEditingController(text: widget.toDoData.name);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Edit ToDo"),
      ),
      body: SafeArea(
        child: Column(
          children: [
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: TextField(
                controller: _nameController,
                decoration: const InputDecoration(
                  hintText: "name",
                  border: OutlineInputBorder(),
                ),
              ),
            ),
            ElevatedButton(
              onPressed: () async {
                try {
                  FirebaseFirestore.instance
                      .collection('todos')
                      .doc(widget.toDoData.id)
                      .update({'name': _nameController.text});
                  if(!mounted) return;
                  Navigator.of(context).pop();
                } catch(e){
                  print(e.toString());
                }
              },
              child: const Text("Save"),
            ),
          ],
        ),
      ),
    );
  }
}
```

ポイントはボタン押下時のメソッド内です。
下記でデータを追加できます。
'todos'と`update`の中身については、適宜ご自分の作成したコレクションに合わせて変更してください。
`await FirebaseFirestore.instance.collection('todos').doc(widget.toDoData.id).update({'name': _nameController.text,});`

<a id="#データの削除"></a>
# データの削除
サンプルコードは「データの取得」の章と同じなので割愛します。
実際に削除しているポイントは、`Dismissible`ウィジェットのonDismissed内です。
`await FirebaseFirestore.instance.collection('todos').doc(doc.id).delete();`で削除できます。

# さいごに
Cloud Firesotreについては以上です。
まとめると以下になります。

Stream取得
`FirebaseFirestore.instance.collection('todos').snapshots()`

Stream内でドキュメント取得
`final docs = snapshot.data!.docs;`
`final doc = docs[index];`

ドキュメントのIDと中身を取得
`final id = doc.id;`
`final data = doc.data()! as Map<String, dynamic>;`
`final name = data['name'];`

ドキュメント追加・更新・削除
`FirebaseFirestore.instance.collection('todos').add({'name': "add",});`
`FirebaseFirestore.instance.collection('todos').doc(widget.toDoData.id).update({'name': "update",});`
`FirebaseFirestore.instance.collection('todos').doc(doc.id).delete();`