Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 46 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,11 @@ coroutineScope.launch(Dispatchers.Default) {
}
```

After `initialize()` returns, `setProviderAndWait` waits for the provider’s `status` to move off `NotReady` and `Reconciling`. That is the provider’s contract to fulfill; the SDK does not time out. If a provider never updates `status` correctly, the call never completes. If you need a maximum wait time, wrap the call in your own `withTimeout` from `kotlinx.coroutines` (or similar) at the application level.

Asynchronous API that doesn't wait is also available. It's useful when you want to set a provider and continue with other tasks.

However, flag evaluations are only possible after the provider is Ready.
However, flag evaluations are only possible after the provider is `OpenFeatureStatus.Ready`. The built-in `NoOpProvider` reports `Ready` after initialization and returns default values for flags (a lightweight placeholder until you register a real provider).

```kotlin
OpenFeatureAPI.setProvider(MyProvider()) // can pass a dispatcher here
Expand Down Expand Up @@ -412,11 +414,22 @@ in an Android app.

### Develop a provider

To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency.
You’ll then need to write the provider by implementing the `FeatureProvider` interface exported by the OpenFeature SDK.
Providers are developed in separate artifacts that depend on the OpenFeature SDK. New providers should implement [`StateManagingProvider`](kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/StateManagingProvider.kt) so the SDK can read a single [StateFlow] of [OpenFeatureStatus] and a [Flow] of [OpenFeatureProviderEvents] without ad‑hoc `MutableStateFlow` / `SharedFlow` wiring that can get out of sync.

**Recommended: [`ProviderStatusTracker`](kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/ProviderStatusTracker.kt).** It keeps `status` and the event stream aligned: call [send] with the appropriate [OpenFeatureProviderEvents]; it updates the derived [OpenFeatureStatus] (same mapping as the rest of the SDK) and forwards each [send] to [observe] through a [SharedFlow] with `replay = 1`. For each lifecycle step, [send] the matching event; do not set `StateFlow<OpenFeatureStatus>` elsewhere. New [observe] collectors receive a replay of the most recent [send] (if any), then all later [send] events. For a readiness snapshot, use `status` (or the SDK’s `OpenFeatureAPI.statusFlow` after registration) — the replay is not always a lifecycle-style event (e.g. it can be a prior `ProviderConfigurationChanged`).


* **If you only care about readiness,** collect `status` (or the SDK’s `OpenFeatureAPI.statusFlow` once registered).
* **If you also need the event stream** (e.g. `ProviderConfigurationChanged`), use `observe()`.

`NoOpProvider` in this repo uses `ProviderStatusTracker` the same way.

```kotlin
class NewProvider(override val hooks: List<Hook<*>>, override val metadata: ProviderMetadata) : FeatureProvider {
class NewProvider(override val hooks: List<Hook<*>>, override val metadata: ProviderMetadata) : StateManagingProvider {

private val statusTracker = ProviderStatusTracker()
override val status: StateFlow<OpenFeatureStatus> = statusTracker.status

override fun getBooleanEvaluation(
key: String,
defaultValue: Boolean,
Expand Down Expand Up @@ -467,26 +480,45 @@ class NewProvider(override val hooks: List<Hook<*>>, override val metadata: Prov

override suspend fun initialize(initialContext: EvaluationContext?) {
// add context-aware provider initialization
statusTracker.send(OpenFeatureProviderEvents.ProviderReady())
}

override suspend fun onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) {
// add necessary changes on context change
statusTracker.send(OpenFeatureProviderEvents.ProviderReconciling())
// apply context change
statusTracker.send(OpenFeatureProviderEvents.ProviderReady())
}


override fun shutdown() {
statusTracker.send(
OpenFeatureProviderEvents.ProviderError(
OpenFeatureProviderEvents.EventDetails(
message = "Provider shut down",
errorCode = dev.openfeature.kotlin.sdk.exceptions.ErrorCode.PROVIDER_NOT_READY
)
)
)
// add necessary closure
}

override fun track(
trackingEventName: String,
context: EvaluationContext?,
details: TrackingEventDetails?
trackingEventName: String,
context: EvaluationContext?,
details: TrackingEventDetails?
) {
// Optionally track an event
}

override fun observe(): Flow<OpenFeatureProviderEvents> {
// Optionally return a `Flow` of OpenFeatureProviderEvents
// Optionally track an event
}

override fun observe(): Flow<OpenFeatureProviderEvents> = statusTracker.observe()
}
```

You can also implement [StateManagingProvider] by updating a `MutableStateFlow` and a `Flow` of events yourself, but you must always update `status` before emitting the matching `OpenFeatureProviderEvents` (or only drive both through a single function) so the SDK and `OpenFeatureAPI.observe()` do not see contradictory information.

#### `FeatureProvider` DEPRECATION

`FeatureProvider` is still supported although `StateManagingProvider` is preferred for new providers. It should be noted that `FeatureProvider` is a legacy behavior and will be removed in the next major version, due to its possible race condition in the presence of multi-threading.

> Built a new provider? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=provider&projects=&template=document-provider.yaml&title=%5BProvider%5D%3A+) so we can add it to the docs!

### Develop a hook
Expand Down
49 changes: 46 additions & 3 deletions kotlin-sdk/api/android/kotlin-sdk.api
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ public final class dev/openfeature/kotlin/sdk/ImmutableStructure : dev/openfeatu
public fun keySet ()Ljava/util/Set;
}

public class dev/openfeature/kotlin/sdk/NoOpProvider : dev/openfeature/kotlin/sdk/FeatureProvider {
public class dev/openfeature/kotlin/sdk/NoOpProvider : dev/openfeature/kotlin/sdk/StateManagingProvider {
public fun <init> ()V
public fun <init> (Ljava/util/List;)V
public synthetic fun <init> (Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand All @@ -252,6 +252,7 @@ public class dev/openfeature/kotlin/sdk/NoOpProvider : dev/openfeature/kotlin/sd
public fun getLongEvaluation (Ljava/lang/String;JLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun getMetadata ()Ldev/openfeature/kotlin/sdk/ProviderMetadata;
public fun getObjectEvaluation (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/Value;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun getStatus ()Lkotlinx/coroutines/flow/StateFlow;
public fun getStringEvaluation (Ljava/lang/String;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun initialize (Ldev/openfeature/kotlin/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun observe ()Lkotlinx/coroutines/flow/Flow;
Expand Down Expand Up @@ -283,7 +284,7 @@ public final class dev/openfeature/kotlin/sdk/OpenFeatureAPI {
public final fun getHooks ()Ljava/util/List;
public final fun getProvider ()Ldev/openfeature/kotlin/sdk/FeatureProvider;
public final fun getProviderMetadata ()Ldev/openfeature/kotlin/sdk/ProviderMetadata;
public final fun getProvidersFlow ()Lkotlinx/coroutines/flow/MutableStateFlow;
public final fun getProvidersFlow ()Lkotlinx/coroutines/flow/StateFlow;
public final fun getStatus ()Ldev/openfeature/kotlin/sdk/OpenFeatureStatus;
public final fun getStatusFlow ()Lkotlinx/coroutines/flow/Flow;
public final fun setEvaluationContext (Ldev/openfeature/kotlin/sdk/EvaluationContext;Lkotlinx/coroutines/CoroutineDispatcher;)V
Expand Down Expand Up @@ -402,6 +403,13 @@ public final class dev/openfeature/kotlin/sdk/ProviderMetadata$DefaultImpls {
public static fun getOriginalMetadata (Ldev/openfeature/kotlin/sdk/ProviderMetadata;)Ljava/util/Map;
}

public final class dev/openfeature/kotlin/sdk/ProviderStatusTracker {
public fun <init> ()V
public final fun getStatus ()Lkotlinx/coroutines/flow/StateFlow;
public final fun observe ()Lkotlinx/coroutines/flow/Flow;
public final fun send (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents;)V
}

public final class dev/openfeature/kotlin/sdk/Reason : java/lang/Enum {
public static final field CACHED Ldev/openfeature/kotlin/sdk/Reason;
public static final field DEFAULT Ldev/openfeature/kotlin/sdk/Reason;
Expand All @@ -417,6 +425,18 @@ public final class dev/openfeature/kotlin/sdk/Reason : java/lang/Enum {
public static fun values ()[Ldev/openfeature/kotlin/sdk/Reason;
}

public abstract interface class dev/openfeature/kotlin/sdk/StateManagingProvider : dev/openfeature/kotlin/sdk/FeatureProvider {
public abstract fun getStatus ()Lkotlinx/coroutines/flow/StateFlow;
public abstract fun initialize (Ldev/openfeature/kotlin/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun observe ()Lkotlinx/coroutines/flow/Flow;
public abstract fun onContextSet (Ldev/openfeature/kotlin/sdk/EvaluationContext;Ldev/openfeature/kotlin/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun shutdown ()V
}

public final class dev/openfeature/kotlin/sdk/StateManagingProvider$DefaultImpls {
public static fun track (Ldev/openfeature/kotlin/sdk/StateManagingProvider;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/EvaluationContext;Ldev/openfeature/kotlin/sdk/TrackingEventDetails;)V
}

public abstract interface class dev/openfeature/kotlin/sdk/Structure {
public abstract fun asMap ()Ljava/util/Map;
public abstract fun asObjectMap ()Ljava/util/Map;
Expand Down Expand Up @@ -730,6 +750,15 @@ public final class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$P
public fun toString ()Ljava/lang/String;
}

public final class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderNotReady : dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents {
public static final field INSTANCE Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderNotReady;
public fun equals (Ljava/lang/Object;)Z
public synthetic fun getEventDetails ()Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;
public fun getEventDetails ()Ljava/lang/Void;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderReady : dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents {
public fun <init> ()V
public fun <init> (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;)V
Expand All @@ -743,6 +772,19 @@ public final class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$P
public fun toString ()Ljava/lang/String;
}

public final class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderReconciling : dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents {
public fun <init> ()V
public fun <init> (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;)V
public synthetic fun <init> (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;
public final fun copy (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;)Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderReconciling;
public static synthetic fun copy$default (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderReconciling;Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;ILjava/lang/Object;)Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderReconciling;
public fun equals (Ljava/lang/Object;)Z
public fun getEventDetails ()Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderStale : dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents {
public fun <init> ()V
public fun <init> (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;)V
Expand Down Expand Up @@ -906,7 +948,7 @@ public final class dev/openfeature/kotlin/sdk/multiprovider/FirstSuccessfulStrat
public fun evaluate (Ljava/util/List;Ljava/lang/String;Ljava/lang/Object;Ldev/openfeature/kotlin/sdk/EvaluationContext;Lkotlin/jvm/functions/Function4;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
}

public final class dev/openfeature/kotlin/sdk/multiprovider/MultiProvider : dev/openfeature/kotlin/sdk/FeatureProvider {
public final class dev/openfeature/kotlin/sdk/multiprovider/MultiProvider : dev/openfeature/kotlin/sdk/StateManagingProvider {
public static final field Companion Ldev/openfeature/kotlin/sdk/multiprovider/MultiProvider$Companion;
public fun <init> (Ljava/util/List;Ldev/openfeature/kotlin/sdk/multiprovider/MultiProvider$Strategy;)V
public synthetic fun <init> (Ljava/util/List;Ldev/openfeature/kotlin/sdk/multiprovider/MultiProvider$Strategy;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand All @@ -917,6 +959,7 @@ public final class dev/openfeature/kotlin/sdk/multiprovider/MultiProvider : dev/
public fun getLongEvaluation (Ljava/lang/String;JLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun getMetadata ()Ldev/openfeature/kotlin/sdk/ProviderMetadata;
public fun getObjectEvaluation (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/Value;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun getStatus ()Lkotlinx/coroutines/flow/StateFlow;
public final fun getStatusFlow ()Lkotlinx/coroutines/flow/StateFlow;
public fun getStringEvaluation (Ljava/lang/String;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun initialize (Ldev/openfeature/kotlin/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
Expand Down
Loading
Loading