KotlinのDelegated PropertyでFirebase Remote Configをちょっとだけ使いやすくする

  • 12
    いいね
  • 0
    コメント

この投稿はKotlin Advent Calendarの15日目の投稿です。

はじめに

Firebase Remote Config使ってますか?
A/Bテストのような本格的な使い方から、ちょっとしたパラメータの変更にも使えたりと非常に強力なツールです。

ただ、個人的に少し不便な点としてアプリ内で使うキー/デフォルト値の管理が煩雑になりがちなところが挙げられます。

この記事では、この個人的な不便を解消するために作った簡単なライブラリを紹介したいと思います。

Firebase Remote Configのアプリ内デフォルト値

Remote Configから値が取得できなかった際などに使われるデフォルト値は、Mapオブジェクトまたはres/xmlのXMLリソースファイルで設定するとあります。
Firebase Remote Config を Android で使用する  |  Firebase

例えば、Mapオブジェクトで設定する場合はobjectクラスなどで定義したMapを使うことになると思います。

FirebaseRemoteConfig.getInstance().setDefaults(ConstantValues.DEFAULTS_MAP)

object ConstantValue {
    val DEFAULTS_MAP: Map<String, Any> = mapOf(
            "loading_message" to "Now loading...",
            "count_condition" to 3,
            "threshold" to 0.6
    )
}

デフォルト値の登録だけならまあいいんですが、あくまでデフォルト値なので同じキーを使ってRemote Configから読み出す必要があります。

FirebaseRemoteConfig.getInstance().getString("loading_message")

こうなってくるとキーをベタ書きすることになってしまい、読み出そうとする度に「あの値のキーはこれでよかったっけ?」とか「値が取得できないと思ったらキーをtypoしていた」みたいな事態が発生することになります。

じゃあ、キーを別のStringに切り出せば?となるんですが、そうなると今度は記述量がかなり増えるとか、FirebaseのConsoleと見比べながらキーとデフォルト値を確認するのが辛くなるといった問題が発生します。
これはXMLリソースを使った場合でも同様です。

FirebaseRemoteConfig.getInstance().setDefaults(ConstantValues.defaultsMap)
// 値の取得にかなりの記述量が必要になる
FirebaseRemoteConfig.getInstance().getString(ConstantValue.KEY_LOADING_MESSAGE)

object ConstantValue {
        // stringリソースを読むためにcontextが必要なので関数に
    fun getDefaultsMap(context: Context): Map<String, Any> = mapOf(
            // パッと見でキーの実際の値が分からないのでConsoleと見比べるのが辛い
            KEY_LOADING_MESSAGE to "Now loading...",
            KEY_COUNT_CONDITION to 3,
            KEY_THRESHOLD to 0.6
    )
    const val KEY_LOADING_MESSAGE = "loading_message"
    const val KEY_COUNT_CONDITION = "count_condition"
    const val KEY_THRESHOLD = "threshold"    
}

キーとデフォルト値を一元管理

以前、KotprefというSharedPreferenceへのアクセスを簡単にするためのライブラリを作った経験から、Delegated Propertyを使えばもっと簡単にキーとデフォルト値の一元管理や、値取得の簡略化ができそうだなーと思い作ってみました。

まずは実際にどのように使うのか紹介します。

キー/デフォルト値の定義

KonfigModelを継承したobjectでby konfig(<キー>, <デフォルト値>)と指定します。

object ReviewDialogConfig : KonfigModel() {
    val dialogTitle: String by konfig("dialog_title", "Thank you for using")
    val dialogMessage: String by konfig("dialog_message", "Are you enjoy this app?")
    val dialogShowCount: Long by konfig("dialog_show_count", 5)
}

object GeneralConfig : KonfigModel() {
    val shouldUpdate: Boolean by konfig("shoud_update", false)
    val splashImage: String by konfig("splash_image", "http://example.com/example.png")
}

デフォルト値の登録

定義しただけではRemote Configにデフォルト値の登録はされません。
次のようにRemoteKonfig#registerで登録をしてください。

RemoteKonfig.register(ReviewDialogConfig, GeneralConfig)

これで、registerで指定したobjectのキー/デフォルト値がFirebase Remote Configにセットされます。

値の取得

各objectのプロパティにアクセスすることでRemote Configの値を取得することができます。
この際、キーを意識する必要は全くありません。

ReviewDialogConfig.dialogTitle // -> Thank you for using

// サーバから値を取得
FirebaseRemoteConfig.getInstance()
        .fetch()
        .addOnSuccessListener {
            FirebaseRemoteConfig.getInstance().activateFetched()

            ReviewDialogConfig.dialogTitle // -> サーバから取得した新しい値
        }

いかがでしょうか?
キーとデフォルト値の管理がしやすくなった上に、データを用途毎にまとめることができ、値の読出も簡単になりました。
これらの機能はKotlinの標準機能を活用して作ることができます。

なにをやっているか

RemoteConfigから値を取得する

これは皆さんご察しの通り、次のような自作のDelegated Propertyを使っています。
get〜がデータの型で分かれているのでその分の実装が必要なのがちょっと辛いですね…

RemoteKonfigStringDelegate.kt
class RemoteKonfigStringDelegate
internal constructor(private val key: String) : ReadOnlyProperty<Any, String> {
    override fun getValue(thisRef: Any, property: KProperty<*>): String {
        return FirebaseRemoteConfig.getInstance().getString(key)
    }
}

キー/デフォルト値を登録する

byキーワードの後ろで関数が指定されている場合、その関数はインスタンスの生成時に一度だけ実行されます。
それを利用して、各object毎にkonfig関数に渡された値を保持するようにしています。

KonfigModel.kt
open class KonfigModel {

    internal var isRegistered: Boolean = false
    internal var defaultsMap: MutableMap<String, Any>? = null
        get() = if (!isRegistered) {
            field ?: run {
                field = HashMap<String, Any>()
                field
            }
        } else {
            null
        }

    companion object {

        fun KonfigModel.konfig(key: String, default: Long): RemoteKonfigLongDelegate {
            defaultsMap?.put(key, default)
            return RemoteKonfigLongDelegate(key)
        }

        fun KonfigModel.konfig(key: String, default: String): RemoteKonfigStringDelegate {
            defaultsMap?.put(key, default)
            return RemoteKonfigStringDelegate(key)
        }

        fun KonfigModel.konfig(key: String, default: Double): RemoteKonfigDoubleDelegate {
            defaultsMap?.put(key, default)
            return RemoteKonfigDoubleDelegate(key)
        }

        fun KonfigModel.konfig(key: String, default: Boolean): RemoteKonfigBooleanDelegate {
            defaultsMap?.put(key, default)
            return RemoteKonfigBooleanDelegate(key)
        }
    }
}

RemoteKonfig#register実行時に指定されたobject内のdefaultsMapを1つに統合し、FirebaseRemoteConfigに登録しています。

object RemoteKonfig {

    private val defaultValues: MutableMap<String, Any> = HashMap()

    ...

    fun register(vararg model: KonfigModel) {
        model.forEach {
            it.defaultsMap?.forEach { entry ->
                if (!defaultValues.containsKey(entry.key)) {
                    defaultValues.put(entry.key, entry.value)
                    it.isRegistered = true
                    it.defaultsMap = null
                } else {
                    throw IllegalArgumentException("Key ${entry.key} in ${it.javaClass.simpleName} is already registered.")
                }
            }
        }

        FirebaseRemoteConfig.getInstance().setDefaults(defaultValues)
    }

    ...
}

KotlinのDelegated Propertyを使うことで比較的簡単な実装で、キー/デフォルト値を収集し1つにまとめて登録するということが実現できました。
Annotation Processingなどを使用してコード生成を行ったほうが効率の良いコードを作ることができるはずです。
しかし、KotlinのDelegated Propertyは実装の手軽さの割に強力な機能を実装することが可能ではないかと思います。

おわりに

まだまだREADMEもテストもない状態なので公開するのも憚られる状態ですが、弊社のフルKotlinで実装されたアプリ トクバイ の次バージョンに導入しているので実運用するなかで改善と知見を貯めていければいいなと思っています。

ということで今回作ったライブラリはこちら: chibatching/remote-konfig

自分のちょっとした不便をサクッと解決できるKotlinサイコー

この投稿は Kotlin Advent Calendar 201615日目の記事です。