やるぜ！Flutter！結局FirebaseだよFirestore編

（これまでのあらすじ）
Flutterのアプリを開発するため、環境構築からCRUDまでいろいろな手順をQiitaに投稿してきたが、アプリ開発にはFirebaseの力が必要であることを知った。Firebaseの力を得るべく環境構築をした俺たちの前に、最強の敵が立ちふさがる！

やるぜ！Flutter！とか言ってるくせに、前回「Firebase王に、俺はなる！」とか言って、完全に迷走気味だ。人気の落ちた少年漫画みたい。ONE PEACE見習え。

てなわけでFirestoreだ。
新しく色々するのも面倒だから、[前に作ったFlutterのCRUDアプリ](https://qiita.com/apricotcomic/items/1ef423088c5f67dd0ae4)を基に、データベースの部分をFirestoreに置き換えてみるよ。

一応言っとくと、環境はこんな感じだ
Windows11
VSCode
Flutter3.0.5

# 1.Firestoreって、結局何？
色んな所でいろいろ解説されているから、その中から自分が納得できる説明を探してね（投げやり）。
おじいちゃん技術者からすると、汎用機のツリー型若しくはネットワーク型のデータベースみたいな感じに見える。ツリー型とかは逆引きとかできないけどね。でも、そう思うとおじいちゃんはとっつきやすいよ。
【昔のツリー型DB概念図】
![firecloud_drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137858/60d3c9f3-b967-c7b0-7e84-4ec1e6164bad.png)
ほら似てる。
おじいちゃんはこのままツリー型DB風の考え方で押し通すよ。ふぉっふぉっふぉ。

# 2.Firestoreを使えるようにしよう
それでは、さっそくFirestoreを使えるようにしてみよう。
作成したプロジェクトの画面から、Firestoreを開こう。
左のメニューや下のアイコンなど、いろんなところから飛べるよ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137858/5981a67d-065a-0637-2b5e-e48ecbdfc898.png)
下にスクロールすると、Firestoreが出てくるよ。これをクリックだ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137858/a0d986c1-c0fe-4e37-5889-5f233325dbda.png)

Firestoreは現状こんな画面 in 2022.08.16
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137858/c2a59ed8-0587-df99-9031-92e68ba603ad.png)
データベースの作成をぽちっとな。
そうすると、いきなり高難度なことを聞かれるよ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137858/ce2e2fe7-30c4-6d30-5f33-8661d158f92e.png)
おいおい、本番環境モードってなんだよ？テストモードってなんだよ？
検索していろいろなページ見ても、今ひとつピンとこない。
つい何でもありなテストモードに逃げたくなるが、逃げちゃダメだ逃げちゃだめだ逃げちゃ駄目だ。先延ばししたって、30日後には何かしないとアクセスできなくなる。設定の仕方は後で確認するとして、ここは本番環境モードを選択だ。
そうすると、次にロケーション選択画面になる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137858/b9e4ec98-2d86-e692-ecb3-18ba93c9bdd6.png)
wow、ここはわーるどわいどにusか？なんて馬鹿なことは言わず、日本人なら日本に近いところにしておいたほうがどう考えたっていろいろ早いでしょ。ユーザがusの人が多いなら別だけど。
[ドキュメント](https://firebase.google.com/docs/firestore/locations?hl=ja)によると、東京は「asia-northeast1」らしい。それなら迷わずこいつを選択だ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137858/bd5f7943-1976-d111-99f9-f59902be0add.png)
ちなみに「asia-northeast2」は大阪やで。浪速っ子はこっち使いや。
有効にするをクリックすると、Firestoreの画面が開いたよ。ok, I got it!
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137858/ced936e1-9dd2-1ba1-b093-7b8e73746964.png)
これでFirestoreが使えるようになったよ。

# 3.ルールをざっくり設定しよう
さて、まずはさっき謎のままにしていた本番環境モードで設定必須となる、ルールを設定してみよう。
ルールのタブをクリックすると、もうなんか入ってる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137858/f91af629-c60e-72a8-f9dc-81972f525eeb.png)
[公式のルールのページ](https://firebase.google.com/docs/firestore/security/get-started?hl=ja#deny-all)を見ると、今入っている設定って「すべて拒否」となっているみたいだ。なるほど、これをなんちゃらすると、かんちゃらされるわけだな（意味不明）。
そのなんちゃらをする方法ってどこ見たら書いてある？いろいろ探したら[ここ](https://firebase.google.com/docs/rules/rules-language?hl=ja)にあった。
とりあえず何でもアクセスOKにするなら、デフォルトの設定のfalseをtrueにすればよいとな。

```
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
// 下の行の「false」を「true」にすればフルアクセス可能
      allow read, write: if false;
    }
  }
}
```

そうするとテストモードと変わらなくなっちゃうんだろうけど、まだコレクション作ってないし、とりあえずオールオッケーな状態にしておくことにする。コレクション作ったら、個別に権限考えよう。
編集すると、画面が変わったよ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137858/1218eaeb-b73a-3446-757a-c20396ceb9f7.png)
直し終わったら、公開をクリックして編集内容を保存しよう。
反映されるまでに多少タイムラグがあるらしい。膝の上に手を置いて、じっと待つのだ。
まあ、待っていたって「反映されました！」なんてメッセージは出てこないけどね。気持ちの問題だ。

# 4.アプリからFirestoreにアクセスする
アプリからFirestoreにアクセスできるようにするには、以下の対応が必要だそうだ。
①Flutterの中でFirestoreを使えるようにする
③Firebaseにプロジェクトを登録する
③アプリに初期設定の処理を入れる
こうするとFirestoreが使えるようになるらしい。
順番に設定していくよ。
## 4.1　最初の設定
最初にFlutterの中でFirestoreを使えるように設定しよう。
### (1)Firebase cliをインストールする
Firebaseのコマンドラインツールをインストールする。これは1回インストールしておけば、この後新規プロジェクトを追加してもそのまま使えるよ。
```cmd
npm install -g firebase-tools
```
尚、このコマンドは管理者モードで実行しないと、途中のmkdirで落ちるっぽい。VSCodeのterminalでちゃらっと実行するとはまるので注意。俺のことだ。

### (2)FIrebaeにログインしてテストする
コマンドラインからFirebaseにログインできるかテストします。
```cmd
firebase login
```
ブラウザでFirebaseのプロジェクト開いたままこのコマンド入れると、「もうつながっているぜこるぁ！」と巻き舌で指摘されるが、気にしないでいいよ。テストはOKってことだ。

### (3)最新のCLIのバージョンに更新する。
[公式](https://firebase.google.com/docs/cli?hl=ja#sign-in-test-cli)を見ると、ログインのテストをした後最新のバージョンの更新しろと言っている。その方法は！同じコマンドを打て！と。
まじか。本当にそれでいいのか？
言われるがままにやってみましょう。
…正直に言うと、さっきのインストールコマンドをもう一度実行したらやたら早く終わり、その後ログインコマンドもバージョンコマンドもエラーになってしまった。そこでもう一回インストールしたら元に戻った。バージョンも変わらなかった。謎だ。
とりあえず良しとしよう。

### (4)FlutterFire CLIをインストールする
次はFlutter用のFirebaseのCLIをインストールする。
```
dart pub global activate flutterfire_cli
```
これは管理者でなくても動いたよ。
ここまで一番最初にやっておく。一度やったらもうやらなくていいよ。多分。

## 4.2　プロジェクトの設定
次は、プロジェクト単位に行う初期設定だ。
ここでは省略しているけど、普通通りFlutterのプロジェクトを作成しておいてね。
中身は空で大丈夫だ。
terminalでプロジェクトのフォルダに移動して、次のコマンドを打つよ。
```
flutterfire configure
```
そうしたら、作ったプロジェクトが表示されるから、そいつを選んでenterする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137858/75f44411-fdeb-511a-472e-912411e6644b.png)
そうすると、今度はOSを選べって言われる。上下キーで動かしてスペースキーを押すと、選択したOSのチェックがついたり外れたりするよ。Flutterはandroid,ios,webにチェックがついた状態になっているので、ターゲットにしたいOSを選んでENTERだ。今回は全部付でやってみるよ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137858/0fcefff2-d748-58f1-df85-442aa2aa9389.png)
ENTERすると、なんかいろいろいいようにやってくれるようだよ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137858/c032a70c-65cb-33d7-cebe-846c756f32c0.png)
なんかコンプリートしたらしい。よしゃ。
プロジェクトの中を見てみると、「firebase_options.dart」というソースが追加されているよ。

## 4.3 アプリに初期設定の処理を入れる

### (1)firebase_coreをインストールする
さっき追加されたfirebase_options.dartはエラーになっている。ソースを開くと、firebase_core/firebase_core.dartがないと言っている。そりゃそうだ、入れてないし。なので以下のコマンドでインストールします。
```
flutter pub add firebase_core
```
なんかインストールされたら、もう一度flutterfire configureを入れろ、と公式には書いてある。またか。まあ、言われた通りにするさ。

あ、さっきと同じにコマンド実行したら、今度はfirebase_options.dartがエラーになってないよ。よしよし。
### (2)Firebase初期化コマンドを発行する
Firebaseを使うには、main.dartでirebaseの初期化を行うコマンドを発行しないといけないらしい。どうやってやるんだ？
[このサイト](https://flutternyumon.com/how-to-call-firebase-initializeapp/)を参考にさせていただきました。ありがとうございます。
mainのすぐ後に、初期化の処理を入れればいいらしい。
```dart:main.dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}
```

### (3)cloud_firestoreを追加する
最期に、cloud_firestoreをプロジェクトに追加する。
```
flutter pub add cloud_firestore
```
追加したら、flutter runして再ビルドしておくよ。
場合によってはminSdkVersionが古い、ってエラーになることがあるよ。そのときはbuild.gradleに「minSdkVersion xx」（xxはエラーメッセージに表示されたバージョンの数字以上の数字)を追加したら直るかも。
```gradle:build.gradle
 defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId "com.example.apricotcomicdemo"
        minSdkVersion flutter.minSdkVersion
        minSdkVersion 23  // これを追加
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }
```
こんな感じ。

# 5 アプリをFirestore対応に変更する

## 5.1 どんな構成にしたらいいの？
FirestoreはNoSQLなのでテーブルという概念がない。最初に言っていた、昔のtree型DBと比較するとこんな感じの絵になるのかな。
【FirestoreとTree型DBとの対比】
![forestoreとtreeの比較.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137858/06d751d8-e625-c396-e14a-ffa0704317bf.png)
RDBで考えたい人は、ドキュメントとかSEGがテーブルに当たる、って思うと少しわかりやすいかも。
【FirestoreとRDBの対比】
![firestoreとRDBの比較.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137858/af70c8d7-2d95-4187-4272-e7194a98e7dc.png)
で、ドキュメント=セグメント=テーブルの中にデータ=項目=項目（おんなじか）が設定されるんだね。
では、前にSQLiteで作ったテーブルを、Firestoreに置き換えてみよう。
前に作ったテーブルはこんな感じ。
catsテーブル
|No|項目名|属性|
|-:|:-|:-|
|1|id|integer|
|2|名前|String|
|3|性別|String|
|4|誕生日|String|
|5|メモ|String|
|6|createdAt|DateTime|

これをFirestoreに入れたいのだが。
当初のアプリはスマホ内に持っているテーブル（SQLite）に対してアクセスしていたので、そのスマホのユーザしかテーブルにアクセスすることはなかった。でも、Firestoreだと複数ユーザが同じコレクションを参照するので、自分のデータは自分だけが見られるように工夫しておかないといけない。そうでないと、世界中の猫情報見放題になってしまう。それはそれで楽しそうだが。
なので、構造を少し変えてみます。
![cats変換イメージdrawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137858/c8c8b390-da36-3e7c-f240-b04fcfd08eb7.png)
catsテーブルはcatsドキュメントにそのまま移行するけど、その上にusersドキュメントを作成して、ログインしたユーザの猫情報だけアクセス可能になるようにします。

## 5.2 定義ってどうするの？
で、この定義ってどこれすればいい？
なんとFirestoreでは、定義を明示的に設定しなくていい。というか定義を設定する画面がない。コレクションの内容を確認したり、データを登録できる画面はあるが、あらかじめ定義をいれるところはない。何も設定しないでも、アプリからデータを追加したらそのまま登録されるそうな。便利な世の中になったもんだ。
じゃあ、アプリの対応をしていこう。

## 5.3 アプリを魔改造しよう
冒頭にも書いた通り、[前に作ったFlutterのCRUDアプリ](https://qiita.com/apricotcomic/items/1ef423088c5f67dd0ae4)をFirestore用に改造していくよ。改造方針は以下の通りだ。
・main.dartにFirebase初期化処理を入れる
・cats.dart（catsテーブルのmodelだね）をFirestore用に変更する
・db_helper.dartをFirestore用に変更する
・cat_list.dart、cat_detail.dart、cat_detail_edit.dartでこれまでテーブルアクセスしていたところをFirestoreにアクセスする処理に変更する。
では、順番に見ていこう。
### (1) main.dart
Firebaseの初期化は、4.3(2)に書いた通りだ。実際に入れると、こんな風になるよ。

```dart:main.dart
import 'package:flutter/material.dart';
import 'package:apricotcomicdemo/view/cat_list.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();   //Firebase初期化処理　ここから
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );                                           //Firebase初期化処理　ここまで
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(                    //初期画面の設定
      title: '猫一覧',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      routes: <String, WidgetBuilder>{
        '/': (_) => const CatList(),       //cat_list.dartを呼び出し
      },
    );
  }
}
```
main.dartの修正はこれだけ。
### (2)cats.dart
基本的に考え方は変わらない。fromJson,toJsonにしていたところを、[公式に書いてある](https://firebase.google.com/docs/firestore/query-data/get-data?hl=ja&authuser=1)ように、fromFirestore,toFirestoreに変更した。
あと、copyは不要になったので削除した。
ちなみにあえてファイル名はcats→firestore_catsに変更しています。

```dart:firestore_cats.dart
import 'package:cloud_firestore/cloud_firestore.dart';

// catsテーブルの定義
class Cats {
  String id;       //←SQLiteの時はintだったが、Firestoreでは特に使わないのでStringに変更
  String name;
  String gender;
  String birthday;
  String memo;
  DateTime? createdAt;  //←SQLiteでは自動で設定されたが、Firestoreではそういう機能はなく、自力で設定する

  Cats({
    required this.id,
    required this.name,
    required this.gender,
    required this.birthday,
    required this.memo,
    required this.createdAt,
  });
// ↓公式ドキュメントを参考に、以下の処理を追加
  factory Cats.fromFirestore(
    DocumentSnapshot<Map<String, dynamic>> snapshot,
    SnapshotOptions? options,
  ) {
    final data = snapshot.data();
    return Cats(
        id: data?['id'],
        name: data?['name'],
        gender: data?['gender'],
        birthday: data?['birthday'],
        memo: data?['memo'],
        createdAt: data?['createdAt'].toDate());
  }

  Map<String, dynamic> toFirestore() {
    return {
      "id": id,
      "name": name,
      "gender": gender,
      "birthday": birthday,
      "memo": memo,
      "createdAt": createdAt,
    };
  }
//↑追加ここまで
// copy,fromJson,toJsonは削除
}
```
### (3)db_helper.dart
当然だけど、ここが一番大きく変わります。
大きな変更点は下記のとおり。
・テーブルのカラム名の設定は削除しました。
・データベースのオープン、テーブルがないときのcreate処理は削除しました。collectionやdocがなくてもFirestoreは大丈夫です。
・insertとupdateが同じ実装で処理できるので、firestore_helperではinsertしか実装していません
これも、変更後のソースにコメント入れて説明するね。
```dart:firestore_helper.dart
import 'package:apricotcomicdemo/model/firestore_cats.dart'; //←変更したmodelに変更
import 'package:cloud_firestore/cloud_firestore.dart'; //←追加します

// catsテーブルへのアクセスをまとめたクラス
class FirestoreHelper {
  // DbHelperをinstance化する
  static final FirestoreHelper instance = FirestoreHelper._createInstance();

  FirestoreHelper._createInstance();

  // catsテーブルのデータを全件取得する
  selectAllCats(String userId) async {
    final db = FirebaseFirestore.instance;
  //↓catsコレクションにあるdocを全て取得する
  //　この時、fromFirestore、toFirestoreを使ってデータ変換する
    final snapshot =
        db.collection("users").doc(userId).collection("cats").withConverter(
              fromFirestore: Cats.fromFirestore,
              toFirestore: (Cats cats, _) => cats.toFirestore(),
            );
    final cats = await snapshot.get();
    return cats.docs;
  }

// nameをキーにして1件のデータを読み込む
// ※catsのキーはidでなくnameに変更している
  catData(String userId, String name) async {  
    final db = FirebaseFirestore.instance;
    final docRef = db
        .collection("users")
        .doc(userId)
        .collection("cats")
        .doc(name)
        .withConverter(
          fromFirestore: Cats.fromFirestore,
          toFirestore: (Cats cats, _) => cats.toFirestore(),
        );
    final catdata = await docRef.get();
    final cat = catdata.data();
    return cat;
  }

// データをinsertする
// ※updateも同じ処理で行うことができるので、共用している
  Future insert(Cats cats, String userId) async {  
    final db = FirebaseFirestore.instance;
    final docRef = db
        .collection("users")
        .doc(userId)
        .collection("cats")
        .doc(cats.name)
        .withConverter(
          fromFirestore: Cats.fromFirestore,
          toFirestore: (Cats cats, options) => cats.toFirestore(),
        );
    await docRef.set(cats);
  }

// データを削除する
  Future delete(String userId, String name) {
    final db = FirebaseFirestore.instance;
    return db
        .collection("users")
        .doc(userId)
        .collection("cats")
        .doc(name)
        .delete();
  }
}
```
Firestoreは、取得してきたデータの型がQuerysnapshotとかDocumentsnapshotとか、Firestoreならではの型で返される。これをMapにしたりListにしたりしないと普通に処理できない。
FutureBuilderとかStreamBuilderを使えばもっと楽に処理できるかも？だけど、今回はSQLite用の処理を魔改造することにスポットを当てているので、敢えてこの形にしたよ。
自動更新したいときはStreamを使わないとならないから、それはまた今度考えることにするよ。
何はともあれ、firestore_helperは以上だ。

### (4)cat_list.dart
今回usersコレクションの中にcatsコレクションを作成しているが、userコレクションのキーは本来ログインユーザなんだろうなー、と考えています。が、今回認証までやるのは面倒なので、usersコレクションのキーは固定で決めちゃったよ。そのうち認証処理を実装したら変更するけど、今回はこれで見逃してくれ。

```dart:cat_list.dart
import 'package:cloud_firestore/cloud_firestore.dart'; //←追加
import 'package:flutter/material.dart';
import 'package:apricotcomicdemo/model/firestore_cats.dart';　//←cats.dartを変更
import 'package:apricotcomicdemo/model/firestore_helper.dart'; //←db_helper.dartを変更
import 'package:apricotcomicdemo/view/cat_detail.dart';
import 'package:apricotcomicdemo/view/cat_detail_edit.dart';

// catテーブルの内容全件を一覧表示するクラス
class CatList extends StatefulWidget {
  const CatList({Key? key}) : super(key: key);

  @override
  _CatListPageState createState() => _CatListPageState();
}

class _CatListPageState extends State<CatList> {
  List<DocumentSnapshot> catSnapshot = []; //←firestore_helperからの戻り値の型がDocumentsnapshotなので、それを受ける項目を追加
  List<Cats> catList = []; //catsテーブルの全件を保有する
  bool isLoading = false;
  static const String userId = 'test@apricotcomic.com'; //仮のユーザID。認証機能を実装したら、本物のIDに変更する。

  @override
  void initState() {
    super.initState();
    getCatsList();
  }

// catsテーブルに登録されている全データを取ってくる
  Future getCatsList() async {
    setState(() => isLoading = true);
    catSnapshot = await FirestoreHelper.instance
        .selectAllCats(userId); //←users配下のcatsコレクションのドキュメントを全件読み込む
    catList = catSnapshot //←受け取ったDocumentsnapshotの値をListに変換する
        .map((doc) => Cats(
            id: doc['id'],
            name: doc['name'],
            gender: doc['gender'],
            birthday: doc['birthday'],
            memo: doc['memo'],
            createdAt: doc['createdAt'].toDate()))  //←dartのdatetime型をfirestoreのtimestamp型に変換するため.toDateを追加
        .toList();
    setState(() => isLoading = false);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('猫一覧')),
      body: isLoading
          ? const Center(
              child: CircularProgressIndicator(),
            )
          : SizedBox(
              child: ListView.builder(
                itemCount: catList.length,
                itemBuilder: (BuildContext context, int index) {
                  final cat = catList[index]; //←List変換しているので、SQLiteの時の処理を変更なしでそのまま使える
                  return Card(
                    child: InkWell(
                      child: Padding(
                        padding: const EdgeInsets.all(15.0),
                          child: Row(
                            children: <Widget>[
                              Container(
                                width: 80,height: 80,
                                decoration: const BoxDecoration(
                                  shape: BoxShape.circle,
                                  image: DecorationImage(
                                    fit: BoxFit.fill,
                                    image: AssetImage('assets/icon/dora.png')
                                  )
                                )
                              ),
                              Text(cat.name,style: const TextStyle(fontSize: 30),), 
                            ]
                          ),
                      ),
                      onTap: () async {
                        await Navigator.of(context).push(
                          MaterialPageRoute(
                            builder: (context) => CatDetail(userId: userId,name: cat.name),
                          ),
                        );
                        getCatsList();
                      },
                    ),
                  );
                },
              ),
            ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () async {
          await Navigator.of(context).push(
            MaterialPageRoute(
              builder: (context) => const CatDetailEdit(userId: userId, cats: null,) //←CatDetailEditに渡す値にuserIdを追加。これをキーにusersコレクションにアクセスする
            ),
          );
          getCatsList();
        },
      ),
    );
  }
}
```
### (5)cat_detail.dart
userIdをもらってくるところとdb_helperをfirestore_helperに変更したところ以外は変更なしでいけたよ。

```dart:cat_detail.dart
import 'package:flutter/material.dart';
import 'package:apricotcomicdemo/model/firestore_cats.dart'; //←catsを変更
import 'package:apricotcomicdemo/model/firestore_helper.dart'; //←db_helperを変更
import 'package:apricotcomicdemo/view/cat_detail_edit.dart';

// catsテーブルの中の1件のデータに対する操作を行うクラス
class CatDetail extends StatefulWidget {
  final String userId;  //←追加
  final String name;  //←catsコレクションのキーをnameにしたので、idからnameに変更した

  const CatDetail({Key? key, required this.userId, required this.name}) //←userIdを追加、idをnameに変更
      : super(key: key);

  @override
  _CatDetailState createState() => _CatDetailState();
}

class _CatDetailState extends State<CatDetail> {
  late Cats cats;
  bool isLoading = false;
  static const int textExpandedFlex = 1;
  static const int dataExpandedFlex = 4;

  @override
  void initState() {
    super.initState();
    catData();
  }

// catsコレクションから指定されたnameのデータを1件取得する
  Future catData() async {
    setState(() => isLoading = true);
    cats = await FirestoreHelper.instance.catData(widget.userId, widget.name); //←firestore_helperに変更。userIdとnameを渡すよう変更
    setState(() => isLoading = false);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('猫詳細'),
        actions: [
          IconButton(
            onPressed: () async {
              await Navigator.of(context).push(
                MaterialPageRoute(
                  builder: (context) => CatDetailEdit(
                    cats: cats,
                    userId: widget.userId, //←userIdを追加
                  ),
                ),
              );
              catData();
            },
            icon: const Icon(Icons.edit),
          ),
          IconButton(
            onPressed: () async {
              await FirestoreHelper.instance
                  .delete(widget.userId, widget.name); //←firestore_helperに変更。渡userId,nameを渡すよう変更
              Navigator.of(context).pop();
            },
            icon: const Icon(Icons.delete),
          )
        ],
      ),
      body: isLoading
          ? const Center(
              child: CircularProgressIndicator(),
            )
          : Column(
              children :[
                Container(
                  width: 80,height: 80,
                  decoration: const BoxDecoration(
                    shape: BoxShape.circle,
                    image: DecorationImage(
                      fit: BoxFit.fill,
                      image: AssetImage('assets/icon/dora.png')
                    )
                  )
                ),
                  Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      Row(children: [
                        const Expanded(
                          flex: textExpandedFlex,
                          child: Text('名前',
                            textAlign: TextAlign.center,
                          ),
                        ),
                        Expanded(
                          flex: dataExpandedFlex,
                          child: Container(
                            padding: const EdgeInsets.all(5.0),
                            child: Text(cats.name),
                          ),
                        ),
                      ],),
                      Row(children: [
                        const Expanded(
                          flex: textExpandedFlex,
                          child: Text('性別',
                            textAlign: TextAlign.center,
                          ),
                        ),
                        Expanded(
                          flex: dataExpandedFlex,
                          child: Container(
                            padding: const EdgeInsets.all(5.0),
                            child: Text(cats.gender),
                          ),
                        ),
                      ],),
                      Row(children: [
                        const Expanded(
                          flex: textExpandedFlex,
                          child: Text('誕生日',
                            textAlign: TextAlign.center,
                          ),
                        ),
                        Expanded(
                          flex: dataExpandedFlex,
                          child: Container(
                            padding: const EdgeInsets.all(5.0),
                            child: Text(cats.birthday),
                          ),
                        )
                      ],),
                      Row(children: [
                        const Expanded(
                          flex: textExpandedFlex,
                          child: Text('メモ',
                            textAlign: TextAlign.center,
                          )
                        ),
                        Expanded(
                          flex: dataExpandedFlex,
                          child: Container(
                            padding: const EdgeInsets.all(5.0),
                            child: Text(cats.memo),
                          ),
                        ),
                      ],),
                    ],
                  ),
              ],
          )
    );
  }
}
```
### (6)cat_detail_edit.dart
これも基本的にはdb_helperをfirestore_helperに変更したところを直しただけ。
```dart:cat_detail_edit.dart
import 'package:flutter/material.dart';
import 'package:apricotcomicdemo/model/firestore_cats.dart'; //←catsを変更
import 'package:apricotcomicdemo/model/firestore_helper.dart'; //←db_helperを変更

class CatDetailEdit extends StatefulWidget {
  final String userId; //←追加
  final Cats? cats;

  const CatDetailEdit({Key? key,required this.userId, this.cats}) : super(key: key); //←userIdも受け取るよう変更

  @override
  _CatDetailEditState createState() => _CatDetailEditState();
}

class _CatDetailEditState extends State<CatDetailEdit> {
  late String id; //←Stringに変更しました。根拠はない。数字だと面倒だから。
  late String name;
  late String birthday;
  late String gender;
  late String memo;
  late DateTime createdAt;
  final List<String> _list = <String>['男の子', '女の子', '不明'];
  late String _selected;
  String value = '不明';
  static const int textExpandedFlex = 1;
  static const int dataExpandedFlex = 4;

  @override
  void initState() {
    super.initState();
    id = widget.cats?.id ?? ''; //←Stringに変更したので、初期値も変更
    name = widget.cats?.name ?? '';
    birthday = widget.cats?.birthday ?? '';
    gender = widget.cats?.gender ?? '';
    _selected = widget.cats?.gender ?? '不明';
    memo = widget.cats?.memo ?? '';
    createdAt = widget.cats?.createdAt ?? DateTime.now();
  }

  void _onChanged(String? value) {
    setState(() {
      _selected = value!;
      gender = _selected;
    });
  }

// 詳細編集画面を表示する
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('猫編集'),
        actions: [
          buildSaveButton(),
        ],
      ),
      body: SingleChildScrollView(
        child: Column(children: <Widget>[
          Row(children: [
            // 名前の行の設定
            const Expanded(
              flex: textExpandedFlex,
              child: Text('名前',
                textAlign: TextAlign.center,
              ), 
            ),
            Expanded(
              flex: dataExpandedFlex,
              child: TextFormField(
                maxLines: 1,
                initialValue: name,
                decoration: const InputDecoration(
                  hintText: '名前を入力してください',
                ),
                validator: (name) => name != null && name.isEmpty
                    ? '名前は必ず入れてね'
                    : null,
                onChanged: (name) => setState(() => this.name = name),
              ),
            ),
          ]),
          Row(children: [
            const Expanded(
              flex: textExpandedFlex,
              child: Text('性別',
                textAlign: TextAlign.center,
              ),
            ),
            Expanded(
              flex: dataExpandedFlex,
              child: DropdownButton(
                items: _list.map<DropdownMenuItem<String>>((String value) {
                  return DropdownMenuItem(
                    value: value,
                    child: Text(value),
                  );
                }).toList(),
                value: _selected,
                onChanged: _onChanged,
              ),
            ),
          ]),
          Row(children: [
            const Expanded(
              flex: textExpandedFlex,
              child: Text('誕生日',
                textAlign: TextAlign.center,
              ),
            ),
            Expanded(
              flex: dataExpandedFlex,
              child: TextFormField(
                maxLines: 1,
                initialValue: birthday,
                decoration: const InputDecoration(
                  hintText: '誕生日を入力してください',
                ),
                onChanged: (birthday) =>
                    setState(() => this.birthday = birthday),
              ),
            ),
          ]),
          Row(children: [
            const Expanded(
              flex: textExpandedFlex,
              child: Text('メモ',
                textAlign: TextAlign.center,
                )
            ),
            Expanded(
              flex: dataExpandedFlex,
              child: TextFormField(
                maxLines: 1,
                initialValue: memo,
                decoration: const InputDecoration(
                  hintText: 'メモを入力してください',
                ),
                onChanged: (memo) => setState(() => this.memo = memo),
              ),
            ),
          ]),
        ]),
      ),
    );
  }

  Widget buildSaveButton() {
    final isFormValid = name.isNotEmpty;

    return Padding(
      padding: const EdgeInsets.all(10.0),
      child: ElevatedButton(
        child: const Text('保存'),
        style: ElevatedButton.styleFrom(
          onPrimary: Colors.white,
          primary: isFormValid ? Colors.redAccent : Colors.grey.shade700,
        ),
        onPressed: createOrUpdateCat,
      ),
    );
  }

  void createOrUpdateCat() async {
    final isUpdate = (widget.cats != null);

    if (isUpdate) {
      await updateCat();
    } else {
      await createCat();
    }

    Navigator.of(context).pop();
  }

  // 更新処理の呼び出し
  Future updateCat() async {
    final cat = Cats( //←画面から項目をもってきていたが、渡されたCatsからセットするよう変更
      id: id, //←値はないけど、一応追加
      name: name,
      birthday: birthday,
      gender: gender,
      memo: memo, 
      createdAt: createdAt,
    );

    await FirestoreHelper.instance.insert(cat, widget.userId); //←userIdを追加
  }

  // 追加処理の呼び出し
  Future createCat() async {
    final cat = Cats(
      id: id, //←値はないけど、一応追加
      name: name,
      birthday: birthday,
      gender: gender,
      memo: memo,
      createdAt: createdAt,
    );
    await FirestoreHelper.instance.insert(cat, widget.userId);  //←userIdを追加
  }
}
```
# 6.完成したよ！
5.で書いたソースで全部だから、コピペすれば動くけど、フォルダ構成とか下記の前提なので、デフォルトで足りないところは自力で作ってね。
```
《プロジェクトフォルダ》
 ├ assets ─ icon ─ dora.png　←一覧画面のアイコン画像。画像ファイルは自力で何とかしてね
 └ lib ┬ model ┬ firestore_cats.dart　←modelフォルダ追加
       │       └ firestore_helper.dart
       ├ view  ┬ cat_detail.dart　←viewフォルダ追加
       │       ├ cat_detail_edit.dart
       │       └ cat_kist.dart
       ├ firebase_options.dart ←自動追加される
       ├ generated_plugin_registrant.dart ←自動追加される
       └ main.dart
```
読んでくるときの戻り値の扱いに異常に嵌ったけど、そこさえクリアできれば後は違和感なく変更出来たよ。
認証処理追加したり、Streamでリアルタイムに変更できるようにしたり、これからも夢は広がるね。
今回はこんなところで。トバヨ！