feat_sdk 0.1.0
feat_sdk: ^0.1.0 copied to clipboard
Dart and Flutter client for the feat feature-flag platform: sync flag getters, change listeners, live SSE streaming, anonymous contexts, and offline snapshots.
feat Dart & Flutter SDK #
Dart and Flutter client for feat feature flags.
feat evaluates flags server side for client keys and returns resolved values only. This SDK is a thin client over those remote-evaluation endpoints: it fetches resolved values and, optionally, holds a live stream for near-instant updates. It does no local evaluation and ships no bucketing engine.
The core package is pure Dart (no Flutter dependency), so it runs the same
in Flutter apps and in Dart backends or CLIs. Flutter-only conveniences (a
shared_preferences-backed store and a lifecycle observer) are opt-in glue -
see Flutter integration.
Install #
dependencies:
feat_sdk: ^0.1.0
Usage #
import 'package:feat_sdk/feat_sdk.dart';
final client = await FeatClient.initialize(FeatClientConfig(
apiKey: 'feat_cs_...', // client-side id key
context: EvalContext.user('user-123', attributes: {'plan': 'pro'}),
));
final enabled = client.getBool('checkout-v2', false); // sync
final greeting = client.getString('hero-greeting', 'Hi');
final ratio = client.getNumber('rollout-ratio', 0);
final config = client.getObject('theme', const {});
final detail = client.getDetail('checkout-v2', defaultValue: false);
// detail.value / detail.variationId / detail.reason
Use a client-side id key (feat_cs_...) or a mobile key. Both are
non-secret client keys and safe to ship in an app; never embed a server key.
initialize() returns once the first evaluation lands, or after
initializeTimeout (default 5s) elapses - whichever comes first. On timeout the
client is still usable and serves cached or default values until the network
responds.
Reacting to changes #
final sub = client.changes.listen((change) {
print('${change.flagKey}: ${change.oldValue} -> ${change.newValue}');
});
await client.setContext(EvalContext.user('user-456'));
changes emits once per flag whose resolved value flips, after either a context
change, a poll, or a streamed update. setContext() re-polls and reconnects the
stream so the server evaluates against the new context.
Live streaming #
The SDK can hold a Server-Sent Events connection so changes land near-instantly
instead of waiting for the next poll. On connect the server sends a full
snapshot (put); subsequent changes arrive as incremental patch frames.
Updates are adopted in version order (only strictly newer versions win).
By default streaming follows your subscription: the stream opens when the
first changes listener is added and closes when the last one is removed, so
code that never listens pays nothing. Override with streaming:
FeatClientConfig(apiKey: k, streaming: StreamingMode.always); // always
FeatClientConfig(apiKey: k, streaming: StreamingMode.never); // never
FeatClientConfig(apiKey: k, streaming: StreamingMode.followSubscription); // default
Polling stays on as a safety net in every mode. While the stream is connected the poll relaxes to a slow (~10 min) backstop; the instant the stream drops it snaps back to the normal interval, so a dropped stream self-heals quickly. The stream reconnects on its own with jittered exponential backoff.
Anonymous contexts #
Let the SDK mint a stable per-install identifier instead of supplying a context:
final client = await FeatClient.initialize(FeatClientConfig(
apiKey: 'feat_cs_...',
anonymous: true,
storage: myStorage, // persist the key across restarts (see below)
));
This produces { targetingKey: uuid, user: { key: uuid, anonymous: true } }.
With the default in-memory storage the key is stable for the process only; pass
a persistent [FeatStorage] to keep it across restarts.
Offline #
The last evaluated snapshot is persisted per context and served on the next start before the network responds, so a cold launch renders real values immediately. This uses the same pluggable [FeatStorage]; the default is in-memory (no persistence).
class MyStorage implements FeatStorage {
@override Future<String?> read(String key) async { /* ... */ }
@override Future<void> write(String key, String value) async { /* ... */ }
@override Future<void> delete(String key) async { /* ... */ }
}
Flutter integration #
The Flutter glue lives in the example
(example/lib/feat_flutter.dart); copy it into
your app or adapt it. It provides:
SharedPreferencesFeatStorage- a [FeatStorage] backed byshared_preferencesfor cross-restart persistence.FeatLifecycleObserver- aWidgetsBindingObserverthat pauses live updates when the app is backgrounded and resumes on foreground.
final storage = await SharedPreferencesFeatStorage.create();
final client = await FeatClient.initialize(
FeatClientConfig(apiKey: 'feat_cs_...', anonymous: true, storage: storage),
);
final observer = FeatLifecycleObserver(client)..attach();
If you prefer not to use a WidgetsBindingObserver, call client.pause() and
client.resume() (or client.handleAppLifecycleState(...)) directly.
Configuration #
| Option | Default | Notes |
|---|---|---|
apiKey |
(required) | Client-side id (feat_cs_...) or mobile key. |
baseUrl |
https://data-01.feat.so |
Must be https:// (loopback http:// allowed for tests). |
context |
none | Initial context; or set later via setContext. |
anonymous |
false |
Mint a stable anonymous context when no context is given. |
storage |
InMemoryStorage |
Anonymous key + offline snapshot persistence. |
pollInterval |
30s |
Floored at 5s. Primary refresh when not streaming. |
safetyNetPollInterval |
10m |
Relaxed cadence while the stream is connected. |
streaming |
followSubscription |
always / never / followSubscription. |
initializeTimeout |
5s |
Max wait for the first evaluation. |
How it works #
POST /sdk/v1/evaluatewith{ "context": ... }returns{ "flags": { key: { value, variationId, reason } }, "version": int }.404(no datafile yet),403, and429are treated as "no change".GET /sdk/v1/eval/stream?key=...&context=<base64url(JSON(context))>is a Server-Sent Events stream ofput(full snapshot) andpatch(incremental) frames, adopted in version order.- Every request carries an
X-Feat-Sdk: dart/<version>header. - Resolved values are cached so the typed getters are synchronous.
License #
MIT