avatarMaxi Rosson

Summary

The provided content outlines a robust approach to managing in-app default parameter values for Firebase Remote Config, emphasizing the importance of setting appropriate default values and addressing potential issues with asynchronous value application.

Abstract

The article discusses the significance of properly configuring in-app default values for Firebase Remote Config parameters to ensure that applications function correctly before connecting to the Remote Config backend. It explains the concept of in-app default parameter values and the process of setting them within the remoteConfig object. The author advises against using XML for defining defaults due to modularization difficulties. The article also highlights the potential pitfalls of asynchronous default value application and proposes a solution involving a RemoteConfigParameter interface, implementation with enums, a StaticFirebaseRemoteConfigValue class, and a RemoteConfigLoader. This solution ensures that default values are always available, even before the app fetches server-side values, and provides a more stable and testable codebase. The proposed approach also includes logging and error reporting mechanisms to enhance debugging and monitoring.

Opinions

  • The author suggests that using an XML to define default values is not recommended because it complicates the modularization of remote config parameters.
  • It is crucial to select default values that represent the most stable configuration for the app to avoid potential issues during refactoring or feature rollouts.
  • The asynchronous nature of setting default values can lead to the use of static type values instead of the intended in-app defaults, which must be addressed to maintain app stability.
  • The proposed solution aims to prevent developers from forgetting to assign in-app default values and to mitigate the risks associated with asynchronous default value application.
  • The author emphasizes the benefits of the solution, including easier modularization, consolidation of parameter keys, increased code testability, and the addition of useful logging for parameter values.
  • The article suggests that integrating the RemoteConfigLoader with dependency injection frameworks like Dagger/Hilt can improve the maintainability and testability of the code.
  • The author values the importance of logging and

Firebase Remote Config: in-app default parameter values

Learn how to properly configure the in-app default values for your Firebase Remote Config parameters

In-app default parameter values concept

When using Firebase Remote Config, you define one or more parameters (key-value pairs) and provide in-app default values for those parameters. You can override in-app default values by defining server-side parameter values.

You can set in-app default parameter values in the remoteConfig object, so that your app behaves as intended before it connects to the Remote Config backend, and so that default values are available if none are set in the backend.

Firebase.remoteConfig.apply {
    setDefaultsAsync(mapOf(
        "key" to "default_value"
    ))
}

Firebase also supports defining those default values using an XML. I don’t recommend that approach because makes the modularization of your remote config parameters harder.

Wisely pick your default values

It’s very important to wisely pick the correct default value for each parameter. You should always use a value that represents the more stable configuration for your app.

For example, suppose you have a stable algorithm working on production and you decide to refactor it to clean up and improve your source code. You could add a remote config parameter to decide which algorithm to use: the old one or the new one. So, you can gradually roll out the new one and measure the impact on your users. In this case, the old (and stable) algorithm should be referenced on the in-app default value for the remote config parameter. This approach guarantees you always use the old algorithm if the user couldn’t fetch the server-side value yet.

Defining defaults is not enough

But defining your in-app defaults is not enough. Let’s understand why.

In your app, parameter values are returned by get methods according to the following priority list:

  1. If a value was fetched from the backend and then activated, the app uses the fetched value. Activated parameter values are persistent.
  2. If no value was fetched from the backend, or if values fetched from the Remote Config backend have not been activated, the app uses the in-app default value.
  3. If no in-app default value has been set, the app uses a static type value (such as 0 for int and false for boolean).

The last item causes an issue in certain situations (even if you correctly defined the in-app default values). Given that the setDefaultsAsync is asynchronous, after installing the app for the first time, you could try to get a remote config parameter even before your defaults are applied and cached. So, the static type values will be used (such as 0 for int and false for boolean), which could be different to your in-app default values.

The proposed solution

The following proposal fixes two problems:

  • devs forget to assign in-app default values for all the parameters
  • the asynchronous application of in-app default values

It also provides some other advantages:

  • provides an easier way to modularize your parameters, so they are located in the proper modules.
  • consolidate the parameters keys constants avoiding duplications
  • makes your codes more testable
  • add useful logging about the parameters

You just need to follow these 5 steps:

1. Define a RemoteConfigParameter interface

Define a RemoteConfigParameter interface to be implemented by all your remote config parameters. This interface forces you to define a default value for each parameter.

package com.dipien.firebase.remoteconfig

interface RemoteConfigParameter {

    fun getKey(): String

    fun getDefaultValue(): Any

}

2. Implement the RemoteConfigParameter interface

Implement the RemoteConfigParameter interface with your parameters. Using an enum could be a good idea. You can have multiple enums, so you can group your related parameters and put them on the proper modules.

enum class SampleRemoteConfigParameter constructor(private val defaultValue: Any) : RemoteConfigParameter {

    SAMPLE_BOOLEAN_PARAMETER(true),
    SAMPLE_STRING_PARAMETER("value1"),
    SAMPLE_LONG_PARAMETER(5),
    SAMPLE_DOUBLE_PARAMETER(1.2);
  
    override fun getKey(): String {
        return name.lowercase()
    }

    override fun getDefaultValue(): Any {
        return defaultValue
    }
}

3. Define a StaticFirebaseRemoteConfigValue

This class will return the default values defined on the RemoteConfigParameter. It will be used in the next step.

package com.dipien.firebase.remoteconfig

import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.firebase.remoteconfig.FirebaseRemoteConfigValue

class StaticFirebaseRemoteConfigValue(private val remoteConfigParameter: RemoteConfigParameter) : FirebaseRemoteConfigValue {

    override fun asLong(): Long {
        return remoteConfigParameter.getDefaultValue().toString().toLong()
    }

    override fun asDouble(): Double {
        return remoteConfigParameter.getDefaultValue().toString().toDouble()
    }

    override fun asString(): String {
        return remoteConfigParameter.getDefaultValue().toString()
    }

    override fun asByteArray(): ByteArray {
        return remoteConfigParameter.getDefaultValue().toString().toByteArray()
    }

    override fun asBoolean(): Boolean {
        return remoteConfigParameter.getDefaultValue().toString().toBoolean()
    }

    override fun getSource(): Int {
        return FirebaseRemoteConfig.VALUE_SOURCE_STATIC
    }
}

4. Define a RemoteConfigLoader

Define a RemoteConfigLoader to initialize the remote config and access the remote config values.

package com.dipien.firebase.remoteconfig

import android.content.Context
import android.util.Log
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.ktx.Firebase
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.firebase.remoteconfig.FirebaseRemoteConfigValue
import com.google.firebase.remoteconfig.ktx.remoteConfig

open class FirebaseRemoteConfigLoader(
    private val applicationContext: Context,
    private val remoteConfigParameters: Set<RemoteConfigParameter>
) {

    companion object {
        private val TAG = FirebaseRemoteConfigLoader::class.simpleName
    }

    private val remoteConfig: FirebaseRemoteConfig by lazy {
        Firebase.remoteConfig.apply {
            val defaults = mutableMapOf<String, Any>()
            remoteConfigParameters.forEach {
                defaults[it.getKey()] = it.getDefaultValue()
            }
            setDefaultsAsync(defaults)
        }
    }

    override fun getString(remoteConfigParameter: RemoteConfigParameter): String {
        return getValue(remoteConfigParameter).asString()
    }

    override fun getStringList(remoteConfigParameter: RemoteConfigParameter): List<String> {
        return getString(remoteConfigParameter).split(STRING_LIST_SEPARATOR)
    }

    override fun getBoolean(remoteConfigParameter: RemoteConfigParameter): Boolean {
        return getValue(remoteConfigParameter).asBoolean()
    }

    override fun getLong(remoteConfigParameter: RemoteConfigParameter): Long {
        return getValue(remoteConfigParameter).asLong()
    }

    override fun getDouble(remoteConfigParameter: RemoteConfigParameter): Double {
        return getValue(remoteConfigParameter).asDouble()
    }

    private fun getValue(parameter: RemoteConfigParameter): FirebaseRemoteConfigValue {
        if (!remoteConfigParameters.contains(parameter)) {
            val message = "The Firebase Remote Config Parameter [${parameter.getKey()}] default value is not registered"
            Log.e(TAG, message)
            FirebaseCrashlytics.getInstance().recordException(RuntimeException(message))
        }
        var configValue = remoteConfig.getValue(parameter.getKey())
        if (configValue.source == FirebaseRemoteConfig.VALUE_SOURCE_STATIC) {
            configValue = StaticFirebaseRemoteConfigValue(parameter)
        }
        Log.i(TAG, "Read Firebase Remote Config Parameter. Key [${parameter.getKey()}] | Value [${configValue.asString()}] | Source [${configValue.source}]")
        return configValue
    }
}

The key to solving the async issue is the following. You can ask Firebase about the source of each parameter value:

// Indicates that the value returned is the static default value.
public static final int VALUE_SOURCE_STATIC = 0;

// Indicates that the value returned was retrieved from the defaults 
// set by the client.
public static final int VALUE_SOURCE_DEFAULT = 1;

// Indicates that the value returned was retrieved from the Firebase 
// Remote Config Server.
public static final int VALUE_SOURCE_REMOTE = 2;

So, every time Firebase returns VALUE_SOURCE_STATIC, we ignore the value returned by Firebase and use the default defined in the enum instead. That’s why we have the StaticFirebaseRemoteConfigValue class.

Some other considerations about this loader:

  • If you use Dagger/Hilt, I recommend annotating the RemoteConfigLoader and injecting it into the places you need to use remote config. You could also define an interface and create a fake loader for your tests.
  • The RemoteConfigLoader receives in the constructor a set with all the parameters. Remember to send all the enum values here, if you have more than once.
  • If you ask for the value of a parameter not passed in the constructor, it will report an exception on Crashlytics
  • Every time you ask for the value of a parameter, it will be logged on Logcat, so you can see the value and the source.

5. Use the RemoteConfigLoader

Use the RemoteConfigLoader to access the remote config parameter values.

For example:

val value = remoteConfigLoader.getString(
  SampleRemoteConfigParameter.SAMPLE_STRING_PARAMETER)
Follow us for more productivity tools & ideas for Android, Kotlin & Gradle projects.

Support Us

There are different ways to support our work:

  • With Bitcoin Lightning using Alby:
  • With PayPal or a credit card using Ko-fi.

Related Articles

If you enjoyed this article, you might get value out of these ones as well!

Firebase
Firebase Remote Config
Android
Android App Development
Android Development
Recommended from ReadMedium