Skip to main content

zenmanage 1.0.0 copy "zenmanage: ^1.0.0" to clipboard
zenmanage: ^1.0.0 copied to clipboard

Official Dart/Flutter SDK for Zenmanage feature flags with local evaluation. Control feature rollouts, A/B test, and manage configurations without deploying code.

Zenmanage Flutter SDK #

pub package CI Coverage Gate Dart SDK Version Flutter SDK Version

The official Dart/Flutter SDK for Zenmanage feature flags and experiments.

Add feature flags to your Flutter app in minutes. Control feature rollouts, A/B test, and manage configurations without deploying code.

Why Zenmanage? #

  • ๐Ÿš€ Fast: Rules cached locally - ~1ms evaluation time
  • ๐ŸŽฏ Targeted: Roll out features to specific users, organizations, or segments
  • ๐Ÿ›ก๏ธ Safe: Graceful fallbacks and error handling built-in
  • ๐Ÿ“Š Insightful: Automatic usage tracking (optional)
  • ๐Ÿงช Testable: Easy to mock in tests
  • โšก Async-first: Designed for mobile with async/await throughout

Installation #

Add this to your pubspec.yaml:

dependencies:
  zenmanage: ^1.0.0

Then run:

flutter pub get

Requirements:

  • Dart 3.0+
  • Flutter 3.0+

Quick Start #

1. Get Your Mobile Token #

Get your mobile token (mob_...) from zenmanage.com. Use a client token (cli_...) instead for Flutter web. Server tokens are not accepted in Flutter apps.

2. Initialize the SDK #

import 'package:zenmanage/zenmanage.dart';

final zenmanage = Zenmanage(
  ConfigBuilder.create()
      .withEnvironmentToken('mob_your_token_here')
      .build(),
);

// Optional: prefetch rules during startup
await zenmanage.initialize();

3. Check a Feature Flag #

final flag = await zenmanage.flags().single('new-dashboard');

if (flag.isEnabled()) {
  showNewDashboard();
} else {
  showOldDashboard();
}

That's it! ๐ŸŽ‰

Flutter Bindings #

Use [ZenmanageInitializer] for app-wide async initialization and [ZenmanageFlagBuilder] for widget-level consumption.

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(),
        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();
          },
        ),
      ),
    );
  }
}

Common Use Cases #

Roll Out a New Feature Gradually #

import 'package:zenmanage/zenmanage.dart';

// Create user context
final userContext = Context.user(
  id: 'user_123',
  name: 'John Doe',
  attributes: {'country': 'US', 'plan': 'premium'},
);

// Check if user has access to beta features
final betaAccess = await zenmanage.flags()
    .withContext(userContext)
    .single('beta-program');

if (betaAccess.isEnabled()) {
  enableBetaFeatures();
}

A/B Testing #

final userContext = Context.user(id: userId);

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

if (variant.asString() == 'test_v1') {
  showCheckoutV1();
} else if (variant.asString() == 'test_v2') {
  showCheckoutV2();
} else {
  showCheckoutControl();
}

Multiple Contexts #

final userContext = Context.user(id: 'user_123');
final orgContext = Context.organization(id: 'org_456');

final enterpriseFeature = await zenmanage.flags()
    .withContexts([userContext, orgContext])
    .single('enterprise-analytics');

Handling Missing Flags #

// Option 1: Use singleOrDefault
final flag = await zenmanage.flags().singleOrDefault(
  'optional-feature',
  Flag(key: 'optional-feature', value: false, type: 'boolean'),
);

// Option 2: Use catchError
final flag = await zenmanage.flags()
    .single('optional-feature')
    .catchError((e) => Flag(
      key: 'optional-feature',
      value: false,
      type: 'boolean',
    ));

Type-Safe Value Access #

Flags support type-safe accessors:

final flag = await zenmanage.flags().single('my-flag');

// Boolean access
if (flag.asBool()) { }

// String access
final theme = flag.asString();

// Number access
final limit = flag.asInt();
final timeout = flag.asDouble();

// JSON/Map access (a json flag whose decoded value is a JSON object)
final config = flag.asJson();

// List access (a json flag whose decoded value is a JSON array)
final items = flag.asList<String>();

// Raw value
final value = flag.getValue();

Value Types & Cross-Type Coercion #

A flag's type is one of boolean, string, number, or json. A json flag's decoded value is either map-shaped (a JSON object, read with asJson()) or list-shaped (a JSON array, read with asList<T>()).

asJson() and asList<T>() never throw. Each returns the decoded value when the flag's json value has the matching shape, and a safe zero value ({} or []) for every other case โ€” including a json flag whose value has the other shape (e.g. calling asJson() on a list-shaped json flag), or a flag that isn't json-typed at all:

Called on โ†’
Flag type โ†“
asBool asString asNumber asJson (map) asList (list)
boolean the bool value-derived* value-derived* {} []
string value-derived* the string value-derived* {} []
number value-derived* value-derived* the number {} []
json (map-shaped) true "" 0 the decoded map []
json (list-shaped) true "" 0 {} the decoded list

* asBool(), asString(), and asNumber() on a boolean/string/number flag convert the underlying value directly (e.g. asBool() on the string "true" returns true, asNumber() on "42" returns 42) โ€” see the accessor doc comments in lib/src/flag/flag.dart for the exact rules. Calling asBool(), asString(), or asNumber() on a json-typed flag never attempts a lossy conversion of the decoded map/list: it always falls back to the same safe zero value the table shows (true/""/0), the same way asJson()/asList() do for non-json flags.

Caching Strategies #

In-Memory Cache (Default) #

Fastest but lost on app restart:

final zenmanage = Zenmanage(
  ConfigBuilder.create()
      .withEnvironmentToken('mob_your_token_here')
      .withCache(InMemoryCache())
      .build(),
);

SharedPreferences Cache (Persistent) #

Persists rules across app restarts:

import 'package:zenmanage/zenmanage.dart';

final cache = SharedPreferencesCache(namespace: 'zenmanage');
await cache.initialize();

final zenmanage = Zenmanage(
  ConfigBuilder.create()
      .withEnvironmentToken('mob_your_token_here')
      .withCache(cache)
      .withCacheTtl(86400) // 24 hours
      .build(),
);

No Cache #

Always fresh from API (not recommended for production):

final zenmanage = Zenmanage(
  ConfigBuilder.create()
      .withEnvironmentToken('mob_your_token_here')
      .withCache(NullCache())
      .build(),
);

Configuration #

Environment Token #

Use a mobile key (mob_*) in iOS and Android apps, or a client key (cli_*) for Flutter web, where there is no mobile key. Server keys (srv_*) are rejected when the config is built: a key bundled into an app can be extracted by anyone.

ConfigBuilder.create()
    .withEnvironmentToken('mob_your_token_here') // Required
    .build()

API Endpoint #

Override the API endpoint:

ConfigBuilder.create()
    .withEnvironmentToken('mob_your_token_here')
    .withApiEndpoint('https://api.example.com') // Default: https://api.zenmanage.com
    .build()

Cache TTL #

Control how long rules are cached (in seconds):

ConfigBuilder.create()
    .withEnvironmentToken('mob_your_token_here')
    .withCacheTtl(7200) // 2 hours
    .build()

Usage Reporting #

Enable/disable usage reporting (default: enabled):

ConfigBuilder.create()
    .withEnvironmentToken('mob_your_token_here')
    .withUsageReporting(false) // Disable usage tracking
    .build()

Request Timeout and Retries #

A rules request waits up to 10 seconds by default. If it fails, the SDK tries again, up to 3 attempts in total with 100ms and then 200ms of backoff. A rejected token (HTTP 401 or 403) is not retried, since another attempt with the same token can't succeed. This is the same behavior as the other Zenmanage SDKs.

ConfigBuilder.create()
    .withEnvironmentToken('mob_your_token_here')
    .withRequestTimeout(const Duration(seconds: 5)) // Default: 10 seconds
    .build()

Usage reports are fire-and-forget and wait at most 5 seconds (or the request timeout, if that is shorter).

Error Handling #

The SDK throws typed exceptions for different error conditions:

import 'package:zenmanage/zenmanage.dart';

try {
  final flag = await zenmanage.flags().single('my-flag');
} on ConfigurationException catch (e) {
  print('Configuration error: ${e.message}');
} on ApiException catch (e) {
  print('API error (HTTP ${e.statusCode}): ${e.message}');
} on FlagNotFoundException catch (e) {
  print('Flag not found: ${e.message}');
} on TypeConversionException catch (e) {
  print('Type conversion error: ${e.message}');
} on ZenmanageException catch (e) {
  print('General Zenmanage error: ${e.message}');
}

Logging #

The SDK logs nothing by default. To see what it is doing (rule fetch attempts, retries, failures), pass a ZenmanageLogger to the config:

import 'package:zenmanage/zenmanage.dart';

class AppLogger implements ZenmanageLogger {
  @override
  void debug(String message, [Map<String, Object?>? metadata]) =>
      debugPrint('[zenmanage] $message ${metadata ?? ''}');

  @override
  void info(String message, [Map<String, Object?>? metadata]) =>
      debugPrint('[zenmanage] $message ${metadata ?? ''}');

  @override
  void warn(String message, [Map<String, Object?>? metadata]) =>
      debugPrint('[zenmanage] WARN $message ${metadata ?? ''}');

  @override
  void error(String message, [Map<String, Object?>? metadata]) =>
      debugPrint('[zenmanage] ERROR $message ${metadata ?? ''}');
}

final config = ConfigBuilder.create()
    .withEnvironmentToken('mob_your_token_here')
    .withLogger(AppLogger())
    .build();

Testing #

Mock the API #

import 'package:mocktail/mocktail.dart';
import 'package:zenmanage/zenmanage.dart';

void main() {
  test('shows feature when flag is enabled', () async {
    // Use InMemoryCache or NullCache for deterministic tests
    final zenmanage = Zenmanage(
      ConfigBuilder.create()
          .withEnvironmentToken('mob_test_token')
          .withCache(NullCache())
          .build(),
    );

    // Your test here
  });
}

Use Default Values #

void main() {
  test('handles missing flags gracefully', () async {
    final flag = await zenmanage.flags().singleOrDefault(
      'optional-feature',
      Flag(key: 'optional-feature', value: false, type: 'boolean'),
    );

    expect(flag.isEnabled(), isFalse);
  });
}

Examples #

See the example/ directory for complete examples:

Run a script example from the package root with flutter test (plain dart can't load a Flutter package), or run the example app on a device:

flutter test example/simple_flags.dart

cd example && flutter run

API Reference #

Zenmanage #

Main SDK class:

class Zenmanage {
  Future<void> initialize({List<Context>? contexts}); // Async startup prefetch
  FlagManager flags(); // Get the flag manager
  String get cacheBackendName; // Get current cache backend name
  Future<void> clearCache(); // Clear all cached rules
}

Flutter Widgets #

class ZenmanageInitializer extends StatefulWidget {
  const ZenmanageInitializer({
    required Zenmanage zenmanage,
    required Widget child,
    List<Context> contexts = const <Context>[],
    WidgetBuilder? loadingBuilder,
    Widget Function(BuildContext, Object, StackTrace?)? errorBuilder,
  });
}

class ZenmanageFlagBuilder extends StatefulWidget {
  const ZenmanageFlagBuilder({
    required String flagKey,
    required Widget Function(BuildContext, AsyncSnapshot<Flag>) builder,
    dynamic defaultValue,
    Context? context,
  });
}

ConfigBuilder #

Fluent configuration builder:

class ConfigBuilder {
  static ConfigBuilder create();
  ConfigBuilder withEnvironmentToken(String token);
  ConfigBuilder withCache(Cache cache);
  ConfigBuilder withApiEndpoint(String endpoint);
  ConfigBuilder withCacheTtl(int ttl);
  ConfigBuilder withUsageReporting(bool enabled);
  ConfigBuilder withRequestTimeout(Duration timeout);
  ConfigBuilder withLogger(ZenmanageLogger logger);
  Config build();
}

FlagManager #

Flag evaluation:

class FlagManager {
  Future<Flag> single(String flagKey);
  Future<List<Flag>> all();
  Future<Flag> singleOrDefault(String flagKey, Flag defaultFlag);
  FlagManager withContext(Context context);
  FlagManager withContexts(List<Context> contexts);
  FlagManager clearContext();
  Future<void> clearCache();
}

Flag #

Feature flag value:

class Flag {
  String get key;
  dynamic get value;
  String get type;
  
  bool isEnabled();
  dynamic getValue();
  bool asBool();
  String asString();
  num asNumber();
  int asInt();
  double asDouble();
  Map<String, dynamic> asJson();
  List<T> asList<T>();
}

Context #

Entity context for targeting:

class Context {
  factory Context({
    required String type,
    required String identifier,
    String? name,
    Map<String, dynamic>? attributes,
  });
  
  factory Context.user({
    required String id,
    String? name,
    Map<String, dynamic>? attributes,
  });
  
  factory Context.organization({
    required String id,
    String? name,
    Map<String, dynamic>? attributes,
  });
  
  factory Context.account({
    required String id,
    String? name,
    Map<String, dynamic>? attributes,
  });
}

Troubleshooting #

"Invalid environment token" #

Ensure you're using a mobile token that starts with mob_ (or a client token that starts with cli_ for Flutter web). Server tokens (srv_) are rejected. Get one from zenmanage.com.

"Flag not found" #

Check that the flag key matches exactly (case-sensitive). Use singleOrDefault or catchError for optional flags.

No cache persisting #

Use SharedPreferencesCache instead of InMemoryCache for persistence across app restarts.

Tests fail with API errors #

Use NullCache and provide default values with singleOrDefault.

Contributing #

We welcome contributions! Please see CONTRIBUTING.md for guidelines.

License #

MIT - See LICENSE file for details

Support #

0
likes
160
points
--
downloads

Documentation

Documentation
API reference

Publisher

verified publisherzenmanage.com

Weekly Downloads

Official Dart/Flutter SDK for Zenmanage feature flags with local evaluation. Control feature rollouts, A/B test, and manage configurations without deploying code.

Repository (GitHub)
View/report issues
Contributing

Topics

#feature-flags #feature-toggles #ab-testing #experiments #remote-config

License

MIT (license)

Dependencies

flutter, http, shared_preferences

More

Packages that depend on zenmanage