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.
Install from pub.dev bash

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.

Initialize and evaluate dart

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.

Use flags in widgets dart

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.

Target a user with context dart

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();
}
Run an A/B test with a string flag dart

final variant = await zenmanage
    .flags()
    .withContext(userContext)
    .single('checkout-flow', defaultValue: 'control');

if (variant.asString() == 'one-page') {
  showCheckoutOnePage();
} else {
  showCheckoutMultiPage();
}
Read a JSON value dart

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.

Keep rules across app restarts dart

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.

Handle errors dart

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.