Firebase Remote Config でリアルタイムの更新を取得 

どうも、モバイルエンジニアのEtsuwoです。

先日5/11にGoogle I/O 2023があり新しい技術やデバイスなどが発表されました。

僕は初めてリアタイしようと意気込み４時間仮眠の後臨みましたが、デベロッパー基調講演の途中で力尽きて寝てしまいました。基調講演後のコンテンツを一つ見てから寝ようと思っていたのですが、深夜は中々キツですね...

本記事では、Google I/O 2023で発表されたFirebaseの新機能の一つであるReal-time Remote Configを実際に使ってみます。

# Real-time Remote Configとは

「[What new in Firebase](https://io.google/2023/program/d2aa0ce5-06e2-4f01-b67c-3a5dea8b9caa/intl/ja/)」で紹介されている、Real-time Remote ConfigはFirebase Remote Configに追加された機能です。（API自体が公開されているのは3月末）

![remoteConfig.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383380/04415caa-84c6-e5e0-8c88-201b47e91e1c.png)


Firebase Remote Config Realtime APIを用いてFirebaseコンソール上での設定の変更を**リアルタイム**で受け取ることができます。

これにより、アプリ側から能動的にfetchしなくてもリスナーを登録するだけでコンソール上の設定に合わせてアプリを変更できます。（What new in Firebaseではアプリ上のボタンの色をリアルタイムで変更するデモを行なっていました。）

# これまではできなかったの？

Firebase Cloud Functionと連携することで実現できました。
しかしながら、Real-time Remote Configを受けて、以下のように非推奨になっています。

>Apple プラットフォーム SDK v10.7.0 以降または Android SDK v21.3.0 以降（Firebase BoM v31.3.0 以降）を使用している場合は、アプリの公開時に Remote Config の更新を自動的にフェッチするために、アプリのリアルタイム Remote Config を使用する必要があります。ここで説明する方法は、アプリがリアルタイム Remote Config に移行された後は推奨されず、ベスト プラクティスにもなりません。

[Remote Configの更新をリアルタイムで伝搬する](https://firebase.google.com/docs/remote-config/propagate-updates-realtime?hl=ja&authuser=0)

# 実装編
以前記事にしていますが、Remote Configのよくあるユースケースの一つが「アプリの強制アップデート」です。

[Firebase Remote Configで強制アップデート](https://qiita.com/Etsuwo/items/14c4698f17924923fc29)

今回は強制アップデート機能をAndroidで実装したいと思います。

### 環境
- Android Studio Flamingo | 2022.2.1 Patch 1

### コード

各クラスの説明は以下の通りです。

|クラス|機能|
|---|---|
|FirebaseRemoteConfigProvider|Firebase Remote Configとの通信とパラメータ取得|
|ForceUpdateManager|強制アップデートに関する処理を担当|
|ConfigKey|Firebase Remote Configのパラメータ名を定義|
|MainActivity|強制アップデートが必要な場合アラートを表示|

`FirebaseRemoteConfigProvider`で`Firebase.remoteConfig.addOnConfigUpdateListener()`を呼び出しリスナーを登録しています。これにより、Firebaseコンソール上での変更を検知することができます。

```kotlin:RemoteConfigProvider.kt
class FirebaseRemoteConfigProvider {

    // Remote Configの初期設定
    fun configure() {
        val configSettings = remoteConfigSettings {
            minimumFetchIntervalInSeconds = 1
        }
        Firebase.remoteConfig.setConfigSettingsAsync(configSettings)
    }

    // Remote Configの取得
    fun fetch() {
        Firebase.remoteConfig.fetchAndActivate()
    }

    /*
    Remote Configの変更を監視、変更がある場合 onUpdate()が呼ばれる
    また、本記事ではlistenerをflowに変換
     */
    fun listenConfigUpdate(): Flow<Unit> = callbackFlow {
        Firebase.remoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener {
            override fun onUpdate(configUpdate: ConfigUpdate) {
                Firebase.remoteConfig.activate()
                trySend(Unit)
            }

            override fun onError(error: FirebaseRemoteConfigException) {
                error.printStackTrace()
            }
        })
        awaitClose()
    }

    fun <T> getConfig(key: ConfigKey<T>): T {
        return key.getConfig(Firebase.remoteConfig)
    }
}
```

```kotlin:ForceUpdateManager.kt
class ForceUpdateManager {
    private val remoteConfigProvider = FirebaseRemoteConfigProvider()

    // Firebaseコンソール上の変更を検知して強制アップデートが必要か確認
    val status: Flow<ForceUpdateStatus>
        get() {
            return remoteConfigProvider.listenConfigUpdate().mapLatest {
                calcForceUpdateStatus()
            }
        }

    init {
        remoteConfigProvider.configure()
    }

    // アプリから能動的にRemote Configの値を取りに行く場合に呼び出す
    fun check(): ForceUpdateStatus {
        remoteConfigProvider.fetch()
        return calcForceUpdateStatus()
    }

    // 「現在のバージョン < Remote Configから取得した最低要求バージョン」ならアップデートが必要
    private fun calcForceUpdateStatus(): ForceUpdateStatus {
        val currentVersion = ComparableVersion(BuildConfig.VERSION_NAME)
        val minimumVersionString = remoteConfigProvider.getConfig(ConfigKey.MinimumVersion)
        val minimumVersion = ComparableVersion(minimumVersionString)
        val storeUrl = remoteConfigProvider.getConfig(ConfigKey.StoreUrl)
        return ForceUpdateStatus(storeUrl, currentVersion < minimumVersion)
    }
}

data class ForceUpdateStatus(
    val storeUrl: String,
    val requireForceUpdate: Boolean
)

```

```kotlin:ConfigKey.kt
// 別にenumでも良い
sealed interface ConfigKey<T> {
    val key: String
    fun getConfig(remoteConfig: FirebaseRemoteConfig): T

    sealed interface StringConfigKey: ConfigKey<String> {
        override fun getConfig(remoteConfig: FirebaseRemoteConfig): String {
            return remoteConfig.getString(key)
        }
    }

    // ストアURL
    object StoreUrl: StringConfigKey {
        override val key: String
            get() = "store_url"
    }

    // 最低要求バージョン
    object MinimumVersion: StringConfigKey {
        override val key: String
            get() = "minimum_version"
    }
}
```

```kotlin:MainActivity.kt
class MainActivity : ComponentActivity() {

    private val forceUpdateManager = ForceUpdateManager()

    ...

    // onCreate()等から呼び出す
    private fun configureForceUpdate() {
        // 強制アップデートの有無を初回確認
        val status = forceUpdateManager.check()
        if (status.requireForceUpdate) {
            // アラート表示処理
        }

        // 強制アップデートの有無を監視
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                forceUpdateManager.status.collect { (storeUrl, requireForceUpdate) ->
                    // アラート表示処理
                }
            }
        }
    }
}
```

# 実装上の注意点
気をつけたい点として、`Firebase.remoteConfig.addOnConfigUpdateListener()`で検知できるのは、アプリ起動中の変更のみだということです。（当たり前ですが）
アプリが起動していない時の変更は検知できないので、アプリ起動時に従来通りの方法でチェックしてあげるようにしましょう。

# 参考文献

- [What new in Firebase](https://io.google/2023/program/d2aa0ce5-06e2-4f01-b67c-3a5dea8b9caa/intl/ja/)
- [FirebaseRemoteConfigを使ってみる](https://firebase.google.com/docs/remote-config/get-started?hl=ja&authuser=0&platform=android)
- [FirebaseRemoteConfig（リファレンス）](https://firebase.google.com/docs/reference/kotlin/com/google/firebase/remoteconfig/FirebaseRemoteConfig#addOnConfigUpdateListener(com.google.firebase.remoteconfig.ConfigUpdateListener))