OpenFeature Providers

OpenFeature Android Provider

ConfigDirector Provider for the OpenFeature Kotlin SDK on Android

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.

ArtifactMinimum SDKJava bytecode
com.configdirector:configdirector-openfeature-android-provider2111

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.

Before version 0.6.0 the OpenFeature Kotlin SDK was published as 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")
}

The provider ships Java 11 bytecode, which the OpenFeature Kotlin SDK requires, so your application module has to compile for Java 11 or newer:

build.gradle.kts
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

  1. 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 Keys in the dashboard's navigation panel.
  2. Set the OpenFeature provider, along with the initial evaluation context.
  3. Get a client instance from OpenFeature.
ConfigDirectorSetup.kt
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:

MyApplication.kt
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.

The underlying SDK watches the application behind the context it was given so it can release its network connection while the app is backgrounded. Pass the provider any context — the application behind it is what matters. See the 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:

ConfigDirectorSetup.kt
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:

MyApplication.kt
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:

MainActivity.kt
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 getterConfigDirector config value
getBooleanValueBoolean
getStringValueString or enum
getIntegerValue, getDoubleValueNumber
getObjectValueJSON 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.

MainActivity.kt
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:

OutcomeReasonError code
A value was foundTARGETING_MATCH
The config carries no valueDEFAULT
The config key is unknownERRORFLAG_NOT_FOUND
No config state has arrived yetERRORPROVIDER_NOT_READY
The value does not match the requested typeERRORTYPE_MISMATCH

When a value was found, the variant is ConfigDirector's identifier for that value. In every other case the default value is returned.

MainActivity.kt
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:

DarkModeToggle.kt
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

MainActivity.kt
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 contextConfigDirector user context
The targeting key, or otherwise an id attributeid
The name attributename
The traits structure attributetraits
The boolean anonymous attributeanonymous

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.

In client SDKs (browser and mobile), updating the user context re-establishes a new connection to ConfigDirector servers with the new context. While the new connection is in flight, the OpenFeature SDK reports the 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:

ConfigDirectorSetup.kt
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.