Flutter
Not released yet. The Flutter SDK for e-volv Launch is built and conformance-tested, but its first release with flags is not on the registry yet. This page documents that release: until it ships, flutter pub add evolve_logs finds either nothing or an earlier Observer-only version without flags. The SDKs index shows what can be installed today.
evolve_logs is the e-volv Launch SDK for Flutter. It is a client SDK: with a public key (evk_pub_…) it receives evaluated values for the current context — never targeting rules — so a flag check is synchronous, never throws and never blocks on the network. A server (evk_) key is refused by EvolveLogs.initialize with an ArgumentError: it would sit readable in every shipped binary. Flutter is a wrapper, not a port: the native Android (io.e-volv:logs-android) and iOS (EvolveLogs) clients own fetching, the on-device cache, polling and lifecycle refresh, and push snapshots over the platform channel; Dart holds the latest snapshot and answers reads from memory. The behaviour is pinned by the Launch SDK contract, and flags ride the same init, key and transport as your logs (see the Observer Flutter SDK).
Install
flutter pub add evolve_logs
evolve_logs is a federated plugin: adding it resolves the platform interface and the endorsed evolve_logs_android / evolve_logs_ios implementations, which bridge to the native clients — apply those from source until they reach Maven Central and CocoaPods (see the Observer page).
Native API
import 'package:evolve_logs/evolve_logs.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await EvolveLogs.initialize(
key: 'evk_pub_…', // public key — server keys are refused
appId: 'com.acme.app', // Android application id / iOS bundle id
environment: 'production',
release: '1.2.3',
flags: FlagsOptions(context: {'targetingKey': user.id, 'plan': user.plan}),
);
runApp(const MyApp());
}
Widget checkout() {
// A read is synchronous — it answers from the snapshot in memory.
return EvolveLogs.flags.bool('checkout.new', false)
? const NewCheckout()
: const LegacyCheckout();
}
void banner() {
// The full answer when you need the variant and the reason:
final d = EvolveLogs.flags.detail('checkout.banner', 'default');
// d.value, d.variant ('on' | 'off' | 'control' | …), d.reason
// ('RULE:0' | 'ROLLOUT' | 'TARGET_MATCH' | 'DEFAULT' | 'FLAG_NOT_FOUND' | …)
}
// On login/logout, re-identify: values for the new context are fetched
// and cached on device under the same key.
Future<void> onLogin(User user) async {
await EvolveLogs.flags.identify({'targetingKey': user.id, 'plan': user.plan});
}Evaluation methods: bool, string, number, json and detail — all synchronous, all safe. A flag absent from the served values returns your default with FLAG_NOT_FOUND; a value of the wrong type returns your default with TYPE_MISMATCH (booleans are never coerced from 0/1, and number accepts both int and double serves). detail performs no type check — the value as served, plus its variant and reason.
There is no per-call context: a client SDK holds one current context. It starts as FlagsOptions.context and is replaced with identify, which fits the map to the 4,096-character budget, removes private attributes, re-fetches and resolves whether the served values are for the new context — the last values keep serving until the new ones land. Exposures are recorded on each evaluation of a flag present in the values; FLAG_NOT_FOUND, TYPE_MISMATCH and ERROR never record.
Options
Flags are configured as a FlagsOptions object, passed as the flags: argument of EvolveLogs.initialize — the Dart spelling of the contract’s flags.* keys:
| Option | Default | Meaning |
|---|---|---|
enabled | true | Start the flags client. False gives a defaults-only stub: reads return your default with FLAG_NOT_FOUND and no I/O happens. |
url | null (derived) | Base URL ending in /api/public/v1/flags; derived from the Observer url, else the e-volv cloud. |
pollIntervalSeconds | 60 (native min 15) | Poll period. Client SDKs never stream; the native client also refreshes on lifecycle events. |
cache | true | Keep the last-known values on device and serve them when the control plane is unreachable (contract §6). |
context | {} | The context the first fetch uses, until identify replaces it. |
exposuresEnabled | true | Record an exposure on each evaluation of a flag present in the served values. |
exposureSampleRate | 1.0 | Probability an evaluation is recorded; sent as sampleRate. |
exposureDedupeWindowSeconds | 300 | Suppress repeats of the same (flagKey, variant, contextKind, subject) within the window; suppressed counts ride the next exposure. |
sendAttributes | false | Send context attributes with exposures (private attributes are always removed). |
privateAttributes | [] | Attribute names never sent anywhere. |
The app-id allowlist
A native request carries x-evolve-app-id with your appId, and the control plane only answers from app ids on the environment key’s allowlist — edited under the environment’s Access action. A refused app id produces exactly one warning naming the app id and the Access action, and the SDK keeps serving defaults while retrying. The per-key and per-install quotas are what bound abuse of an extracted key.
Install check and lifecycle
// Optional: true once values confirmed by the control plane for the // current context arrive (waits at most 5 s by default). final fresh = await EvolveLogs.flags.ready(); // Changed flag keys, pushed by the native client; the snapshot is // replaced before the stream emits. EvolveLogs.flags.onChange().listen((keys) => rebuild(keys)); // Force a coalesced refresh. Never throws. await EvolveLogs.flags.refresh(); // When the held values were last confirmed, or null when nothing // has ever been held. final at = EvolveLogs.flags.lastUpdatedAt;
// Install check: is this key bound, and to which environment?
final ping = await EvolveLogs.flags.verify();
// { environment: 'production', keyKind: 'client', flags: 42, etag: '3f2a…' }
// null when the control plane did not answer.There is no close() to forget: the native client owns the lifecycle. flags.ready(timeout:) waits at most 5 s by default, and flags.pendingExposures reports the unsent exposure count for diagnostics.
// Exposures drain themselves: a timer flushes every two seconds while // any are pending, and again when the app is backgrounded. A batch that // cannot cross is requeued; flush manually to force the queue out now. await EvolveLogs.flags.flushExposures();
Troubleshooting by status code
Evaluation never breaks because of the control plane — it degrades. What each response means for the SDK (contract §2.6):
| Response | SDK behaviour |
|---|---|
401 | Key invalid or revoked. The SDK keeps serving held values or defaults, logs once, and retries bootstrap every 5 minutes. |
403 — lacks the scope flags:read | Flags are disabled for this key with one warning; telemetry is unaffected. No retry until re-init. |
403 — app id refused | This app id is not on the key's allowlist. Log once naming the app id and the environment's Access action; keep serving defaults. |
404 | Launch is not enabled for the workspace, or the key is not bound to an environment. Same handling as 401. |
429 | Honours Retry-After (seconds). The delivery rate limit is 3,000 requests/min per key. |
5xx, timeout, network error | Retries with backoff while serving the last held values from the on-device cache. |
Guarantees
- A flag read is local and synchronous — bounded by CPU, not the network; it never crosses the platform channel, and
ready()is the only call that waits, and only when you ask it to. - An unreachable or slow control plane never changes an answer mid-flag: the last held values (or the on-device cache, or your defaults) keep serving while the native client backs off and retries.
- Before
initialize— or withflags.enabled: false— the handle is a defaults-only stub: reads return your default withFLAG_NOT_FOUNDand no I/O happens. - The context is fitted to the 4,096-character budget before it is sent: attributes come off largest-first — never
key,kindortargetingKey— and private attributes never leave the device. - Exposures are sampled, de-duplicated, capped at 10,000 pending and batched at 1,000 onto the native exposure transport — the same queue and retry schedule as your logs.
The e-volv Launch overview covers the concepts, the SDKs index lists every platform, and the HTTP API page documents the wire format the SDK speaks.