Flutter SDK
Mobile feature flags for Flutter and Dart apps with built-in circuit breaker, caching, and cross-platform support. Pure Dart â works on iOS, Android, Web, and Desktop.
Circuit Breaker
Auto-recovery
Cross-Platform
iOS, Android, Web, Desktop
Caching
In-memory cache
Lightweight
13 KB pure Dart
Installation
Add rollgate to your pubspec.yaml:
dependencies:
rollgate: ^1.2.1Then run:
flutter pub get
# or
dart pub getQuick Start
import 'package:rollgate/rollgate.dart';
final client = RollgateClient(
config: RollgateConfig(
apiKey: 'rg_client_...',
baseUrl: 'https://api.rollgate.io',
refreshInterval: Duration(seconds: 30),
),
);
await client.init();
// Check flags
final enabled = client.isEnabled('my-feature', false);
// Identify user
await client.identify(UserContext(
id: 'user-123',
email: '[email protected]',
attributes: {'plan': 'pro'},
));
// Cleanup
await client.close();Configuration
| Option | Type | Default | Description |
|---|---|---|---|
| apiKey | String | required | Client API key (rg_client_...) |
| baseUrl | String | 'https://api.rollgate.io' | API base URL |
| refreshInterval | Duration | Duration(seconds: 30) | Polling interval, Duration.zero to disable |
| timeout | Duration | Duration(seconds: 5) | HTTP request timeout |
| maxRetries | int | 3 | Maximum retry attempts on failure |
| cacheTtl | Duration | Duration(minutes: 5) | Cache time-to-live |
Note: The Flutter SDK uses polling only (no SSE). This is intentional for mobile environments where persistent connections drain battery and are unreliable on cellular networks.
Methods
init()
Initialize the client and fetch flags from the server. Call this once at app startup.
await client.init();close()
Stop polling and clean up resources. Call this when the app is disposed.
await client.close();isEnabled(key, defaultValue)
Check if a boolean flag is enabled. Returns the flag value or the default.
final showFeature = client.isEnabled('new-feature', false);
if (showFeature) {
// New feature code
}getValue<T>(key, defaultValue)
Get a flag value of any type. Use for string, number, or JSON flags.
// String flag
final theme = client.getValue<String>('theme', 'light');
// Number flag
final maxRetries = client.getValue<int>('max-retries', 3);
// Dynamic flag
final config = client.getValue<Map<String, dynamic>>(
'rate-limit-config',
{'enabled': false, 'limit': 100},
);getString(key, defaultValue)
Type-safe helper for string flags.
final buttonColor = client.getString('cta-color', 'blue');
final variant = client.getString('checkout-variant', 'control');getNumber(key, defaultValue)
Type-safe helper for number flags.
final pageSize = client.getNumber('results-per-page', 20);
final timeout = client.getNumber('api-timeout-ms', 5000);getJson(key, defaultValue)
Type-safe helper for JSON flags. Returns a Map<String, dynamic>.
final config = client.getJson('feature-config', {
'enabled': false,
'maxItems': 10,
'allowedRoles': ['admin'],
});
if (config['enabled'] == true) {
// Feature logic
}getAllFlags()
Get all flags as a map.
final flags = client.getAllFlags();
// {'feature-a': true, 'feature-b': false}identify(userContext)
Set user context for targeting rules. Re-fetches flags with the new context.
await client.identify(UserContext(
id: 'user-123',
email: '[email protected]',
attributes: {
'plan': 'enterprise',
'country': 'US',
'app_version': '2.1.0',
},
));refresh()
Force refresh flags from the server.
await client.refresh();Flutter Widget Integration
Use RollgateClient directly in your Flutter widgets. Initialize in initState and dispose in dispose.
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late RollgateClient _rollgate;
@override
void initState() {
super.initState();
_rollgate = RollgateClient(
config: RollgateConfig(apiKey: 'rg_client_...'),
);
_rollgate.init();
}
@override
void dispose() {
_rollgate.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: _rollgate.isEnabled('new-ui', false)
? NewUI()
: LegacyUI(),
),
);
}
}FutureBuilder Pattern
Wait for initialization before rendering flag-dependent UI:
class FeatureScreen extends StatelessWidget {
final RollgateClient rollgate;
const FeatureScreen({required this.rollgate});
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: rollgate.init(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
// Rollgate unavailable - show default UI
return const DefaultFeatureUI();
}
final showNewFeature = rollgate.isEnabled('new-feature', false);
return showNewFeature
? const NewFeatureUI()
: const DefaultFeatureUI();
},
);
}
}StreamBuilder Pattern
React to flag changes from polling updates using a stream:
class LiveFeature extends StatelessWidget {
final RollgateClient rollgate;
const LiveFeature({required this.rollgate});
@override
Widget build(BuildContext context) {
return StreamBuilder<Map<String, dynamic>>(
stream: rollgate.flagsStream,
builder: (context, snapshot) {
final flags = snapshot.data ?? {};
final enabled = flags['premium-feature'] == true;
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: enabled
? const PremiumWidget(key: ValueKey('premium'))
: const StandardWidget(key: ValueKey('standard')),
);
},
);
}
}User Targeting
User context enables server-side targeting rule evaluation. Set the user after authentication to receive personalized flag values.
// After login
Future<void> onUserLogin(User user) async {
await rollgate.identify(UserContext(
id: user.id,
email: user.email,
attributes: {
'plan': user.subscription.plan,
'country': user.country,
'app_version': packageInfo.version,
'platform': Platform.operatingSystem,
},
));
}
// After logout - reset to anonymous
Future<void> onUserLogout() async {
await rollgate.identify(UserContext(id: 'anonymous'));
}Available operators: equals, not_equals, contains, starts_with, ends_with, in, not_in, gt, gte, lt, lte, regex, is_set, is_not_set, semver_gt, semver_lt, semver_eq
Error Handling & Graceful Degradation
The SDK is designed to never throw exceptions during flag evaluation. If Rollgate is unavailable, flags return their default values. The circuit breaker automatically switches to cached values when the API is unreachable.
// Default values ensure your app works even without Rollgate
final showNewFeature = client.isEnabled('new-feature', false);
// Returns false if: flag doesn't exist, API down, circuit open
// Check circuit state for monitoring
final state = client.getCircuitState();
// 'closed' | 'open' | 'half_open'
// Safe initialization with error handling
Future<void> initFlags() async {
try {
await client.init();
debugPrint('Rollgate initialized with ${client.getAllFlags().length} flags');
} catch (e) {
// Log but don't crash - SDK will use defaults
debugPrint('Rollgate init failed: $e');
debugPrint('Continuing with default flag values');
}
}Retry & Circuit Breaker
The SDK includes automatic retry with exponential backoff and a circuit breaker for resilience on unreliable mobile networks:
// Circuit breaker states:
// - closed: Normal operation, requests go through
// - open: API unreachable, using cached flags
// - half_open: Testing if API is back
// The SDK handles this automatically:
// 1. Request fails -> retry with backoff (up to maxRetries)
// 2. Consecutive failures -> circuit opens
// 3. After cooldown -> circuit half-opens, tries one request
// 4. Success -> circuit closes, normal operation resumes