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

## はじめに
Flutterを使ってネイティブアプリを作っているが、Widgetの使い方やFirestoreとのデータ連携の仕方など、コードの書き方が分からずそれなりに苦労している。
一旦分かってしまえばかなり高速でアプリ開発ができそうなので、使い方やコードの書き方のメモを残しておく。
今回はタイトルにある通り、`Firestore`と連携したリアルタイム更新のやり方ついてメモを残す。
※Flutter2をインストールした開発環境では、一部動かない部分があったため、修正を加えた記事も投稿した。[**【FlutterでFirestoreと連携したリアルタイム更新（Mac M1）】**](https://qiita.com/smiler5617/items/1ba0dfcead4516c643a2)

## Flutterの実行環境

- Ubuntu 18.04LTS（GCP上）
- Flutter 1.22.6
- Dart 2.10.5
- Android Studio 4.1.2
- VScode 1.53.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)
 ※Firebaseへの接続が上手く行かない場合、こちらの記事を参考に。
　[**FlutterからCloud Firestoreのデータ取得 ＆ データ書き込み**](https://qiita.com/smiler5617/items/8dcf720f5477a5f4b7d7)


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

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

void main() {
 runApp(App());
}

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

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

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

  // ボタンクリック時のアクション内容
  set_data(){

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

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

    setState(() {});
  }

 @override
 Widget build(BuildContext context) {

    return MaterialApp(
      home: Scaffold(

        // Stateの更新時に、Widgetが構築される
        body: FutureBuilder<QuerySnapshot>(
          future: Firestore.instance.collection('TestCollection').getDocuments(),
          builder:(context, snapshot) {
            if (snapshot.hasData) {

              // List<DocumentSnapshot>`をsnapshotから取り出す。
              fire_documents = snapshot.data.documents;

              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 Error'));
            }
          },
        ),

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

しかし、上記のコードだと別の誰かが`Firestore`に書き込みをして中身が変わっていても、Stateが更新されない限りは反映されない。
そこで、リアルタイムに反映するために使えるのが`StreamBuilder`。
`StreamBuilder`を使ったコードを書いてみると以下の様なコードになる。

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

void main() {
 runApp(App());
}

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

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

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

  // ボタンクリック時のアクション内容
  set_data(){
    add_number = (fire_documents.length + 1).toString();
    add_title = 'title' '$add_number';

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

  @override
   Widget build(BuildContext context) {

    return MaterialApp(
      home: Scaffold(

        // リアルタイム更新  監視先は指定のコレクション全体
        body: StreamBuilder<QuerySnapshot>(
          stream: Firestore.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");
            }

            // user_id[array型]の中に自分のIDが含まれているドキュメントのみ取得されている
            fire_documents = snapshot.data.documents;

            // 中身のリスト表示部分
            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(),
          ),
        ),
    ),);
  }
}
```
上記のコードは、画面上はあまり変化がない様に見えるが、試しに`Firestore`にコンソールなどからデータを直接入力してみると、すぐに反映されるはず。