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
deviceId
toid
(stable user id). - On logout, remove
id
and usedeviceId
until 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 optionalfeatureId
for 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: true
when building the SDK instance. - Optionally pass
streamingRequestHeaders
(e.g., auth) andLast-Event-Id
to resume after restarts.
- Enable
- Refresh handler
- Use
onFeaturesRefreshed
to 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-Id
to 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
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
hostURL
to your GrowthBook Proxy or secured backend base URL. - Auth: Send authentication via
requestHeaders
to both fetching and remote eval endpoints. - Cache keys: Use
cacheKeyAttributes
to 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
apiKey
as public. Protect sensitive data with server-side auth on your proxy. - Use short-lived tokens in
requestHeaders
when 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
id
for logged-in users; avoid mutable identifiers. - For logged-out sessions, use a device/install identifier and switch to
id
on 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
onFeaturesRefreshed
to 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
decryptionKey
matches 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
SharedPreferences
or 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