Kotlin, LiveData, coroutine なんかを使って初めてのAndroidアプリを作る(13)Firebase Cloud Firestore使ってみた編

[Firebaseの導入(Analytics/Crashlytics)](https://qiita.com/kasa_le/items/560775076c3b2f00fcf7)、[Firebaseでのユーザー認証](https://qiita.com/kasa_le/items/0ec2b993cd52041f393c)とやってきて、ようやくFirebaseのCloud Firestoreへの保存をやります。

# 今回の目標
Firebase Cloud Firestoreに簡単なデータを保存し、取得できる。
また、ユーザー認証と組み合わせてセキュリティルールを設定する。

# Firebase Cloud Firestore概要

https://firebase.google.com/docs/firestore?hl=ja

Cloudと付いているとおり、クラウドへの保存です。まあつまりサーバーにデータがありますよってことです。
で、SQLiteのようなテーブルに絡むがあってSQLでクエリー発行して・・・というSQLデータベースではなく、NoSQLと言われています。

データ保存が出来るFirebaseの似たようなサービスに、Realtime Databaseというのがあります。
大きな違いは、Realtime DatabaseはJsonを保存していて、階層が深かったりデータ量が多かったりする複雑なデータだとクエリーなどが難しくなります。
Firestoreはドキュメントをコレクションとして保存し、サブコレクションを付けて階層化することが出来ます。
公式に違いを説明しているページがありますので、詳細はこちらを読んでください。
https://firebase.google.com/docs/firestore/rtdb-vs-firestore?hl=ja

まあ要するにFirebaseはFirestoreをオススメしてますよって感じです。
ちなみに、Googleに買収される前のFirebaseは、そもそもこのRealtime Databaseが目玉だったようです。

あともう一つ、**Storage**というサービスもあります。
https://firebase.google.com/docs/storage?authuser=0
こちらは、写真、動画、ドキュメントファイルなど、いわゆる**ファイル**単位でアップロードして管理するものです。

実は、このシリーズでは、当初はこちらを使う予定でした。
というのも、「ユーザーがバックアップと復元を任意のタイミングで行うだけ」という方式を考えていたからです。Roomで使っているデータはSQLのテーブルですから、CSVで書き出せます。なので、その都度、CSVファイルを作ってアップロード、復元したければダウンロード、というように考えていました。
まあこれは「課金怖い」だったからというのもあるんですけどね^^;

でも、UploadTask自分でやんなきゃいけないのかとか、いろいろ調べていくうちに、「Firestoreの方が、(Roomとのデータコンバートは必要だけど)簡単そうだしリアルタイムにデータ同期できるのって楽しそうじゃん」となって、Firestoreに決めました。

写真や動画のコンテンツをユーザー同士でアップロードして共有するようなサービスだったら、Storageが良いのでしょうね。


# Firestore Databaseの作成
こちらの手順に従ってやっていきます。
https://firebase.google.com/docs/firestore/quickstart?hl=ja#read_data

## 1.Databaseを作成
- Firestoreコンソールのプロジェクトの左側のメニューから[Database]を選ぶ
- [データベースの作成]をクリック

![qiita13_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/113cf8dc-4699-fd1a-5471-84a14bf50f3a.png)

## 2.セキュリティモデル
- [テストモードで開始]を選ぶ
  - 注意書きにあるとおり、30日間だけユーザーがすべてのデータを見られます。当然、セキュリティがガバガバです。開発が終わったらちゃんとセキュリティを設定してねってことです。

![qiita13_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/ca3bad53-fda8-17ef-53f2-bcfd42dfc80a.png)

## 3.ロケーションの設定
- 任意のリージョンを選ぶ
  - どこが良いとかはよく分かりませんが、ひとまずマルチリージョンである必要は、今回のプロジェクトではないでしょう。マルチリージョンだと、1つのリージョンで障害が起こってデータベースにアクセスできなくなっても、他のリージョンが生きていればサービスが止まることがなくなります。サービスの規模や要件に応じて検討しましょう。

![qiita13_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/750f420d-2f91-6d98-a5e7-8fac4b1fcc9d.png)

## 4.作成
[完了]をタップして、少し待ちます。多分VMをどこかに起動しているんでしょうね。

|![qiita13_04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/99ff8e6a-7720-d8c6-c7e0-a60047fc04ba.png)|
|:--|

終わると、次のようなページが表示されます。

|![qiita13_05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/7c29bdfd-9e03-6a63-9093-7b5e2364b5c1.png)|
|:--|

データベースの作成はこれだけです！
このコンソールから手動でデータを追加することも出来ますが、アプリからデータを登録するのをやっていきます。

デフォルトデータとか、アプリから端末に配信したいデータなんかは、ここで追加するか、Firebase CLIを使ってデプロイする、という運用になっていくのではと思います。

# Firestoreを使ってみる

## 1.アプリに依存関係を設定する

`app/build.gradle`にいつものごとく依存関係を追加します。

```app/build.gradle
    implementation 'com.google.firebase:firebase-firestore:21.4.3'
```

## 2.データを追加してみる

### (1)Firestoreの初期化

Firestoreの初期化をしてインスタンスを取得します。
実験用に`MainActivity`のプロパティに持たせます。

```MainActivity.kt
private val db = FirebaseFirestore.getInstance()
```

この`db`に対して処理を呼び出していきます。

### (2)データを登録する
さっそくFirestoreにデータを登録してみましょう。
適当な場所がないので、[AnalyticsとCrashlyticsの回](https://qiita.com/kasa_le/items/560775076c3b2f00fcf7)で使ったHasPetダイアログ再びといきましょうか。

登録データには`HashMap`を使います。

```MainActivity.kt
    /**
     * 犬を飼っているかの選択肢を送信
     */
    override fun onSelected(hasDog: Boolean) {
        analyticsUtil.setPetDogProperty(hasDog)
        if (!hasDog) {
//            // Crashlyticsに送るサンプル用
//            throw RuntimeException("Test Crash")
        }
        settingRepository.savePetDog(hasDog)

        // Firestoreお試し用
        val pet = hashMapOf(
            "petDog" to hasDog,
            "message" to "Test"
        )
        db.collection("pets")
            .add(pet)
            .addOnSuccessListener { documentReference ->
                Log.d("FIRESTORE", "DocumentSnapshot added with ID: ${documentReference.id}")
            }
            .addOnFailureListener { e ->
                Log.w("FIRESTORE", "Error adding document", e)
            }
    }
```

今回は、犬を飼ってないと答えても強制クラッシュはしないようにします(笑)

もう、これだけです。
面倒なテーブル定義など全く不要！

### (3)実行

アプリを実行してみて下さい。既にインストール済みだった場合は、いったんアンインストールするか、データの削除を行って下さい。

ポップアップでは、どっちでもいいのでボタンをタップします。
すると、Logcatに以下のように出力されるはずです。

```console
FIRESTORE: DocumentSnapshot added with ID: XXXXXXXXXXX
```

Firebaseコンソールにいくと、コレクションに`pets`というのが追加されていますね。

|![qiita13_06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/0df1c981-9907-bae9-1752-48c7c6c0fe41.png)|
|:--|

あっという間にデータベースにデータ保存が出来てしまいました！
しかもクラウドにです。
なんて簡単なんでしょう・・・
便利な**Room**だってもうちょっと大変だったのに。

### (4)コレクションにフィールドを追加する
コレクションに**フィールド**、すなわち`key-value`のセットを追加してみます。
そうですね、犬を飼ってると答えたかどうかで、登録する内容を変えてみましょう。

```MainActivity.kt
        val pet =
            if (hasDog) {
                hashMapOf(
                    "petDog" to hasDog,
                    "message" to "Test",
                    "petName" to "Pochi",
                    "born" to 2018
                )
            } else {
                hashMapOf(
                    "petDog" to hasDog,
                    "message" to "Test"
                )
            }
```

アプリをアンインストールするかデータを削除してもう一度起動し、「はい」をタップしてみて下さい。

|![qiita13_07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/1d09ebb7-42e8-8c6f-b469-79f9bcfa9181.png)|
|:--|

コレクションが増えてますね。しかも先ほど追加したコレクションとフィールドが異なります(増えている)。
SQLなデータベースだと、テーブル構成はカチッと決まっていて、データを増やそうとしたらカラムを追加する必要があり、テーブル定義の変更となってしまいます。特にAndroidのSQLiteでは結構手間なんです、これが。

それが、NoSQLだと、こんな風に柔軟にコレクションにフィールドを追加できるんですね。
いやはや、なんとも便利です。
ただ、気をつけないと行けないのは、取り出す方でしょうね。すべてのフィールドがあるとは限らない、という実装にしないと落ちまくりのアプリが出来上がりそうです。
とはいえ、Kotlinなら、すべての要素を`nullable`として扱うように実装しておけば、そういったことも防げますけれどね。

## 3.データを読み取る
次はクラウドのデータを読み込んでみます。
HasPetダイアログに回答済みだったら、クラウドのデータを取り込んでリスト表示しましょう。
リストの行レイアウトを考えるのはちょっと面倒なので(本筋でない)、単に文字列で出しますが、拘ってみたい人はトライしてみて下さい。

### (1)データの読込コード

`onCreate`で設定ファイルにpet情報があるかチェックして、あればデータを読みに行くようにします。

```MainActivity.kt
   override fun onCreate(savedInstanceState: Bundle?) {
        ...

        val hasPet = settingRepository.readPetDog()
        if (hasPet == null) {
            val dialog = SelectPetDialog()
            dialog.show(supportFragmentManager, null)
        } else {
            db.collection("pets")
                .get()
                .addOnSuccessListener { result: QuerySnapshot ->
                    val list = arrayListOf<HasPet>()
                    for (document in result) {
                        Log.d("FIRESTORE", "${document.id} => ${document.data}")
                        list.add(HasPet(document.data))
                    }
                    val dialog = ListDialogFragment.Builder(list).create()
                    dialog.show(supportFragmentManager, null)
                }
                .addOnFailureListener { exception ->
                    Log.w("FIRESTORE", "Error getting documents.", exception)
                }
        }
    }
```

`db.collection("pets").get()`で、コレクションデータ"pets"を取得し、`addOnSuccessListener`や`addOnFailureListener`はそれぞれ成功時、失敗時のコールバックを登録しています。

リストダイアログを表示するために、`QuerySnapshot`である`result`の数だけ`for`を回して`HasPet`を要素に持つ`ArrayList`を得ています。

### (2)Parcelableなクラス
`HasPet`クラスは、次のようにしました。

```ListDialogFragment.kt
@Parcelize
data class HasPet(val map: @RawValue Map<String, Any>) : Parcelable {
    private val hasPet: Boolean
        get() {
            return map["petDog"] as Boolean
        }
    private val petName: String?
        get() {
            return map["petName"] as String?
        }
    private val born: Long?
        get() {
            return map["born"] as Long?
        }

    fun titleString(): String {
        return if (hasPet) {
            "$petName($born)"
        } else {
            "ペットを飼っていない"
        }
    }
}
```

`@Parcelize`というのは、`Parcelable`なクラスに付けるKotlin拡張プラグインのアノテーションです。`Parcelable`というのは、`Intent`や`Bundle`に`setXXX`や`putXXX`といった関数が用意されていない型のオブジェクトを入れたいときに使えるAndroid独自のインターフェースです。`Serializale`でも良いのですが、今回`HashMap`を入れるために`Parcelable`を使ってみることにしました。

`Parcelable`はインターフェースですから、本来、実装しなければならない関数がいくつかあります。でも結構単純なクラスでも長ーくなるんですよね。その割には、コードは割とボイラーテンプレートというか、お決まりのものになりがち。
そこで、自動的にコード生成してくれるのが、`@Parcelize`のアノテーションです。

あとは`Map`から必要な情報を取り出すプロパティアクセスとそれらのプロパティを組み合わせて文字列を作る関数です。

### (3)リストダイアログ
リストダイアログというものが正式にクラスであるわけではありません。リストを表示するダイアログ、というだけです。
`AlertDialog`はリスト表示を簡単にさせることが出来るので、今回はそれを使ってみます。

このアプリの本筋では使わないダイアログですが、リストでのダイアログ表示は覚えておくと便利なので是非やってみて下さい。

```ListDialog.kt
class ListDialogFragment : DialogFragment() {
    private val analyticsUtil: AnalyticsUtil by inject()

    class Builder(val list: ArrayList<HasPet>) {
        fun create(): ListDialogFragment {
            val d = ListDialogFragment()
            d.arguments = Bundle().apply {
                putParcelableArrayList(KEY_LIST, list)
            }
            return d
        }
    }

    companion object {
        const val KEY_LIST = "list"
        const val SCREEN_NAME = "ユーザーペット情報リスト"
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        // AlertDialogで作成する
        val builder = AlertDialog.Builder(requireContext())

        // メッセージの決定
        val list = arguments!!.getParcelableArrayList<HasPet>(KEY_LIST)!!.map { t -> t.titleString() }

        // AlertDialogのセットアップ
        builder.setItems(list.toTypedArray(), null)
            .setTitle(R.string.user_has_pet)
            .setIcon(android.R.drawable.ic_dialog_info)
        return builder.create()
    }

    override fun onResume() {
        super.onResume()
        activity?.let { analyticsUtil.sendScreenName(it, SCREEN_NAME) }
    }
}
```

`HasPet`が`Parcelable`なので、`Bundle#putParcelableArrayList`でセットできています。取り出すときは`getParcelableArrayList`です。

このダイアログを表示するとこうなります。

![qiita13_08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/4b6f15b3-eb92-7c37-d767-3292f9ec0f18.png)

※デバッグ端末にOS4.4の実機を引っ張り出してみました^^;

# もう少し踏み込んで使ってみる
## 1.認証して使う
今は認証をせずに使っていますが、実は、Firestore的には「匿名ログイン」というのを使っています。これを、Firebase Authenticationのユーザーログインと認証を関連付けていきます。

認証したユーザーだけが読み書きできるようにします。
この設定は、Firebaseのコンソールで行います。

Databaseのページに、「ルール」というタブがあるのでクリックします。

|![qiita13_09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/e0dca230-75d3-3915-345f-950e288a72a5.png)|
|:--|

何やらコードが書ける部分がありますね。
そこに次のように記入します。

```javascript
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // とりあえずログインしたユーザーは読み書き可能
    match /{document=**} {
      allow read, write: if request.auth != null;
    }
  }
}
```

`request.auth`が`null`でなければ、つまりログイン済みユーザーであれば、読み書きが出来るというルールになります。

これで実行してみましょう。データを削除するか、一度アプリをアンインストールしてから再インストールします。

ダイアログで「いいえ」と回答しても、権限がないのでエラーがLogcatに出力されているはずです。

```console
W/Firestore: (21.4.3) [Firestore]: Write failed at pets/WVtO6mZzyOqhf6LFRgJw: Status{code=PERMISSION_DENIED, description=Missing or insufficient permissions., cause=null}
W/FIRESTORE: Error adding document
```

アプリを再起動すると、データを取得してリストを表示しようとするはずですが、やはりエラーになります。

```console
W/Firestore: (21.4.3) [Firestore]: Listen for Query(target=Query(pets order by __name__);limitType=LIMIT_TO_FIRST) failed: Status{code=PERMISSION_DENIED, description=Missing or insufficient permissions., cause=null}
W/FIRESTORE: Error getting documents.
    com.google.firebase.firestore.FirebaseFirestoreException: PERMISSION_DENIED: Missing or insufficient permissions.
```

ログインして、もう一度アプリを起動してみましょう。
リストが出るはずです。ログアウトして再起動すると、またエラーになるはずです。

エラーがLogcatだけで分かりづらいという人は、`ErrorDialog`やトーストメッセージで出してみると良いでしょう。

## 2.データを更新(上書き)
ペット選択ダイアログを「いいえ」にした人だけ、アプリ起動時に毎回選択ダイアログを表示し、「はい」と答えるまで毎回表示されるという意地悪仕様にしてみます。

そのためには、ダイアログ表示の条件を以下のように変更します。

```kotlin
        val hasPet = settingRepository.readPetDog()
//        if (hasPet == null) {
        if (hasPet != true) {
```

`readPetDog`の戻り値は`Boolean?`ですから、`hasPet`は`null`の可能性があります。が、`!=true`で比較すると、`null`の場合はこの比較結果値が`true`になるため、ダイアログ表示の分岐に入るというわけです。`null`は`true`とは**等しくない**ので、当たり前ですね。

アプリをアンインストールして再インストール後再起動して、一度「いいえ」で保存した後、サインインします。アプリをもう一度再起動すると、リストダイアログでは無くまた質問ダイアログが表示されるかと思います。そこでまた「いいえ」を選択してみて下さい。（ここでやっと保存が成功するので）

で、またアプリを終了して再起動して、今度は「はい」にしてみます。
で、またアプリを終了して再起動します(酷い手順ですみませんw)

はい、「ペットを飼っていない」と、「Pochi(2018)」が1つずつ増えちゃったかと思います(汗)
毎回`db.collection("pets").add`してるんだから、当然です。

データを更新するためには、`DocumentReference`というのを取得して、そのドキュメントを更新する、という手順を踏まなければなりません。ちなみに、`db.collection("pets")`というのはコレクション、その中の一つ一つのデータの集まりを、「ドキュメント」と言います。

さて、`DocumentReference`を得るには、どのドキュメントか、というのを特定する情報をアプリに保持しておかなければなりません。ドキュメントを追加したときに、`DocumentReference`を受け取っているのでその`id`を保存しておくことにしましょう。

`SettingsRepository`に以下の関数を追加します。

```SettingsRepository.kt
    fun saveDocReferenceId(refId: String) {
        val pref =
            applicationContext.getSharedPreferences(PREF_FILE_NAME, AppCompatActivity.MODE_PRIVATE)
        pref.edit().putString("docRefId", refId).apply()
    }

    fun getDocReferenceId():String? {
        val pref =
            applicationContext.getSharedPreferences(PREF_FILE_NAME, AppCompatActivity.MODE_PRIVATE)
        return pref.getString("docRefId", null)
    }
```

保存するのはここです。

```MainActivity.kt
    override fun onSelected(hasDog: Boolean) {
            ...

            db.collection("pets")
                .add(pet)
                .addOnSuccessListener { documentReference ->
                    // document reference idを保存
                    settingRepository.saveDocReferenceId(documentReference.id)
                    Log.d("FIRESTORE", "DocumentSnapshot added with ID: ${documentReference.id}")
                }

```

ここで保存する前に、Preferenceに保存したIDが無いか取ってきます。

```MainActivity.kt
    override fun onSelected(hasDog: Boolean) {
        ...

        settingRepository.savePetDog(hasDog)
        val savedDocReferenceId = settingRepository.getDocReferenceId()
```

`savedDocReferenceId`が`null`だったら新規追加、保存されていれば上書きとします。

```MainActivity.kt
        if (savedDocReferenceId == null) {
            // 新規登録
            db.collection("pets")
                .add(pet)
                ....
        } else {
            val docRef = db.collection("pets").document(savedDocReferenceId)
            // 上書き更新
            docRef.update(pet)
                .addOnSuccessListener {
                    Log.d("FIRESTORE", "DocumentSnapshot Updated.")
                }
                .addOnFailureListener { e ->
                    Log.w("FIRESTORE", "Error updating document", e)
                }
        }
```

これでもう一度、データを削除して手順を踏んでみましょう。
「いいえ」を選んだ後(未ログインなので保存できない)、ログインし、アプリを再起動、「いいえ」を選んでデータを保存、もう一度アプリを再起動して、今度は「はい」を選び、また再起動すれば、リスト表示が確認できます(我ながら面倒くさすぎるw)

今度はデータは1件しか増えていないはずです。

なお、今回はデータの保存と読み出しのタイミングが離れているので、ドキュメントIDをpreferenceに保存するという策を採りましたが、実際のアプリではまずしない方法では無いかと思います。というのも、例えばこのアプリではデータをカレンダーに表示することになりますが、その表示するデータを取ってきたときに毎回`DocumentReference`を取れますよね。だからDocumentReferenceをローカルに保存しておく必要が無いんです。この辺については次回に詳しくやります。
今回は、「触ってみる」を目的としていますので、こんな風にやるのか、どんな関数があるのか、という感じで捉えれば良いかと思います。

## 3.クエリ
さて、今は無条件にすべてのデータを取得していますが、条件検索、つまりクエリーをしてみます。

### (1)Boolean比較
まあ条件といえば、`hasPet`を使うくらいしかありませんね。
全件検索だと、今はこのように「ペットを飼っていない」と飼っている情報が混ざって表示されています。

![qiita13_10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/8875c4ce-0b83-f564-c6db-138c0b638f81.png)

これを、ペットを飼っている情報だけにしてみましょう。
あるフィールドが特定の値のものだけを抽出するには、`whereEqualTo`を使います。これを`get()`の前に呼びます。

```MainActivity.kt
           db.collection("pets")
                .whereEqualTo("petDog", true)
                .get()
                .addOnSuccessListener {...}
```

![qiita13_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/a3a37233-8538-b124-a973-9044d1913f76.png)

「ペットを飼っていない」情報が取り除かれました^-^

### (2)Long比較
せっかくなので`born>2000`みたいなクエリーもやってみましょう。
その前に、このままだとペットを飼っている情報はすべて`born=2018`で引っかかってしまうので、ちょっとランダムで入れるようにしてみました。

```HasPet.kt
@Parcelize
data class HasPet(val map: @RawValue Map<String, Any>) : Parcelable {
    ...

    companion object{
        val names = listOf("Hachi", "Coma", "Suzuri")
        val years = listOf(1923, 2006, 2018)
        val messages = listOf("Test", "Sample", "Cute")
        fun randomPet() : HashMap<String, Any>{
            val i = Random(System.currentTimeMillis()).nextInt(3)
            return hashMapOf(
                "petDog" to true,
                "message" to messages[i],
                "petName" to names[i],
                "born" to years[i]
            )
        }
    }
}
```

```MainActivity.kt
    override fun onSelected(hasDog: Boolean) {
        ...

        val pet: HashMap<String, Any> =
            if (hasDog) {
                HasPet.randomPet()
            } else {
                ...
            }
```

これで、何度かアプリ再インストールしてダイアログで「はい」と答えてみてください。
手順が面倒で済みません。気になる人は何度も登録可能にしてみてください^^;
ちなみに、**adbコマンド**で以下のようにすればアプリのデータ削除が簡単に行えます。

```console
$ adb shell pm clear パッケージネーム
```

debugビルド用にパッケージに`SuffixId`付けてる人は付け忘れないようにしてくださいね〜

いくつかデータが登録できたら、検索条件を変えてみましょう。
">"は、`whereGreaterThan`を使います。

```MainActivity.kt
           db.collection("pets")
                .whereGreaterThan("born", 2000)
                .get()
```

残念ながら"Hachi"さんは生まれが昔すぎるので表示されません^^;
(もはや何のアプリだかw)

`hasPet=false`のデータ、つまり`born`が無いデータは引っかからないようですね。


## 4.もう少しセキュリティ保護をちゃんとする
今はルールはこうなっている状態です。

- write : 認証済みユーザーのみ
- read : 認証済みユーザーのみ

これを次のように変えてみます。

- write : 認証済みユーザーのみ
- read : all

`read`のルールだけ変えれば良さそうですね。

```javascript
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // とりあえずログインしたユーザーは読み書き可能
    match /{document=**} {
      allow write: if request.auth != null;
      allow read: if true
    }
  }
}
```

`if true`で、権限無しに読み取れることになります。

アプリの方は、今のままだと「飼っている」と答えるまでリストが表示出来ないので、ダイアログで「はい」や「いいえ」を選択したら直ぐにリストを表示することにしてみます。
まず、リストでのダイアログ表示部分をprivateな関数に出します。
(クエリー条件はいったん無くしました。)

```MainActivity.kt
    private fun showPetListDialog(){
        db.collection("pets")
//                .whereEqualTo("petDog", true)
//                .whereGreaterThan("born", 2000)
            .get()
            .addOnSuccessListener { result: QuerySnapshot ->
                val list = arrayListOf<HasPet>()
                for (document in result) {
                    Log.d("FIRESTORE", "${document.id} => ${document.data}")
                    list.add(HasPet(document.data))
                }
                val dialog = ListDialogFragment.Builder(list).create()
                dialog.show(supportFragmentManager, null)
            }
            .addOnFailureListener { exception ->
　　　　　　　　　 Toast.makeText(this, "データを読み込めませんでした。", Toast.LENGTH_SHORT).show()
                Log.w("FIRESTORE", "Error getting documents.", exception)
            }
    }
```

これを、`onCreate`と`onSelected`(ペット選択ダイアログのコールバック)で呼ぶようにします。

```MainActivity.kt
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        val hasPet = settingRepository.readPetDog()
        if (hasPet != true) {
            val dialog = SelectPetDialog()
            dialog.show(supportFragmentManager, null)
        } else {
            showPetListDialog()
        }
        ...
    }

    override fun onSelected(hasDog: Boolean) {
        ...
        
        if (savedDocReferenceId == null) {
            // 新規登録
            db.collection("pets")
                .add(pet)
                .addOnSuccessListener { documentReference ->
                    // document reference idを保存
                    settingRepository.saveDocReferenceId(documentReference.id)
                    Log.d("FIRESTORE", "DocumentSnapshot added with ID: ${documentReference.id}")
                }
                .addOnFailureListener { e ->
                    Toast.makeText(this, "登録できませんでした。", Toast.LENGTH_SHORT).show() // 追加
                    Log.w("FIRESTORE", "Error adding document", e)
                }
        } else {
            val docRef = db.collection("pets").document(savedDocReferenceId)
            // 上書き更新
            docRef.update(pet)
                .addOnSuccessListener {
                    Log.d("FIRESTORE", "DocumentSnapshot Updated.")
                }
                .addOnFailureListener { e ->
                    Toast.makeText(this, "更新できませんでした。", Toast.LENGTH_SHORT).show() // 追加
                    Log.w("FIRESTORE", "Error updating document", e)
                }
        }
        showPetListDialog() // 追加
    }
```

ついでに、Firestoreの各処理後の`addOnFailureListener`で、エラー時にはToast出すようにしておきました。

アプリデータをクリーンしてから実行すると、「いいえ」を押したときにエラーのToastが表示されますが、リストは表示されるはずです。
これで、未ログイン状態でも、リストは見られるようになりました。
(いったい何のアプリ^^;)

実際のアプリでは、「自分が作ったデータのみ自分が読み書きできる」にする予定なのでまた違ってきますが、ルールの書き方の参考にはなると思います。

# テスト
今回はアプリの本質と関係ない実験的コードなので、テストは入れません。
逆に、テストを動かすときにペット選択ダイアログが出るとまずいので、CI等をpushで回している人はコメントアウトなどを忘れずに^^

# ライブラリ内参照モジュールのexclude指定
リリースビルドするときに、以下のようなエラーが出ていました。

```console
##[error]Process completed with exit code 1.
           Dependency path 'qiita_pedometer:app:unspecified' --> 'androidx.room:room-testing:2.2.3' --> 'androidx.room:room-migration:2.2.3' --> 'com.google.code.gson:gson:2.8.0'
           Constraint path 'qiita_pedometer:app:unspecified' --> 'com.google.code.gson:gson:{strictly 2.7}' because of the following reason: debugRuntimeClasspath uses version 2.7
           Dependency path 'qiita_pedometer:app:unspecified' --> 'com.google.firebase:firebase-firestore:21.4.3' --> 'io.grpc:grpc-android:1.21.0' --> 'io.grpc:grpc-core:1.21.0' --> 'com.google.code.gson:gson:2.7'
```

どうやら、`androidx.room:room-testing`が参照しているgsonのバージョンと、Firestoreが参照しているgsonのバージョンが違くてコンフリクトしているようです。
テストパッケージである`androidx.room:room-testing`の方のGsonは最終ビルド成果物には不要なので、`exclude`(除外指定)というのをします。

```app/build.gradle
    androidTestImplementation ("androidx.room:room-testing:$room_version"){
        exclude group: 'com.google.code.gson'
    }
```

このように、参照しているライブラリが増えてくると、その中で参照しているライブラリに同じ物があることはよくある現象になります。そのバージョンが違うとビルドが通らなくなってしまうのは、本来意図しない方のバージョンを参照してしまって実行時に不定な動作になってしまう可能性があるので、事前に検出して教えてくれているわけです。

ですので、`exclude`した場合バージョンは統一されるので(今回の場合Firestoreが参照している方のGsonのバージョンがパッケージされます)、ビルドは通るようになりますが、本来別のバージョンを参照してビルドされているパッケージ(今回の場合はroom-testing)は動作がおかしくなる可能性があります。なのでその後の動作確認はよく行う必要があります。今回で言えば、room-testingはテスト用パッケージなので、Room関連のUnitTestをよく確認する必要がありますね。

実は、この「バージョン合わせ」は、大きなプロジェクトであるほど結構難しいハードルになってきます。
あるライブラリAのバージョンを最新に上げようとしたら、中で参照している別のライブラリBのバージョンが上がってしまうが、Bを参照しているライブラリCも一緒に最新に上げる必要が生じます。しかしライブラリCの最新版がまだ対応してないとか、最新版で削除された機能をまだ使いたいので上げられない、とか・・・
そんなジレンマが多発するようになります。
更に、ここ数年でGoogleも「常に最新OSに対応したアプリじゃないと(=TargetSDKが最新でないと)登録・アップデートさせてあげないよ」と言ってきているので、そこを上げようとすると付随していろいろ上げなきゃならなくなり、しかしサードパーティー製がまだ追いついてなかったりで、結構大変なことになります。

そんなわけで、依存関係のバージョン合わせは、精神的にしんどい作業です(汗)


# まとめ
ここまでで、「Firestoreを使ってみた」は終わりです。
意外に簡単だったのではないでしょうか？
Roomなどのローカルデータ保存をせず、初めからクラウド保存ありきで考えておくのもありですね。ただ、UnitTestしづらそうですが^^;

ここまでのコードは以下のブランチにアップしてあります。
https://github.com/le-kamba/qiita_pedometer/tree/feature/qiita_13

# 予告
アプリの本質に関係ないデータのやりとりを見てきましたが、次回はRoomで保存してきたデータをFirestoreに上げられる形に直して、読み書きをするところを実装していきます。

# 参考サイトなど

KotlinでのParcelableについて参考になりました。
https://qiita.com/sadashi/items/fd902619f5b3491e969f

FirestoreのCodelabです。
https://codelabs.developers.google.com/codelabs/firestore-android