---
title: "OpenFeature Android Provider"
description: "ConfigDirector Provider for the OpenFeature Kotlin SDK on Android"
canonical_url: "https://docs.configdirector.com/sdks/openfeature/android"
---
# OpenFeature Android Provider

> ConfigDirector Provider for the OpenFeature Kotlin SDK on Android

<div hidden="true" dataTitleIcon="openfeature-android">



</div>

<external-badges package-url="/sdks/maven-repository" registry="maven" repo="android-sdk/tree/main/configdirector-openfeature-android-provider">



</external-badges>

## Introduction

The OpenFeature Android Provider is intended to be used in combination with the [OpenFeature Kotlin SDK](https://openfeature.dev/docs/reference/sdks/client/kotlin/) in Android applications. The provider wraps the ConfigDirector [Android SDK](https://docs.configdirector.com/sdks/mobile/android).

<table>
<thead>
  <tr>
    <th>
      Artifact
    </th>
    
    <th>
      Minimum SDK
    </th>
    
    <th>
      Java bytecode
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        com.configdirector:openfeature-android-provider
      </code>
    </td>
    
    <td>
      21
    </td>
    
    <td>
      11
    </td>
  </tr>
</tbody>
</table>

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](https://docs.configdirector.com/sdks/mobile/android) directly instead.

<note>

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.

</note>

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 is published to the [ConfigDirector Maven repository](https://docs.configdirector.com/sdks/maven-repository). Add the repository to `settings.gradle.kts` or `settings.gradle`, next to `google()` and `mavenCentral()`:

<code-group>

```kotlin [settings.gradle.kts]
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        exclusiveContent {
            forRepository {
                maven {
                    name = "ConfigDirector"
                    url = uri("https://maven.configdirector.com")
                }
            }
            filter {
                includeGroup("com.configdirector")
            }
        }
    }
}
```

```groovy [settings.gradle]
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        exclusiveContent {
            forRepository {
                maven {
                    name = 'ConfigDirector'
                    url = 'https://maven.configdirector.com'
                }
            }
            filter {
                includeGroup 'com.configdirector'
            }
        }
    }
}
```

</code-group>

Then add the dependency to your app module:

<code-group>

```kotlin [build.gradle.kts]
dependencies {
    implementation("com.configdirector:openfeature-android-provider:1.6.1")
}
```

```groovy [build.gradle]
dependencies {
    implementation 'com.configdirector:openfeature-android-provider:1.6.1'
}
```

```toml [libs.versions.toml]
[versions]
configdirector-openfeature = "1.6.1"

[libraries]
openfeature-provider = { module = "com.configdirector:openfeature-android-provider", version.ref = "configdirector-openfeature" }
```

</code-group>

The OpenFeature Kotlin SDK and the ConfigDirector Android SDK are included as transitive dependencies.

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

```kotlin [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.

```kotlin [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 outside offline mode, 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:

```kotlin [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](#reading-configs-in-jetpack-compose).

<note>

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](https://docs.configdirector.com/sdks/mobile/android#connection) to manage that yourself instead.

</note>

### 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:

```kotlin [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`, `caching` and `logger` options as the Android SDK client, [refer to the additional configuration options section of the Android SDK for a full list](https://docs.configdirector.com/sdks/mobile/android#additional-configuration-options).

That includes [running without a connection](https://docs.configdirector.com/sdks/mobile/android#run-without-a-connection): created with `ConfigDirectorProvider(applicationContext, options)` and no client SDK key, the provider serves the values you give it without connecting to ConfigDirector, and building options whose values cannot be used throws a `ConfigDirectorValidationException`.

### Values kept on the device

By default, the provider keeps the last values it received from ConfigDirector on the device, in the app's `SharedPreferences`, and serves them on the next launch of the same context until live values arrive. An app opened without a network, after at least one successful connection, resolves flags to the values ConfigDirector last sent instead of the default values passed to the OpenFeature client.

Registering the provider still waits for live values or the connection `timeoutMillis`: `setProviderAndWait` returns after the timeout as it does without kept values, and the provider then reports the `Stale` status, with a `ProviderStale` event, while it serves the kept values. It emits `ProviderStale` from the moment the kept values are loaded, and again right after the `Error` status a timeout reports. Once live values arrive, the provider emits `ProviderReady`, the status is `Ready` and the kept values are replaced. A context change that cannot connect reports `Reconciling`, `Error` and then `Stale` again, and keeps serving the current values. A flag served from the kept values resolves with the reason `CACHED` and the served value's id as its variant; see [Evaluation details](#evaluation-details).

The values are kept for the context they were evaluated for, and served on the next launch only when the evaluation context has the same `kind` and targeting key. A context without a targeting key matches on the same install, as long as the app has not been uninstalled, because ConfigDirector derives its identifier from the instance ID the SDK stores; such a context keeps its percentage rollout bucket for as long as that instance ID lasts. Other attributes take no part in the match: live values correct them within a second when there is a network. A context change keeps serving the current values until the new context's values arrive, as it does with live values.

Turn it off, or supply your own store, with the `caching` option of `ClientOptions`, which takes the same `CachingOptions` the [Android SDK](https://docs.configdirector.com/sdks/mobile/android#values-kept-on-the-device) takes: `enabled`, defaulting to `true`, and `provider`, a `CachingProvider` with `read()`, `write(value)`, `remove()` and `sizeLimit()` in bytes, defaulting to the app's `SharedPreferences` under a key derived from the client SDK key with a size limit of 256 KB. The entry is one string whose format belongs to the SDK and may change between SDK versions. An entry above the size limit is removed with one warning, a store that fails is logged once and never stops the provider, and nothing is kept in `OFFLINE` mode or by a provider over a test client.

```kotlin [ConfigDirectorSetup.kt]
import com.configdirector.ClientOptions
import com.configdirector.openfeature.ConfigDirectorProvider

val provider = ConfigDirectorProvider(
    applicationContext,
    "YOUR-CLIENT-SDK-KEY",
    ClientOptions.build {
        caching { enabled(false) }
    },
)
```

### Shut down

Shutting OpenFeature down, or replacing the provider, closes the provider, which closes its connection and reports any pending telemetry:

```kotlin [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:

```kotlin [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:

<table>
<thead>
  <tr>
    <th>
      OpenFeature getter
    </th>
    
    <th>
      ConfigDirector config value
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        getBooleanValue
      </code>
    </td>
    
    <td>
      Boolean
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        getStringValue
      </code>
    </td>
    
    <td>
      String or enum
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        getIntegerValue
      </code>
      
      , <code>
        getDoubleValue
      </code>
    </td>
    
    <td>
      Number
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        getObjectValue
      </code>
    </td>
    
    <td>
      JSON object or JSON array
    </td>
  </tr>
</tbody>
</table>

`getIntegerValue` takes and returns an `Int`, as the OpenFeature Kotlin SDK defines it; a value written as a decimal is truncated. `getStringValue` reads a string, enum, URL, or custom config, and a JSON config as its raw document; a boolean, integer, or float config resolves to the default value with the `TYPE_MISMATCH` error code.

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.

```kotlin [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](https://openfeature.dev/docs/reference/sdks/client/kotlin/).

### Evaluation details

The detailed getters of the OpenFeature client, such as `getBooleanDetails`, report why an evaluation produced the value that it did:

<table>
<thead>
  <tr>
    <th>
      Outcome
    </th>
    
    <th>
      Reason
    </th>
    
    <th>
      Error code
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      A value was found
    </td>
    
    <td>
      <code>
        TARGETING_MATCH
      </code>
    </td>
    
    <td>
      
    </td>
  </tr>
  
  <tr>
    <td>
      A value kept on the device was found
    </td>
    
    <td>
      <code>
        CACHED
      </code>
    </td>
    
    <td>
      
    </td>
  </tr>
  
  <tr>
    <td>
      The config carries no value
    </td>
    
    <td>
      <code>
        DEFAULT
      </code>
    </td>
    
    <td>
      
    </td>
  </tr>
  
  <tr>
    <td>
      The config key is unknown
    </td>
    
    <td>
      <code>
        ERROR
      </code>
    </td>
    
    <td>
      <code>
        FLAG_NOT_FOUND
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      No config state has arrived yet
    </td>
    
    <td>
      <code>
        ERROR
      </code>
    </td>
    
    <td>
      <code>
        PROVIDER_NOT_READY
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      The value does not match the requested type
    </td>
    
    <td>
      <code>
        ERROR
      </code>
    </td>
    
    <td>
      <code>
        TYPE_MISMATCH
      </code>
    </td>
  </tr>
</tbody>
</table>

When a value was found, live or [kept on the device](#values-kept-on-the-device), the `variant` is ConfigDirector's identifier for that value. In every other case the default value is returned.

```kotlin [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:

```kotlin [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 context

```kotlin [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 identifier
        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"))),
        ),
    ),
)
```

To evaluate flags for an entity other than a user, such as a tenant, set the context kind with a top-level `kind` attribute:

```kotlin [MainActivity.kt]
OpenFeatureAPI.setEvaluationContextAndWait(
    ImmutableContext(
        targetingKey = "acme",
        attributes = mapOf(
            "kind" to Value.String("tenant"),
            "name" to Value.String("Acme Inc."),
            "traits" to Value.Structure(mapOf("plan" to Value.String("enterprise"))),
        ),
    ),
)
```

Leaving `kind` out means `user`. A `kind` inside `traits` is an ordinary trait, not the context kind. A context kind is 1 to 64 lowercase letters, digits, dashes, or underscores. A context with an invalid context kind is ignored: the SDK logs an error, and flags still resolve successfully, as if no context had been given. Context kinds are available from version 1.4.0 of the provider.

The evaluation context maps onto the ConfigDirector context as follows:

<table>
<thead>
  <tr>
    <th>
      OpenFeature evaluation context
    </th>
    
    <th>
      ConfigDirector context
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      The targeting key, or otherwise an <code>
        id
      </code>
      
       attribute
    </td>
    
    <td>
      <code>
        id
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      The top-level <code>
        kind
      </code>
      
       attribute
    </td>
    
    <td>
      <code>
        kind
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      The <code>
        name
      </code>
      
       attribute
    </td>
    
    <td>
      <code>
        name
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      The <code>
        traits
      </code>
      
       structure attribute
    </td>
    
    <td>
      <code>
        traits
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      The boolean <code>
        anonymous
      </code>
      
       attribute
    </td>
    
    <td>
      <code>
        anonymous
      </code>
    </td>
  </tr>
</tbody>
</table>

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`.

<note>

In client SDKs (browser and mobile), updating the 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 context.<br />


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.

</note>

`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](https://openfeature.dev/docs/reference/sdks/client/kotlin/).

## 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:

```kotlin [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`, and `ProviderStale` while it serves the [values kept on the device](#values-kept-on-the-device).

## Test your code

Code that reads flags through OpenFeature is tested by swapping the provider, so it never has to change for a test. The OpenFeature Kotlin SDK ships no in-memory provider yet, so this provider accepts a **test client** from the [Android SDK's testing tools](https://docs.configdirector.com/sdks/mobile/android#test-your-code), `com.configdirector:android-sdk-testing`, instead: the SDK's real client connected to an in-memory server your test controls. The provider then resolves every flag from the values the test sets, opens no network connection, sends no telemetry, and needs no Android `Context`, so the test runs in a plain JVM unit test, under Robolectric, or in an instrumented test alike.

The testing tools must be the same version as the `android-sdk` on your test classpath, which the provider brings transitively. Add them in test scope, with `kotlinx-coroutines-test` for the Main dispatcher a plain JVM test lacks:

<code-group>

```kotlin [build.gradle.kts]
dependencies {
    testImplementation("com.configdirector:android-sdk-testing:1.9.3")
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0")
}
```

```groovy [build.gradle]
dependencies {
    testImplementation 'com.configdirector:android-sdk-testing:1.9.3'
    testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0'
}
```

</code-group>

The constructor that takes a client sits behind the `@ConfigDirectorProviderTestingApi` opt-in, which keeps it out of application code, so the test opts in:

```kotlin [CheckoutTest.kt]
import com.configdirector.openfeature.ConfigDirectorProvider
import com.configdirector.openfeature.ConfigDirectorProviderTestingApi
import com.configdirector.testing.createTestClient
import dev.openfeature.kotlin.sdk.ImmutableContext
import dev.openfeature.kotlin.sdk.OpenFeatureAPI
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain

@OptIn(ConfigDirectorProviderTestingApi::class)
class CheckoutTest {

    @Before
    fun setUpMainDispatcher() = Dispatchers.setMain(UnconfinedTestDispatcher())

    @After
    fun tearDown() = runBlocking {
        OpenFeatureAPI.shutdown()
        Dispatchers.resetMain()
    }

    @Test
    fun `shows the new checkout`() = runBlocking {
        val testClient = createTestClient(values = mapOf("new-checkout" to true, "max-items" to 20))
        OpenFeatureAPI.setProviderAndWait(
            ConfigDirectorProvider(testClient.client),
            ImmutableContext(targetingKey = "user-123"),
        )
        val client = OpenFeatureAPI.getClient()

        assertThat(client.getBooleanValue("new-checkout", false)).isTrue()
        assertThat(client.getIntegerValue("max-items", 0)).isEqualTo(20)

        testClient.setValue("new-checkout", false)
        assertThat(client.getBooleanValue("new-checkout", false)).isFalse()

        testClient.client.close()
    }
}
```

The provider hands its events to OpenFeature through the client's `events` flow, which runs on the Main dispatcher. Robolectric and instrumented tests have one; a plain JVM test sets one from `kotlinx-coroutines-test` as above, and without it the first call that needs one fails with an `IllegalStateException` saying that `Dispatchers.Main` is missing. `OpenFeatureAPI` is one global, so tests that register their own provider on it cannot run in parallel; shut it down after each test, as above, so the next one starts from a clean slate.

The test client's controls are the same as with the Android SDK: `setValue`, `removeValue`, `replaceValues`, the initialization and context update holds and failures, and `contextUpdates`. [The Android SDK's testing section](https://docs.configdirector.com/sdks/mobile/android#test-your-code) describes each of them and the values they take. Through the provider:

- `setValue` makes the OpenFeature client resolve the new value at once, with the `TARGETING_MATCH` reason, and the provider emit `ProviderConfigurationChanged` with that key; `removeValue` makes the flag resolve to its default value with the `FLAG_NOT_FOUND` error code.
- The provider initializes the client when it is registered and updates the client's context whenever the evaluation context changes, as it does with a client it creates itself. A test client that was already initialized therefore reconnects on registration, and its `contextUpdates` records the provider's context after the test's own.
- `holdInitialization()` before registering keeps `setProviderAndWait` waiting, with the status `NotReady` and flags at their default values with the `PROVIDER_NOT_READY` error code, until `completeInitialization()` or the test client's `timeoutMillis` elapses. A timed-out or failed initialization (`failInitialization()`) fails the provider's initialization with a `ProviderNotReadyError`, so OpenFeature reports the `Error` status, as a connection failure would.
- The provider never closes a client it was given, whether it is shut down explicitly, replaced with `setProvider`, or shut down with `OpenFeatureAPI.shutdown()`. Close the test client's `client` when the test is done, as in the example.


## Sitemap

See the full [sitemap](https://docs.configdirector.com/sitemap.md) for all pages.
