AndroidでFirebaseのCloudFirestoreを使ってみた(Kotlin)


#はじめに
Androidアプリを作っていて、サーバーでデータを保存して複数端末で同じデータを利用したいなと思ったのがきっかけで利用しました。
かなり小規模だったため、無料で使えるものがないかと思って探していました。
最初はRealtime Databaseを使おうと思ったのですが、ただ保存して読み出せればよくリアルタイム性はいらないなと思っていたら目に入った次第です。
β版と書いてありますが、使いやすかったため、使用手順とポイントを記します。(投稿当時)
`現在はstableになっているので、積極的に使っていきたいですね！`

# 導入方法
Firebaseを使ったことある人は[Firestoreをアプリレベルのbuild.gradleに追加](#firestoreをアプリレベルのbuildgradleに追加)からどうぞ
## Firebaseが初めての場合
### GoogleアカウントでFirebaseにログイン
すでにFirebaseを導入していればいらないですが、まずはGoogleアカウントでFirebaseを始める必要があります。
https://firebase.google.com/docs/android/setup
ルートレベルのbuild.gradleに下記を追記

```groovy:build.gradle(プロジェクトレベル)
dependencies {
    classpath 'com.google.gms:google-services:3.2.0'
}

repositories {
    maven {
        url "https://maven.google.com"
    }
}
```
### プロジェクトを作成しgoogle-services.jsonを導入
Firebase Consoleからプロジェクトを作成します。
プロジェクト名とかパッケージ名とかを入れて終わり
google-services.jsonをapp配下に配置
アプリレベルのbuild.gradleにfirebase-coreとgoogle-servicesプラグインを追加。

```groovy:build.gradle(アプリレベル)
apply plugin: 'com.android.application'

android {
  // ...
}

dependencies {
  // ...
  implementation 'com.google.firebase:firebase-core:16.0.0'

  // Getting a "Could not find" error? Make sure you have
  // added the Google maven respository to your root build.gradle
}

// ADD THIS AT THE BOTTOM
apply plugin: 'com.google.gms.google-services'
```

## Firestoreをアプリレベルのbuild.gradleに追加

```groovy:build.gradle(アプリレベル)
dependencies {
  implementation 'com.google.firebase:firebase-firestore:17.0.1'
}
```

# FirebaseコンソールのFirestore
FirebaseのDEVELOPのDatabase→Cloud Firestoreを選択することで見られます。
ここからデータを追加することも可能ですし、アプリからデータを追加すればここから見られます。
![Screenshot_1.jpg](https://qiita-image-store.s3.amazonaws.com/0/192408/d0969dd1-b32c-aaf9-e65c-507b0642e67f.jpeg)

# Firestoreの構成
コレクション、ドキュメント、データという構成で出来ています。
詳しくは本家に。
https://firebase.google.com/docs/firestore/?authuser=4#how_does_it_work

#利用のためのコーディング
## データの書き込み
Firestoreではデータクラスをそのまま追加することが出来ます。
例えば、得点データを保存したいときはこんな感じ。

```kotlin:ScreItem.kt
data class ScoreItem(val name: String = "",
                     val score: Long = 0,
                     val missCount: Int = 0,
                     val time: Long = 0,
                     val registerTime: Date = Date())
```

```kotlin:書き込み処理
val db = FirebaseFirestore.getInstance()
val user = ScoreItem(nickname, score, missCount, resultTime)
db.collection("ranking")
        .document()
        .set(user)
        .addOnCompleteListener { snackbar.dismiss() }
        .addOnSuccessListener({
            context?.toast("送信完了")
        })
        .addOnFailureListener({ context?.toast("送信失敗") })
```
コンソール画面
![Screenshot_4_2.jpg](https://qiita-image-store.s3.amazonaws.com/0/192408/4296c06d-a098-2d01-338c-6268f57c4616.jpeg)

collectionに入れた文字列が、コレクションに入り、その中にdocumentに入れたものが入ります。
documentは一意のIDで、引数無しで呼び出すと自動生成してくれます。
コンソール上では並び替えが出来ないようなので、順番に作りたいのであれば自分でIdを生成する処理を書いてもいいかもしれません。(時刻+乱数とか)
setでオブジェクトを入れられます。これが「ドキュメント」の「フィールド」になります。

リスナーは3種類あるので使いたいものを使えばOK
OnCompleteListenerは、引数に成功失敗が入ってきます。
(上はとりあえず全て使ってみた例)

## データの読み込み

クエリベースはメソッドに用意されています。
Realmみたいな感じに直感的に使えます。

```kotlin
// Create a reference to the cities collection
val citiesRef : CollectionReference = db.collection("cities")

citiesRef.whereEqualTo("state", "CA")
citiesRef.whereLessThan("population", 100000)
citiesRef.whereGreaterThanOrEqualTo("name", "San Francisco")
```
クエリを繋げることもできます。
> Cloud Firestore で単純なクエリと複合クエリを実行する
> https://firebase.google.com/docs/firestore/query-data/queries?hl=ja

以下は、rankingからスコアリストを取得する例。
スコア順登録日順で上から50件を取得しています。

```kotlin
val db = FirebaseFirestore.getInstance()
db.collection("ranking")
        .orderBy(ScoreItem::score.name)
        .orderBy(ScoreItem::registerTime.name, Query.Direction.DESCENDING)
        .limit(50)
        .get()
        .addOnCompleteListener({
            progress.visibility = View.GONE
            if (it.isSuccessful) {
                val scoreList = it.result.toObjects(ScoreItem::class.java)
                scoreAdapter.addAll(scoreList)
                scoreAdapter.notifyDataSetChanged()
            } else {
                context?.toast("失敗")
            }
        })
```

いくつかポイントがあります。
### プロパティ名の取得
今回の本質とはズレますが、Kotlinではプロパティ名が簡単に取得できます。

```kotlin
ScoreItem::score.name // "score"
ScoreItem::registerTime.name // "registerTime"
```
自分で定数で定義しなくてよいので楽+タイプミスもないので最高。
Realmを使うときも重宝しそうです。

### 複合クエリにはカスタムインデックスが必要
複数orderby、whereなどを使う場合には、カスタムインデックス作成が必要です。
![Screenshot_3.jpg](https://qiita-image-store.s3.amazonaws.com/0/192408/eddcbf3e-f1b3-85d6-02d5-4995ac63c2f1.jpeg)
コンソールの「インデックス」から簡単に作ることができます。

これを作らずに複合クエリを実行すると、Exceptionが発生します。
>Caused by: io.grpc.StatusException: FAILED_PRECONDITION: The query requires an index. That index is currently building and cannot be used yet. See its status here: https://...

URLまで載せてくれている、涙が出るほどに親切なException。
それならリファレンスにも書いてくれよと思わなくもない。笑

### 結果のオブジェクト化
取得した結果クラスには、オブジェクト化するメソッドが用意されています(神)


```kotlin:コレクション型に対して、toObjects
val scoreList : List<ScoreItem> = it.result.toObjects(ScoreItem::class.java)
```

```kotlin:単一の結果を取得したい場合は、documentsに対して`toObject`
it.result.documents[0].toObject(ScoreItem::class.java)
```
一つ注意ですが、これらを行う場合にはデフォルトコンスタラクタが必要です。
無いとRuntimeExceptionになります。

>java.lang.RuntimeException: Could not deserialize object. Class ScoreItem does not define a no-argument constructor. If you are using ProGuard, make sure these constructors are not stripped

kotlinでデフォルトコンストラクタを作るには、[ScoreItem](#データの書き込み)のようにフィールドにデフォルト値を入れて上げる必要があります。

#最後に
以上、今回はFirebaseのCloud Firestoreを使ってみました。
サーバーレスでクエリに対応したDBを利用できるのでとても便利でした。
利用するデータ量にもよりますが、無料から利用できるところも良いですし、APIも使いやすかったです。