Skip to main content

Flutter

A Flutter SDK for integrating feature flags into your app with real-time updates over WebSocket, environment targeting, and widgets for conditional UI.

Table of Contents​

Features​

  • 🚀 Real-time Updates: socket.io WebSocket for instant flag changes
  • 🧩 Widgets: FeatureFlagBuilder and FeatureGate for declarative conditional UI
  • 🎯 BuildContext extension: context.isFeatureEnabled('…') for imperative checks
  • 🌍 Environment Targeting: load and evaluate flags per environment
  • 📦 State-management agnostic: FlagpoleClient is a plain ChangeNotifier — use it with ListenableBuilder, provider, Riverpod, or on its own
  • ⚡ Zero Config: works out of the box with sensible defaults

Installation​

flutter pub add flagpole

Or add it to pubspec.yaml:

dependencies:
flagpole: ^0.0.1

Requirements​

  • Flutter >= 3.16.0
  • Dart >= 3.2.0

Quick Start​

1. Wrap Your App​

Wrap your app (or the relevant subtree) in a FlagpoleScope with your project's API key (available from the FlagPole dashboard):

import 'package:flagpole/flagpole.dart';
import 'package:flutter/material.dart';

void main() {
runApp(
FlagpoleScope(
apiKey: 'fp_live_your_api_key',
environments: const ['production'],
child: const MyApp(),
),
);
}

FlagpoleScope creates a FlagpoleClient, fetches the flag set, opens the live-updates socket, and disposes everything when it leaves the tree.

2. Read Feature Flags​

// Rebuilds this subtree whenever the flag changes
FeatureFlagBuilder(
flag: 'new-checkout',
builder: (context, isEnabled, _) =>
isEnabled ? const NewCheckout() : const LegacyCheckout(),
);

// Show a widget only when a flag is on (optional `fallback:`)
const FeatureGate(
flag: 'beta-banner',
child: BetaBanner(),
);

// Imperative check — also subscribes the calling widget to changes
if (context.isFeatureEnabled('promo-2026')) {
showPromo();
}

3. Handle Loading & Error States​

FlagpoleScope exposes its client through context.flagpole. Because it's a ChangeNotifier, drive your UI from it with a ListenableBuilder:

class Home extends StatelessWidget {
const Home({super.key});

@override
Widget build(BuildContext context) {
final flagpole = context.flagpole;

return ListenableBuilder(
listenable: flagpole,
builder: (context, _) {
if (flagpole.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (flagpole.error != null && flagpole.flags.isEmpty) {
return Center(child: Text('Could not load flags: ${flagpole.error}'));
}
return const HomeContent();
},
);
}
}

API Reference​

FlagpoleScope​

Provides a FlagpoleClient to the widget subtree.

Constructors​

Description
FlagpoleScope({ apiKey, environments, environment, autoInitialize, child })Creates and owns a client — initializes it on mount, disposes on unmount
FlagpoleScope.value({ client, child })Wraps a client you create and dispose

Props​

PropTypeRequiredDefaultDescription
apiKeyString✅–Your FlagPole API key
environmentsList<String>?❌allFlag environments to load
environmentFlagpoleEnvironment?❌production in release, development otherwiseWhich backend to talk to
autoInitializebool❌trueCall initialize() on mount
childWidget✅–Your app

Static methods​

FlagpoleClient client = FlagpoleScope.of(context);       // throws if no scope
FlagpoleClient? maybe = FlagpoleScope.maybeOf(context); // null if no scope

FlagpoleClient​

A ChangeNotifier that loads, tracks, and evaluates flags.

final flagpole = FlagpoleClient(
apiKey: 'fp_live_your_api_key',
environments: const ['production'], // filter; defaults to all
environment: FlagpoleEnvironment.production, // host; defaults by build mode
requestTimeout: const Duration(seconds: 10),
enableRealtime: true, // open the WebSocket
);

await flagpole.initialize(); // fetch + connect
MemberTypeDescription
initialize()Future<void>Fetch flags, then connect the socket if enableRealtime
refresh()Future<void>Re-fetch over REST only
isEnabled(name)boolfalse for unknown flags, disabled flags, or a non-matching environment
flag(name)FeatureFlag?The full flag, or null
flagsMap<String, FeatureFlag>All loaded flags (unmodifiable)
isLoadingbooltrue until the first fetch resolves
isConnectedboolWebSocket connection state
errorObject?Last fetch/socket error
connect() / disconnect()voidManage the socket
replaceFlags(list) / applyFlagUpdate(flag) / applyFlagDelete(id)voidPush updates from another source (cache, push notification)
dispose()voidClose the socket and HTTP client

FeatureFlagBuilder​

FeatureFlagBuilder(
flag: 'flag-name',
builder: (context, isEnabled, child) => /* … */,
child: /* optional, passed straight through to `builder` */,
);

FeatureGate​

FeatureGate(
flag: 'flag-name',
child: EnabledWidget(),
fallback: DisabledWidget(), // optional; defaults to SizedBox.shrink()
);

BuildContext extension​

context.flagpole                      // the nearest FlagpoleClient
context.isFeatureEnabled('flag-name') // bool
context.featureFlag('flag-name') // FeatureFlag?

FeatureFlag​

class FeatureFlag {
final String id;
final String name;
final String description;
final bool isEnabled; // raw switch — prefer client.isEnabled()
final String project;
final String organization;
final Map<String, dynamic> conditions;
final List<String> environments; // empty means "all"
final DateTime? createdAt;
final DateTime? updatedAt;
}

Advanced Usage​

Managing the client yourself​

For background work, tests, or integration with your own state management, create the client directly and share it with FlagpoleScope.value:

final flagpole = FlagpoleClient(apiKey: 'fp_live_your_api_key');
await flagpole.initialize();

runApp(
FlagpoleScope.value(
client: flagpole,
child: const MyApp(),
),
);

// on shutdown
flagpole.dispose();

With the provider package​

ChangeNotifierProvider<FlagpoleClient>(
create: (_) => FlagpoleClient(apiKey: 'fp_live_your_api_key')..initialize(),
child: const MyApp(),
);

// in a widget
final enabled = context.watch<FlagpoleClient>().isEnabled('new-checkout');

A/B testing with conditions​

final flag = context.featureFlag('checkout-experiment');
final variant = flag?.conditions['variant'] as String? ?? 'control';

switch (variant) {
case 'a':
return const CheckoutA();
case 'b':
return const CheckoutB();
default:
return const CheckoutControl();
}

Seeding flags from a cache​

final cached = await loadFlagsFromDisk(); // List<FeatureFlag>
flagpole.replaceFlags(cached); // instant UI, refreshed on initialize()

Configuration​

Environments​

environments: is a filter — it controls which flags are loaded and how isEnabled evaluates targeting. A flag with an empty environments list always applies.

FlagpoleScope(
apiKey: 'fp_live_your_api_key',
environments: const ['production', 'staging'],
child: const MyApp(),
);

Backend host​

environment: selects which Flagpole backend the SDK talks to. It defaults to FlagpoleEnvironment.production in release builds and FlagpoleEnvironment.development otherwise.

FlagpoleEnvironmentRESTWebSocket
developmenthttp://localhost:5000ws://localhost:5000
staginghttps://api.staging.useflagpole.devwss://api.staging.useflagpole.dev
productionhttps://useflagpole-api.onrender.comwss://useflagpole-api.onrender.com
FlagpoleScope(
apiKey: 'fp_live_your_api_key',
environment: FlagpoleEnvironment.staging,
child: const MyApp(),
);

Error Handling​

The SDK fails safe: isEnabled returns false whenever a flag is missing, the fetch failed, or the API key is invalid. Inspect client.error for details:

if (flagpole.error is FlagpoleApiException) {
final e = flagpole.error as FlagpoleApiException;
debugPrint('Flagpole API ${e.statusCode}: ${e.body}');
}

A failed initial fetch does not throw from initialize() — it sets error and leaves flags empty. Real-time updates keep retrying in the background.

Best Practices​

1. Keep one client​

Create a single FlagpoleScope (or one long-lived FlagpoleClient) near the root of your app. Multiple scopes mean multiple sockets.

2. Always provide a safe default​

// disabled path is the safe one
FeatureGate(flag: 'risky-feature', child: RiskyFeature());

3. Use descriptive flag names​

onboarding-redesign-v2, not flag1.

4. Scope rebuilds​

Prefer FeatureFlagBuilder / FeatureGate over context.isFeatureEnabled in a large build method — the extension rebuilds the whole widget on any flag change.

5. Dispose clients you own​

Anything created with FlagpoleClient(...) directly (not through the default FlagpoleScope) must be dispose()d.

Troubleshooting​

FlagpoleScope.of() was called with a context that does not contain a FlagpoleScope​

The widget calling FlagpoleScope.of / context.isFeatureEnabled / FeatureFlagBuilder is not below a FlagpoleScope. Move the scope higher, or check you're not reading it from the same build that creates it.

Flags are always false​

  • Wrong or expired API key
  • The flag targets environments not in your environments: list
  • No network on first load — check client.error

WebSocket never connects​

Make sure the host is reachable from the device/emulator:

development: ws://localhost:5000   (use 10.0.2.2 on the Android emulator)
production: wss://useflagpole-api.onrender.com

On the Android emulator, localhost refers to the emulator itself — point environment at a custom host or use 10.0.2.2.

Updates don't arrive​

enableRealtime must be true (the default) and connect() must have run (initialize() does this). Check client.isConnected.

Contributing​

The SDK lives in the flagpole-sdks monorepo under packages/client/flutter.

git clone https://github.com/flagpole-corp/flagpole-sdks.git
cd flagpole-sdks/packages/client/flutter

flutter pub get
dart format .
flutter analyze
flutter test

License​

MIT

Support​