OpenFeature Android Provider
Introduction
The OpenFeature Android Provider is intended to be used in combination with the OpenFeature Kotlin SDK in Android applications. The provider wraps the ConfigDirector Android SDK.
| Artifact | Minimum SDK | Java bytecode |
|---|---|---|
com.configdirector:configdirector-openfeature-android-provider | 21 | 11 |
It requires version 0.8.0 or newer of the OpenFeature Kotlin SDK, dev.openfeature:kotlin-sdk. That SDK's provider contract is built on suspend functions and flows, so the provider is written for Kotlin; a Java application uses the Android SDK directly instead.
dev.openfeature:android-sdk, in the package dev.openfeature.sdk. The provider depends on the current artifact, dev.openfeature:kotlin-sdk, whose package is dev.openfeature.kotlin.sdk. Update your imports if you are upgrading from the old one.Config evaluation is synchronous and reads config state the provider already holds in memory, which is what makes it safe to call from a composable or from the main thread. Telemetry about those evaluations is aggregated and reported on its own coroutine, so evaluating a config never waits on the network.
Installation
The provider can be installed from Maven Central: https://central.sonatype.com/artifact/com.configdirector/configdirector-openfeature-android-provider
The OpenFeature Kotlin SDK and the ConfigDirector Android SDK are included as transitive dependencies.
dependencies {
implementation("com.configdirector:configdirector-openfeature-android-provider:1.0.0")
}
dependencies {
implementation 'com.configdirector:configdirector-openfeature-android-provider:1.0.0'
}
[versions]
configdirector-openfeature = "1.0.0"
[libraries]
openfeature-provider = { module = "com.configdirector:configdirector-openfeature-android-provider", version.ref = "configdirector-openfeature" }
The provider ships Java 11 bytecode, which the OpenFeature Kotlin SDK requires, so your application module has to compile for Java 11 or newer:
android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11
}
}
Configure and initialize the client
- Create an instance of the provider providing an Android context and your client SDK key. You can retrieve a client SDK key for each environment under
SDK Keysin the dashboard's navigation panel. - Set the OpenFeature provider, along with the initial evaluation context.
- Get a client instance from OpenFeature.
import com.configdirector.openfeature.ConfigDirectorProvider
import dev.openfeature.kotlin.sdk.ImmutableContext
import dev.openfeature.kotlin.sdk.OpenFeatureAPI
// Creating the provider makes no network calls. It throws only when the SDK key is
// blank or an option holds an unusable value.
val provider = ConfigDirectorProvider(applicationContext, "YOUR-CLIENT-SDK-KEY")
// Connects and waits until config values are received, or until it times out.
// After a timeout the provider keeps trying to connect in the background.
OpenFeatureAPI.setProviderAndWait(provider, ImmutableContext(targetingKey = "user-123"))
val client = OpenFeatureAPI.getClient()
The constructor throws a ConfigDirectorValidationException in only two cases, both of which are programming errors rather than runtime conditions: a blank client SDK key, and an option that cannot be used, such as a negative timeout or a base URL that is not absolute. Connection failures are not thrown from it.
setProviderAndWait is a suspend function that returns once the initial config state arrives or the configured timeout elapses. When it times out, the provider's initialization fails with a ProviderNotReadyError, the OpenFeature SDK reports the Error status, and the provider keeps connecting in the background; it then emits ProviderReady as soon as config state arrives. Until then, flags resolve to their default values with the PROVIDER_NOT_READY error code.
Most applications register the provider once during startup and let it live for the lifetime of the app. That usually means doing so from the Application, with setProvider, which returns at once and initializes in the background:
import android.app.Application
import com.configdirector.openfeature.ConfigDirectorProvider
import dev.openfeature.kotlin.sdk.ImmutableContext
import dev.openfeature.kotlin.sdk.OpenFeatureAPI
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
val provider = ConfigDirectorProvider(this, "YOUR-CLIENT-SDK-KEY")
OpenFeatureAPI.setProvider(provider, initialContext = ImmutableContext(targetingKey = "user-123"))
}
}
Flag evaluations are possible once OpenFeatureAPI.getStatus() is Ready; OpenFeatureAPI.statusFlow is the same thing as a Flow, which is how a screen follows it. See reading configs in Jetpack Compose.
connection options of the Android SDK to manage that yourself instead.Additional configuration options
Additional configuration options can be passed into the provider as ClientOptions in the optional third argument of the constructor. They are the Android SDK's own options, so configuring the provider needs the SDK's types alongside it.
For example, the metadata can be provided like this:
import com.configdirector.ClientOptions
import com.configdirector.openfeature.ConfigDirectorProvider
val provider = ConfigDirectorProvider(
applicationContext,
"YOUR-CLIENT-SDK-KEY",
ClientOptions.build {
metadata("YOUR-APP-NAME", "1.0.2")
},
)
The provider accepts the same metadata, connection and logger options as the Android SDK client, refer to the additional configuration options section of the Android SDK for a full list.
Shut down
Shutting OpenFeature down, or replacing the provider, closes the provider, which closes its connection and reports any pending telemetry:
OpenFeatureAPI.shutdown()
An instance serves a single registration. After it has been shut down, create a new one rather than registering it again.
Most applications never need to do this — the provider lives as long as the process, and Android reclaims everything when the process ends. Shut it down when an application wants the connection gone before that, on sign-out for instance.
Retrieve config values
To retrieve config values, use the OpenFeature client:
val booleanValue = client.getBooleanValue("my-config-key", false)
val stringValue = client.getStringValue("my-string-config-key", "Default")
Each OpenFeature getter maps to a ConfigDirector config type:
| OpenFeature getter | ConfigDirector config value |
|---|---|
getBooleanValue | Boolean |
getStringValue | String or enum |
getIntegerValue, getDoubleValue | Number |
getObjectValue | JSON object or JSON array |
getIntegerValue takes and returns an Int, as the OpenFeature Kotlin SDK defines it; a value written as a decimal is truncated. Every config can be read with getStringValue, including a JSON config's raw document.
For getObjectValue, the default value decides the type the config is read as: a Value.Structure reads a JSON config as a structure, a Value.List reads one as a list, and a Value.String, Value.Integer, Value.Double or Value.Boolean reads the config as that type. A Value.Null or Value.Instant default names no type to read, so it resolves to itself with the TYPE_MISMATCH error code.
import dev.openfeature.kotlin.sdk.Value
val settings = client.getObjectValue("my-json-config-key", Value.Structure(emptyMap()))
val theme = settings.asStructure()?.get("theme")?.asString()
For additional information regarding the OpenFeature client refer to the OpenFeature Kotlin SDK documentation.
Evaluation details
The detailed getters of the OpenFeature client, such as getBooleanDetails, report why an evaluation produced the value that it did:
| Outcome | Reason | Error code |
|---|---|---|
| A value was found | TARGETING_MATCH | |
| The config carries no value | DEFAULT | |
| The config key is unknown | ERROR | FLAG_NOT_FOUND |
| No config state has arrived yet | ERROR | PROVIDER_NOT_READY |
| The value does not match the requested type | ERROR | TYPE_MISMATCH |
When a value was found, the variant is ConfigDirector's identifier for that value. In every other case the default value is returned.
val details = client.getBooleanDetails("my-config-key", false)
Log.i("MyApp", "my-config-key is ${details.value} because ${details.reason} (variant ${details.variant})")
Reading configs in Jetpack Compose
The getters are synchronous and make no network calls, so they are safe to call directly from a composable. That gives you the current value, but the composable will not recompose on its own when the value changes.
To have a composable follow a config, re-read it whenever the SDK status moves or the provider reports a configuration change. OpenFeatureAPI.statusFlow carries the status, and OpenFeatureAPI.observe() is a Flow of the provider's events:
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.openfeature.kotlin.sdk.OpenFeatureAPI
import dev.openfeature.kotlin.sdk.OpenFeatureStatus
import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents
@Composable
fun DarkModeToggle() {
val status by OpenFeatureAPI.statusFlow.collectAsState(initial = OpenFeatureStatus.NotReady)
var generation by remember { mutableIntStateOf(0) }
// Counts every configuration change, so the read below runs again after each one.
LaunchedEffect(Unit) {
OpenFeatureAPI.observe<OpenFeatureProviderEvents.ProviderConfigurationChanged>()
.collect { generation++ }
}
// Re-read when the status moves -- to Ready once config state arrives, for instance --
// and after every configuration change.
val darkMode = remember(status, generation) {
OpenFeatureAPI.getClient().getBooleanValue("dark-mode", false)
}
Switch(checked = darkMode, onCheckedChange = null)
}
Until the status is Ready, the read returns the default value, which is a good moment to show a loading state rather than flashing the in-code default on a slow connection.
Update the user context
import dev.openfeature.kotlin.sdk.ImmutableContext
import dev.openfeature.kotlin.sdk.OpenFeatureAPI
import dev.openfeature.kotlin.sdk.Value
OpenFeatureAPI.setEvaluationContextAndWait(
ImmutableContext(
targetingKey = "12345", // In OpenFeature, the targeting key represents the context's user ID
attributes = mapOf(
"name" to Value.String("Example User"),
// Any arbitrary traits which can be referenced in targeting rules
"traits" to Value.Structure(mapOf("region" to Value.String("North America"))),
),
),
)
The evaluation context maps onto the ConfigDirector user context as follows:
| OpenFeature evaluation context | ConfigDirector user context |
|---|---|
The targeting key, or otherwise an id attribute | id |
The name attribute | name |
The traits structure attribute | traits |
The boolean anonymous attribute | anonymous |
Any other attribute is ignored. Put the values your targeting rules depend on inside traits. Traits are JSON-shaped values; a context the SDK could not send, such as one holding a trait that is not a finite number, is rejected with an InvalidContextError.
Reconciling status and config values continue to evaluate to the currently cached values from the prior user context.Awaiting
setEvaluationContextAndWait will wait until the new config values are downloaded or the connection times out. In the case of a timeout, the provider reports the Error status and continues to attempt to connect with the new context in the background, then emits ProviderReady once it succeeds.setEvaluationContext, without the wait, returns at once and reconciles in the background; follow OpenFeatureAPI.statusFlow to know when the new values are in effect.
For additional information regarding the OpenFeature client refer to the OpenFeature Kotlin SDK documentation.
Events
The provider publishes its events through OpenFeatureAPI.observe(), a Flow filtered by event type. It emits ProviderConfigurationChanged whenever configs are updated on the dashboard or via the admin API, carrying the keys of the configs in the update:
import dev.openfeature.kotlin.sdk.OpenFeatureAPI
import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents
lifecycleScope.launch {
OpenFeatureAPI.observe<OpenFeatureProviderEvents.ProviderConfigurationChanged>().collect { event ->
Log.i("MyApp", "Configs updated: ${event.eventDetails?.flagsChanged}")
}
}
lifecycleScope.launch {
OpenFeatureAPI.statusFlow.collect { status ->
Log.i("MyApp", "OpenFeature status: $status")
}
}
It emits ProviderReady when config state arrives after initialization or a context change has already timed out, which moves the status from Error back to Ready.