Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

Firestoreのカスタムオブジェクト利用時の注意事項

こんにちは、モバイルクライアントグループの隅山です。

今回はCloud Firestore(以下、Firestore)をAndroidアプリに導入した際、難読化問題が発生したのでその対応方法を紹介します。 Firestore自体はとても便利なため、簡単な利用方法も併せて紹介します。

Firestoreについて

FirestoreとはGoogleのNoSQLクラウドデータベースであり、クライアントのデータを保存や同期ができます。

firebase.google.com

Firestoreを利用すべき場面として下記が挙げられます。

  • 複数端末で同じデータを同期させたい
  • オフラインでデータ読み書きでき、オンライン時にローカルの変更内容を保存したい

弊社でも上記の場面があったため、Firestoreを利用しました。 ここからは簡単に利用方法について紹介します。

簡単な利用方法

Firestoreデータベース作成

Firebaseコンソール からデータベース作成ワークフローに従って作成していきます。 Firestoreの スタートガイド が非常にわかりやすいため、ここでは割愛させていただきます。

以下からは弊社のアプリに導入した事例を紹介します。

データ初期化

まずはFirestoreのインスタンスをアプリ内で初期化する必要があります。 Kotlinのプロジェクトであればktxも用意されています。

弊社では取り回しの良さなどから、 Dagger を用いて初期化および必要クラスへの提供をしています。

@Module
object FirebaseModule {
    @Singleton
    @Provides
    fun provideFirestore(): FirebaseFirestore = Firebase.firestore
}

データ操作

Firestoreのデータ操作は基本的にMapオブジェクトを利用します。 ただMapオブジェクトを使うとわかりづらくなるため、弊社では カスタムオブジェクト を利用しました。 カスタムオブジェクトを利用すると 通常のMapオブジェクトを使った場合 より可読性が上がるのでおすすめです。

data class User(
  val first: String? = null,
  val last: String? = null,
  val born: Int? = null,
)
class Repository @Inject constructor(private val firestore: FirebaseFirestore) {

  // データ追加
  fun add() {
    val user = User(
      first = "Ada",
      last = "Lovelace",
      born = 1815,
    )

    firestore
      .collection("users")
      .add(user)
  }

  // データ取得
  fun get(user: (User) -> Unit) {
    firestore
      .collection("users")
      .get()
      .addOnSuccessListener { result ->
        user(result.toObjects(User::class.java))
      }
  }

  // データ削除
  fun remove(born: Int) {
    val ref = firestore.collection("users")

    ref
      .get()
      .addOnSuccessListener { result ->
        result.documents.forEach { document ->
          val user = document.toObjects(User::class.java)
          if (user?.born == born) {
            ref.document(document.id).delete()
          }
        }
      }
  }
}

基本的にデータ操作は全て非同期で行われます。 非同期であるためデータ取得方法が若干難しいですが、上記のコードのように書くだけでデータ操作できるため非常に使いやすいです。

カスタムオブジェクトの注意事項

カスタムオブジェクトを用いたデータ操作のサンプルをご紹介しました。 ただ、弊社のアプリに導入した際、注意事項が2点あったのでここで紹介します。

  • Kotlinのprefix問題
  • 難読化問題

Kotlinのprefix問題

FirestoreのコードはJavaで書かれています。 そのためカスタムオブジェクトをKotlinで書いている場合、FirestoreはJavaコードに変換された後のカスタムオブジェクトを参照することになります。

この変換の際、Booleanのプロパティ名が変更されます。 例えば、Booleanのプロパティ名が isStudent だとFirestoreからは student として見えてしまいます。

この言語仕様からカスタムオブジェクトにBooleanのプロパティが含まれている場合、想定とは違うプロパティで保存されてしまうことに注意してください。

data class User(
  val first: String? = null,
  val last: String? = null,
  val born: Int? = null,
  val isStudent: Boolean? = null, // Firestore(Java)からは student として見える
)

対応方法

Kotlinのprefix問題を解決するにはJvmFieldアノテーションをつけるだけです。(参考資料) もし、カスタムオブジェクトにBooleanのプロパティが含まれる場合はJvmFieldアノテーションを忘れずにつけてください。

data class User(
  val first: String? = null,
  val last: String? = null,
  val born: Int? = null,
  @field:JvmField
  val isStudent: Boolean? = null,
)

難読化問題

アプリのサイズをできる限り小さくするために、リリースビルドでは難読化と最適化を行い、サイズを削減しているプロダクトが多いです。 弊社でもR8を用いてアプリを難読化、最適化を行っています。 その際、アプリのクラスとプロパティの名前を短くする難読化によってFirestoreのデータ操作でクラッシュが発生しました。

原因はカスタムオブジェクトと難読化で大体わかると思いますが、プロパティ名が圧縮されていたことが理由です。 カスタムオブジェクトを用いてデータを保存するまではクラッシュしませんが、保存されたデータが圧縮されたプロパティ名であるためデータ取得時にRuntimeExceptionが発生します。

難読化起因の不具合は普段の開発中で確認できないため、かなり注意が必要となります。

対応方法

難読化問題を解決するためには難読化させないようにする必要があります。 カスタムオブジェクトのプロパティを難読化されないようにKeepアノテーションをつけてください。

@Keep
data class User(
  val first: String? = null,
  val last: String? = null,
  val born: Int? = null,
  @field:JvmField
  val isStudent: Boolean? = null,
)

終わりに

ここではFirestoreの簡単な事例紹介と注意事項を紹介しました。

Firestoreは非常に便利なNoSQLクラウドデータベースであるため、複数端末やオフライン対応する場合検討してみてください。 その場合はここであげた注意事項2点を気をつけながら導入してください。