---
title: "How to add feature flags and experiments to a Flutter app"
description: "Show Flutter report variants with Amplitude, keep flags aligned with signed-in identity, and measure saves after exposure."
canonical: "https://amplitude.com/en-us/build/flutter-feature-flags"
---

# How to add feature flags and experiments to a Flutter app

Show Flutter report variants with Amplitude, keep flags aligned with signed-in identity, and measure saves after exposure.

Source: https://amplitude.com/en-us/build/flutter-feature-flags

---

# How to add feature flags and experiments to a Flutter app

Show a baseline report or a summary variant, then measure report saves with the same analytics identity. Allow about 25 minutes.

- Flutter 3.32+
- Dart 3.8+
- iOS and Android
- Experiment SDK 1.0
- 25 min

Chris Tam · Last updated 22 October 2026

Flutter keeps a screen alive across many widget rebuilds. I use [feature flags](https://amplitude.com/feature-management) to choose the report layout when someone opens it, then keep that choice for the screen's lifetime.

In this tutorial, I extend [Flutter product analytics](https://amplitude.com/build/flutter-product-analytics), fetch a report flag with the current user, show a summary to the treatment group, and compare saves after exposure.

A report with `control` and `treatment` layouts. Opening it reads `report-summary`; the treatment adds a summary, and **Save report** records `Report Saved`. The sample holds the saved state on the current screen; connect that action to your application's persistence when using it in production.

## Prerequisites

- The app from [Flutter product analytics](https://amplitude.com/build/flutter-product-analytics), including its single `AppAnalytics` instance, named screen observer and verified-session adapter. Keep `lib/analytics.dart` and `lib/auth.dart` unchanged.
- Flutter 3.32 or later and Dart 3.8 or later for `amplitude_experiment` 1.0.0. The analytics package remains pinned to 4.7.3. Configure an iOS or Android target and retain its platform files.
- A client deployment key from the same Amplitude project as the analytics API key. Use a client deployment in this mobile app; server deployment keys stay on a server. Match the project's region in both SDKs.

[Sign up for free](https://app.amplitude.com/signup) if you need an Amplitude account. Use the [experiment quick start](https://amplitude.com/docs/feature-experiment/experiment-quick-start) for project setup and experiment configuration.

## 1. Configure the report experiment

In Amplitude, create a feature experiment with flag key `report-summary`, variants `control` and `treatment`, and the client deployment used by the app. Define the exposure step and use `Report Saved` as the primary conversion metric. Save the configuration and use your test users to check both variants before widening the audience.

Keep the same key in the app's experiment module. Flutter and the dashboard need to agree on this string:

**lib/flags.dart, flag key inside ReportFlags**

lib/flags.dart, flag key inside ReportFlags

```dart
static const flagKey = 'report-summary';
```

Use the [experiment configuration workflow](https://amplitude.com/docs/feature-experiment/workflow/configure) to choose targeting and traffic allocation. A rollout selects who receives a variant; experiment analysis compares the defined conversion after exposure.

## 2. Add one Experiment client

Install the client package in the existing app:

**Terminal, project root**

Terminal, project root

```sh
flutter pub add amplitude_experiment:1.0.0
```

Create the complete module below. The [Amplitude integration](https://amplitude.com/docs/sdks/experiment-sdks/experiment-flutter#initialize) uses the existing analytics instance for identity and exposure delivery. Its instance name and region match that client.

**lib/flags.dart**

lib/flags.dart

```dart
import 'package:amplitude_experiment/amplitude_experiment.dart' as experiment;
import 'package:amplitude_flutter/constants.dart' as analytics;

import 'analytics.dart';
import 'auth.dart';

class ReportFlags {
  ReportFlags._(this._client, this._analytics);
  final experiment.ExperimentClient _client;
  final AppAnalytics _analytics;
  bool _ready = false;
  static const flagKey = 'report-summary';

  static Future<ReportFlags> create({
    required AppAnalytics analyticsClient,
    required String deploymentKey,
  }) async {
    if (deploymentKey.trim().isEmpty) {
      throw ArgumentError('Set AMPLITUDE_DEPLOYMENT_KEY with --dart-define.');
    }
    final client = await experiment.Experiment.initializeWithAmplitude(
      deploymentKey,
      experiment.ExperimentConfig(
        instanceName: analyticsClient.client.configuration.instanceName,
        serverZone:
            analyticsClient.client.configuration.serverZone ==
                analytics.ServerZone.eu
            ? experiment.ServerZone.eu
            : experiment.ServerZone.us,
        fetchTimeoutMillis: 1500,
        retryFetchOnFailure: false,
        automaticFetchOnAmplitudeIdentityChange: false,
      ),
    );
    return ReportFlags._(client, analyticsClient);
  }

  // Await identity transitions before enabling report navigation again.
  Future<void> refresh(AppUser? user) async {
    _ready = false;
    try {
      await _client.clear();
      final deviceId = await _analytics.client.getDeviceId();
      if (deviceId == null || deviceId.length < 5) return;
      await _client.fetch(
        experiment.ExperimentUser(
          userId: user?.id,
          deviceId: deviceId,
          userProperties: user == null ? {} : {'plan': user.plan},
        ),
        const experiment.FetchOptions(flagKeys: [flagKey]),
      );
      _ready = true;
    } catch (_) {
      // Keep the baseline available without reading another user's cache.
    }

  Future<String> layoutAtUse() async {
    if (!_ready) return 'control';
    try {
      final variant = await _client.variant(flagKey);
      return variant.value == 'treatment' ? 'treatment' : 'control';
    } catch (_) {
      return 'control';
    }

  Future<void> clear() async {
    _ready = false;
    await _client.clear();
  }
```

`refresh` clears the prior cache, sends the current identity and plan, and waits for the fetch. Passing the plan explicitly avoids relying on asynchronous profile enrichment. The baseline remains available when no variant is assigned or a request fails. That baseline does not establish a successful control assignment.

**Why.**` variant()` can track exposure. I call it only when opening the report, after the fetch, and retain that result for the screen. Reading a flag from every widget build would tie measurement to rebuilds.

## 3. Synchronize authentication and flag refresh

Keep analytics authentication separate from Experiment. Call this coordinator after the application's existing login succeeds, on relevant plan changes, and after sign-out completes. Its queue keeps two identity changes from racing.

**lib/flag\_session.dart**

lib/flag\_session.dart

```dart
// Keep Replay pause/resume and engagement cleanup added by later Flutter pages.
import 'analytics.dart';
import 'auth.dart';
import 'flags.dart';

class FlagSession {
  FlagSession(this.analytics, this.flags);
  final AppAnalytics analytics;
  final ReportFlags flags;
  Future<void> _pending = Future<void>.value();

  Future<void> update(AppUser? user, {bool signingOut = false}) {
    final operation = _pending.then((_) async {
      // Run engagement closeAll and pause Replay here before identity changes.
      if (signingOut) {
        await analytics.signedOut();
      } else if (user != null) {
        await analytics.signedIn(user);
      }
      await flags.refresh(user);
      // Resume Replay only after it has the current analytics IDs.
    });
    _pending = operation.catchError((Object _) {});
    return operation;
  }
```

Connect a successful login by awaiting `session.update(verifiedUser)`. On sign-out, await `session.update(null, signingOut: true)` before allowing the next person's report navigation. Keep the report UI disabled while either operation is pending. On anonymous startup, call `update(null)` without `signingOut`; resetting on each launch would discard the existing device identity.

The sign-out sequence resets analytics, then clears and refetches flags. If you also use guides and surveys, close them before analytics reset. Pause replay before the identity change and resume it only after synchronizing its IDs. Merge these additions into this coordinator; keep the existing hooks.

## 4. Render the variant and record saves

Add the conversion helper. The action records the same canonical user and device identity already set on the analytics client.

**lib/report\_events.dart**

lib/report\_events.dart

```dart
import 'package:amplitude_flutter/events/base_event.dart';
import 'analytics.dart';

Future<void> reportSaved(AppAnalytics analytics) async {
  try {
    await analytics.client.track(
      BaseEvent(
        'Report Saved',
        eventProperties: {
          'report_type': 'usage',
          'days': 7,
          'framework': 'flutter',
        },
      ),
    );
  } catch (_) {
    // Saving a report remains usable if analytics is unavailable.
  }
```

Merge the following root and report-screen changes with existing routes, observers and application imports. This complete sample retains Home and a named `/report` route. The native platform configuration and the two unchanged base modules remain in place.

**lib/main.dart**

lib/main.dart

```dart
// Keep analytics observers and Replay/engagement additions from other Flutter pages.
import 'package:amplitude_flutter/constants.dart';
import 'package:amplitude_flutter/observers/amplitude_navigator_observer.dart';
import 'package:flutter/material.dart';

import 'analytics.dart';
import 'auth.dart';
import 'flag_session.dart';
import 'flags.dart';
import 'report_events.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  const zone = String.fromEnvironment(
    'AMPLITUDE_SERVER_ZONE',
    defaultValue: 'US',
  );
  final analytics = await AppAnalytics.create(
    apiKey: const String.fromEnvironment('AMPLITUDE_API_KEY'),
    serverZone: zone == 'EU' ? ServerZone.eu : ServerZone.us,
  );
  final flags = await ReportFlags.create(
    analyticsClient: analytics,
    deploymentKey: const String.fromEnvironment('AMPLITUDE_DEPLOYMENT_KEY'),
  );
  final session = FlagSession(analytics, flags);
  await session.update(await restoreSignedInUser());
  runApp(FlagReportsApp(analytics: analytics, flags: flags));
}

class FlagReportsApp extends StatelessWidget {
  const FlagReportsApp({
    super.key,
    required this.analytics,
    required this.flags,
  });
  final AppAnalytics analytics;
  final ReportFlags flags;

  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'Usage reports',
    navigatorObservers: [AmplitudeNavigatorObserver(analytics.client)],
    routes: {'/': (context) => ReportHome(analytics: analytics, flags: flags)},
  );
}

class ReportHome extends StatefulWidget {
  const ReportHome({super.key, required this.analytics, required this.flags});
  final AppAnalytics analytics;
  final ReportFlags flags;
  @override
  State<ReportHome> createState() => _ReportHomeState();
}

class _ReportHomeState extends State<ReportHome> {
  bool _opening = false;
  Future<void> _open() async {
    if (_opening) return;
    setState(() => _opening = true);
    final layout = await widget.flags.layoutAtUse();
    if (!mounted) return;
    await Navigator.of(context).push(
      MaterialPageRoute<void>(
        settings: const RouteSettings(name: '/report'),
        builder: (_) =>
            FlagReportScreen(analytics: widget.analytics, layout: layout),
      ),
    );
    if (mounted) setState(() => _opening = false);
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: const Text('Usage reports')),
    body: Center(
      child: FilledButton(
        onPressed: _opening ? null : _open,
        child: const Text('Preview report'),
      ),
  );
}

class FlagReportScreen extends StatefulWidget {
  const FlagReportScreen({
    super.key,
    required this.analytics,
    required this.layout,
  });
  final AppAnalytics analytics;
  final String layout;
  @override
  State<FlagReportScreen> createState() => _FlagReportScreenState();
}

class _FlagReportScreenState extends State<FlagReportScreen> {
  bool _saved = false;
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) widget.analytics.reportPreviewed();
    });
  }

  void _save() {
    if (_saved) return;
    setState(() => _saved = true);
    // This sample saves to the current screen's state; persist in your app here.
    reportSaved(widget.analytics);
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: const Text('Seven-day usage')),
    body: Column(
      children: [
        const Text('Your usage report is ready.'),
        if (widget.layout == 'treatment')
          const Text('Your last seven days are ready to save.'),
        FilledButton(
          onPressed: _saved ? null : _save,
          child: Text(_saved ? 'Report saved' : 'Save report'),
        ),
      ],
    ),
  );
}
```

The report preview event stays on the destination screen. Saving changes the sample's screen state and queues one `Report Saved` event. It does not fetch the flag again, manually send an exposure, or duplicate the preview.

**MCP.** Use the [Amplitude MCP server](https://amplitude.com/docs/amplitude-ai/amplitude-mcp) with the existing project to inspect event names before creating a second conversion name for an action your app already measures.

## 5. Run with matching project and deployment keys

Pass both values into the Flutter build. Set `AMPLITUDE_SERVER_ZONE=EU` through a third `--dart-define` when using an EU project.

**Terminal, project root**

Terminal, project root

```sh
flutter run --dart-define=AMPLITUDE_API_KEY=your-project-api-key --dart-define=AMPLITUDE_DEPLOYMENT_KEY=your-client-deployment-key
```

Restart after changing build-time values. Keep server keys and application authentication credentials out of these defines.

## Verify it worked

1. Assign a test user to control, launch the app, and open the report. Confirm the summary is absent. Return Home and repeat with a user assigned to treatment; the summary should appear before the report is used.
2. Rebuild the report widget without reopening it. Confirm the app does not read the variant or record another report preview solely because it rebuilt.
3. In **Users > User Profiles**, inspect the same user or device. Confirm `Report Previewed`, the report screen event and `Report Saved` after tapping Save. Verify `report_type: usage`, `days: 7`, and `framework: flutter` on the custom events.
4. Verify exposure delivery from the Analytics integration and the `[Experiment] Exposure` event used in experiment analysis. Confirm the metric uses `Report Saved` from that same project and identity.
5. Change the signed-in plan, await the coordinated update and reopen the report. Check the new targeting context. Sign out, await reset/clear/refetch, and verify the next report uses the new anonymous device identity rather than the previous account's variant.
6. Exercise a failed fetch and an unassigned user. Both should show the baseline; neither is evidence that the user was successfully assigned to the control group.

## Troubleshooting

### The deployment key is missing or belongs to another project

Pass a nonempty client deployment key with `--dart-define`. The module rejects an empty value. Check that the flag belongs to that deployment and that the deployment's project matches analytics.

### EU users receive no variants

Use the EU project key and EU deployment key together. The module maps the analytics region to Experiment's separate `ServerZone` enum. Keep the analytics instance name identical to Experiment's.

### Plan targeting uses an old value

Await `session.update(verifiedUser)` after changing the trusted plan, then reopen the report. `refresh` sends the new plan explicitly instead of waiting for a remote profile update.

### A new account receives a previous account's layout

Block navigation through the whole identity transition. Reset analytics at sign-out, clear the variant cache and refetch before reading the report flag. Replacing only the visible profile name does not synchronize either SDK.

### Conversions arrive without useful experiment results

Check the exposure step, experiment allocation and conversion identity. A visible baseline after an error is an availability fallback. Inspect processed exposures and the experiment analysis before interpreting a conversion difference.

## FAQ

### Does fetching a flag count as using its variant?

This example fetches before rendering and reads `variant()` when opening the report. The Analytics integration supplies exposure tracking for that read. Saving the report does not read it again.

### Does the flag control report permissions?

Authentication and authorization remain in your application. This experiment changes a cosmetic summary. Keep access checks outside the flag decision.

### Does Save report persist anything remotely?

The sample keeps a saved indicator on the current screen and sends the analytics event. Connect your actual save operation before that event in an existing app, so the conversion represents its successful outcome.

## Full code reference

Unchanged from [Flutter product analytics](https://amplitude.com/build/flutter-product-analytics); keep your versions: `lib/analytics.dart`, `lib/auth.dart`, native platform files and app-specific authentication. Merge the root changes instead of deleting other routes or product integrations.

Full code reference

```dart
// -- lib/flags.dart --
import 'package:amplitude_experiment/amplitude_experiment.dart' as experiment;
import 'package:amplitude_flutter/constants.dart' as analytics;

import 'analytics.dart';
import 'auth.dart';

class ReportFlags {
  ReportFlags._(this._client, this._analytics);
  final experiment.ExperimentClient _client;
  final AppAnalytics _analytics;
  bool _ready = false;
  static const flagKey = 'report-summary';

  static Future<ReportFlags> create({
    required AppAnalytics analyticsClient,
    required String deploymentKey,
  }) async {
    if (deploymentKey.trim().isEmpty) {
      throw ArgumentError('Set AMPLITUDE_DEPLOYMENT_KEY with --dart-define.');
    }
    final client = await experiment.Experiment.initializeWithAmplitude(
      deploymentKey,
      experiment.ExperimentConfig(
        instanceName: analyticsClient.client.configuration.instanceName,
        serverZone:
            analyticsClient.client.configuration.serverZone ==
                analytics.ServerZone.eu
            ? experiment.ServerZone.eu
            : experiment.ServerZone.us,
        fetchTimeoutMillis: 1500,
        retryFetchOnFailure: false,
        automaticFetchOnAmplitudeIdentityChange: false,
      ),
    );
    return ReportFlags._(client, analyticsClient);
  }

  // Await identity transitions before enabling report navigation again.
  Future<void> refresh(AppUser? user) async {
    _ready = false;
    try {
      await _client.clear();
      final deviceId = await _analytics.client.getDeviceId();
      if (deviceId == null || deviceId.length < 5) return;
      await _client.fetch(
        experiment.ExperimentUser(
          userId: user?.id,
          deviceId: deviceId,
          userProperties: user == null ? {} : {'plan': user.plan},
        ),
        const experiment.FetchOptions(flagKeys: [flagKey]),
      );
      _ready = true;
    } catch (_) {
      // Keep the baseline available without reading another user's cache.
    }

  Future<String> layoutAtUse() async {
    if (!_ready) return 'control';
    try {
      final variant = await _client.variant(flagKey);
      return variant.value == 'treatment' ? 'treatment' : 'control';
    } catch (_) {
      return 'control';
    }

  Future<void> clear() async {
    _ready = false;
    await _client.clear();
  }

// -- lib/flag_session.dart --
// Keep Replay pause/resume and engagement cleanup added by later Flutter pages.
import 'analytics.dart';
import 'auth.dart';
import 'flags.dart';

class FlagSession {
  FlagSession(this.analytics, this.flags);
  final AppAnalytics analytics;
  final ReportFlags flags;
  Future<void> _pending = Future<void>.value();

  Future<void> update(AppUser? user, {bool signingOut = false}) {
    final operation = _pending.then((_) async {
      // Run engagement closeAll and pause Replay here before identity changes.
      if (signingOut) {
        await analytics.signedOut();
      } else if (user != null) {
        await analytics.signedIn(user);
      }
      await flags.refresh(user);
      // Resume Replay only after it has the current analytics IDs.
    });
    _pending = operation.catchError((Object _) {});
    return operation;
  }

// -- lib/report_events.dart --
import 'package:amplitude_flutter/events/base_event.dart';
import 'analytics.dart';

Future<void> reportSaved(AppAnalytics analytics) async {
  try {
    await analytics.client.track(
      BaseEvent(
        'Report Saved',
        eventProperties: {
          'report_type': 'usage',
          'days': 7,
          'framework': 'flutter',
        },
      ),
    );
  } catch (_) {
    // Saving a report remains usable if analytics is unavailable.
  }

// -- lib/main.dart --
// Keep analytics observers and Replay/engagement additions from other Flutter pages.
import 'package:amplitude_flutter/constants.dart';
import 'package:amplitude_flutter/observers/amplitude_navigator_observer.dart';
import 'package:flutter/material.dart';

import 'analytics.dart';
import 'auth.dart';
import 'flag_session.dart';
import 'flags.dart';
import 'report_events.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  const zone = String.fromEnvironment(
    'AMPLITUDE_SERVER_ZONE',
    defaultValue: 'US',
  );
  final analytics = await AppAnalytics.create(
    apiKey: const String.fromEnvironment('AMPLITUDE_API_KEY'),
    serverZone: zone == 'EU' ? ServerZone.eu : ServerZone.us,
  );
  final flags = await ReportFlags.create(
    analyticsClient: analytics,
    deploymentKey: const String.fromEnvironment('AMPLITUDE_DEPLOYMENT_KEY'),
  );
  final session = FlagSession(analytics, flags);
  await session.update(await restoreSignedInUser());
  runApp(FlagReportsApp(analytics: analytics, flags: flags));
}

class FlagReportsApp extends StatelessWidget {
  const FlagReportsApp({
    super.key,
    required this.analytics,
    required this.flags,
  });
  final AppAnalytics analytics;
  final ReportFlags flags;

  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'Usage reports',
    navigatorObservers: [AmplitudeNavigatorObserver(analytics.client)],
    routes: {'/': (context) => ReportHome(analytics: analytics, flags: flags)},
  );
}

class ReportHome extends StatefulWidget {
  const ReportHome({super.key, required this.analytics, required this.flags});
  final AppAnalytics analytics;
  final ReportFlags flags;
  @override
  State<ReportHome> createState() => _ReportHomeState();
}

class _ReportHomeState extends State<ReportHome> {
  bool _opening = false;
  Future<void> _open() async {
    if (_opening) return;
    setState(() => _opening = true);
    final layout = await widget.flags.layoutAtUse();
    if (!mounted) return;
    await Navigator.of(context).push(
      MaterialPageRoute<void>(
        settings: const RouteSettings(name: '/report'),
        builder: (_) =>
            FlagReportScreen(analytics: widget.analytics, layout: layout),
      ),
    );
    if (mounted) setState(() => _opening = false);
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: const Text('Usage reports')),
    body: Center(
      child: FilledButton(
        onPressed: _opening ? null : _open,
        child: const Text('Preview report'),
      ),
  );
}

class FlagReportScreen extends StatefulWidget {
  const FlagReportScreen({
    super.key,
    required this.analytics,
    required this.layout,
  });
  final AppAnalytics analytics;
  final String layout;
  @override
  State<FlagReportScreen> createState() => _FlagReportScreenState();
}

class _FlagReportScreenState extends State<FlagReportScreen> {
  bool _saved = false;
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) widget.analytics.reportPreviewed();
    });
  }

  void _save() {
    if (_saved) return;
    setState(() => _saved = true);
    // This sample saves to the current screen's state; persist in your app here.
    reportSaved(widget.analytics);
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: const Text('Seven-day usage')),
    body: Column(
      children: [
        const Text('Your usage report is ready.'),
        if (widget.layout == 'treatment')
          const Text('Your last seven days are ready to save.'),
        FilledButton(
          onPressed: _saved ? null : _save,
          child: Text(_saved ? 'Report saved' : 'Save report'),
        ),
      ],
    ),
  );
}
```

## Hand it to your agent

**Coding-agent prompt**

Coding-agent prompt

```text
Extend the Flutter report app using https://amplitude.com/build/flutter-feature-flags.
Use amplitude_experiment 1.0.0 on Flutter >=3.32 and Dart >=3.8, alongside the existing amplitude_flutter 4.7.3 analytics client. Keep analytics/auth and native setup from the product analytics page.
InitializeWithAmplitude once with a client deployment key for the same project, matching instanceName and region. Reject an empty deployment key. Use a bounded fetch and disable background retry and automatic identity-change fetch in this explicitly coordinated flow.
Create report-summary with control and treatment variants. Clear cached variants at identity transitions; fetch a fresh ExperimentUser with the current user ID, current device ID and explicit plan, restricted to the report flag. Await auth/refresh before enabling report navigation. Do not reset an anonymous device at each startup.
Read variant only at report opening, preserve the layout during rebuilds, and retain named routes and the screen observer. Treatment adds the summary. Preserve Report Previewed once per opening. Record Report Saved once after the sample save action; integrate real persistence before calling it in an existing app. Do not manually duplicate exposure or read variants on save.
At logout, close engagement and pause replay if installed; reset analytics, clear/refetch flags, then synchronize replay IDs before resuming. Merge shared files and keep every other Flutter hub addition.
Verify variants, plan targeting, failure baseline, identity transitions, one conversion per save, processed exposure/conversion identity and experiment configuration. Report each file changed and the tests actually run.
```

## What this unlocks

[Feature flags](https://amplitude.com/feature-management): choose the report summary for the intended audience.

[Experimentation](https://amplitude.com/amplitude-experiment): compare `Report Saved` after exposure to each layout.

[Product analytics](https://amplitude.com/amplitude-analytics): follow report previews and saves with the same identity.

## Next steps

[Web frameworksFlutter product analyticsRevisit the shared analytics and authentication foundation.](https://amplitude.com/build/flutter-product-analytics)[Web frameworksFlutter session replayInspect report sessions with synchronized analytics identifiers.](https://amplitude.com/build/flutter-session-replay)[Web frameworksFlutter guides and surveysHelp users on the same named report screens.]()

Reference docs

[Experiment Flutter SDK →](https://amplitude.com/docs/sdks/experiment-sdks/experiment-flutter)[Configure an experiment →](https://amplitude.com/docs/feature-experiment/workflow/configure)[Experiment quick start →](https://amplitude.com/docs/feature-experiment/experiment-quick-start)[Exposure tracking →](https://amplitude.com/docs/feature-experiment/under-the-hood/event-tracking)[Debug Analytics →](https://amplitude.com/docs/analytics/debug-analytics)[Amplitude MCP server →](https://amplitude.com/docs/amplitude-ai/amplitude-mcp)

For agents

[View raw .md](/build/flutter-feature-flags.md)[llms.txt](/llms.txt)

## Compare the report layouts

Inspect exposure and save events, then use experiment results to decide which experience to keep.

[Create a free account](https://app.amplitude.com/signup)[Browse all Build tutorials](https://amplitude.com/build)
