# Optimizely

- Tool: FlutterFlow
- Difficulty: Advanced
- Time required: 45 minutes
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to Optimizely Feature Experimentation using a Custom Action (Dart) wrapping the official `optimizely_flutter_sdk` pub.dev package. You initialize the SDK with your public SDK Key on app start, then call `createUserContext(userId).decide(flagKey)` inside a second Custom Action to gate features or run A/B test variants. The SDK Key is public — safe to ship in the app — but test only on a real device, not in FlutterFlow's web preview.

## Run Feature Flags and A/B Tests Inside Your FlutterFlow App with Optimizely

Optimizely Feature Experimentation is unlike the other marketing integrations in this series. Rather than calling a REST API to push or pull data, Optimizely ships an official Flutter SDK (`optimizely_flutter_sdk` on pub.dev) that runs entirely on the device. The SDK downloads a JSON datafile from Optimizely's CDN, keeps it updated via polling, and evaluates flag decisions locally — fast, no per-decision API call, and works even with intermittent connectivity. For a FlutterFlow app, this means you can gate a screen behind a feature flag, roll out a new checkout flow to 20% of users, or run a real A/B test on button copy — all controlled from the Optimizely console without a new app release.

The auth model differs fundamentally from every other tool here: the SDK Key is a public, per-environment key found in Optimizely under Settings → Environments. It is designed to ship inside client apps and does not grant write access. Do not confuse it with Optimizely's REST API personal access token (used for admin operations at `api.optimizely.com`) — that one must stay server-side.

Implementation uses two FlutterFlow Custom Actions and the `optimizely_flutter_sdk` pub.dev dependency. The first Custom Action initializes the SDK on app start. The second evaluates a flag and writes the result to App State so any widget on any screen can react to it with conditional visibility. Critical constraint: Custom Actions with native Flutter plugins do not run in FlutterFlow's web-based Test Mode — you must test on a real device or via Local Run. Optimizely Feature Experimentation is on paid/enterprise plans; verify current pricing at optimizely.com/pricing.

## Before you start

- An Optimizely Feature Experimentation account with at least one project and one feature flag created (check optimizely.com/pricing for current plan options)
- Your SDK Key copied from Optimizely → Settings → Environments (Development or Production)
- A FlutterFlow project open in the browser, with App State variables ready to store flag results
- Access to a real iOS or Android device (or FlutterFlow Local Run) for testing — Custom Actions do not run in web Test Mode
- Basic familiarity with FlutterFlow's Custom Code panel and Action Flow Editor

## Step-by-step guide

### 1. Create a flag in Optimizely and copy your SDK Key

Log into app.optimizely.com and select your project (or create one). In the left navigation, go to 'Feature Flags' and click 'Create New Feature Flag'. Give it a key like `new_onboarding_v2` or `checkout_button_test` — this key is what you will pass to `decide()` in your Dart code, so note it exactly. The flag key is case-sensitive.

If you want to run an A/B test rather than a simple on/off flag, add variations to the flag: click 'Variables & Variations' and add a string or boolean variable that each variation sets differently (e.g., `button_text` = 'Get Started' in control, 'Start Free Trial' in challenger).

To get the SDK Key, navigate to Settings → Environments. You will see at least two environments: Development and Production. Each environment has its own SDK Key. Copy the Development SDK Key for testing — you will use the Production key when you are ready to publish. The SDK Key is a long alphanumeric string and is safe to include in client code.

Before leaving the Optimizely console, activate the flag (toggle it 'On') and if you have an audience or traffic allocation rule, configure it. For testing, set traffic to 100% and audience to 'Everyone'. Save your changes.

**Expected result:** You have a feature flag with a key (e.g., `new_onboarding_v2`) created and activated in Optimizely, and you have the Development SDK Key copied.

### 2. Add `optimizely_flutter_sdk` as a FlutterFlow pub dependency

In your FlutterFlow project, click 'Custom Code' in the left navigation bar. In the Custom Code panel, look for the 'Dependencies' or 'Pub Dependencies' section. Click '+ Add Dependency' and type `optimizely_flutter_sdk`. FlutterFlow will look up the package on pub.dev — select the latest stable version from the dropdown. Do not add a version constraint manually; let FlutterFlow handle version resolution. Click 'Save'.

FlutterFlow automatically adds this package to the `pubspec.yaml` of the generated Flutter project. You do not need to run `flutter pub get` — FlutterFlow handles this when you build or test the app. This is a native Flutter plugin, which is why it will not function in FlutterFlow's web-based Test Mode (the 'Run' button in the browser). The plugin requires platform code that only works on compiled iOS and Android builds or FlutterFlow Local Run.

Verify the dependency was accepted: refresh the Custom Code panel and confirm `optimizely_flutter_sdk` appears in the list of dependencies without an error badge. If it shows an error, check that the package name is spelled correctly — the exact name on pub.dev is `optimizely_flutter_sdk` (all lowercase, underscores).

**Expected result:** The `optimizely_flutter_sdk` dependency appears in FlutterFlow's Custom Code → Dependencies list without an error badge.

### 3. Write the Initialize Optimizely Client Custom Action

In the Custom Code panel, click '+ Add' and choose 'Action'. Name it `initOptimizely`. Set the return type to void (this action has no return value — it initializes a shared client). In the arguments section, add one argument: `sdkKey` (String). This is where you will pass your SDK Key from FlutterFlow's App Values or directly from an action parameter.

Paste the Dart code below into the code editor. The action initializes the Optimizely Flutter client using the provided SDK Key, waits for the datafile to be downloaded, and stores the initialized client in a static variable so the decide action can reference it later. The `OptimizelyDecideOption` and `OptimizelyClientConfig` classes come from the `optimizely_flutter_sdk` package.

After pasting the code, click 'Save'. If FlutterFlow shows any import errors, confirm the dependency is correctly added in the Dependencies tab. Then go to your app's initial screen (usually the main screen or a splash screen) and add this action in the 'On Page Load' action chain: Action Flow Editor → '+ Add Action' → 'Custom Actions' → 'initOptimizely' → pass your SDK Key string. For the SDK Key value, either type it directly or store it in FlutterFlow's App Values (left nav → App Values → + Add Constant) and reference it there.

Note: this action is async and the Optimizely client takes a moment to download the datafile. Guard your decide calls to run only after initialization completes. A simple way is to use a Boolean App State variable `optimizelyReady` that the init action sets to true on completion.

```
// Custom Action: initOptimizely
// Argument: sdkKey (String)
// Return: void

import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart';

// Static holder so decideFlag can access the same client
class OptimizelyHolder {
  static OptimizelyFlutterSdk? client;
}

Future<void> initOptimizely(String sdkKey) async {
  // Create the client
  final client = OptimizelyFlutterSdk(sdkKey);

  // Start the client and wait for the datafile to be fetched
  final response = await client.start();

  if (response.success) {
    OptimizelyHolder.client = client;
    // The SDK now has the datafile and is ready for decide() calls
  } else {
    // Log or handle initialization failure
    // response.reason contains the error message
    debugPrint('Optimizely init failed: ${response.reason}');
  }
}
```

**Expected result:** The `initOptimizely` Custom Action is saved in FlutterFlow without errors and is wired to run on the app's initial screen load.

### 4. Write the Decide Flag Custom Action and bind results to App State

In the Custom Code panel, click '+ Add' and choose 'Action'. Name it `decideFlagAction`. Add two arguments: `userId` (String — a unique ID for the current user, typically the user's UID from Firebase Auth or a device ID) and `flagKey` (String — the flag key from Optimizely, e.g., `new_onboarding_v2`). Set the return type to Boolean (or JSON if you need the full decision object including variation key).

Paste the Dart code below. The action retrieves the shared client from `OptimizelyHolder.client`, creates a user context with the provided userId, calls `decide(flagKey)`, and returns the `enabled` boolean. If the client is null (init has not completed yet), it returns false as the safe default.

After saving, go to the App State panel in FlutterFlow (left nav → App State → + Add Field) and add a Boolean field called `flagEnabled` (default: false). Optionally add a String field `flagVariation` to store the variation key.

To use the decide action: in the Action Flow Editor of the relevant screen or widget, add '+ Add Action' → 'Custom Actions' → 'decideFlagAction'. Pass `userId` from your authentication state (e.g., Firebase Auth user UID, or `FFAppState().userId` if stored). Pass the flag key as a string literal (`new_onboarding_v2`). Set the action's 'Return Value' output to update the `flagEnabled` App State variable. After the action, add conditional navigation or use FlutterFlow's conditional visibility settings to show different UI based on `flagEnabled`.

```
// Custom Action: decideFlagAction
// Arguments: userId (String), flagKey (String)
// Return: bool

import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart';

Future<bool> decideFlagAction(
  String userId,
  String flagKey,
) async {
  final client = OptimizelyHolder.client;

  // Guard: if init hasn't completed, return safe default (off)
  if (client == null) {
    debugPrint('Optimizely client not initialized — returning false for $flagKey');
    return false;
  }

  // Create a user context for this user
  final userContext = await client.createUserContext(userId: userId);

  if (userContext == null) {
    debugPrint('Failed to create Optimizely user context for $userId');
    return false;
  }

  // Decide the flag
  final decision = await userContext.decide(flagKey);

  // decision.enabled is the main result
  // decision.variationKey is the variation name for A/B tests
  debugPrint(
    'Flag $flagKey: enabled=${decision.enabled}, variation=${decision.variationKey}'
  );

  return decision.enabled;
}
```

**Expected result:** The `decideFlagAction` Custom Action is saved and wired to update the `flagEnabled` App State variable. When triggered, FlutterFlow widgets using conditional visibility on `flagEnabled` respond correctly.

### 5. Test on a real device and add conditional UI based on flag state

Custom Actions that use native Flutter plugins — including `optimizely_flutter_sdk` — do not run in FlutterFlow's browser-based Test Mode (the 'Run' button). This is a hard limitation: the web preview uses a JavaScript renderer that does not execute native Dart plugin code. When you tap the preview in the browser and nothing happens, the Custom Action code is not running — this is expected, not a bug in your code.

To test, you have two options. First, FlutterFlow Local Run: install the FlutterFlow companion app on a physical iOS or Android device, connect via Local Run from the FlutterFlow toolbar, and your app runs natively on the device with live reload. Second, download the Flutter project from FlutterFlow (via the Download Code button) and run it locally with `flutter run` — but Local Run is the easier option that stays in the FlutterFlow workflow.

Once running on device, observe the debug console for the `debugPrint` messages from the init and decide actions. You should see `Optimizely init failed` or confirmation of the initialized client, then the decide output showing the flag key, enabled status, and variation key.

For the conditional UI: on the screen where you want to gate a feature, select the widget (Container, Screen, or Button) you want to show/hide. In its properties panel, find 'Conditional Visibility' and set the condition to 'App State → flagEnabled == true'. If you are running an A/B test, use a conditional value on a Text widget's content property: if `App State → flagVariation == 'challenger'` then 'Start Free Trial', else 'Get Started'.

If you would rather skip writing the Custom Actions yourself, RapidDev's team builds FlutterFlow integrations like this every week — free scoping call at rapidevelopers.com/contact.

**Expected result:** On a real device, the `initOptimizely` action runs on startup, `decideFlagAction` returns the correct enabled/variation state, and conditional UI renders the right variant for the flag state configured in the Optimizely console.

## Best practices

- Initialize the Optimizely client once on app start (in the initial screen's On Page Load action) and store the result in App State — calling initialize on every screen wastes datafile downloads and adds latency.
- Use a static Dart class (like `OptimizelyHolder`) to share the initialized client between Custom Actions, since FlutterFlow Custom Actions cannot directly pass Dart objects through App State variables.
- Always guard decide calls with a null check on the client and return false (the safe default) if initialization has not completed — this prevents a blank or broken UI state during startup.
- Use the Development SDK Key for testing and the Production SDK Key only in your production build — they control separate traffic allocations and you do not want test users affecting production experiment results.
- Do not confuse the Optimizely SDK Key (public, safe to ship in client code) with the REST API personal access token (secret, admin-level, must stay server-side) — they are different credentials with very different security implications.
- Test on a real device or via FlutterFlow Local Run — Custom Actions using native Flutter plugins will never run in the web-based Test Mode, and this is expected, not a bug.
- Use Optimizely's forced decisions feature during development to pin specific test user IDs to a specific variant, so you can test each code path without relying on random traffic allocation.

## Use cases

### Feature flag controlling a new onboarding flow

A team is rebuilding their onboarding flow in FlutterFlow and wants to test it with 10% of new users before a full rollout. They create a flag `new_onboarding_v2` in Optimizely, initialize the SDK on app start, and use the decide result to conditionally show the new flow or the existing one — controlled entirely from the Optimizely console.

Prompt example:

```
When the app launches, initialize Optimizely and check the flag 'new_onboarding_v2' for this user. If enabled, navigate to the new onboarding screen; otherwise show the existing one.
```

### A/B test on subscription page button copy and colour

A SaaS founder wants to test whether 'Start Free Trial' or 'Get Started' converts better on their paywall screen. They create an Optimizely flag with two variations, write the variation key to App State when the user reaches the screen, and bind the button text and colour to conditional values based on the variation. Optimizely tracks which variation the user saw alongside conversion events.

Prompt example:

```
On the subscription screen, check the 'paywall_button_test' Optimizely flag. If variation is 'control' show a green button with 'Get Started'. If variation is 'challenger' show a blue button with 'Start Free Trial'.
```

### Progressive rollout of a new AI-powered search screen

A marketplace app is adding an AI search screen but wants to roll it out to 5% of users first, then gradually increase. The Optimizely flag controls visibility of the search icon in the navigation bar — no code change, no app store release needed to adjust the rollout percentage.

Prompt example:

```
Check the 'ai_search_enabled' flag on app start. Store the result in App State. Show the AI search navigation icon only when the flag is enabled for this user.
```

## Troubleshooting

### Optimizely decide always returns `false` (flag disabled) even though the flag is enabled in the console

Cause: Either the SDK Key is wrong (points to the wrong environment), the flag key string has a typo, or the Optimizely client is not yet initialized when decide is called.

Solution: Check the debug console for the initOptimizely debugPrint message — if you see 'Optimizely init failed', the SDK Key is likely wrong. Copy the SDK Key again from Optimizely → Settings → Environments and ensure you are using the right environment (Development vs Production). If init succeeds but decide still returns false, log `decision.variationKey` and `decision.reasons` from the decide response — `decision.reasons` lists why a flag is disabled (e.g., flag is paused, user does not match audience, traffic is 0%).

```
// Log full decision reasons for debugging
final decision = await userContext.decide(flagKey);
debugPrint('Decide reasons: ${decision.reasons}');
```

### Nothing happens when triggering Custom Actions in FlutterFlow's web Test Mode (browser preview)

Cause: Native Flutter plugin Custom Actions do not run in FlutterFlow's web-based Test Mode. The browser preview uses a JavaScript renderer and cannot execute native Dart plugin code.

Solution: This is expected behavior, not a bug. Test the integration using FlutterFlow Local Run (connect a physical device via the Local Run option in the FlutterFlow toolbar) or download the Flutter code and run it with `flutter run` on a device. Your web-built FlutterFlow UI will work fine — only native plugin Custom Actions require device testing.

### `OptimizelyHolder.client` is null when `decideFlagAction` runs

Cause: The decide action was called before the initialize action completed, or the initialize action ran on a different action thread and the static holder was not populated.

Solution: Ensure `initOptimizely` is called at the very beginning of the app (in the 'On Page Load' of your initial/splash screen) and that it completes before any decide calls. Add an App State Boolean `optimizelyReady` that is set to true inside the `if (response.success)` block in the init action, and wrap all decide calls in a conditional that checks `optimizelyReady` first.

### FlutterFlow shows a dependency conflict error after adding `optimizely_flutter_sdk`

Cause: The package version conflicts with another Flutter dependency in the project (FlutterFlow manages its own set of packages).

Solution: Check pub.dev for the latest stable version of `optimizely_flutter_sdk` and try pinning to an earlier version that resolves the conflict. In FlutterFlow's Dependencies field, enter the version as `^X.Y.Z` (e.g., `^2.0.0`). If the conflict persists, check the Optimizely Flutter SDK GitHub repository for known compatibility issues with recent FlutterFlow Flutter versions.

## Frequently asked questions

### Is the Optimizely SDK Key safe to include in the compiled Flutter app?

Yes. The SDK Key is a public, client-side key that Optimizely designed to be shipped in mobile and web apps. It identifies your project and environment but does not grant write access to your Optimizely account. Do not confuse it with the Optimizely REST API personal access token — that is a secret admin credential that must stay server-side.

### Why does nothing happen when I test my Custom Actions in FlutterFlow's web preview?

FlutterFlow's web-based Test Mode (the 'Run' button in the browser) uses a JavaScript renderer that cannot execute native Dart plugin code. The `optimizely_flutter_sdk` is a native Flutter plugin, so it will not fire in web preview. This is expected behavior. Use FlutterFlow Local Run (connect a real device via the Local Run option in the toolbar) to test your Custom Actions with the Optimizely SDK.

### Can I use Optimizely to run an A/B test on two completely different screens in FlutterFlow?

Yes. The `decideFlagAction` returns both an `enabled` boolean and a `variationKey` string. Store the variation key in App State, then use FlutterFlow's conditional navigation in your Action Flow to navigate to Screen A when the variation is 'control' and Screen B when it is 'challenger'. You can also use conditional visibility to show or hide entire Containers on a single screen.

### How does Optimizely know which user is which across app sessions?

You pass a `userId` string to `createUserContext()`. This ID is the bucket key — Optimizely uses it to consistently assign the same user to the same variation across sessions. Use a stable, unique identifier like the user's Firebase Auth UID, Supabase user ID, or a UUID stored in shared preferences on first launch. Do not use a random ID that changes per session, as that would put the user in a different variation on every open.

### What Optimizely plan do I need to use the Flutter SDK?

Optimizely Feature Experimentation is available on paid and enterprise plans. There is no free tier for Feature Experimentation (the free Rollouts product was discontinued). Verify current pricing and plan availability at optimizely.com/pricing before building, as plans and pricing change.

### Can I use Optimizely Feature Experimentation on a Flutter web build from FlutterFlow?

The `optimizely_flutter_sdk` package targets native iOS and Android. For Flutter web builds, the native plugin code will not compile. If you need feature flagging for a web-only FlutterFlow app, you would need to use Optimizely's JavaScript SDK through a Custom Action that uses a `dart:js` interop bridge — a significantly more complex setup that requires advanced Dart and Flutter web knowledge.

---

Source: https://www.rapidevelopers.com/flutterflow-integrations/optimizely
© RapidDev — https://www.rapidevelopers.com/flutterflow-integrations/optimizely
