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
dependencies:
configdirector_flutter_client_sdk: ^0.1.0
Configure and initialize the client
- 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 Keystab. - Initialize the client to initiate its connection lifecycle.
import 'package:configdirector_flutter_client_sdk/configdirector_flutter_client_sdk.dart';
final client = ConfigDirectorClient(clientSdkKey: 'YOUR-CLIENT-SDK-KEY');
await client.initialize();
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:
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());
}
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.
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:
final client = ConfigDirectorClient(
clientSdkKey: 'YOUR-CLIENT-SDK-KEY',
options: ConfigDirectorClientOptions(
logger: ConsoleLogger(level: ConfigDirectorLogLevel.debug),
),
);
Implement your own logger adapter:
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, oroneTime. It is recommended to use the default ofstreamingunless you have a specific need to use one of the others instead. oneTimefetches config state during initialization and on context updates only.- Defaults to
ConnectionMode.streaming
- The connection mode, which can be
pollingInterval- How often to re-fetch config state when
modeisConnectionMode.polling. It has no effect in any other mode. - Defaults to
Duration(seconds: 60)
- How often to re-fetch config state when
timeout- The timeout to be used in initialization and when updating the context. This is how long
initializewill wait for data from ConfigDirector services before completing itsFuture. If the timeout is reached,initializewill 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
initializemethod call - Defaults to
Duration(seconds: 3)
- The timeout to be used in initialization and when updating the context. This is how long
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
falseto manage the connection yourself withpauseNetworkandresumeNetwork. - Defaults to
true
- 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
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
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:
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 beClientConnectAction.initialization,ClientConnectAction.contextUpdate, orClientConnectAction.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 benull.
- Emitted whenever the user context is updated, including during initialization. It is emitted before
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- AList<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
getValueand evaluations delivered to listeners viawatch. evaluation- AConfigEvaluationobject containing the details of the evaluation:key: String- The config key that was evaluatedvalue: Object- The value the config evaluated to. It can be the default value provided togetValueorwatch. 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 thevalue. This can be useful for large values, like a JSON config, or to avoid disclosing the values themselves to third parties. It may benullif 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 befoundMatch. If the evaluation had to fall back to the default value, thereasonwill encode why the fallback was required:clientNotReady- The evaluation happened before the client finished initializationconfigStateMissing- 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.
- Emitted whenever a config is evaluated. This includes calls to
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.
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.
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:
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:
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:
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());
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.