Skip to content

Flutter / Dart SDK

feat_sdk is the Flutter and Dart client SDK. Like the browser SDK, it evaluates flags remotely: it sends the current context to feat’s edge, receives the resolved values back, and caches them so reads are synchronous. Your targeting rules and segments never reach the device. A live stream keeps the cached values fresh.

It has a single runtime dependency (http), so it works in pure-Dart backends as well as Flutter apps.

dependencies:
feat_sdk: ^0.1.0

Then dart pub get (or flutter pub get).

import 'package:feat_sdk/feat_sdk.dart';
final client = await FeatClient.initialize(
FeatClientConfig(
apiKey: 'feat_cs_...', // client-side ID or mobile key
context: EvalContext.user('user-123'), // optional: set now or later
),
);
final enabled = client.getBool('checkout-v2', false); // sync
final greeting = client.getString('hero-greeting', 'Hi');
final limit = client.getNumber('upload-limit', 10);
final config = client.getObject('theme', {'mode': 'light'});

Use a client-side ID (feat_cs_…) or a mobile key. Never ship a server key in an app. Values evaluate against the context you set, so reads return the default until a context is present.

getDetail returns the reason and variation alongside the value:

final detail = client.getDetail('checkout-v2', defaultValue: false);
// detail.value, detail.variationId, detail.reason

Build a context from a raw map or with the typed builder:

// Raw map (matches the wire shape exactly):
final ctx = EvalContext.fromMap({
'targetingKey': 'user-123',
'user': {'key': 'user-123', 'plan': 'pro'},
'device': {'key': 'ios-42', 'os': 'ios'},
});
// Typed builder:
final ctx = (EvalContextBuilder()
..targetingKey('user-123')
..kind('user', 'user-123', attributes: {'plan': 'pro'}))
.build();
await client.setContext(ctx);

EvalContext.user('user-123') is a shorthand for the common single-user case. See Contexts for the model.

client.changes is a broadcast stream of per-flag changes, fired after a context change or a new snapshot from the server:

final sub = client.changes.listen((change) {
print('${change.key} changed');
setState(() {}); // in Flutter, re-read the flags you render
});

Adding the first listener opens the live stream; removing the last closes it (the default StreamingMode.followSubscription). Use StreamingMode.always to keep it open, or StreamingMode.never to rely on polling only.

With anonymous: true and no context, the client mints a stable per-install anonymous context. It stores the anonymous key and the last evaluated snapshot through a FeatStorage, so the next launch renders immediately while the SDK refreshes. A persisted snapshot holds resolved values only (keyed by context), never your rules.

The core package is pure Dart, so its default InMemoryStorage keeps everything for the process lifetime only. To survive app restarts, pass a persistent FeatStorage:

final storage = await SharedPreferencesFeatStorage.create();
final client = await FeatClient.initialize(
FeatClientConfig(apiKey: 'feat_cs_...', anonymous: true, storage: storage),
);

SharedPreferencesFeatStorage is Flutter glue (it pulls in shared_preferences), so it lives in the SDK’s example rather than the pure-Dart core: copy it into your app or implement the small FeatStorage interface (read / write / delete) over any store you like.

Poll cadence defaults to 30 seconds and relaxes while the stream is connected.

In Flutter, pause live updates while the app is backgrounded and resume on foreground. The example ships a FeatLifecycleObserver (a WidgetsBindingObserver) for this; or call client.pause() / client.resume() directly.

Call close() when you are done (e.g. in a dispose) to stop polling, close the stream, and release the HTTP client:

await client.close();

To use feat through the OpenFeature Dart API, add the feat_openfeature provider. See OpenFeature.