Flutter
This SDK supports the following versions:
- Android version 21 & above
- iOS version 12 & Above
- Apple TvOS version 13 & Above
- Apple WatchOS version 7 & Above
Installation
Add this to your pubspec.yaml file
growthbook_sdk_flutter: ^4.0.0
Quick Usage
Create a GrowthBookSDK instance with GBSDKBuilderApp, set attributes, then evaluate features or run experiments.
final sdk = await GBSDKBuilderApp(
apiKey: "sdk-abc123", // Client key from GrowthBook
hostURL: "https://cdn.growthbook.io", // or your Proxy URL
attributes: {
"id": "123",
"env": "dev",
"betaUser": true,
},
growthBookTrackingCallBack: (exp, result) {
// Track exposures
},
).initialize();
final feature = sdk.feature("my-feature");
if (feature.on) {
// Feature is enabled
}
Using Features
The feature method takes a String feature name and returns a GBFeatureResult object with a few useful properties:
- value (
dynamic) - The assigned value of the feature - on (
bool) - The value cast to a boolean - off (
bool) - The value cast to a boolean and then negated - source (
String) - Why the value was assigned to the user. One of "unknownFeature", "defaultValue", "force", or "experiment"
When the source is "experiment", there are 2 additional properties that tell you which experiment was used and more details about the result of the experiment:
- experiment (
GBExperiment) - experimentResult (
GBExperimentResult)
Here are some examples:
GBFeatureResult feature = gb.feature("my-feature")
// Do something if feature is truthy
if (feature.on) { }
// Do something if feature is falsy
if (feature.off) { }
// Print the actual value of the feature
// (depending on the feature, might be a string, number, boolean, etc.)
print(feature.value)
// Print the experiment id used to assign the feature value
if (feature.source == "experiment") {
Println(feature.experiment.key)
}
Attributes
Attributes define the current user and request "context" used for targeting rules and bucketing.
Common attributes
id(string): primary user identifier for consistent bucketingdeviceId(string): device/install-scoped fallback when user not logged incountry,locale(string)plan,company,role(string)loggedIn(bool)appVersion(string, supports semver targeting)
Use setAttributes() method to set the user context.
sdk.setAttributes({
"id": "123",
"country": "US",
"plan": "pro",
"appVersion": "1.3.0",
});
What is the user context?
- Identity: stable identifiers for bucketing (e.g.,
id, and optionally a logged-out fallback likedeviceId). - Demographics and traits: e.g.,
country,company,plan,loggedIn. - App/runtime context: e.g.,
appVersion,platform,locale. - Request context (if applicable): e.g.,
url,path,device.
Data types supported include strings, numbers, booleans, and arrays/objects for JSON-based conditions.
Identity changes (login/logout)
- On login, switch from
deviceIdtoid(stable user id). - On logout, remove
idand usedeviceIduntil the next login.
// Logged-out (device-scoped)
sdk.setAttributes({
"deviceId": "device-abc",
"loggedIn": false,
});
// After login (user-scoped)
sdk.setAttributes({
"id": "user-123",
"loggedIn": true,
});
If using Remote Evaluation, consider limiting network calls to meaningful identity changes with cacheKeyAttributes and refreshing when identity changes:
final sdk = await GBSDKBuilderApp(
apiKey: "sdk-abc123",
hostURL: "https://gb-proxy.example.com",
remoteEval: true,
cacheKeyAttributes: ["id", "email"],
attributes: {"deviceId": "device-abc"},
).initialize();
// Later, after login
sdk.setAttributes({
"id": "user-123",
"email": "user@example.com",
});
await sdk.refreshForRemoteEval();
Experimentation (A/B Testing)
To run the experiements with GrowthBook, use run method, which takes a GBExperiment object as an argument and returns a GBExperimentResult object:
var exp = GBExperiment()
exp.key = "my-experiment"
exp.variations = List.of("control", "variation")
var result = gb.run(exp)
// Either "control" or "variation"
print(result.value)
The GBExperiment class has two required properties - key and variations. There are also a number of optional properties:
- key (
String) - The unique identifier for this experiment - variations (
dynamic[]) - Array of variations to decide between - weights (
double[]) - How to weight traffic between variations. Must add to 1. - active (
bool) - If set to false, always return the control (first variation) - coverage (
double) - What percent of users should be included in the experiment (between 0 and 1, inclusive) - condition (
GBCondition) - Optional targeting condition - namespace (
[String, int, int]) - Adds the experiment to a namespace - force (
int) - All users included in the experiment will be forced into the specific variation index - hashAttribute (
String) - What user attribute should be used to assign variations (defaults toid)
The GBExperimentResult object returns the following properties:
- inExperiment (
bool) - variationId (
int) - The array index of the assigned variation - value (
dynamic) - The value of the assigned variation - hashAttribute (
String) - The user attribute used to assign a variation - hashValue (
String) - The value of the attribute used to assign a variation
Tracking & Subscriptions
Use growthBookTrackingCallBack to receive experiment exposure events whenever a user is assigned to a variation (via features or inline experiments).
For feature usage events (non-experiment), wrap your feature(...) calls in a small utility to emit custom app analytics.
final sdk = await GBSDKBuilderApp(
apiKey: "sdk-abc123",
hostURL: "https://cdn.growthbook.io",
growthBookTrackingCallBack: (exp, result) {
// e.g., send to analytics
},
).initialize();
To Subscribe :
final exposures = StreamController<Map<String, dynamic>>.broadcast();
final sdk = await GBSDKBuilderApp(
apiKey: "sdk-abc123",
hostURL: "https://cdn.growthbook.io",
growthBookTrackingCallBack: (exp, result) {
exposures.add({
"experimentKey": exp.key,
"variationId": result.variationId,
"featureId": result.featureId,
});
},
).initialize();
final sub = exposures.stream.listen((e) {
// send to analytics
});
// Later, unsubscribe to avoid leaks
await sub.cancel();
When subscriptions fire vs. don't fire
- Fire: on first assignment per experiment key and after assignments change due to attribute updates or payload changes.
- Don't fire: repeated reads of the same assignment that hasn't changed.
Efficient subscription patterns
- Keep callbacks lightweight; offload heavy work to background tasks.
- Coalesce multiple exposure events before sending to analytics to reduce overhead.
- Include
experiment.key,variationId, and optionalfeatureIdfor correlation.
Unsubscribing and memory management
- Remove any of your app-layer listeners in
dispose(). - Dispose the SDK instance when not needed (e.g., app shutdown) to release resources and close streams.
Loading Features
Built-in fetching and caching
If you pass a hostURL and apiKey into the builder, the SDK handles network requests, caching, retry/backoff, and decryption (when configured in your SDK connection). You can enable streaming updates with backgroundSync: true.
final sdk = await GBSDKBuilderApp(
apiKey: "sdk-abc123",
hostURL: "https://cdn.growthbook.io",
backgroundSync: false,
).initialize();
// Manually refresh (e.g., on app start or navigation)
await sdk.refreshCache();
Custom integration (local evaluation)
If you prefer to control network and caching yourself, you can set a payload directly on the SDK. This enables fully local evaluation and offline-first behavior.
await sdk.setPayload({
"features": {
"feature-1": {"defaultValue": true},
"feature-2": {"defaultValue": "blue"}
}
});
Caching
The SDK persists downloaded feature payloads and related metadata. Configure TTL to control how long cached features are considered fresh before a background refresh.
- To use Stale-While-Revalidate strategy with TTL and streaming:
final sdk = await GBSDKBuilderApp(
apiKey: "sdk-abc123",
hostURL: "https://cdn.growthbook.io",
ttlSeconds: 300,
backgroundSync: true, // keep a live stream for fast updates
).initialize();
- Manual refresh at lifecycle boundaries:
// App resume, significant navigation, pull-to-refresh, etc.
await sdk.refreshCache();
- Identity or environment switch:
// Switch attributes to new identity, then refresh
sdk.setAttributes({"id": "new-user"});
await sdk.refreshCache();
// If you store sticky assignments yourself, clear the old user's keys
await myStickyStorage.clearForUser("old-user");
Web-specific caching considerations
- For Flutter Web, ensure your CDN respects cache-control headers for GrowthBook endpoints.
- Avoid overly aggressive service worker caching for evaluated payloads unless you manage invalidation carefully.
Real-time Updates (SSE)
Setup and lifecycle
- Initialization
- Enable
backgroundSync: truewhen building the SDK instance. - Optionally pass
streamingRequestHeaders(e.g., auth) andLast-Event-Idto resume after restarts.
- Enable
- Refresh handler
- Use
onFeaturesRefreshedto update UI or invalidate caches after new data is applied. - The callback fires for both streaming updates and manual refreshes.
- Use
- Teardown
- Dispose your SDK instance when your app shuts down. The connection will be closed automatically.
final sdk = await GBSDKBuilderApp(
apiKey: "sdk-abc123",
hostURL: "https://cdn.growthbook.io",
backgroundSync: true,
onFeaturesRefreshed: (success) {
if (success) {
// e.g., notify listeners / rebuild widgets
} else {
// network failure or payload issue
}
},
).initialize();
You can also add custom headers for secure requests:
final sdk = await GBSDKBuilderApp(
apiKey: "sdk-abc123",
hostURL: "https://cdn.growthbook.io",
requestHeaders: {"Authorization": "Bearer <token>"},
streamingRequestHeaders: {
"Authorization": "Bearer <token>",
// Optionally resume after disconnects
"Last-Event-Id": "<persisted-last-id>"
},
backgroundSync: true,
).initialize();
Network failures and reconnection
- The SDK uses an exponential backoff strategy on streaming errors and attempts to auto-reconnect.
- Provide stable connectivity hints to users only when necessary; otherwise allow background reconnection.
- Persist and reuse the
Last-Event-Idto avoid duplicate events on resume.
// Example: Persist last event id you receive from your stream handler
await storage.write(key: 'gb_last_event_id', value: lastEventId);
// Then pass it back during init via `streamingRequestHeaders` as shown above
Performance Considerations
- Prefer streaming when you need near real-time flag updates (admin toggles, ops tooling).
- Prefer polling/manual refresh when updates are infrequent or the app is latency- or battery-sensitive.
- Background sync opens a single lightweight SSE connection; avoid running multiple SDK instances with streaming in the same app.
Remote Evaluation
See the Remote Evaluation overview for more information about what Remote Evaluation is, how it works, and deployment options.
Run GrowthBook in Remote Evaluation mode to evaluate flags on a private server (e.g., GrowthBook Proxy). Sensitive rules never reach the client.
If you want Sticky Bucketing with Remote Evaluation, configure your remote backend (e.g., GrowthBook Proxy) with a persistent store (e.g., Redis). You do not need to provide a sticky service on the client.
final sdk = await GBSDKBuilderApp(
apiKey: "sdk-abc123",
hostURL: "https://gb-proxy.yourcompany.com",
remoteEval: true,
// Optional: only trigger a new evaluation when selected attributes change
cacheKeyAttributes: ["id", "email"],
attributes: {"id": "123", "email": "user@example.com"},
).initialize();
// Manually trigger a remote evaluation when attributes change
sdk.setAttributes({"plan": "pro"});
await sdk.refreshForRemoteEval();
Remote vs local evaluation
- Use remote evaluation when:
- You need to keep targeting rules and unused variations off-device for privacy/compliance.
- Payload size is large and you want device-optimized responses for the current user.
- Use local evaluation when:
- You need offline-first behavior or minimal round trips.
- The full feature payload is small and regularly reused.
Configuration
- Endpoint: Set
hostURLto your GrowthBook Proxy or secured backend base URL. - Auth: Send authentication via
requestHeadersto both fetching and remote eval endpoints. - Cache keys: Use
cacheKeyAttributesto limit remote calls to only when relevant identity fields change.
final sdk = await GBSDKBuilderApp(
apiKey: "sdk-abc123",
hostURL: "https://gb-proxy.example.com",
remoteEval: true,
cacheKeyAttributes: ["id", "email"],
requestHeaders: {"Authorization": "Bearer <token>"},
attributes: {"id": "u_123", "email": "u@example.com"},
).initialize();
Debugging Remote Evaluation
- Verify required attributes are present before calling
refreshForRemoteEval(). - Log the response status/source from your SDK callbacks to confirm whether updates came from network or cache.
- Check your proxy/server logs for rejected requests, auth failures, or schema mismatches.
Security considerations
- Treat the
apiKeyas public. Protect sensitive data with server-side auth on your proxy. - Use short-lived tokens in
requestHeaderswhen possible and rotate regularly. - Avoid putting PII directly in attributes unless hashed/secured per your policies.
Fallback strategies
- If remote eval fails, keep using the last known values from cache and retry later.
- Consider a hybrid approach: bootstrap with a local payload and switch to remote eval when online.
- Implement a user-visible retry or manual refresh action when business-critical.
Sticky Bucketing
Sticky bucketing ensures users see the same experiment variant across sessions. Read more about Sticky Bucketing Feature
- Enable Sticky Bucketing on the experiment or feature rule in GrowthBook.
- Provide a stable identity in attributes (e.g.,
id) after login; use a device-scoped fallback when logged out. - Use a globally unique
idfor logged-in users; avoid mutable identifiers. - For logged-out sessions, use a device/install identifier and switch to
idon login. - When identity changes (login/logout), update attributes immediately to avoid cross-user assignment mixing.
Implement a GBStickyBucketService to persist assignments. Attach your GBStickyBucketService when building the SDK to persist assignments across sessions.
class MyAppStickyBucketService extends GBStickyBucketService {
Future<Map<String, String>?> getAllAssignments(
Map<String, dynamic> attributes,
) async {
// Retrieve from local storage
// ...
return null;
}
Future<void> saveAssignments(
Map<String, dynamic> attributes,
Map<String, String> assignments,
) async {
// Save to local storage
// ...
}
}
final sdk = await GBSDKBuilderApp(
apiKey: "sdk-abc123",
stickyBucketService: MyAppStickyBucketService(),
).initialize();
For Remote evaluations, if your Remote Eval backend (e.g., GrowthBook Proxy) is configured for sticky bucketing, persistence happens server-side and you do not need to provide a client service.
// Remote evaluation with server-side sticky (no client sticky service needed)
final sdk = await GBSDKBuilderApp(
apiKey: "sdk-abc123",
hostURL: "https://gb-proxy.yourcompany.com",
remoteEval: true,
attributes: {"id": "user_123"},
).initialize();
Custom Sticky Bucketing Service configurations
- Namespacing: use an environment-specific key prefix (e.g.,
prod_,staging_). - Storage backend: choose durable storage for your platform (
SharedPreferences, secure storage, SQLite). - Migration: when changing storage layout, migrate existing assignment keys to preserve continuity.
Troubleshooting stale assignments
- Logout or user-switch: clear assignments for the previous identity or use identity-specific keys.
- Experiment retired/changed: allow non-sticky behavior to take effect; optionally prune retired experiment keys.
- Unexpected variant: confirm which identity key is being used and verify read/write paths in your storage.
See the Flutter repo for examples and updates: growthbook-flutter.
Encrypted features
Configure SDK Connection to deliver encrypted features, provide the decryption key during initialization.
final sdk = await GBSDKBuilderApp(
apiKey: "sdk-abc123",
hostURL: "https://cdn.growthbook.io",
decryptionKey: "<your-32-byte-key>",
).initialize();
Or prefer using a Remote Evaluation backend (e.g., GrowthBook Proxy) that decrypts server-side. This keeps secrets off the device.
final sdk = await GBSDKBuilderApp(
apiKey: "sdk-abc123",
hostURL: "https://gb-proxy.yourcompany.com", // Proxy handles decryption
remoteEval: true,
attributes: {"id": "user_123"},
).initialize();
Performance optimizations
- Track refresh times and payload sizes; reduce attribute surface area where possible.
- Avoid multiple SDK instances with streaming enabled; prefer a single shared instance.
- Use
onFeaturesRefreshedto scope UI rebuilds and avoid unnecessary widget tree updates.
Memory usage patterns
- Keep attribute maps small and reuse them when doing partial updates.
- Periodically prune stale sticky assignments if you implement a custom store.
- Avoid holding multiple SDK instances; share a single instance across the app.
Error handling and debugging
Debug logging configuration
// Simple logger wrapper for development
void gbLog(String message) {
assert(() { // only in debug mode
// ignore: avoid_print
print('[GrowthBook] ' + message);
return true;
}());
}
// Example usage
gbLog('Initializing GrowthBook');
Common error scenarios and solutions
- Initialization returned from cache only
- Ensure network connectivity and correct
hostURL/apiKey. - Call
await sdk.refreshCache()on resume or via pull-to-refresh.
- Ensure network connectivity and correct
- Unauthorized (401/403)
- Verify
requestHeaders(e.g., Bearer token) and that your proxy/CDN forwards headers.
- Verify
- Decryption errors
- Confirm
decryptionKeymatches the SDK Connection and is valid; prefer server-side decryption.
- Confirm
Feature evaluation debugging techniques
final res = sdk.feature('checkout_v2');
gbLog('feature=checkout_v2 value=${res.value} source=${res.source}');
if (res.source == 'experiment') {
gbLog('exp=' + (res.experiment?.key ?? 'n/a') + ' varId=${res.experimentResult?.variationId}');
}
Inspect attributes used during evaluation:
// Keep your own copy of the attributes you set
final currentAttrs = {"id": "123", "country": "US"};
sdk.setAttributes(currentAttrs);
gbLog('attrs=' + currentAttrs.toString());
Network connectivity troubleshooting
try {
await sdk.refreshCache();
gbLog('Refresh succeeded');
} catch (e) {
gbLog('Refresh error: ' + e.toString());
}
If streaming is enabled, rely on auto-reconnect with backoff. Allow a manual refresh UI if business-critical.
Platform-specific considerations
Web vs mobile behavior differences
- SSE support and networking may differ in browsers; validate CORS and service worker behavior for Flutter Web.
- Storage persistence differs (IndexedDB/localStorage on Web vs SharedPreferences/Secure Storage on mobile).
- For Web, prefer IndexedDB for larger storage; be careful with quota and private browsing modes.
- For Mobile: use
SharedPreferencesor secure storage for sticky assignments; consider SQLite for large datasets.
iOS/Android specific considerations
- iOS backgrounding may pause network activity; prefer manual refresh on foreground via app lifecycle hooks.
- Android Doze/App Standby can throttle background sync; design UX that tolerates delayed updates.
Do not rely on continuous streaming while the app is fully backgrounded; on resume, call refreshCache() and rely on backoff reconnects.
See Flutter SDK repo: https://github.com/growthbook/growthbook-flutter
Supported Features
FeaturesAll versions
ExperimentationAll versions
Sticky Bucketing≥ v3.8.0
Remote Evaluation≥ v3.7.0
Streaming≥ v3.4.0
Prerequisites≥ v3.2.0
Encrypted Features≥ v3.1.0
v2 Hashing≥ v3.1.0
SemVer Targeting≥ v3.1.0