Flutter SDK
Use Toggly's Flutter SDK in Flutter applications.
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);
},
)
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.
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-Matchand WebSocket?rev=sync. ImplementTogglyRevisionCacheProviderto 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):
| Package | Backend |
|---|---|
feature_flags_toggly_secure_storage | Encrypted platform secure storage |
feature_flags_toggly_disk | Plain JSON files on disk |
feature_flags_toggly_sqlite | SQLite via sqflite |
feature_flags_toggly_isar | Isar 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
- Initialize Once: Initialize Toggly in your app's initialization (e.g., in
initStateof your root widget) - Use Feature widgets: Prefer
Featurefor show/hide andFeatureGateBuilder(orFeature.builder) when UI stays visible but behavior or styling depends on the gate - Provide User Context: Include user identifiers for accurate targeting and rollouts
- Set Flag Defaults: Provide defaults for offline scenarios
- Handle Errors: Always handle errors gracefully when evaluating features
- Monitor Refresh Status: Check the initialization response to understand how flags were loaded
- Use Signed Definitions: Enable signed definitions in production for enhanced security
- Clean Up Resources: Call
dispose()when appropriate to prevent memory leaks
Next Steps
- Learn about JavaScript/Web SDK
- Explore Integrations
- Read the API Reference