🎯 Feature flags • 🧪 A/B testing • 📊 Analytics integration
Quick Start • Features • Documentation • Examples • Resources
GrowthBook is an open source feature flagging and experimentation platform. This Flutter SDK allows you to use GrowthBook with your Flutter based mobile application.
Platform Support:
- 📱 Android 21+ • iOS 12+
- 📺 tvOS 13+ • ⌚ watchOS 7+
- 🖥️ macOS • Windows • Linux
- 🌐 Web (Flutter Web)
- Lightweight - Minimal performance impact
- Type Safe - Full Dart/Flutter type safety
- Caching - Smart caching with TTL and stale-while-revalidate
- Cross-platform - Works on Android, iOS, Web, Desktop
- Feature Flags - Toggle features on/off without code deployments
- A/B Testing - Run experiments with statistical significance
- Targeting - Advanced user segmentation, variation weights and targeting rules
- Tracking - Use your existing event tracking (GA, Segment, Mixpanel, custom)
- Real-time Updates - Features update instantly via streaming
- Sticky Bucketing - Consistent user experiences across sessions
- Remote Evaluation - Server-side evaluation for enhanced security
- Multi-variate Testing - Test multiple variations simultaneously
- Rollout Management - Gradual feature rollouts with traffic control
| Feature | Support | Since Version |
|---|---|---|
| ✅ Feature Flags | Full Support | All versions |
| ✅ A/B Testing | Full Support | All versions |
| ✅ Sticky Bucketing | Full Support | ≥ v3.8.0 |
| ✅ Remote Evaluation | Full Support | ≥ v3.7.0 |
| ✅ Streaming Updates | Full Support | ≥ v3.4.0 |
| ✅ Prerequisites | Full Support | ≥ v3.2.0 |
| ✅ Encrypted Features | Full Support | ≥ v3.1.0 |
| ✅ v2 Hashing | Full Support | ≥ v3.1.0 |
| ✅ SemVer Targeting | Full Support | ≥ v3.1.0 |
| ✅ TTL Caching | Full Support | ≥ v3.9.10 |
Add to your pubspec.yaml:
dependencies:
growthbook_sdk_flutter: ^3.9.10import 'package:growthbook_sdk_flutter/growthbook_sdk_flutter.dart';
// Initialize the SDK
final sdk = await GBSDKBuilderApp(
apiKey: "sdk_your_api_key",
hostURL: "https://growthbook.io",
attributes: {
'id': 'user_123',
'email': 'user@example.com',
'country': 'US',
},
growthBookTrackingCallBack: (trackData) {
// Track experiment exposures
final experiment = trackData.experiment;
final result = trackData.experimentResult;
print('Experiment: ${experiment.key}, Variation: ${result.variationID}');
},
).initialize();
// Use feature flags
final welcomeMessage = sdk.feature('welcome_message');
if (welcomeMessage.on) {
print('Feature is enabled: ${welcomeMessage.value}');
}
// Run A/B tests
final buttonExperiment = GBExperiment(key: 'button_color_test');
final result = sdk.run(buttonExperiment);
final buttonColor = result.value ?? 'blue'; // Default colorclass MyHomePage extends StatelessWidget {
final GrowthBookSDK sdk;
@override
Widget build(BuildContext context) {
final newDesign = sdk.feature('new_homepage_design');
return Scaffold(
body: newDesign.on
? NewHomepageWidget()
: ClassicHomepageWidget(),
);
}
}final sdk = await GBSDKBuilderApp(
apiKey: "your_api_key",
growthBookTrackingCallBack: (trackData) {
final experiment = trackData.experiment;
final result = trackData.experimentResult;
// Google Analytics
FirebaseAnalytics.instance.logEvent(
name: 'experiment_viewed',
parameters: {
'experiment_id': experiment.key,
'variation_id': result.variationID,
'variation_name': result.key,
},
);
// Mixpanel
Mixpanel.track('Experiment Viewed', {
'Experiment ID': experiment.key,
'Variation ID': result.variationID,
});
// Custom analytics
YourAnalytics.trackExperiment(experiment, result);
},
).initialize();final sdk = await GBSDKBuilderApp(
// Required
apiKey: "sdk_your_api_key",
hostURL: "https://growthbook.io",
// User Context
attributes: {
'id': 'user_123',
'email': 'user@example.com',
'plan': 'premium',
'country': 'US',
},
// Performance & Caching
ttlSeconds: 300, // Cache TTL (default: 60s)
backgroundSync: true, // Real-time updates
// Testing & QA
qaMode: false, // Disable randomization for QA
forcedVariations: { // Force specific variations
'button_test': 1,
},
// Analytics Integration
growthBookTrackingCallBack: (trackData) {
// Send to your analytics platform
final experiment = trackData.experiment;
final result = trackData.experimentResult;
analytics.track('Experiment Viewed', {
'experiment_id': experiment.key,
'variation_id': result.variationID,
'variation_name': result.key,
});
},
// Advanced Features
remoteEval: false, // Server-side evaluation
encryptionKey: "...", // For encrypted features
).initialize();The SDK logs internal events (feature evaluation skips, cache errors, refresh
attempts) through a logger package instance. Verbosity is controlled by
GrowthBookSDK.setLogLevel(...), which accepts an SDK-owned GBLogLevel enum:
import 'package:growthbook_sdk_flutter/growthbook_sdk_flutter.dart';
void main() {
// Configure once at app startup — before initializing the SDK.
GrowthBookSDK.setLogLevel(GBLogLevel.debug);
runApp(const MyApp());
}Available levels: verbose, debug, info, warning (default), error, off.
⚠️ Process-global: log level is shared across allGrowthBookSDKinstances in the same process. Set it once at app startup rather than per-instance. If you run multiple SDK instances, they will share the same verbosity.
// Boolean flags
final isEnabled = sdk.feature('new_feature').on;
// String values
final welcomeText = sdk.feature('welcome_message').value ?? 'Welcome!';
// Number values
final maxItems = sdk.feature('max_items').value ?? 10;
// JSON objects
final config = sdk.feature('app_config').value ?? {
'theme': 'light',
'animations': true,
};
// Check feature source
final feature = sdk.feature('premium_feature');
switch (feature.source) {
case GBFeatureSource.experiment:
print('Value from A/B test');
break;
case GBFeatureSource.force:
print('Forced value');
break;
case GBFeatureSource.defaultValue:
print('Default value');
break;
}// Define experiment
final experiment = GBExperiment(
key: 'checkout_button_test',
variations: ['🛒 Buy Now', '💳 Purchase', '✨ Get It Now'],
weights: [0.33, 0.33, 0.34], // Traffic distribution
);
// Run experiment
final result = sdk.run(experiment);
// Use result
Widget buildButton() {
final buttonText = result.value ?? '🛒 Buy Now';
return ElevatedButton(
onPressed: () => handlePurchase(),
child: Text(buttonText),
);
}
// Track conversion
if (purchaseCompleted) {
// Your analytics will receive this via trackingCallBack
}// Update user attributes dynamically
sdk.setAttributes({
'plan': 'enterprise',
'feature_flags_enabled': true,
'last_login': DateTime.now().toIso8601String(),
});
// Target users with conditions
// Example: Show feature only to premium users in US
// This is configured in GrowthBook dashboard, not in codeThe SDK supports the following operators in targeting conditions:
| Operator | Description |
|---|---|
$eq |
Equal to |
$ne |
Not equal to |
$lt |
Less than |
$lte |
Less than or equal |
$gt |
Greater than |
$gte |
Greater than or equal |
| Operator | Description |
|---|---|
$in |
Value is in array |
$nin |
Value is not in array |
$all |
Array contains all values |
$ini |
Value is in array (case-insensitive string comparison) |
$nini |
Value is not in array (case-insensitive string comparison) |
$alli |
Array contains all values (case-insensitive string comparison) |
For
$ini,$nini, and$alli: string values are compared after lowercasing; non-string values (numbers, booleans, null) are compared as-is.
| Operator | Description |
|---|---|
$regex |
Matches regex (case-sensitive) |
$notRegex |
Does not match regex (case-sensitive) |
$regexi |
Matches regex (case-insensitive) |
$notRegexi |
Does not match regex (case-insensitive) |
| Operator | Description |
|---|---|
$exists |
Attribute exists (true) or is absent (false) |
$type |
Attribute type matches string ("string", "number", "boolean", "array", "object", "null") |
$not |
Negates a condition |
$size |
Array length matches condition |
$elemMatch |
At least one array element matches condition |
$vgt / $vlt / $vgte / $vlte / $veq / $vne |
Semantic version comparison |
// Example: case-insensitive membership
// Matches users where country is "us", "US", "Us", etc.
final condition = {
'country': {'\$ini': ['US', 'CA', 'GB']}
};
// Example: case-insensitive regex
// Matches "Hello", "hello", "HELLO", etc.
final condition2 = {
'greeting': {'\$regexi': '^hello'}
};The SDK implements an intelligent caching system for optimal performance:
final sdk = await GBSDKBuilderApp(
apiKey: "your_api_key",
ttlSeconds: 300, // Cache TTL: 5 minutes
backgroundSync: true, // Enable background refresh
).initialize();- 🚀 Instant Response - Serve cached features immediately
- 🔄 Background Refresh - Fetch updates in the background
- ⚡ Stale-While-Revalidate - Show cached data while updating
- 📊 Smart Invalidation - Refresh only when needed
- ✅ Zero loading delays for feature flags
- ✅ Always up-to-date with background sync
- ✅ Reduced API calls with intelligent caching
- ✅ Offline resilience with cached fallbacks
Ensure consistent user experiences across sessions:
class MyAppStickyBucketService extends GBStickyBucketService {
@override
Future<Map<String, String>?> getAllAssignments(
Map<String, dynamic> attributes,
) async {
// Retrieve from local storage
final prefs = await SharedPreferences.getInstance();
final json = prefs.getString('gb_sticky_assignments');
return json != null ? jsonDecode(json) : null;
}
@override
Future<void> saveAssignments(
Map<String, dynamic> attributes,
Map<String, String> assignments,
) async {
// Save to local storage
final prefs = await SharedPreferences.getInstance();
await prefs.setString('gb_sticky_assignments', jsonEncode(assignments));
}
}
// Use with SDK
final sdk = await GBSDKBuilderApp(
apiKey: "your_api_key",
stickyBucketService: MyAppStickyBucketService(),
).initialize();The Flutter SDK may be run in Remote Evaluation mode. This mode brings the security benefits of a backend SDK to the front end by evaluating feature flags exclusively on a private server. Using Remote Evaluation ensures that any sensitive information within targeting rules or unused feature variations are never seen by the client.
You must enable Remote Evaluation in your SDK Connection settings. Cloud customers are also required to self-host a GrowthBook Proxy Server or a custom remote evaluation backend.
To use Remote Evaluation, add the remoteEval: true property to your SDK instance.
final sdk = await GBSDKBuilderApp(
apiKey: "your_api_key",
remoteEval: true, // Enable remote evaluation
attributes: userAttributes,
).initialize();
// Features are evaluated server-side
// Sensitive targeting rules never reach the clientThe GrowthBook SDK supports streaming with Server-Sent Events (SSE). When enabled, changes to features within GrowthBook will be streamed to the SDK in realtime as they are published. This is only supported on GrowthBook Cloud or if running a GrowthBook Proxy Server.
final sdk = await GBSDKBuilderApp(
apiKey: "your_api_key",
backgroundSync: true, // Enable streaming updates
).initialize();
// Features automatically update when changed in GrowthBook
// No need to restart the app or refresh manuallyPlugins observe SDK lifecycle events (feature evaluated, experiment viewed) and can implement custom side effects such as forwarding events to an analytics backend. The SDK ships with GrowthBookTrackingPlugin, which batches events and sends them to the GrowthBook ingest endpoint.
import 'package:growthbook_sdk_flutter/growthbook_sdk_flutter.dart';
Future<void> main() async {
final sdk = await GBSDKBuilderApp(
apiKey: 'sdk-xxx',
hostURL: 'https://cdn.growthbook.io',
growthBookTrackingCallBack: (_) {},
)
.addPlugin(GrowthBookTrackingPlugin())
.initialize();
runApp(MyApp(sdk: sdk));
}Events go to https://us1.gb-ingest.com by default. Override ingestorHost to
send them to another region, for example https://eu-west-1.gb-ingest.com:
final trackingPlugin = GrowthBookTrackingPlugin(
config: GrowthBookTrackingPluginConfig(
ingestorHost: 'https://eu-west-1.gb-ingest.com',
batchSize: 50,
batchTimeout: Duration(seconds: 30),
),
);
await GBSDKBuilderApp(...)
.addPlugin(trackingPlugin)
.initialize();A flag read on every screen produces the same evaluation over and over. The plugin remembers the last 1,000 events and drops repeats, so only distinct evaluations are sent. The key is the event name plus its own fields — attributes are deliberately excluded, since a feature evaluation carries no unit identity of its own and every attribute change would otherwise look like a new event. A batch that fails to send gives its keys back, so nothing is lost to a dropped connection.
Set dedupeCacheSize to tune the window, or to 0 to send every evaluation:
GrowthBookTrackingPluginConfig(dedupeCacheSize: 5000)Feature evaluations are by far the larger volume. An app that only needs experiment data can drop them and keep its exposures:
GrowthBookTrackingPluginConfig(enableFeatureUsageEvents: false)Experiment exposures and custom events are unaffected.
logEvent sends an event of your own through the same envelope as the SDK's
own events. These are never deduplicated — a repeat is a second occurrence,
not a redundant re-evaluation — and they carry the SDK's current attributes.
sdk.logEvent('Checkout Completed', {'total': 42.0, 'currency': 'USD'});
sdk.logEvent('App Opened');Like every other event they are buffered, so call flush() before the app is
suspended if you need them delivered.
flush() delivers what the plugins have buffered and leaves them recording.
Without it, events sit in the buffer for up to batchTimeout and are lost if
the OS kills the suspended app — which is how most mobile apps end.
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
sdk.flush();
}
}
}Use dispose() instead when the SDK instance is done for good — it flushes as
well, but nothing is recorded afterwards.
Tracking plugins buffer events in memory to reduce network overhead. You must call await sdk.dispose() when the SDK instance is no longer needed so buffered events are flushed and any resources (timers, HTTP clients) are released. Without this, queued tracking events will be dropped during app shutdown.
class _MyAppState extends State<MyApp> {
late final GrowthBookSDK sdk;
@override
void initState() {
super.initState();
_initSdk();
}
Future<void> _initSdk() async {
sdk = await GBSDKBuilderApp(...)
.addPlugin(GrowthBookTrackingPlugin())
.initialize();
}
@override
void dispose() {
// Flushes pending tracking events and releases plugin resources
sdk.dispose();
super.dispose();
}
}For CLI tools, server processes, or short-lived isolates, wrap SDK usage in a try/finally:
final sdk = await GBSDKBuilderApp(...).addPlugin(GrowthBookTrackingPlugin()).initialize();
try {
// ... SDK usage
} finally {
await sdk.dispose();
}Extend GrowthBookPlugin to implement your own tracking, logging, or analytics forwarding:
class MyAnalyticsPlugin extends GrowthBookPlugin {
@override
void initialize(String clientKey) {
// one-time setup (e.g. start a periodic flush timer)
}
@override
void onFeatureEvaluated(String id, GBFeatureResult result, Map<String, dynamic>? attributes) {
// forward to your analytics
}
@override
void onExperimentViewed(GBExperiment experiment, GBExperimentResult result, Map<String, dynamic>? attributes) {
// forward to your analytics
}
@override
Future<void> close() async {
// flush any buffered state, release resources
}
}
⚠️ Plugin errors are isolated per plugin — a throw in one plugin's callback does not affect other plugins or SDK evaluation. If you need stronger delivery guarantees than best-effort batching, implement retry/requeue with bounded storage inside your plugin'sclose()method.
class ProductPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final sdk = context.read<GrowthBookSDK>();
// Feature flags
final showReviews = sdk.feature('show_product_reviews').on;
final freeShipping = sdk.feature('free_shipping_threshold').value ?? 50.0;
// A/B test for pricing display
final pricingExperiment = GBExperiment(key: 'pricing_display_test');
final pricingResult = sdk.run(pricingExperiment);
return Scaffold(
body: Column(
children: [
ProductImage(),
ProductTitle(),
// Dynamic pricing display based on A/B test
if (pricingResult.value == 'with_discount')
PricingWithDiscount()
else
StandardPricing(),
// Conditional features
if (showReviews) ProductReviews(),
if (freeShipping > 0) FreeShippingBanner(threshold: freeShipping),
AddToCartButton(),
],
),
);
}
}class NewFeatureService {
final GrowthBookSDK sdk;
NewFeatureService(this.sdk);
bool get isNewDashboardEnabled {
final feature = sdk.feature('new_dashboard_v2');
// Feature is rolled out gradually:
// 0% → 5% → 25% → 50% → 100%
// Configured in GrowthBook dashboard
return feature.on;
}
Widget buildDashboard() {
return isNewDashboardEnabled
? NewDashboardWidget()
: LegacyDashboardWidget();
}
}# Clone the repository
git clone https://github.com/growthbook/growthbook-flutter.git
cd growthbook-flutter
# Install dependencies
flutter pub get
# Generate code (for json_serializable)
dart run build_runner build --delete-conflicting-outputs# Run all tests
flutter test
# Run tests with coverage
flutter test --coverage
# Run specific test file
flutter test test/features/feature_test.dartThe SDK uses json_serializable for JSON parsing. When you modify model classes with @JsonSerializable(), run:
# Watch for changes and auto-generate
dart run build_runner watch
# One-time generation
dart run build_runner build --delete-conflicting-outputs# Check linting
dart analyze
# Format code
dart format .
# Fix auto-fixable issues
dart fix --applycd example
flutter pub get
flutter run// Mock SDK for testing
class MockGrowthBookSDK implements GrowthBookSDK {
final Map<String, dynamic> mockFeatures;
MockGrowthBookSDK({required this.mockFeatures});
@override
GBFeatureResult feature(String key) {
final value = mockFeatures[key];
return GBFeatureResult(
value: value,
on: value == true,
source: GBFeatureSource.force,
);
}
}
// Use in tests
void main() {
testWidgets('shows new feature when enabled', (tester) async {
final mockSDK = MockGrowthBookSDK(
mockFeatures: {'new_feature': true},
);
await tester.pumpWidget(MyApp(sdk: mockSDK));
expect(find.text('New Feature'), findsOneWidget);
});
}// Different configurations for different environments
final sdk = await GBSDKBuilderApp(
apiKey: kDebugMode
? "sdk_dev_your_dev_key"
: "sdk_prod_your_prod_key",
hostURL: kDebugMode
? "https://growthbook-dev.yourcompany.com"
: "https://growthbook.yourcompany.com",
qaMode: kDebugMode, // Disable randomization in debug
).initialize();We welcome contributions! Here's how to get started:
- Fork the repository and create a feature branch
- Make your changes following our coding conventions
- Add tests for any new functionality
- Ensure all tests pass:
flutter test - Format code:
dart format . - Submit a pull request with a clear description
- ✅ Include tests for new features
- ✅ Update documentation if needed
- ✅ Follow existing code style
- ✅ Keep commits atomic and well-described
- ✅ Reference any related issues
- Uses automated releases via release-please
- Follow conventional commits:
feat:,fix:,docs:, etc. - Automatic version bumping and changelog generation
- Once a release PR is merged,
release-please.ymldispatchespublish.ymlat the new tag, and that run publishes to pub.dev over OIDC
Publishing requirements:
-
pub.dev automated publishing must allow the
workflow_dispatchevent forpublish.yml(pub.dev → package admin → automated publishing), in addition to the configured tag pattern -
publish.ymlonly runs from a tag ref — pub.dev validates theref/ref_typeclaims of the run's OIDC token, so a run started from a branch is rejected no matter which ref it checks out -
Tags created before this setup landed have a
publish.ymlwithoutworkflow_dispatchand cannot be dispatched. To release one of them, re-push the tag so thepush: tags:trigger fires (note that deleting a tag turns its GitHub Release into a draft, which has to be re-published afterwards):git push --delete origin v4.3.1 git push origin v4.3.1
This project is licensed under the MIT License - see the LICENSE file for details.
Originally contributed by the team at Alippo. The core GrowthBook platform remains open-source and free forever.
Made with ❤️ by the GrowthBook community
