Flutter / Dart
Native Flutter SDK with widget integration, reactive flag rendering, and automatic Flutter error capture. Works on iOS, Android, web, and desktop.
dependencies:
toggleai: ^1.0.0
# Or use path for local development:
# toggleai:
# path: ../dart_sdkflutter pub getThe Flutter SDK is designed to work in fluid harmony with ToggleAI backend services, fully optimizing mobile performance, battery usage, and network efficiency. Flag resolutions are handled through two powerful patterns:
⚡ In-Memory Evaluation (60fps widget rebuilds)
When wrapped in ToggleAIProvider, the SDK fetches the environment config payload from GET /sdk/config on startup. In-memory local evaluations (e.g. within a reactive FlagBuilder) are calculated instantly under sub-millisecond speeds—ensuring silky, jank-free 60fps widget rendering.
🛰️ Edge Remote Evaluation
For security-sensitive actions demanding absolute, real-time resolution (bypassing client-side caches entirely), client.evaluateFlagRemote() makes a direct network query to the edge endpoint POST /sdk/evaluate, returning fresh determinations.
import 'package:flutter/material.dart';
import 'package:toggleai/dart_sdk.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final client = ToggleAIClient(
options: ToggleAIOptions(
clientId: 'pk_live_xxx',
secret: 'sk_live_xxx',
pollingInterval: Duration(seconds: 30),
onReady: () => print('ToggleAI ready'),
onError: (e) => print('ToggleAI error: $e'),
),
);
// Capture Flutter framework errors
final logger = client.getLogger();
FlutterError.onError = logger.captureFlutterError;
runApp(
ToggleAIProvider(
client: client,
loadingBuilder: (_) => const MaterialApp(
home: Scaffold(body: Center(child: CircularProgressIndicator())),
),
child: const MyApp(),
),
);
}FlagBuilder
Reactive widget that rebuilds when a flag value changes (on config refresh).
FlagBuilder(
flagKey: 'new-checkout-ui',
userId: 'user_42',
attributes: {'plan': 'pro', 'country': 'US'},
builder: (context, enabled, evaluation) {
print('Reason: ${evaluation.reason}');
return enabled
? const NewCheckoutScreen()
: const LegacyCheckoutScreen();
},
)ConfigBuilder
Reactive widget for remote config values.
ConfigBuilder<String>(
configKey: 'app_theme',
defaultValue: 'light',
builder: (context, theme) => Text('Current theme: $theme'),
)Access Client Anywhere
// In any widget below ToggleAIProvider:
final client = ToggleAIProvider.of(context);
final isDarkMode = client.getFlag('dark-mode');
final timeout = client.getConfig<int>('api_timeout_ms', defaultValue: 5000);ToggleAIMixin
Convenience mixin for StatefulWidgets — provides getFlag() and getConfig() directly.
class _HomeState extends State<HomeScreen> with ToggleAIMixin {
@override
Widget build(BuildContext context) {
final showBanner = getFlag(context, 'promo-banner');
final bannerText = getConfig<String>(context, 'promo_text', defaultValue: '');
return Column(
children: [
if (showBanner) PromoBanner(text: bannerText),
// ... rest of UI
],
);
}
}The Flutter SDK works on Android out of the box. For optimal performance:
Minimum SDK Version
android {
defaultConfig {
minSdkVersion 21 // Required for HTTP/2 support
}
}
// If using ProGuard/R8, no special rules needed —
// the SDK uses only dart:core and package:http.Network Security (Debug)
If testing against a local backend over HTTP (not HTTPS):
<application android:usesCleartextTraffic="true">
<!-- Only for local development -->
</application>No special iOS configuration needed. The SDK uses package:http which relies onNSURLSession under the hood.
<!-- Only if you need to connect to non-HTTPS endpoints (dev) -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>The SDK provides native Flutter error interception:
// Capture Flutter widget/layout errors
FlutterError.onError = logger.captureFlutterError;
// Capture Dart zone errors (uncaught async exceptions)
runZonedGuarded(() {
runApp(const MyApp());
}, (error, stackTrace) {
logger.captureError(error, stackTrace: stackTrace);
});
// Manual exception capture
try {
await riskyOperation();
} catch (error, stackTrace) {
logger.captureError(
error,
stackTrace: stackTrace,
context: {'screen': 'checkout', 'userId': 'u_42'},
);
}| Option | Type | Default |
|---|---|---|
clientId | String | required |
secret | String | required |
baseUrl | String | hosted URL |
pollingInterval | Duration | 30 seconds |
evaluationMode | EvaluationMode | .local |
disableCache | bool | false |
defaultContext | EvaluationContext? | null |
timeout | Duration | 10 seconds |
onReady | Function? | null |
onConfigUpdate | Function? | null |
onError | Function? | null |
| Method | Returns | Description |
|---|---|---|
init() | Future<void> | Fetch config, start polling |
close() | Future<void> | Stop polling, flush logs |
refresh() | Future<void> | Manually refresh config |
waitForReady() | Future<void> | Wait for initialization |
getFlag(key, {userId, attrs, default}) | bool | Boolean flag, local |
getFlagAsync(key, ...) | Future<bool> | Boolean flag, server if needed |
getFlagValue<T>(key, ...) | T? | Typed flag value |
getEvaluation(key, {context}) | FlagEvaluationResult | Full evaluation |
evaluateAllFlags({context}) | Map<String, ...> | All flags, local |
evaluateFlagRemote(key, ...) | Future<...> | Server-side eval |
getConfig<T>(key, {default}) | T? | Typed config value |
getAllConfigs() | Map<String, dynamic> | All configs |
hasConfig(key) | bool | Check if key exists |
getLogger() | ToggleAILogger | Get attached logger |