Skip to main content

Flutter SDK

Use Toggly's Flutter SDK in Flutter applications.

Quick reference

Grab the printable Dart / Flutter cheat sheet (download PDF) — widget API, signed defs, lifecycle-aware refresh, offline cache.

Installation​

$ flutter pub add feature_flags_toggly

This will add a line like this to your package's pubspec.yaml (and run flutter pub get):

dependencies:
feature_flags_toggly: ^0.0.1

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

Now in your Dart code, you can use:

import 'package:feature_flags_toggly/feature_flags_toggly.dart';

Initialization​

Initialize Toggly by running the Toggly.init method and by providing your App Key from your Toggly application page


void initState() {
initToggly();
super.initState();
}

void initToggly() async {
await Toggly.init(
appKey: '<YOUR_APP_KEY>',
environment: '<YOUR_APP_ENVIRONMENT>',
);
}

Configuration Options​

You can customize Toggly's behavior using TogglyConfig:

await Toggly.init(
appKey: '<YOUR_APP_KEY>',
environment: '<YOUR_APP_ENVIRONMENT>',
// useSignedDefinitions defaults to true
config: TogglyConfig(
baseURI: 'https://definitions.toggly.io', // Default API URL
connectTimeout: 5000, // Connection timeout in milliseconds (default: 5000)
featureFlagsRefreshInterval: 180000, // Refresh interval in milliseconds (default: 3 minutes)
trustedKeyIds: ['KEY_ID_1', 'KEY_ID_2'], // Optional: Whitelist of trusted key IDs
jwksCacheDuration: Duration(days: 30), // JWKS TTL (default 30 days, min 1 minute)
maxCacheKeys: 20, // Optional LRU cap for identity-scoped cache keys (default: unlimited)
onError: (message, error, stackTrace) {
// Report SDK failures to your app logger, Sentry, or Crashlytics
debugPrint('Toggly error: $message');
},
),
);

Signed Definitions​

Flutter verifies ECDSA-signed definitions by default (useSignedDefinitions: true), including when flags are loaded from a persistence backend. Pass useSignedDefinitions: false only if you intentionally need unsigned definitions.

await Toggly.init(
appKey: '<YOUR_APP_KEY>',
environment: '<YOUR_APP_ENVIRONMENT>',
config: TogglyConfig(
trustedKeyIds: ['TRUSTED_KEY_ID'], // Optional: Restrict to specific key IDs
jwksCacheDuration: Duration(days: 30),
),
);

Initialization Response​

The init method returns a TogglyInitResponse that indicates how the flags were loaded:

final response = await Toggly.init(
appKey: '<YOUR_APP_KEY>',
environment: '<YOUR_APP_ENVIRONMENT>',
);

switch (response.status) {
case TogglyLoadFeatureFlagsResponse.fetched:
print('Flags fetched from API');
break;
case TogglyLoadFeatureFlagsResponse.cached:
print('Flags loaded from cache');
break;
case TogglyLoadFeatureFlagsResponse.defaults:
print('Using default flags');
break;
case TogglyLoadFeatureFlagsResponse.error:
print('Error loading flags');
break;
}

Anonymous Identity​

If you don't provide an identity during initialization, Toggly generates an anonymous in-memory identity for the session. This identity is not persisted and changes on every cold start. For consistent feature flag evaluation across app restarts — and for offline support — pass a stable identity (and a cache provider, see Caching and Offline Support).

Feature Widget​

The feature widget is designed to easily wrap your functionality for toggling with feature flags

Feature(
featureKeys: const ['ExampleFeatureKey1'],
child: const Text('This text will show if ExampleFeatureKey1 is enabled'),
),

You can also use multiple widgets with the children parameter:

Feature(
featureKeys: const ['ExampleFeatureKey1'],
children: [
const Text('First widget'),
const Text('Second widget'),
const Button(),
],
),

You can also use multiple feature keys for one Feature widget and make use of the requirement (FeatureRequirement.all, FeatureRequirement.any) and negate (bool) options.

Show if any of the listed features are on​

Feature(
featureKeys: const ['ExampleFeatureKey1', 'ExampleFeatureKey2'],
requirement: FeatureRequirement.any,
child: const Text('This text will show if ANY of the provided feature keys are TRUE'),
),

Show if all of the listed features are on​

Feature(
featureKeys: const ['ExampleFeatureKey1', 'ExampleFeatureKey2'],
requirement: FeatureRequirement.all,
child: const Text('This text will show if ALL of the provided feature keys are TRUE'),
),

Show if a feature is off (negate)​

Feature(
featureKeys: const ['ExampleFeatureKey1'],
negate: true,
child: const Text('This text will show if ExampleFeatureKey1 is FALSE'),
),

FeatureGateBuilder​

Use Feature when you want simple show/hide: the child is rendered when the gate is on, otherwise an empty placeholder is returned.

Use FeatureGateBuilder (or Feature.builder) when the widget tree stays visible but behavior or styling depends on the gate — for example, disabling link styling and tap handlers while keeping content on screen.

Both use the same evaluation path: remote flags, local post-filter gates, requirement, negate, and optional variant. FeatureGateBuilder always invokes its builder; it never returns an empty placeholder itself.

Conditional styling​

FeatureGateBuilder(
featureKeys: const ['PremiumCheckout'],
builder: (context, premiumCheckoutEnabled) {
return Text(
'USDA ID',
style: TextStyle(
color: premiumCheckoutEnabled ? Colors.blue : Colors.grey,
decoration: premiumCheckoutEnabled ? TextDecoration.underline : null,
),
);
},
)

Feature.builder​

Feature.builder is sugar over the same gate resolution. Use the enabled argument for show/hide or conditional UI:

Feature.builder(
featureKeys: const ['PremiumCheckout'],
builder: (context, enabled) {
return MyDataGrid(premiumCheckoutEnabled: enabled);
},
)
Avoid manual StreamBuilder boilerplate

You do not need to wire StreamBuilder + Toggly.evaluateFeatureGateSync at every call site. FeatureGateBuilder subscribes to Toggly.featureFlagsStream and Toggly.onLocalGatesChanged and exposes the resolved boolean for you.

// Before (verbose, easy to get out of sync with Feature)
StreamBuilder<Map<String, bool>>(
stream: Toggly.featureFlagsStream,
initialData: Toggly.featureFlagsSnapshot,
builder: (context, flagsSnapshot) {
final flags = flagsSnapshot.data ?? Toggly.featureFlagsSnapshot;
final enabled = Toggly.evaluateFeatureGateSync(
['PremiumCheckout'],
flags: flags,
);
return MyWidget(premiumCheckoutEnabled: enabled);
},
)

// After
FeatureGateBuilder(
featureKeys: const ['PremiumCheckout'],
builder: (context, enabled) => MyWidget(premiumCheckoutEnabled: enabled),
)

Directly Checking a Flag​

You can evaluate the value of a Feature gate by calling evaluateFeatureGate directly

final isEnabled = await Toggly.evaluateFeatureGate(
["ExampleFeatureKey1", "ExampleFeatureKey2"],
requirement: FeatureRequirement.all,
negate: true,
);

if (isEnabled) {
// Feature is enabled
}

Manual Refresh​

You can manually refresh feature flags from the server:

final response = await Toggly.refresh();

switch (response.status) {
case TogglyLoadFeatureFlagsResponse.fetched:
print('Flags refreshed from API');
break;
case TogglyLoadFeatureFlagsResponse.cached:
print('Using cached flags (not modified)');
break;
// ... other cases
}

The SDK automatically refreshes flags when:

  • The app comes to the foreground (lifecycle-aware)
  • The configured refresh interval elapses
  • You call refresh() manually

Error reporting and fallback behavior​

Use TogglyConfig.onError to observe fetch, cache, storage, signature, and JWK verification failures. The callback receives the error message, original error, and stack trace when available.

await Toggly.init(
appKey: '<YOUR_APP_KEY>',
environment: '<YOUR_APP_ENVIRONMENT>',
config: TogglyConfig(
onError: (message, error, stackTrace) {
crashlytics.recordError(error ?? message, stackTrace, reason: message);
},
),
);

After one successful load, transient refresh or signature/JWK failures preserve the last-known-good flags instead of clearing currently rendered UI. On a cold start with no valid cache, the SDK falls back to flagDefaults and reports the failure through onError.

Feature widgets rebuild from the SDK flag stream, so interval refreshes, manual Toggly.refresh() calls, identity changes, and local-gate changes can update visible content without recreating the widget.

For the shared reliability contract, see Reliability and Error Handling.

Entity context​

Pass the screen entity into Feature / isFeatureOn per evaluation — not via setIdentity. See Entity & page context.

Flutter is a client: register a local mapper. Do not register schemas with the dashboard.

Toggly.registerContext('Order', (order) {
return TogglyEntityContext(
kind: 'Order',
key: order.id.toString(),
attributes: {'Status': order.status},
);
});

if (await Toggly.isFeatureOn('OrderBadge', context: order, kind: 'Order')) {
// show badge for this order only
}
Feature(
featureKeys: const ['OrderBadge'],
context: order,
kind: 'Order',
child: const Badge(),
)

Evaluated-signed may return a gate object; without entity context, evaluation fails closed. setIdentity is user only.

Users and Rollouts​

To facilitate rollouts, and consistently serve the same value for a flag during a session, we need a way of identifying the user, which needs to be supplied by the application

Setting Identity During Initialization​


void initState() {
initToggly();
super.initState();
}

void initToggly() async {
await Toggly.init(
appKey: '<YOUR_APP_KEY>',
environment: '<YOUR_APP_ENVIRONMENT>',
identity: '<UNIQUE_USER_IDENTIFIER>',
);
}

Changing Identity Dynamically​

You can change the user identity at runtime using setIdentity. This is useful when a user logs in or switches accounts:

// When user logs in
await Toggly.setIdentity('user-123');

// When user logs out, reset to an anonymous (in-memory) identity
await Toggly.setIdentity(null);

When the identity or evaluation context changes, the SDK clears in-memory state and refreshes for the new user. Persisted flags, variants, and definitions revisions (ETag) for other users are kept, so switching accounts and switching back reuses cached data without a full re-fetch. The feature-flag stream may briefly emit your flagDefaults until the refresh for the new context completes.

To wipe persisted data for the current user explicitly (for example after a 403 or signature failure), call Toggly.clearFeatureFlagsCache().

Evaluation context (groups and claims)​

For User Claims and group-based Custom Rollout filters, pass the full context after login:

await Toggly.setContext(
identity: 'user-123',
groups: ['beta', 'enterprise'],
claims: {'role': 'admin', 'plan': 'premium'},
);

See Feature filters. Browser/OS/country filters rely on HTTP headers on web; mobile apps typically use identity, groups, and claims only.

success

When using user identifiers, evaluated features are cached per user for 30 minutes by default on a sliding window, so the user might not see the change right away as to not confuse the user. The session length is configurable and the session store can be cleared in App Settings

Flag Defaults​

In case your application can't reach toggly, you can set feature defaults. These defaults are used when:

  • The app is offline
  • The API request fails
  • No cached flags are available

void initState() {
initToggly();
super.initState();
}

void initToggly() async {
await Toggly.init(
appKey: '<YOUR_APP_KEY>',
environment: '<YOUR_APP_ENVIRONMENT>',
flagDefaults: {
"ExampleFeatureKey1": true,
"ExampleFeatureKey2": false,
"ExampleFeatureKey3": true,
},
);
}

You can also access the default flags programmatically:

final defaults = Toggly.featureFlagDefaults;

Using Toggly Without API (Offline Mode)​

You can use Toggly with only flag defaults, without connecting to the Toggly API:

await Toggly.init(
flagDefaults: {
"Feature1": true,
"Feature2": false,
},
);

This is useful for:

  • Development and testing
  • Offline-first applications
  • Prototyping without API setup

App Lifecycle Handling​

The SDK automatically handles app lifecycle events:

  • Foreground: When the app comes to the foreground, flags are automatically refreshed
  • Background: When the app goes to background, refresh operations are skipped to save resources
  • Lifecycle Observer: The SDK registers as a lifecycle observer to handle state changes

This ensures flags are always up-to-date when users return to your app.

Caching and Offline Support​

By default the SDK is memory-only — nothing is written to disk. This keeps the SDK crash-safe and avoids storage access while the app is backgrounded, but it means there is no cache after a cold start until the first successful fetch.

  • In-Memory Cache: Flags are cached in memory for fast access during a session
  • Definitions revision: Scoped per evaluation identity (user, groups, and claims — the same key as flags/variants). Used for HTTP If-None-Match and WebSocket ?rev= sync. Implement TogglyRevisionCacheProvider to persist revisions across cold starts; without it, revision is memory-only and the first request after restart is always a full fetch
  • 304 Not Modified: When the revision matches, the worker returns 304 and the SDK keeps last-known-good flags
  • Automatic Fallback: Falls back to in-memory cached flags, then to flag defaults, if the API is unavailable
  • Per-User Caching: Flags, variants, and revisions are cached per evaluation context for consistent multi-user experiences

See WebSocket sync for how live updates use the same revision.

Persisting across restarts (offline)​

To make flags survive app restarts and work offline, supply a cache provider. Your app decides where data is stored by passing a TogglyCacheProvider to TogglyConfig(cacheProvider: ...). This also requires a stable identity (the anonymous in-memory identity changes on every cold start).

import 'package:feature_flags_toggly/feature_flags_toggly.dart';
import 'package:feature_flags_toggly_secure_storage/feature_flags_toggly_secure_storage.dart';

await Toggly.init(
appKey: '<YOUR_APP_KEY>',
environment: '<YOUR_APP_ENVIRONMENT>',
identity: currentUserId, // stable identity for offline restart
config: TogglyConfig(
cacheProvider: SecureStorageCacheProvider(),
maxCacheKeys: 20, // Optional: evict least-recently-accessed storage keys
),
);

When maxCacheKeys is set, Flutter wraps your provider with an LRU layer. Each flags, variants, and per-identity revision entry counts toward the limit. See Client-side cache limits. Custom providers must implement readCacheLruIndex / writeCacheLruIndex.

Official persistence backends (each published as its own package):

PackageBackend
feature_flags_toggly_secure_storageEncrypted platform secure storage
feature_flags_toggly_diskPlain JSON files on disk
feature_flags_toggly_sqliteSQLite via sqflite
feature_flags_toggly_isarIsar database

You can also implement TogglyCacheProvider yourself for any other backend. For ETag persistence across restarts, implement TogglyRevisionCacheProvider — revision read/write/delete methods take the evaluation context key (appKey, environment, and the same identity string the SDK uses for flags, e.g. u:user-1|g:beta).

Official cache packages migrate legacy appKey/environment-only revision keys to the identity-scoped format on first read after upgrading to 1.8.0.

Clearing cache​

Toggly.clearFeatureFlagsCache() removes in-memory state and deletes persisted flags, variants, and (by default) the definitions revision for the current evaluation context. Pass deletePersistedRevision: false only when you need to invalidate flags without discarding the conditional-fetch ETag (uncommon).

Debugging​

You can get debug information about Toggly's current state:

final debugInfo = Toggly.debug();
print('User: ${debugInfo['user']}');
print('App Key: ${debugInfo['appKey']}');
print('Environment: ${debugInfo['environment']}');
print('Last Checked: ${debugInfo['lastChecked']}');
print('Last Synced: ${debugInfo['lastSynced']}');
print('Last Error: ${debugInfo['lastError']}');

This is useful for troubleshooting feature flag issues.

Cleanup​

When your app is shutting down or you need to clean up resources, call dispose:


void dispose() {
Toggly.dispose(); // Cancels timers and cleans up resources
super.dispose();
}

This cancels background timers and closes streams to prevent memory leaks.

Device-local post-filter gates​

Gate feature bundles behind device-local switches (e.g. iOS Settings) while worker rollouts stay on Toggly. See Post-filter gates.

var apiRedesignEnabled = false;

await Toggly.init(
config: TogglyConfig(
localGates: [
LocalGate(
id: 'apiRedesign',
flagKeys: ['ApiV2Checkout', 'ApiV2Profile'],
isEnabled: () => apiRedesignEnabled,
),
],
),
);

apiRedesignEnabled = false;
Toggly.notifyLocalGatesChanged();

// Feature, FeatureGateBuilder, and evaluateFeatureGate re-read effective flags

Best Practices​

  1. Initialize Once: Initialize Toggly in your app's initialization (e.g., in initState of your root widget)
  2. Use Feature widgets: Prefer Feature for show/hide and FeatureGateBuilder (or Feature.builder) when UI stays visible but behavior or styling depends on the gate
  3. Provide User Context: Include user identifiers for accurate targeting and rollouts
  4. Set Flag Defaults: Provide defaults for offline scenarios
  5. Handle Errors: Always handle errors gracefully when evaluating features
  6. Monitor Refresh Status: Check the initialization response to understand how flags were loaded
  7. Use Signed Definitions: Enable signed definitions in production for enhanced security
  8. Clean Up Resources: Call dispose() when appropriate to prevent memory leaks

Next Steps​