Mobile SDKs

Flutter SDK

Introduction

The Flutter SDK is intended to be used by Flutter applications on any of the platforms Flutter targets: iOS, Android, web, macOS, Windows, and Linux.

The minimum supported version of Flutter is 3.22, and the minimum supported version of Dart is 3.4.

Telemetry collection runs on a background isolate, so preparing and sending telemetry stays off your application's main isolate. The web has no isolates and a package cannot ship a web worker of its own, so on web the reports are prepared on the main thread instead; they are still built one batch per flush interval rather than while a config is being evaluated.

Installation

The SDK can be installed from pub.dev: https://pub.dev/packages/configdirector_flutter_client_sdk

flutter pub add configdirector_flutter_client_sdk

Configure and initialize the client

  1. Create an instance of the client providing your client SDK key. You can retrieve a client SDK key under your project settings in the Environments & SDK Keys tab.
  2. Initialize the client to initiate its connection lifecycle.
config_director_setup.dart (Initialize the client)
import 'package:configdirector_flutter_client_sdk/configdirector_flutter_client_sdk.dart';

final client = ConfigDirectorClient(clientSdkKey: 'YOUR-CLIENT-SDK-KEY');

await client.initialize();
Before the client is initialized, config values will evaluate to the default value provided in code.

Most applications create a single client instance, initialize it during startup, and dispose of it on shutdown. In a Flutter app that usually means owning it in a State:

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

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

class _MyAppState extends State<MyApp> {
  late final ConfigDirectorClient _client;

  @override
  void initState() {
    super.initState();
    _client = ConfigDirectorClient(clientSdkKey: 'YOUR-CLIENT-SDK-KEY');

    unawaited(_client.initialize());
  }

  @override
  void dispose() {
    _client.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => MaterialApp(home: const HomePage());
}
The SDK follows the app lifecycle through WidgetsBinding so it can release its network connection while the app is backgrounded. If you create the client before runApp, call WidgetsFlutterBinding.ensureInitialized() first. Without it the client still works, but the connection is not paused automatically and the SDK logs a warning.

Additional configuration options

These options can be passed in to ConfigDirectorClientOptions.

metadata

The metadata option allows you to provide your application's name and version. These values can be used in targeting rules conditionals. For example, if a certain feature should only be enabled starting with a certain version of your application.

Whichever of the two you leave unset is read from the platform — the application label and versionName on Android, the bundle display name and short version string on iOS and macOS, and version.json on web — so most applications do not need to set either one.

config_director_setup.dart
final client = ConfigDirectorClient(
  clientSdkKey: 'YOUR-CLIENT-SDK-KEY',
  options: const ConfigDirectorClientOptions(
    metadata: ConfigDirectorMetaContext(
      appName: 'YOUR-APP-NAME',
      appVersion: '1.0.2',
    ),
  ),
);

logger

By default, the SDK logs to the console and it is set to log warnings and errors only. You can configure a logger by either creating a ConfigDirector console logger with a different log level, or by implementing the ConfigDirectorLogger interface to provide your own logger. The interface can be used to create an adapter to another logging library.

Configure the ConfigDirector console logger to a different level:

config_director_setup.dart
final client = ConfigDirectorClient(
  clientSdkKey: 'YOUR-CLIENT-SDK-KEY',
  options: ConfigDirectorClientOptions(
    logger: ConsoleLogger(level: ConfigDirectorLogLevel.debug),
  ),
);

Implement your own logger adapter:

config_director_setup.dart
class MyLogger implements ConfigDirectorLogger {
  @override
  void debug(String message, [Object? error, StackTrace? stackTrace]) {
    // your specific logging library implementation here
  }

  @override
  void info(String message, [Object? error, StackTrace? stackTrace]) {
    // your specific logging library implementation here
  }

  @override
  void warn(String message, [Object? error, StackTrace? stackTrace]) {
    // your specific logging library implementation here
  }

  @override
  void error(String message, [Object? error, StackTrace? stackTrace]) {
    // your specific logging library implementation here
  }
}

final client = ConfigDirectorClient(
  clientSdkKey: 'YOUR-CLIENT-SDK-KEY',
  options: ConfigDirectorClientOptions(logger: MyLogger()),
);

connection

ConnectionOptions accepts five optional values:

  • mode
    • The connection mode, which can be streaming, polling, or oneTime. It is recommended to use the default of streaming unless you have a specific need to use one of the others instead.
    • oneTime fetches config state during initialization and on context updates only.
    • Defaults to ConnectionMode.streaming
  • pollingInterval
    • How often to re-fetch config state when mode is ConnectionMode.polling. It has no effect in any other mode.
    • Defaults to Duration(seconds: 60)
  • timeout
    • The timeout to be used in initialization and when updating the context. This is how long initialize will wait for data from ConfigDirector services before completing its Future. If the timeout is reached, initialize will return but the client will still be in an unready status and returning default values. The client will continue to attempt to connect and retrieve config values in the background.
    • If your application is used in environments where the users frequently have poor or no connection, you may want to use a lower timeout, or simply don't await the initialize method call
    • Defaults to Duration(seconds: 3)
  • pauseWhileBackgrounded
    • Whether to pause the connection while the app is in the background and resume it when the app returns to the foreground. Mobile operating systems terminate background connections on their own, so this is enabled by default. Set it to false to manage the connection yourself with pauseNetwork and resumeNetwork.
    • Defaults to true
  • baseUrl
    • The base URL used to connect to ConfigDirector services
    • This should only be provided if your environment requires you to configure a proxy server in order to connect to ConfigDirector services
config_director_setup.dart
final client = ConfigDirectorClient(
  clientSdkKey: 'YOUR-CLIENT-SDK-KEY',
  options: const ConfigDirectorClientOptions(
    connection: ConnectionOptions(
      mode: ConnectionMode.streaming,
      timeout: Duration(seconds: 2),
    ),
  ),
);

Events

Events notify you of some key actions from the client. Each one is exposed as a Stream you can listen to, and every stream is closed when the client is disposed:

config_director_setup.dart
client.onConfigEvaluated.listen((event) {
  debugPrint('Received onConfigEvaluated: ${event.evaluation}');
});

The following events are available:

  • onClientReady: StreamClientReadyEvent
    • Emitted when the client is initialized, or after reconnection due to a context update or a resumed network connection. When this event is emitted it means the client has received a payload from the ConfigDirector servers and is ready to evaluate configs.
    • action - The action that triggered the client to connect/reconnect, which can be ClientConnectAction.initialization, ClientConnectAction.contextUpdate, or ClientConnectAction.networkResume.
  • onContextUpdated: StreamContextUpdatedEvent
    • Emitted whenever the user context is updated, including during initialization. It is emitted before onClientReady, therefore should not be relied upon as a lifecycle event but rather as an informational event to track updates to the user context.
    • context - The user context the client is being updated to. It can be null.
  • onConfigsUpdated: StreamConfigsUpdatedEvent
    • Emitted when a payload is received from the ConfigDirector servers with config data. It is emitted during initialization or context update when an entire config payload is received. After initialization or context update, it is emitted when updates are pushed from the server (or discovered via polling if that connection mode is used).
    • keys - A List<String> listing the config keys that were included in the payload from the server. On a delta update, these are only the configs that changed.
  • onConfigEvaluated: StreamConfigEvaluatedEvent
    • Emitted whenever a config is evaluated. This includes calls to getValue and evaluations delivered to listeners via watch.
    • evaluation - A ConfigEvaluation object containing the details of the evaluation:
      • key: String - The config key that was evaluated
      • value: Object - The value the config evaluated to. It can be the default value provided to getValue or watch. For example, if the config was evaluated before the client was initialized.
      • valueId: String? - The value ID, which is a stable hash of the value that can be used for analytics or other third parties rather than sending the value. This can be useful for large values, like a JSON config, or to avoid disclosing the values themselves to third parties. It may be null if the evaluation had to fall back to the default value.
      • isDefaultValue: bool - Whether or not the evaluation fell back to the default value provided in code.
      • context: ConfigDirectorContext? - The context the config was evaluated against, if one was set.
      • reason: EvaluationReason - The reason for the evaluation resolution. In the case the config successfully evaluated based on server-provided targeting rules, it will be foundMatch. If the evaluation had to fall back to the default value, the reason will encode why the fallback was required:
        • clientNotReady - The evaluation happened before the client finished initialization
        • configStateMissing - The requested config key was not present in the payload received from the server. This could be due to an incorrect config key, or a config that was not enabled to be available to client SDKs.
        • valueMissing - The config had no value for the current context.
        • invalidNumber - The config was requested as a number but the value received from the server could not be converted to a number.
        • invalidBoolean - The config was requested as a boolean but the value received from the server could not be converted to a boolean.
        • invalidJson - The config was requested as JSON but the value received from the server could not be converted to a JSON document.
        • typeMismatch - The config was requested with a data type that did not match the type of the config and no reasonable type conversion was possible.

Retrieve config values

To synchronously retrieve config values, use the client's getValue method. It requires two arguments. The first argument is the config key, and the second is the default value to be returned if the client has not yet received config values from ConfigDirector services.

You can also subscribe to config value changes via the watch method. It requires two arguments, the config key and the default value, and returns a Stream that emits the config's current value on subscription and then every time the evaluated value changes. Consecutive identical values are not re-emitted. Cancel the subscription to stop watching, or call the unwatch method to close every stream watching a specific config key.

main.dart (Use the client)
import 'config_director_setup.dart';

// Retrieve the current value
final value = client.getValue('my-config-key', false);

// Subscribe to value updates
final subscription = client.watch('my-config-key', false).listen((newValue) {
  debugPrint('Value updated: $newValue');
});

await subscription.cancel(); // Cancel the subscription to remove the listener

client.unwatch('my-config-key'); // Closes every stream watching that key

Both getValue and watch infer their type from the default value, and accept any type supported by that specific config (bool, String, int, double, num, or any type a JSON config decodes into). If there is a type mismatch at runtime, the SDK will attempt to cast to the return value. If the cast fails, it will return the default value. Mismatched types at runtime are captured by the telemetry collector and will surface as warnings in the ConfigDirector dashboard.

main.dart
client.getValue('my-string-config-key', 'Default');

client.getValue('my-integer-config-key', 100);

client.getValue('my-boolean-config-key', false);

client.getValue<Map<String, dynamic>>('my-json-config-key', const {});

In widgets, watch pairs with a StreamBuilder so the widget rebuilds whenever the config changes, whether because it was edited in the ConfigDirector dashboard or because the context was updated:

home_page.dart
StreamBuilder<bool>(
  stream: client.watch('dark-mode', false),
  initialData: false,
  builder: (context, snapshot) => MyHomePage(darkMode: snapshot.data ?? false),
)
watch hands out a new stream on every call, so call it when its inputs change rather than on every build, from initState or didChangeDependencies, holding onto the stream in between. Calling it directly in build resubscribes on each rebuild.

Update the user context

A user context can be provided when initializing the client:

config_director_setup.dart
import 'package:configdirector_flutter_client_sdk/configdirector_flutter_client_sdk.dart';

final client = ConfigDirectorClient(clientSdkKey: 'YOUR-CLIENT-SDK-KEY');

await client.initialize(
  const ConfigDirectorContext(
    id: '12345',
    name: 'Example User',
    traits: {
      // Any arbitrary traits which can be referenced in targeting rules
      'region': 'North America',
    },
  ),
);

The user context can also be updated via updateContext:

main.dart
import 'config_director_setup.dart';

await client.updateContext(
  const ConfigDirectorContext(
    id: '654321',
    name: 'Another User',
    traits: {'region': 'Australia'},
  ),
);

// Update it to a context with no `id` (anonymous user context) when a user
// signs out. The SDK generates a random identifier for it.
await client.updateContext(const ConfigDirectorContext());
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, config values will continue to evaluate to the currently cached values from the prior user context.
Awaiting updateContext will wait until the new config values are downloaded or the connection times out. In the case of a timeout, the client will continue to attempt to connect with the new context in the background.

Other useful client features

The client provides additional properties and methods.

context

Returns the current user context (which may be null). When calling updateContext, the context is not immediately updated. The context is updated after the new connection for the new context succeeds (or times out and goes on retry).

isReady

Returns a bool indicating if the client has been successfully initialized and is ready to evaluate configs for the given user context. It is initially false and becomes true after initialize succeeds. Upon calling updateContext it becomes false again and it is set to true once updateContext succeeds.

isInitializing

Returns a bool indicating whether the client is currently initializing. It is false on creation, true after initialize is called, and false again once initialization completes. This is useful to show a loading state instead of transitioning from the in-code default value to the evaluated value on a slow connection.

unwatchAll

Closes every stream that was previously created via watch, for all config keys.

pauseNetwork and resumeNetwork

pauseNetwork releases the network connection without discarding config state, event listeners, or watch streams, and resumeNetwork re-establishes it using the last context given to initialize or updateContext.

The client already does this on its own while the app is backgrounded, so these are only needed if you set pauseWhileBackgrounded to false in order to manage the connection yourself.

dispose

Closes the connection, every watch stream, and every event stream, and reports whatever telemetry is left. Only call dispose when your application shuts down and it will no longer make use of the client instance. The client cannot be used afterwards.

Copyright © 2026