FlutterでFirestoreと連携したリアルタイム更新（Mac M1）

## はじめに
以前にFlutterからFirestoreを利用する方法まとめたが、今回Macでそれを試したら、Flutterやパッケージのバージョンが上がっていたため動かなかった。
大した変更ではないが、一応整理し直したのでメモとして残しておく。
以前の記事はこちら。[【FlutterでFirestoreと連携したリアルタイム更新】](https://qiita.com/smiler5617/items/ca2317c6bb1922e6c0b9)

## 実行環境
【PC】
　MacBook Air (M1, 2020)

【各SWバージョン】
　・macOS Big Sur 11.6.1
　・Flutter 2.5.3　（dart 2.14.4）
　・Xcode 13.1
　・Cocoapods 1.11.2
  ・VScode(AplleSilicon) 1.62.2

【パッケージ】
　・firebase_core: ^1.10.0
　・cloud_firestore: ^3.1.0
　　
## メモ内容
試しに作るものは以前と同じ。
事前準備として、`Firestore`に**TestCollection**というコレクションを作成しておき、その中に`title`というフィールドを持つドキュメントを１つだけ作成しておく。
![Qiita-No033_img01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654767/9d9e407e-a09e-a7cb-a3bf-7309429de807.jpeg)

リアルタイム更新のやり方を書く前に、まずは、リアルタイムではなくボタンをクリックして、データを追加した後に画面を更新するやり方について。`setState()`を利用して状態を更新する様なプログラム。

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

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(App());
}

class App extends StatefulWidget {
  @override
  _AppPage createState() => _AppPage();
}

class _AppPage extends State<App> {
  // ボタンクリックの挙動で使う変数
  var insert_data;
  int doc_num = 0;
  late String add_number;
  late String add_title;

  // Firestoreからのデータを格納しておく変数
  List<DocumentSnapshot> fire_documents = [];

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        // Stateの更新時に、Widgetが構築される
        body: FutureBuilder<QuerySnapshot>(
          future: FirebaseFirestore.instance.collection('TestCollection').get(),
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              // List<DocumentSnapshot>`をsnapshotから取り出す。
              if (snapshot.data != null) {
                fire_documents = snapshot.data!.docs;
              }

              return ListView.builder(
                shrinkWrap: true,
                itemCount: fire_documents.length, //配列の長さの分だけ作成する。
                itemBuilder: (context, index) {
                  return ListTile(
                    title: Text(fire_documents[index]["title"]),
                  );
                },
              );
            } else if (snapshot.hasError) {
              return Center(child: Text('snapshot Error1'));
            } else {
              return Center(child: Text('snapshot Error2'));
            }
          },
        ),

        // 追加ボタン
        floatingActionButton: Container(
          margin: EdgeInsets.only(bottom: 10.0), // ボタンの配置
          //width: 40.0, // ボタンのサイズ。形にも依るが先に記載されている大きさが優先っぽい。
          //height: 40.0,

          child: FloatingActionButton.extended(
            backgroundColor: Colors.blue,
            icon: Icon(Icons.add),
            label: Text("追加"),

            // テーマ追加ボタンクリック時の処理 ⇒ ダイアログ立ち上げる
            onPressed: () => set_data(),
          ),
        ),
      ),
    );
  }

  // ボタンクリック時のアクション内容
  set_data() {
    if (fire_documents.length == null) {
      doc_num = 0;
    } else {
      doc_num = fire_documents.length;
    }

    add_number = (doc_num + 1).toString();
    add_title = 'title' '$add_number';

    insert_data = {
      'title': add_title,
    };
    FirebaseFirestore.instance.collection('TestCollection').add(insert_data);

    setState(() {});
  }
}
```

**以前との変更点は以下７つ。**
・`null`が入り得る変数に`late`、`?`、`!`の宣言。どの条件分岐でも`return`が返る様に修正。
　※Flutter2がサポートしているDart2.12からNullを許容しなくなった(null safety)ので、明示的に書く必要がある。
・`import 'package:firebase_core/firebase_core.dart';`の追加
・`runApp()`の前に以下を２つの行を追加。
　　□`WidgetsFlutterBinding.ensureInitialized();`
　　□`await Firebase.initializeApp();`
・`Firestore`のメソッド名を`FirebaseFirestore`に変更。
・Firestoreからのドキュメント取得を `.getDocuments()`ではなく、`.get()`に変更。
・ドキュメント内のデータ取得も `snapshot.documents`ではなく、`snapshot.docs`に変更。


上記のコードは、右下のFloatingボタンをクリックすると、Firestoreにデータが追加されて`setState()`することで再描画される仕組みになっている。 ※細かな作り込みはしてないので悪しからず。。。

しかし、上記のコードだと別の誰かが`Firestore`に書き込みをして中身が変わっていても、Stateが更新されない限りは反映されない。

そこで、リアルタイムに反映するために使えるのが`StreamBuilder`。
`StreamBuilder`を使ったコードを書いてみると以下の様なコードになる。

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

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(App());
}

class App extends StatefulWidget {
  @override
  _AppPage createState() => _AppPage();
}

class _AppPage extends State<App> {
  // ボタンクリックの挙動で使う変数
  var insert_data;
  int doc_num = 0;
  late String add_number;
  late String add_title;

  // Firestoreからのデータを格納しておく変数
  List<DocumentSnapshot> fire_documents = [];

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        // リアルタイム更新  監視先は指定のコレクション全体
        body: StreamBuilder<QuerySnapshot>(
            stream: FirebaseFirestore.instance
                .collection('TestCollection')
                .snapshots(),
            builder:
                (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
              if (snapshot.hasError) {
                return Text('Something went wrong');
              }

              if (snapshot.connectionState == ConnectionState.waiting) {
                return Text("Loading");
              }

              // List<DocumentSnapshot>`をsnapshotから取り出す。
              if (snapshot.data != null) {
                fire_documents = snapshot.data!.docs;
              }

              // 中身のリスト表示部分
              return ListView.builder(
                // padding: const EdgeInsets.all(8),
                shrinkWrap: true,
                itemCount: fire_documents.length, //配列の長さの分だけ作成する。
                itemBuilder: (context, index) {
                  return ListTile(
                    title: Text(fire_documents[index]["title"]),
                  );
                },
              );
            }),

        // 追加ボタン
        floatingActionButton: Container(
          margin: EdgeInsets.only(bottom: 10.0), // ボタンの配置
          //width: 40.0, // ボタンのサイズ。形にも依るが先に記載されている大きさが優先っぽい。
          //height: 40.0,

          child: FloatingActionButton.extended(
            backgroundColor: Colors.blue,
            icon: Icon(Icons.add),
            label: Text("追加"),

            // テーマ追加ボタンクリック時の処理 ⇒ ダイアログ立ち上げる
            onPressed: () => set_data(),
          ),
        ),
      ),
    );
  }

  // ボタンクリック時のアクション内容
  set_data() {
    if (fire_documents.length == null) {
      doc_num = 0;
    } else {
      doc_num = fire_documents.length;
    }

    add_number = (doc_num + 1).toString();
    add_title = 'title' '$add_number';

    insert_data = {
      'title': add_title,
    };
    FirebaseFirestore.instance.collection('TestCollection').add(insert_data);

    setState(() {});
  }
}
```
**修正箇所は前半のコードとほとんど同じようなところ。**

上記のコードは、画面上はあまり変化がない様に見えるが、試しに`Firestore`にコンソールなどからデータを直接入力してみると、すぐに反映されるはず。

（とりあえず動く様に修正したが、ベストプラクティスとは程遠い気がするので、また機会があれば綺麗な書き方も考えてみようと思う。）