---
title: "OpenFeature Flutter Provider"
description: "ConfigDirector Provider for the OpenFeature Dart client SDK in Flutter applications"
canonical_url: "https://docs.configdirector.com/sdks/openfeature/flutter"
---
# OpenFeature Flutter Provider

> ConfigDirector Provider for the OpenFeature Dart client SDK in Flutter applications

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



</div>

<external-badges package-url="https://pub.dev/packages/configdirector_openfeature_flutter_provider" registry="pub.dev" repo="flutter-client-sdk/tree/main/packages/configdirector_openfeature_flutter_provider">



</external-badges>

## Introduction

The OpenFeature Flutter Provider is intended to be used in combination with the [OpenFeature Dart client SDK](https://openfeature.dev/docs/reference/sdks/client/dart/), `openfeature_dart_client_sdk`, in Flutter applications on any of the platforms Flutter targets. The provider wraps the ConfigDirector [Flutter SDK](https://docs.configdirector.com/sdks/mobile/flutter).

The OpenFeature Dart client SDK is the static-context SDK for client applications: one evaluation context is set for the whole application, and every flag is evaluated against it. It is distinct from the OpenFeature Dart server SDK, which the provider does not work with.

<warning>

The OpenFeature Dart client SDK is in beta, so the provider is published as a beta alongside it: a new provider beta for each OpenFeature beta it is built against, until both go stable. Both packages have to be asked for by version, as shown below, because pub never picks a pre-release on its own.

</warning>

The minimum supported version of Flutter is `3.44.2`, which is the first to ship the Dart `3.12.2` the OpenFeature Dart client SDK requires.

Config evaluation is synchronous and reads config state the provider already holds in memory, which is what makes it safe to call from `build`. Telemetry about those evaluations is aggregated and reported off the main isolate, so evaluating a config never waits on the network.

## Installation

The provider can be installed from pub.dev: [https://pub.dev/packages/configdirector_openfeature_flutter_provider](https://pub.dev/packages/configdirector_openfeature_flutter_provider)

The ConfigDirector Flutter SDK is included as a transitive dependency. The OpenFeature Dart client SDK is added alongside the provider, since that is the package your application evaluates flags with.

<code-group>

```bash [flutter]
flutter pub add configdirector_openfeature_flutter_provider:^0.1.0-beta.8 openfeature_dart_client_sdk:^0.0.1-beta.1
```

```yaml [pubspec.yaml]
dependencies:
  configdirector_openfeature_flutter_provider: ^0.1.0-beta.8
  openfeature_dart_client_sdk: ^0.0.1-beta.1
```

</code-group>

## Configure and initialize the client

1. Create an instance of the provider providing 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 evaluation context, then set the OpenFeature provider.
3. Get a client instance from OpenFeature.

```dart [config_director_setup.dart (Register the provider)]
import 'package:configdirector_openfeature_flutter_provider/configdirector_openfeature_flutter_provider.dart';
import 'package:openfeature_dart_client_sdk/openfeature_dart_client_sdk.dart';

// Creating the provider makes no network calls. It throws only when the SDK key is
// blank or an option holds an unusable value.
final provider = ConfigDirectorProvider(clientSdkKey: 'YOUR-CLIENT-SDK-KEY');

// The context the provider connects with. Set it before the provider.
await OpenFeatureAPI.instance.setEvaluationContextAndWait(
  EvaluationContext(targetingKey: 'user-123'),
);

// Connects and waits until config values are received. It throws an
// OpenFeatureException on a timeout, after which the provider keeps trying to
// connect in the background.
await OpenFeatureAPI.instance.setProviderAndWait(provider);

final client = OpenFeatureAPI.instance.getClient();
```

The constructor throws a `ConfigDirectorValidationException` in only two cases, both of which are programming errors rather than runtime conditions: a missing or blank client SDK key outside offline mode, and an option that cannot be used, such as a timeout that is not positive or a base URL that is not absolute. Connection failures are not thrown from it.

In `ConnectionMode.offline` the provider needs no client SDK key, never connects, and resolves every flag from `ConnectionOptions.values` with the reason `TARGETING_MATCH`, the same value for every evaluation context; see [Run without a connection](https://docs.configdirector.com/sdks/mobile/flutter#run-without-a-connection) on the Flutter SDK page for the values accepted. `setProviderAndWait` then completes at once with the provider ready, or throws the `ConfigDirectorInitializationException` the SDK throws when the values cannot be loaded, which the provider exports; the provider is not registered in that case. Offline mode is available from version 0.1.0-beta.6 of the provider.

`setProviderAndWait` returns once the initial config state arrives. When the configured timeout elapses first, the provider reports an error and `setProviderAndWait` throws an `OpenFeatureException`; the OpenFeature SDK reports the `error` status, and the provider keeps connecting in the background, then emits `ready` as soon as config state arrives. Until then, flags resolve to their default values with the `providerNotReady` 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 `State` of the root widget, with `setProvider`, which returns at once and initializes in the background:

```dart [app.dart]
import 'package:configdirector_openfeature_flutter_provider/configdirector_openfeature_flutter_provider.dart';
import 'package:flutter/material.dart';
import 'package:openfeature_dart_client_sdk/openfeature_dart_client_sdk.dart';

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    OpenFeatureAPI.instance.setEvaluationContext(
      EvaluationContext(targetingKey: 'user-123'),
    );
    OpenFeatureAPI.instance.setProvider(
      ConfigDirectorProvider(clientSdkKey: 'YOUR-CLIENT-SDK-KEY'),
    );
  }

  @override
  void dispose() {
    unawaited(OpenFeatureAPI.instance.shutdown());
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => MaterialApp(home: const HomePage());
}
```

Flag evaluations are possible once the client's `providerStatus` is `ProviderStatus.ready`; the client's event handlers are how a widget follows it. See [reading configs in widgets](#reading-configs-in-widgets).

<note>

The underlying SDK follows the app lifecycle through `WidgetsBinding` so it can release its network connection while the app is backgrounded. If you create the provider before `runApp`, call `WidgetsFlutterBinding.ensureInitialized()` first. See the [`connection` options of the Flutter SDK](https://docs.configdirector.com/sdks/mobile/flutter#connection) to manage that yourself instead.

</note>

### Additional configuration options

Additional configuration options can be passed into the provider as `ConfigDirectorClientOptions` in the optional `options` argument of the constructor. They are the Flutter SDK's own options, and the provider exports the types they are built from, so configuring it needs no second import.

For example, the `metadata` can be provided like this:

```dart [config_director_setup.dart]
import 'package:configdirector_openfeature_flutter_provider/configdirector_openfeature_flutter_provider.dart';

final provider = ConfigDirectorProvider(
  clientSdkKey: 'YOUR-CLIENT-SDK-KEY',
  options: const ConfigDirectorClientOptions(
    metadata: ConfigDirectorMetaContext(
      appName: 'YOUR-APP-NAME',
      appVersion: '1.0.2',
    ),
  ),
);
```

The provider accepts the same `metadata`, `connection`, `caching` and `logger` options as the Flutter SDK client, [refer to the additional configuration options section of the Flutter SDK for a full list](https://docs.configdirector.com/sdks/mobile/flutter#additional-configuration-options).

### Values kept on the device

By default, the provider keeps the last values it received from ConfigDirector on the device, through the `shared_preferences` plugin, 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. Such a flag resolves with the reason `CACHED` and ConfigDirector's identifier for the value as its `variant`.

Registering the provider still waits for live values or the connection `timeout`: `setProviderAndWait` throws an `OpenFeatureException` on a timeout as it does without kept values, and the provider then reports the `stale` status, with a `stale` event, while it serves the kept values. Once live values arrive, the provider reports `ready` with a `ready` event and the kept values are replaced. A context change that cannot connect reports `reconciling`, an error and then `stale` again, and keeps serving the current values.

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 `targetingKey`. A context without a `targetingKey` 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` field of `ConfigDirectorClientOptions`, which takes the same `CachingOptions` the [Flutter SDK](https://docs.configdirector.com/sdks/mobile/flutter#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 `shared_preferences` under a key derived from the client SDK key with a size limit of 256 KB. The provider exports both types. 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.

```dart [config_director_setup.dart]
final provider = ConfigDirectorProvider(
  clientSdkKey: 'YOUR-CLIENT-SDK-KEY',
  options: const ConfigDirectorClientOptions(
    caching: CachingOptions(enabled: false),
  ),
);
```

### Shut down

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

```dart [app.dart]
await OpenFeatureAPI.instance.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 app. 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:

```dart [main.dart]
final booleanValue = client.getBooleanValue('my-config-key', false);

final 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>
        getStructureValue
      </code>
    </td>
    
    <td>
      JSON object
    </td>
  </tr>
</tbody>
</table>

`getIntegerValue` takes and returns an `int`; 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, which is also how to read a JSON config whose document is an array: the OpenFeature Dart client SDK has no list getter, so `getStructureValue` reads a JSON object only. A boolean, integer, or float config read with `getStringValue` resolves to the default value with the `TYPE_MISMATCH` error code.

```dart [main.dart]
final settings = client.getStructureValue('my-json-config-key', const {});
final theme = settings['theme'] as String?;
```

For additional information regarding the OpenFeature client [refer to the OpenFeature Dart client SDK documentation](https://openfeature.dev/docs/reference/sdks/client/dart/).

### 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 served
    </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>
        flagNotFound
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      No config state has arrived yet
    </td>
    
    <td>
      <code>
        ERROR
      </code>
    </td>
    
    <td>
      <code>
        providerNotReady
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      The value does not match the requested type
    </td>
    
    <td>
      <code>
        ERROR
      </code>
    </td>
    
    <td>
      <code>
        typeMismatch
      </code>
    </td>
  </tr>
</tbody>
</table>

When a value was found or served from the values kept on the device, the `variant` is the name of the variation served, such as `treatment`, when the config has variations and that variation has a name. Otherwise it is ConfigDirector's ID for the value served, a stable hash you can send to analytics in place of the value itself. Before version 0.1.0-beta.8 of the provider, the variant was always the value ID, so if you send it to an analytics tool, an experiment's evaluations now show up under the variation names. In every other case the in-code default value is returned.

To report an experiment to an analytics tool, follow the page for [Amplitude](https://docs.configdirector.com/integrations/analytics/amplitude), [Mixpanel](https://docs.configdirector.com/integrations/analytics/mixpanel), [Google Analytics 4](https://docs.configdirector.com/integrations/analytics/ga4), [Segment](https://docs.configdirector.com/integrations/analytics/segment) or [PostHog](https://docs.configdirector.com/integrations/analytics/posthog), which names the event that tool reads.

From version 0.1.0-beta.8 of the provider, each evaluation it reports also says which variation was served and which percentage rollout and share served it. That is how ConfigDirector counts the contexts each variation of an experiment reached.

```dart [main.dart]
final details = client.getBooleanDetails('my-config-key', false);
debugPrint('my-config-key is ${details.value} because ${details.reason} (variant ${details.variant})');
```

### Reading configs in widgets

The getters are synchronous and make no network calls, so they are safe to call directly from `build`. That gives you the current value, but the widget will not rebuild on its own when the value changes.

To have a widget follow a config, rebuild it whenever the provider becomes ready or reports a configuration change. The client's `addHandler` registers a handler per event type and returns a subscription to cancel when the widget goes away:

```dart [dark_mode_toggle.dart]
import 'package:flutter/material.dart';
import 'package:openfeature_dart_client_sdk/openfeature_dart_client_sdk.dart';

class DarkModeToggle extends StatefulWidget {
  const DarkModeToggle({super.key});

  @override
  State<DarkModeToggle> createState() => _DarkModeToggleState();
}

class _DarkModeToggleState extends State<DarkModeToggle> {
  final OpenFeatureClient _client = OpenFeatureAPI.instance.getClient();
  final List<ProviderEventSubscription> _subscriptions = [];

  @override
  void initState() {
    super.initState();
    // Rebuild once config state arrives, after every configuration change, and
    // once a new context has taken effect.
    for (final type in const [
      ProviderEventType.ready,
      ProviderEventType.configurationChanged,
      ProviderEventType.contextChanged,
    ]) {
      _subscriptions.add(_client.addHandler(type, (_) => setState(() {})));
    }
  }

  @override
  void dispose() {
    for (final subscription in _subscriptions) {
      subscription.cancel();
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final darkMode = _client.getBooleanValue('dark-mode', false);
    return Switch(value: darkMode, onChanged: null);
  }
}
```

Until `providerStatus` is `ProviderStatus.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

```dart [main.dart]
import 'package:openfeature_dart_client_sdk/openfeature_dart_client_sdk.dart';

await OpenFeatureAPI.instance.setEvaluationContextAndWait(
  EvaluationContext(
    targetingKey: '12345', // In OpenFeature, the targeting key represents the context's identifier
    attributes: {
      'name': 'Example User',
      // Any arbitrary traits which can be referenced in targeting rules
      'traits': {'region': '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:

```dart [main.dart]
await OpenFeatureAPI.instance.setEvaluationContextAndWait(
  EvaluationContext(
    targetingKey: 'acme',
    attributes: {
      'kind': 'tenant',
      'name': 'Acme Inc.',
      'traits': {'plan': '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 0.1.0-beta.5 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 sent as JSON; a `DateTime` inside them is sent as an ISO 8601 string in UTC.

<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, it throws an `OpenFeatureException`, the provider reports the `error` status and continues to attempt to connect with the new context in the background, then emits `ready` once it succeeds.

</note>

`setEvaluationContext`, without the wait, returns at once and reconciles in the background; handle the `contextChanged` event to know when the new values are in effect.

For additional information regarding the OpenFeature client [refer to the OpenFeature Dart client SDK documentation](https://openfeature.dev/docs/reference/sdks/client/dart/).

## Track events

The provider implements OpenFeature's tracking API. `client.track` reports to ConfigDirector that something happened in your application, such as a completed checkout, and ConfigDirector shows it on the Live feed and counts it for each variation of a running experiment; see [Tracked events](https://docs.configdirector.com/monitoring/activity#tracked-events).

```dart [main.dart]
final client = OpenFeatureAPI.instance.getClient();

client.track(
  'checkout-completed',
  details: TrackingEventDetails(
    value: 49.9,
    attributes: {'plan': 'pro', 'seats': 3},
  ),
);
```

The `value` of the tracking event details is sent as the event's value, and their attributes as the event's details. The event is reported for the context flags are currently evaluated against, with its targeting key, or otherwise its `id` attribute, and its `kind`. While a context change is in flight, that is still the previous context.

An event counts for an experiment only when that context has the targeting key the experiment's flag was evaluated with. Without one, the event is still reported and counts for no experiment. For a context marked `anonymous`, the event is reported without the targeting key, so it counts for no experiment either.

The provider sends tracked events as the [Flutter SDK](https://docs.configdirector.com/sdks/mobile/flutter#track-events) does, with the same limits. An attribute can be a string, a number, a boolean, a `DateTime`, which is sent as ISO 8601 text in UTC, or a nested list or map. For anything else, or a structure that refers to itself, OpenFeature's `TrackingEventDetails` constructor throws an `ArgumentError` in your code, before the provider is called. Before version 0.1.0-beta.8 of the provider, a `track` call did nothing.

## Events

The provider publishes its events through the OpenFeature API and clients, one handler per event type. It emits `configurationChanged` whenever configs are updated on the dashboard or via the admin API, carrying the keys of the configs in the update:

```dart [config_director_setup.dart]
import 'package:openfeature_dart_client_sdk/openfeature_dart_client_sdk.dart';

OpenFeatureAPI.instance.addHandler(ProviderEventType.configurationChanged, (details) {
  debugPrint('Configs updated: ${details.flagsChanged}');
});

OpenFeatureAPI.instance.addHandler(ProviderEventType.error, (details) {
  debugPrint('OpenFeature provider error: ${details.message}');
});
```

It emits `ready` when config state arrives after initialization or a context change has already timed out, which moves the status from `error` back to `ready`. It emits `stale` when the values kept on the device are loaded, and again after such an error while it still serves them; see [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 and nothing from ConfigDirector is involved. The OpenFeature Dart client SDK ships an in-memory provider for that, `InMemoryProvider`, which serves the values your test sets.

Its flags are plain values: a `bool`, an `int`, a `double`, a `String`, or a `Map` for a structure, each served as it is:

```dart [checkout_test.dart]
import 'package:flutter_test/flutter_test.dart';
import 'package:openfeature_dart_client_sdk/openfeature_dart_client_sdk.dart';

void main() {
  tearDown(() => OpenFeatureAPI.instance.shutdown());

  testWidgets('shows the new checkout', (tester) async {
    final provider = InMemoryProvider({'new-checkout': true, 'max-items': 20});
    await OpenFeatureAPI.instance.setProviderAndWait(provider);

    await tester.pumpWidget(const MyApp());
    expect(find.text('New checkout'), findsOneWidget);

    provider.replaceAll({'new-checkout': false, 'max-items': 20});
    await tester.pump();
    expect(find.text('New checkout'), findsNothing);
  });
}
```

`replaceAll` replaces every flag, so a flag left out of it resolves to its default value with `flagNotFound` afterwards, and emits `configurationChanged` with the keys it held before and the keys it was given, which is what rebuilds a widget written as in [reading configs in widgets](#reading-configs-in-widgets). The provider serves the same values to every evaluation context. `OpenFeatureAPI.instance` is one global, so shut it down after each test, as above, which lets the next test register its own provider.


## Sitemap

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