feat_sdk 0.1.0 copy "feat_sdk: ^0.1.0" to clipboard
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.so


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 by shared_preferences for cross-restart persistence.
  • FeatLifecycleObserver - a WidgetsBindingObserver that 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/evaluate with { "context": ... } returns { "flags": { key: { value, variationId, reason } }, "version": int }. 404 (no datafile yet), 403, and 429 are treated as "no change".
  • GET /sdk/v1/eval/stream?key=...&context=<base64url(JSON(context))> is a Server-Sent Events stream of put (full snapshot) and patch (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

1
likes
160
points
89
downloads

Documentation

API reference

Publisher

verified publisherfeat.so

Weekly Downloads

Dart and Flutter client for the feat feature-flag platform: sync flag getters, change listeners, live SSE streaming, anonymous contexts, and offline snapshots.

Homepage
Repository (GitHub)

License

MIT (license)

Dependencies

http

More

Packages that depend on feat_sdk