Firebase Remote Config is a really useful tool for implementing feature flags and experiments in your apps. It's also very simple to integrate into a Flutter application and read a value:
FirebaseRemoteConfig.instance.getBool('show_new_login_flow');
However, when working on a large team, or when you have a large number of parameters, the simple implementation above can feel a bit precarious. If the config parameter is used in multiple places, we have to write the same string key every time; it's far too easy to make a spelling mistake and introduce a bug. Additionally, information about the default value of this parameter is elsewhere in our code; how do we know where to look? It's also difficult to see a list of all the remote config parameters we have available.
I came up with an implementation of Remote Config which I think delivers better scalability and robustness. It involves setting up all your parameters as enums, the values of which encapsulate everything you need to know about a parameter.
Setting up the enum
Thanks to Dart's recent improvements to its enum support, we can implement a really useful enum type for our parameters:
enum RemoteConfigParameter<T> {
showNewLoginFlow<bool>(key: 'show_new_login_flow', defaultValue: false),
animationSpeed<int>(key: 'animation_speed', defaultValue: 100);
const RemoteConfigParameter({
required this.key,
required this.defaultValue,
});
final String key;
final T defaultValue;
T getValue(FirebaseRemoteConfig remoteConfig) {
// TODO
}
}
We have now encapsulated the keys and default values for a couple of Remote Config parameters. To configure a new parameter, it's a simple case of adding a new value for this enum.
This also gives us a neat list of all the existing remote config parameters. When we add new ones or remove old ones, it'll show up really nicely in our git diffs, making changes much clearer.
Initialising default values
We can use this enum to get a Map
of all our parameters and their default values. Let's add this static method to our enum class:
static Map<String, dynamic> get defaultValues => Map.fromEntries(
values.map(
(param) => MapEntry(param.key, param.defaultValue),
),
);
In our initialisation code for Remote Config, we then simply pass the defaultValues
value into the setDefaults
method:
Future<void> main() async {
final FirebaseRemoteConfig remoteConfig;
await remoteConfig.setDefaults(RemoteConfigParameter.defaultValues);
await remoteConfig.fetchAndActivate();
// run app
}
Retrieving a Remote Config value
Let's create an implementation for the getValue
method we stubbed earlier. Remote Config only supports a handful of types, so we can create a switch
statement to call the appropriate method:
T getValue(FirebaseRemoteConfig config) {
switch (T) {
case bool:
return config.getBool(key) as T;
case int:
return config.getInt(key) as T;
case double:
return config.getDouble(key) as T;
case String:
return config.getString(key) as T;
default:
throw UnsupportedError('Unsupported type: $T');
}
}
In our widgets, we then simply have to call something like this to get our value:
RemoteConfigParameter.showNewLoginFlow.getValue(FirebaseRemoteConfig.instance);
Bonus: Using Riverpod to improve syntax
In my project, which uses Riverpod, I've created this extension on WidgetRef
to improve the syntax above:
extension RemoteConfigValues on WidgetRef {
T readRemoteConfigValue<T>(RemoteConfigParameter<T> param) {
return param.getValue(read(remoteConfigProvider));
}
}
We can then read a value like this:
ref.readRemoteConfigValue(RemoteConfigParameter.showNewLoginFlow);
I think this is more readable and concise, and does a good job of abstracting the underlying implementation.