> ## Documentation Index
> Fetch the complete documentation index at: https://docs.growthbook.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Flutter SDK

> Install the GrowthBook Flutter SDK to evaluate feature flags and run A/B tests in Flutter mobile, web, and desktop apps.

This SDK supports the following versions:

* **Android version 21 & above**
* **iOS version 12 & Above**
* **Apple TvOS version 13 & Above**
* **Apple WatchOS version 7 & Above**

flutter

Flutter SDK Resources

Get help on Slack

## Installation

Add this to your `pubspec.yaml` file

```yaml theme={null}
growthbook_sdk_flutter: ^4.0.0
```

## Quick Usage

Create a `GrowthBookSDK` instance with `GBSDKBuilderApp`, set attributes, then evaluate features or run experiments.

```dart theme={null}
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:

```dart theme={null}
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 bucketing
* `deviceId` (string): device/install-scoped fallback when user not logged in
* `country`, `locale` (string)
* `plan`, `company`, `role` (string)
* `loggedIn` (bool)
* `appVersion` (string, supports semver targeting)

Use `setAttributes()` method to set the user context.

```dart theme={null}
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 like `deviceId`).
* 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` to `id` (stable user id).
* On logout, remove `id` and use `deviceId` until the next login.

```dart theme={null}
// 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:

```dart theme={null}
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 experiments with `GrowthBook`, use `run` method, which takes a `GBExperiment` object as an argument and returns a `GBExperimentResult` object:

```dart theme={null}
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 to `id`)

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.

```dart theme={null}
final sdk = await GBSDKBuilderApp(
  apiKey: "sdk-abc123",
  hostURL: "https://cdn.growthbook.io",
  growthBookTrackingCallBack: (exp, result) {
    // e.g., send to analytics
  },
).initialize();
```

To Subscribe :

```dart theme={null}
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();
```

> [!NOTE]
>
> **When subscriptions fire vs. don't fire**
>
> 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 optional `featureId` 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`.

```dart theme={null}
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.

```dart theme={null}
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:

```dart theme={null}
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:

```dart theme={null}
// App resume, significant navigation, pull-to-refresh, etc.
await sdk.refreshCache();
```

* Identity or environment switch:

```dart theme={null}
// 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) and `Last-Event-Id` to resume after restarts.
* 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.
* Teardown
  * Dispose your SDK instance when your app shuts down. The connection will be closed automatically.

```dart theme={null}
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:

```dart theme={null}
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.

```dart theme={null}
// 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

> [!TIP]
>
> See the [Remote Evaluation overview](https://docs.growthbook.io/self-host/remote-evaluation.md) 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.

> [!NOTE]
>
> **Sticky Bucketing with Remote Evaluation**
>
> 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.

```dart theme={null}
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.

```dart theme={null}
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](https://docs.growthbook.io/app/sticky-bucketing.md)

* 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.

```dart theme={null}
class MyAppStickyBucketService extends GBStickyBucketService {
  @override
  Future<Map<String, String>?> getAllAssignments(
    Map<String, dynamic> attributes,
  ) async {
    // Retrieve from local storage
    // ...
    return null;
  }

  @override
  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.

```dart theme={null}
// 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](https://github.com/growthbook/growthbook-flutter).

## Encrypted features

Configure SDK Connection to deliver encrypted features, provide the decryption key during initialization.

```dart theme={null}
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.

```dart theme={null}
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

```dart theme={null}
// 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.
* Unauthorized (401/403)
  * Verify `requestHeaders` (e.g., Bearer token) and that your proxy/CDN forwards headers.
* Decryption errors
  * Confirm `decryptionKey` matches the SDK Connection and is valid; prefer server-side decryption.

#### Feature evaluation debugging techniques

```dart theme={null}
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:

```dart theme={null}
// 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

```dart theme={null}
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
