Quickstart by stack
Flutter Quickstart
Install the Dart and Flutter SDK from pub.dev, read a flag in a Flutter app, and target users with context.
Prerequisites
- Dart 3.0 or later and Flutter 3.0 or later.
- A mobile key (prefixed mob_) from your environment for iOS and Android apps, or a client key (prefixed cli_) for Flutter web, which has no mobile key. Server keys (srv_) are rejected when you build the config, because anything bundled in an app can be extracted.
- A flag key you can test in a non-production environment.
flutter pub add zenmanage
That adds zenmanage: ^1.0.0 to your pubspec.yaml and runs flutter pub get. The package is zenmanage on pub.dev.
import 'package:zenmanage/zenmanage.dart';
final zenmanage = Zenmanage(
ConfigBuilder.create()
.withEnvironmentToken('mob_your_mobile_key_here')
.build(),
);
Future<void> check() async {
final flag =
await zenmanage.flags().single('new-dashboard', defaultValue: false);
if (flag.isEnabled()) {
showNewDashboard();
} else {
showOldDashboard();
}
}
Safe defaults
The defaultValue you pass to single is what you get when the flag doesn't exist. If the rules can't be loaded and nothing is cached, single throws an ApiException instead. singleOrDefault and ZenmanageFlagBuilder fall back to the default on any error.
One instance per app
Create a single Zenmanage when your app starts and share it. It keeps the loaded rules in memory and evaluates each flag on the device, so reading a flag never waits on a per-flag network call.
import 'package:flutter/material.dart';
import 'package:zenmanage/zenmanage.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key, required this.zenmanage});
final Zenmanage zenmanage;
@override
Widget build(BuildContext context) {
return MaterialApp(
home: ZenmanageInitializer(
zenmanage: zenmanage,
loadingBuilder: (_) => const CircularProgressIndicator(),
errorBuilder: (_, error, stackTrace) => const OldDashboard(),
child: ZenmanageFlagBuilder(
flagKey: 'new-dashboard',
defaultValue: false,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox.shrink();
}
return snapshot.data!.isEnabled()
? const NewDashboard()
: const OldDashboard();
},
),
),
);
}
}
ZenmanageInitializer loads the rules once and shows loadingBuilder while it does. If that load fails and nothing is cached, it shows errorBuilder, so pass one to keep your app usable offline on first launch. Below it, ZenmanageFlagBuilder reads a flag for a single widget.
final userContext = Context.user(
id: 'user-123',
name: 'Alice',
attributes: {'country': 'US', 'plan': 'premium'},
);
final beta = await zenmanage
.flags()
.withContext(userContext)
.single('beta-program', defaultValue: false);
if (beta.isEnabled()) {
enableBetaFeatures();
}
final variant = await zenmanage
.flags()
.withContext(userContext)
.single('checkout-flow', defaultValue: 'control');
if (variant.asString() == 'one-page') {
showCheckoutOnePage();
} else {
showCheckoutMultiPage();
}
final flag = await zenmanage.flags().single(
'checkout-config',
defaultValue: {'maxItems': 10, 'theme': 'dark'},
);
final Map<String, dynamic> config = flag.asJson();
Typed values
Every flag has a matching accessor: isEnabled() or asBool(), asString(), asInt(), asDouble(), asJson() for a JSON object, and asList<T>() for a JSON array. asJson() and asList() never throw. They return {} or [] when the value has a different shape.
Stable percentage rollouts
The SDK hashes the rollout salt and your context identifier into a bucket, so a user stays in or out of a rollout across launches. Pass a stable identifier, such as your user ID, in the context.
final cache = SharedPreferencesCache(namespace: 'zenmanage');
await cache.initialize();
final zenmanage = Zenmanage(
ConfigBuilder.create()
.withEnvironmentToken('mob_your_mobile_key_here')
.withCache(cache)
.withCacheTtl(86400)
.build(),
);
Caching
Rules are cached in memory for an hour by default and lost when the app closes. SharedPreferencesCache keeps them on the device, so a cold start without a connection still evaluates against the last rules it saw.
Mobile keys in the app
Use a mob_ key in iOS and Android apps and a cli_ key for Flutter web. A server key (srv_) is rejected when you build the config, so keep those on your server.
try {
await zenmanage.flags().single('my-flag');
} on FlagNotFoundException catch (e) {
print(e.message);
} on ApiException catch (e) {
print('HTTP ${e.statusCode}: ${e.message}');
}
Next step
Take the next integration step in your own stack.
Start with the quickstart that matches your runtime, then return to the reference pages when you need exact request and payload details.
Language and framework names and logos shown above are trademarks or registered trademarks of their respective owners. Their use here is for identification purposes only and does not imply endorsement of, or affiliation with, Zenmanage.