-
Notifications
You must be signed in to change notification settings - Fork 0
Providers
ConfigValues composes one optional local provider and one optional remote provider. At least one must be non-null. Remote values take precedence over local values when both are present for the same key.
ConfigValues
├── LocalConfigValueProvider (optional, but at least one required)
└── RemoteConfigValueProvider (optional, but at least one required)
Stores overrides in a plain in-memory Map. No setup, no dependencies. Included in featured-core.
Use cases: unit tests, Compose previews, ephemeral runtime overrides.
Limitation: values are lost when the process terminates.
val configValues = ConfigValues(
localProvider = InMemoryConfigValueProvider(),
)Programmatic set and reset:
val provider = InMemoryConfigValueProvider()
provider.set(GeneratedLocalFlags.newCheckout, true) // override
provider.resetOverride(GeneratedLocalFlags.newCheckout) // revert to default
provider.clear() // remove all overrides (no Flow signal)set and resetOverride notify active observe flows immediately. clear does not emit change signals.
Persists flag overrides using Jetpack DataStore. Recommended for Android.
Dependency: featured-datastore-provider
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import dev.androidbroadcast.featured.datastore.DataStoreConfigValueProvider
val dataStore = PreferenceDataStoreFactory.create {
context.dataStoreFile("feature_flags.preferences_pb")
}
val configValues = ConfigValues(
localProvider = DataStoreConfigValueProvider(dataStore),
)Android-only. Persists overrides using SharedPreferences.
Dependency: featured-sharedpreferences-provider
import dev.androidbroadcast.featured.sharedpreferences.SharedPreferencesConfigValueProvider
val prefs = context.getSharedPreferences("feature_flags", Context.MODE_PRIVATE)
val configValues = ConfigValues(
localProvider = SharedPreferencesConfigValueProvider(prefs),
)JVM/Desktop only. Persists overrides using java.util.prefs.Preferences. Storage is OS-specific: the registry on Windows, a plist on macOS, and ~/.java on Linux.
Dependency: featured-javaprefs-provider
Supported types: String, Int, Long, Float, Double, Boolean.
import dev.androidbroadcast.featured.javaprefs.JavaPreferencesConfigValueProvider
val provider = JavaPreferencesConfigValueProvider()
// Or with a custom node:
val provider = JavaPreferencesConfigValueProvider(
node = Preferences.userRoot().node("com/example/myapp/flags")
)
val configValues = ConfigValues(localProvider = provider)Register a TypeConverter for custom types:
provider.registerConverter<Theme>(
TypeConverter(fromString = { Theme.valueOf(it) }, toString = { it.name })
)iOS/macOS only. Persists flag overrides using NSUserDefaults. Values survive process restarts and are stored in the standard user defaults domain or a custom app-group suite.
Dependency: featured-nsuserdefaults-provider
Supported types natively: String, Int, Long, Float, Double, Boolean. Custom types (e.g. enums) are supported via registerConverter.
import dev.androidbroadcast.featured.nsuserdefaults.NSUserDefaultsConfigValueProvider
// Standard user defaults
val provider = NSUserDefaultsConfigValueProvider()
// Custom suite (e.g. for App Groups)
val provider = NSUserDefaultsConfigValueProvider(suiteName = "com.example.app.flags")
val configValues = ConfigValues(localProvider = provider)set and resetOverride notify active observe flows immediately. clear removes all keys but does not emit change signals.
Register a TypeConverter for custom types such as enums:
import dev.androidbroadcast.featured.nsuserdefaults.registerConverter
import dev.androidbroadcast.featured.enumConverter
provider.registerConverter(enumConverter<CheckoutVariant>())The inline registerConverter<T>(converter) overload avoids passing KClass explicitly. Non-primitive values are serialized to String via the converter and stored as string objects in NSUserDefaults. This mirrors the DataStoreConfigValueProvider and JavaPreferencesConfigValueProvider converter API.
Wraps the ConfigCat KMP SDK. Remote values override local values when present.
Dependency: featured-configcat-provider
Supported types: Boolean, String, Int, Long, Double, Float.
import com.configcat.ConfigCatClient
import dev.androidbroadcast.featured.configcat.ConfigCatConfigValueProvider
val client = ConfigCatClient(sdkKey = "YOUR_SDK_KEY")
val provider = ConfigCatConfigValueProvider(client)
val configValues = ConfigValues(remoteProvider = provider)
// Fetch latest values — suspend function, call from a coroutine
lifecycleScope.launch { configValues.fetch() }fetch() calls ConfigCatClient.forceRefresh() and activates updated values immediately.
Wraps Firebase Remote Config. Remote values override local values when present.
Dependency: featured-firebase-provider
Supported types natively: String, Boolean, Int, Long, Double, Float. Enums are resolved by name automatically.
import dev.androidbroadcast.featured.firebase.FirebaseConfigValueProvider
val configValues = ConfigValues(
localProvider = DataStoreConfigValueProvider(dataStore),
remoteProvider = FirebaseConfigValueProvider(),
)
// Fetch and activate on app start — suspend function, call from a coroutine
lifecycleScope.launch { configValues.fetch() }Pass a custom FirebaseRemoteConfig instance if you manage the Firebase lifecycle yourself:
FirebaseConfigValueProvider(remoteConfig = FirebaseRemoteConfig.getInstance())Fetch strategy:
-
configValues.fetch()callsfetchAndActivate()by default — values are immediately available after the call returns. - Pass
activate = falseto defer activation:configValues.fetch(activate = false). - A
FirebaseConfigExceptionis thrown on network errors or service unavailability; implement exponential backoff for retries.
FirebaseConfigValueProvider implements InitializableConfigValueProvider. Calling configValues.initialize() invokes FirebaseRemoteConfig.ensureInitialized(), which loads the on-disk cache (activated values, fetched-but-not-yet-activated values, and defaults) into memory without a network call. This makes previously persisted Remote Config values readable via getValueCached immediately at app start, before the first fetch().
// Recommended startup sequence
lifecycleScope.launch {
configValues.initialize() // loads disk cache — no network
// getValueCached is now reliable for all warmed params
configValues.fetch() // network fetch + activate
}initialize() does not activate fetched-but-unactivated config — call fetch() for that.
Firebase project setup:
- Add
google-services.json(Android) orGoogleService-Info.plist(iOS) to the project. - In the Firebase console, navigate to Remote Config and add parameters whose keys match
ConfigParam.keyvalues. - Publish, then call
configValues.initialize()followed byconfigValues.fetch()at app start.
ConfigValues accepts an onProviderError: (Throwable) -> Unit callback that is invoked whenever a provider operation fails (a get, observe, set, resetOverride, or fetch that throws). The default behavior logs the error to the platform log:
- Android:
Log.w("Featured", …) - iOS:
NSLog - JVM / Desktop:
System.err
To silence provider errors entirely, pass onProviderError = {}. To route through your own logger:
val configValues = ConfigValues(
localProvider = DataStoreConfigValueProvider(dataStore),
remoteProvider = FirebaseConfigValueProvider(),
onProviderError = { e -> Timber.w(e, "Featured provider error") },
)When both local and remote providers return a value for the same key, remote wins. If only one provider returns a value, that value is used. If neither returns a value, the ConfigParam.defaultValue is used and the source is ConfigValue.Source.DEFAULT.
| Local | Remote | Result | Source |
|---|---|---|---|
| — | — | defaultValue |
DEFAULT |
| present | — | local value | LOCAL |
| — | present | remote value | REMOTE |
| present | present | remote value | REMOTE |
Implement LocalConfigValueProvider for a writable, observable local backend:
class MyLocalProvider : LocalConfigValueProvider {
override suspend fun <T : Any> get(param: ConfigParam<T>): ConfigValue<T>? { … }
override fun <T : Any> observe(param: ConfigParam<T>): Flow<ConfigValue<T>> { … }
override suspend fun <T : Any> set(param: ConfigParam<T>, value: T) { … }
override suspend fun <T : Any> resetOverride(param: ConfigParam<T>) { … }
override suspend fun clear() { … }
}observe emission contract: the returned Flow must emit exactly one value immediately upon collection (the current stored value, or null if absent), then emit again on every subsequent change. This is a normative contract — ConfigValues relies on the initial emission to build its reactive pipeline.
Implement RemoteConfigValueProvider for a fetch-based remote backend:
class MyRemoteProvider : RemoteConfigValueProvider {
override suspend fun fetch(activate: Boolean) { /* fetch from your backend */ }
override suspend fun <T : Any> get(param: ConfigParam<T>): ConfigValue<T>? { … }
override fun <T : Any> observe(param: ConfigParam<T>): Flow<ConfigValue<T>> { … }
}Implement InitializableConfigValueProvider in addition to RemoteConfigValueProvider if your backend supports loading a local disk/memory cache before the first network fetch:
class MyRemoteProvider : RemoteConfigValueProvider, InitializableConfigValueProvider {
override suspend fun initialize() { /* load on-disk cache into memory */ }
// … rest of RemoteConfigValueProvider
}When implemented, ConfigValues.initialize() calls initialize() on the remote provider and then refreshes every param previously registered via warmUp.