Help us understand the problem. What is going on with this article?

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

はじめに

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

導入方法

Firebaseを使ったことある人はFirestoreをアプリレベルのbuild.gradleに追加からどうぞ

Firebaseが初めての場合

GoogleアカウントでFirebaseにログイン

すでにFirebaseを導入していればいらないですが、まずはGoogleアカウントでFirebaseを始める必要があります。
https://firebase.google.com/docs/android/setup
ルートレベルのbuild.gradleに下記を追記

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プラグインを追加。

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に追加

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

FirebaseコンソールのFirestore

FirebaseのDEVELOPのDatabase→Cloud Firestoreを選択することで見られます。
ここからデータを追加することも可能ですし、アプリからデータを追加すればここから見られます。
Screenshot_1.jpg

Firestoreの構成

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

利用のためのコーディング

データの書き込み

Firestoreではデータクラスをそのまま追加することが出来ます。
例えば、得点データを保存したいときはこんな感じ。

ScreItem.kt
data class ScoreItem(val name: String = "",
                     val score: Long = 0,
                     val missCount: Int = 0,
                     val time: Long = 0,
                     val registerTime: Date = Date())
書き込み処理
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

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

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

データの読み込み

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

// 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件を取得しています。

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ではプロパティ名が簡単に取得できます。

ScoreItem::score.name // "score"
ScoreItem::registerTime.name // "registerTime"

自分で定数で定義しなくてよいので楽+タイプミスもないので最高。
Realmを使うときも重宝しそうです。

複合クエリにはカスタムインデックスが必要

複数orderby、whereなどを使う場合には、カスタムインデックス作成が必要です。
Screenshot_3.jpg
コンソールの「インデックス」から簡単に作ることができます。

これを作らずに複合クエリを実行すると、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。
それならリファレンスにも書いてくれよと思わなくもない。笑

結果のオブジェクト化

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

コレクション型に対して、toObjects
val scoreList : List<ScoreItem> = it.result.toObjects(ScoreItem::class.java)
単一の結果を取得したい場合は、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も使いやすかったです。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away