customfit_ai_flutter_sdk 0.1.3
customfit_ai_flutter_sdk: ^0.1.3 copied to clipboard
Flutter SDK for CustomFit.ai feature flags, A/B testing, and analytics. Real-time config updates with offline support and cross-platform compatibility.
CustomFit Flutter SDK #
A comprehensive Flutter SDK for integrating with CustomFit's feature flagging, A/B testing, and event tracking services. Built with performance, reliability, and developer experience in mind.
Table of Contents #
- Key Features
- Architecture
- Installation
- Quick Start
- Configuration
- User Management
- Feature Flags
- Event Tracking
- Session Management
- Listeners & Callbacks
- Offline Support
- Persistence Strategy
- Battery Optimization
- Advanced Features
- Error Handling
- Flutter Integration
- Best Practices
- API Reference
- Troubleshooting
- Support
Key Features #
- 🚀 Feature Flags - Real-time feature toggles and configuration with multiple data types
- 🧪 A/B Testing - Split testing and experimentation capabilities
- 📊 Event Tracking - Comprehensive user analytics and behavior insights
- 👤 User Context - Rich user properties and personalized experiences
- 📱 Cross-platform - iOS, Android, Web, Desktop support
- ⚡ Offline Support - Robust offline mode with intelligent caching
- 🔄 Real-time Updates - Instant configuration changes via listeners
- 🛡️ Error Resilience - Comprehensive error handling and recovery
- 🔒 Privacy Compliant - Built-in privacy controls and data protection
Feature Flag Types #
- Boolean flags: Simple on/off toggles
- String flags: Text values, configuration strings
- Number flags: Numeric values, thresholds, percentages
- JSON flags: Complex objects, configuration maps
- Type-safe flags: Compile-time safe flag definitions with IDE support
Architecture #
SDK Structure #
The CustomFit Flutter SDK follows a modular architecture designed for consistency across platforms:
lib/
├── src/
│ ├── analytics/
│ │ ├── event/ # Event tracking classes
│ │ └── summary/ # Analytics summaries
│ ├── client/
│ │ ├── listener/ # Feature flag and config change listeners
│ │ └── managers/ # Component managers (config, user, etc.)
│ ├── config/
│ │ ├── core/ # Core configuration classes
│ │ └── validation/ # Configuration validation
│ ├── constants/ # SDK constants
│ ├── core/
│ │ ├── error/ # Error handling and recovery
│ │ ├── model/ # Core data models
│ │ ├── session/ # Session management
│ │ ├── service_locator.dart # Service locator pattern
│ │ └── util/ # Core utilities
│ ├── di/ # Dependency injection
│ ├── events/ # Event handling
│ ├── features/ # Type-safe feature flags
│ ├── lifecycle/ # Lifecycle management
│ ├── logging/ # Logging utilities
│ ├── monitoring/ # Performance monitoring
│ ├── network/
│ │ ├── config/ # Configuration fetching
│ │ ├── connection/ # Network connectivity monitoring
│ │ └── efficiency/ # Network optimization
│ ├── platform/ # Platform-specific integrations
│ ├── services/ # SDK services
│ └── testing/ # Testing utilities
├── customfit_ai_flutter_core.dart # Core implementation
└── customfit_ai_flutter_sdk.dart # Main entry point (use this for imports)
Design Principles #
- Consistency Across Platforms - Structure mirrors Kotlin and Swift SDKs
- Separation of Concerns - Each module has clear responsibilities
- Dependency Direction - Higher-level components depend on lower-level ones
- Encapsulation - Implementation details hidden behind interfaces
- Error Resilience - Comprehensive error handling and recovery
Core Components #
- CFClient - Main SDK client (singleton pattern)
- CFConfig - Configuration management with builder pattern
- CFUser - User context and properties with builder pattern
- HttpClient - Network communication with connection pooling
- EventTracker - Analytics and event tracking
- SessionManager - Session lifecycle management
- ConnectionManager - Network connectivity monitoring
Installation #
Prerequisites #
- Flutter 3.0.0 or higher
- Dart 3.2.3 or higher
Add to pubspec.yaml #
dependencies:
customfit_ai_flutter_sdk: ^0.1.0
Install Dependencies #
flutter pub get
Import the SDK #
import 'package:customfit_ai_flutter_sdk/customfit_ai_flutter_sdk.dart';
Quick Start #
1. Initialize the SDK #
import 'package:customfit_ai_flutter_sdk/customfit_ai_flutter_sdk.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
try {
// Simple configuration
final config = CFConfig.builder('your-client-key')
.setDebugLoggingEnabled(true)
.setEventsFlushIntervalMs(60000)
.setNetworkConnectionTimeoutMs(10000)
.build();
// Create user with builder pattern
final user = CFUser.builder('user-123')
.addStringProperty('email', 'user@example.com')
.addStringProperty('plan', 'premium')
.addNumberProperty('age', 25)
.addBooleanProperty('betaUser', true)
.addJsonProperty('preferences', {
'theme': 'dark',
'notifications': true,
})
.build();
// Initialize SDK
final client = await CFClient.initialize(config, user);
print('CustomFit SDK initialized successfully');
} catch (e) {
print('Failed to initialize CustomFit SDK: $e');
}
runApp(MyApp());
}
2. Use Feature Flags #
final client = CFClient.getInstance();
// Boolean flag
bool isEnabled = client?.getBoolean('newFeature', false) ?? false;
// String flag
String theme = client?.getString('appTheme', 'light') ?? 'light';
// Number flag
double discountPercentage = client?.getNumber('discountPercentage', 0.0) ?? 0.0;
// JSON flag
Map<String, dynamic> config = client?.getJson('featureConfig', {}) ?? {};
3. Track Events #
// Track a simple event
await client?.trackEvent('buttonClicked');
// Track event with properties
await client?.trackEvent('purchaseCompleted', properties: {
'productId': 'prod-123',
'amount': 99.99,
'currency': 'USD',
'paymentMethod': 'creditCard',
});
4. Flutter Widget Integration #
// Main App Widget with Provider Pattern
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:customfit_ai_flutter_sdk/customfit_ai_flutter_sdk.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize SDK before running app
await initializeCustomFit();
runApp(
ChangeNotifierProvider(
create: (_) => FeatureFlagProvider(),
child: MyApp(),
),
);
}
// Feature Flag Provider for State Management
class FeatureFlagProvider extends ChangeNotifier {
final CFClient? _client = CFClient.getInstance();
Map<String, dynamic> _flags = {};
String _appTheme = 'light';
bool _premiumEnabled = false;
bool _newDashboard = false;
FeatureFlagProvider() {
_initializeFlags();
_setupListeners();
}
// Getters
String get appTheme => _appTheme;
bool get premiumEnabled => _premiumEnabled;
bool get newDashboard => _newDashboard;
void _initializeFlags() {
_appTheme = _client?.getString('app_theme', 'light') ?? 'light';
_premiumEnabled = _client?.getBoolean('premium_features', false) ?? false;
_newDashboard = _client?.getBoolean('new_dashboard', false) ?? false;
notifyListeners();
}
void _setupListeners() {
_client?.addAllFlagsListener((flags) {
_flags = flags;
_appTheme = flags['app_theme'] ?? 'light';
_premiumEnabled = flags['premium_features'] ?? false;
_newDashboard = flags['new_dashboard'] ?? false;
notifyListeners();
});
}
Future<void> trackEvent(String event, [Map<String, dynamic>? properties]) async {
await _client?.trackEvent(event, properties: properties);
}
}
// Main App Widget
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<FeatureFlagProvider>(
builder: (context, featureFlags, child) {
return MaterialApp(
title: 'CustomFit Demo',
theme: featureFlags.appTheme == 'dark'
? ThemeData.dark()
: ThemeData.light(),
home: MainScreen(),
);
},
);
}
}
// Main Screen with Navigation
class MainScreen extends StatefulWidget {
@override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
final featureFlags = Provider.of<FeatureFlagProvider>(context);
final List<Widget> _screens = [
HomeScreen(),
if (featureFlags.newDashboard) DashboardScreen(),
if (featureFlags.premiumEnabled) PremiumScreen(),
SettingsScreen(),
];
return Scaffold(
body: IndexedStack(
index: _selectedIndex,
children: _screens,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) {
setState(() {
_selectedIndex = index;
});
featureFlags.trackEvent('tab_selected', {
'tab_index': index,
'tab_name': ['home', 'dashboard', 'premium', 'settings'][index],
});
},
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
if (featureFlags.newDashboard)
BottomNavigationBarItem(
icon: Icon(Icons.dashboard),
label: 'Dashboard',
),
if (featureFlags.premiumEnabled)
BottomNavigationBarItem(
icon: Icon(Icons.star),
label: 'Premium',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Settings',
),
],
),
);
}
}
// Home Screen
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final featureFlags = Provider.of<FeatureFlagProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text('CustomFit Demo'),
),
body: ListView(
padding: EdgeInsets.all(16),
children: [
// Feature Banner Widget
if (featureFlags.newDashboard)
FeatureBanner(
title: '🎉 New Dashboard Available!',
subtitle: 'Check out our redesigned dashboard',
onTap: () {
featureFlags.trackEvent('feature_banner_clicked', {
'feature': 'new_dashboard',
});
},
),
SizedBox(height: 20),
// Action Cards
ActionCard(
title: 'Track Custom Event',
icon: Icons.analytics,
onTap: () async {
await featureFlags.trackEvent('custom_action', {
'timestamp': DateTime.now().millisecondsSinceEpoch,
'source': 'home_screen',
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Event tracked!')),
);
},
),
if (featureFlags.premiumEnabled)
ActionCard(
title: 'Premium Features',
icon: Icons.star,
color: Colors.amber,
onTap: () {
featureFlags.trackEvent('premium_card_clicked');
Navigator.push(
context,
MaterialPageRoute(builder: (_) => PremiumScreen()),
);
},
),
],
),
);
}
}
// Reusable Feature Banner Widget
class FeatureBanner extends StatelessWidget {
final String title;
final String subtitle;
final VoidCallback? onTap;
const FeatureBanner({
Key? key,
required this.title,
required this.subtitle,
this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue, Colors.blue.shade700],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.3),
blurRadius: 8,
offset: Offset(0, 4),
),
],
),
child: Row(
children: [
Icon(Icons.new_releases, color: Colors.white, size: 32),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
),
),
],
),
),
Icon(Icons.arrow_forward_ios, color: Colors.white),
],
),
),
);
}
}
// Reusable Action Card Widget
class ActionCard extends StatelessWidget {
final String title;
final IconData icon;
final Color? color;
final VoidCallback onTap;
const ActionCard({
Key? key,
required this.title,
required this.icon,
this.color,
required this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.only(bottom: 16),
elevation: 2,
child: ListTile(
leading: CircleAvatar(
backgroundColor: (color ?? Theme.of(context).primaryColor).withOpacity(0.1),
child: Icon(icon, color: color ?? Theme.of(context).primaryColor),
),
title: Text(title),
trailing: Icon(Icons.arrow_forward_ios, size: 16),
onTap: onTap,
),
);
}
}
// Dashboard Screen (Feature Flagged)
class DashboardScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final featureFlags = Provider.of<FeatureFlagProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text('Dashboard'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.dashboard, size: 64, color: Colors.blue),
SizedBox(height: 16),
Text(
'New Dashboard',
style: Theme.of(context).textTheme.headlineMedium,
),
SizedBox(height: 8),
Text('This is a feature-flagged dashboard'),
],
),
),
);
}
}
// Premium Screen (Feature Flagged)
class PremiumScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Premium Features'),
backgroundColor: Colors.amber,
),
body: ListView(
padding: EdgeInsets.all(16),
children: [
PremiumFeatureCard(
title: 'Advanced Analytics',
description: 'Get detailed insights into your usage',
icon: Icons.analytics_outlined,
),
PremiumFeatureCard(
title: 'Priority Support',
description: '24/7 dedicated support team',
icon: Icons.support_agent,
),
PremiumFeatureCard(
title: 'Custom Themes',
description: 'Personalize your app experience',
icon: Icons.palette,
),
],
),
);
}
}
// Premium Feature Card Widget
class PremiumFeatureCard extends StatelessWidget {
final String title;
final String description;
final IconData icon;
const PremiumFeatureCard({
Key? key,
required this.title,
required this.description,
required this.icon,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.only(bottom: 16),
child: Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.amber.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: Colors.amber, size: 32),
),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4),
Text(
description,
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
],
),
),
],
),
),
);
}
}
// Settings Screen
class SettingsScreen extends StatefulWidget {
@override
_SettingsScreenState createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
bool _notificationsEnabled = true;
bool _analyticsEnabled = true;
@override
Widget build(BuildContext context) {
final featureFlags = Provider.of<FeatureFlagProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text('Settings'),
),
body: ListView(
children: [
ListTile(
title: Text('Theme'),
subtitle: Text(featureFlags.appTheme.capitalize()),
leading: Icon(Icons.palette),
),
Divider(),
SwitchListTile(
title: Text('Push Notifications'),
subtitle: Text('Receive updates and alerts'),
value: _notificationsEnabled,
onChanged: (value) {
setState(() {
_notificationsEnabled = value;
});
featureFlags.trackEvent('setting_changed', {
'setting': 'notifications',
'value': value,
});
},
),
SwitchListTile(
title: Text('Analytics'),
subtitle: Text('Help us improve the app'),
value: _analyticsEnabled,
onChanged: (value) {
setState(() {
_analyticsEnabled = value;
});
featureFlags.trackEvent('setting_changed', {
'setting': 'analytics',
'value': value,
});
},
),
],
),
);
}
}
// Extension to capitalize strings
extension StringExtension on String {
String capitalize() {
return "${this[0].toUpperCase()}${this.substring(1)}";
}
}
Configuration #
Configuration #
The CustomFit Flutter SDK provides flexible configuration options using the builder pattern.
Basic Configuration
// Simple configuration with default settings
final config = CFConfig.builder('your-client-key').build();
Advanced Configuration
final config = CFConfig.builder('your-client-key')
.setDebugLoggingEnabled(true)
.setEventsFlushIntervalMs(60000)
.setNetworkConnectionTimeoutMs(10000)
.setOfflineMode(false)
.build();
Enterprise Configuration #
For enterprise deployments with custom infrastructure:
final config = CFConfig.builder('your-enterprise-key')
.setDebugLoggingEnabled(false)
.setEventsFlushIntervalMs(30000) // More frequent flushing
.setNetworkConnectionTimeoutMs(15000) // Longer timeout for internal networks
.setOfflineMode(false)
.setSummariesFlushIntervalMs(300000) // 5 minutes
.setMaxStoredEvents(500) // Higher capacity
.build();
User Management #
Creating Users with Builder Pattern #
// Basic user
final user = CFUser.builder('user123').build();
// User with properties
final user = CFUser.builder('user123')
.addStringProperty('email', 'user@example.com')
.addStringProperty('plan', 'premium')
.addNumberProperty('age', 25)
.addBooleanProperty('beta_user', true)
.addJsonProperty('preferences', {
'theme': 'dark',
'notifications': true,
})
.build();
// Anonymous user
final anonymousUser = CFUser.anonymousBuilder()
.addStringProperty('source', 'mobile_app')
.build();
Updating User Context #
final client = CFClient.getInstance();
// Add properties to existing user
client?.addStringProperty('plan', 'enterprise'); // Upgrade plan
client?.addBooleanProperty('premium_access', true);
client?.addNumberProperty('usage_count', 42);
// Or add multiple properties at once
client?.addUserProperty('subscription_tier', 'pro');
client?.addJsonProperty('preferences', {
'notifications': true,
'theme': 'dark'
});
Privacy and Security #
The SDK provides built-in privacy controls to mark sensitive data as private or session-level, ensuring compliance with data protection regulations.
Private Fields
Private fields are excluded from analytics and logs to protect sensitive user information:
// Mark properties as private using boolean flags
final user = CFUser.builder('user123')
.addStringProperty('email', 'user@example.com', isPrivate: true)
.addStringProperty('name', 'John Doe') // Not private
.addNumberProperty('ssn', 123456789, isPrivate: true)
.addBooleanProperty('verified', true, isPrivate: true)
.addMapProperty('preferences', {'theme': 'dark'}, isPrivate: true)
.addJsonProperty('metadata', {'version': '1.0'}, isPrivate: true)
.addGeoPointProperty('location', 37.7749, -122.4194, isPrivate: true)
.build();
// Mark existing properties as private
final updatedUser = user.markPropertyAsPrivate('email');
// Using instance methods with privacy flags
final user2 = CFUser(userCustomerId: 'user456')
.addStringProperty('phone', '+1234567890', isPrivate: true)
.addNumberProperty('age', 25) // Not private
.addBooleanProperty('premium', true, isPrivate: true);
Session-Level Fields
Session-level fields are temporary data that should not be persisted beyond the current session:
// Mark properties as session-level using boolean flags
final user = CFUser.builder('user123')
.addStringProperty('session_token', 'abc123', isSession: true)
.addStringProperty('name', 'John Doe') // Persistent
.addNumberProperty('temp_score', 100, isSession: true)
.addBooleanProperty('temp_flag', true, isSession: true)
.addMapProperty('temp_data', {'key': 'value'}, isSession: true)
.build();
// Mark existing properties as session-level
final updatedUser = user.makeAttributeSessionLevel('temp_token');
// Using instance methods with session flags
final user2 = CFUser(userCustomerId: 'user456')
.addStringProperty('temp_id', 'xyz789', isSession: true)
.addNumberProperty('session_count', 5, isSession: true);
Combined Privacy Controls
You can combine both private and session flags for maximum control:
final user = CFUser.builder('user123')
.addStringProperty('email', 'user@example.com', isPrivate: true)
.addStringProperty('session_token', 'abc123', isSession: true)
.addStringProperty('temp_private_data', 'sensitive', isPrivate: true, isSession: true)
.addStringProperty('name', 'John Doe') // Normal property
.build();
Backend Format
The SDK automatically serializes private and session fields to match the backend format:
{
"user_customer_id": "user123",
"anonymous": false,
"properties": {
"name": "John Doe",
"email": "user@example.com",
"session_token": "abc123"
},
"private_fields": {
"properties": ["email"]
},
"session_fields": {
"properties": ["session_token"]
}
}
Privacy Best Practices
- Mark PII as Private: Always mark personally identifiable information as private
- Use Session Fields for Temporary Data: Session tokens, temporary scores, etc.
- Regular Audits: Review which fields are marked as private/session
- Compliance: Ensure privacy settings align with GDPR, CCPA requirements
// Example: E-commerce user with proper privacy controls
final user = CFUser.builder('customer123')
// Public properties
.addStringProperty('plan', 'premium')
.addNumberProperty('age', 30)
.addBooleanProperty('newsletter_subscribed', true)
// Private properties (PII)
.addStringProperty('email', 'user@example.com', isPrivate: true)
.addStringProperty('phone', '+1234567890', isPrivate: true)
.addStringProperty('address', '123 Main St', isPrivate: true)
// Session properties (temporary)
.addStringProperty('cart_token', 'cart_abc123', isSession: true)
.addNumberProperty('session_duration', 1800, isSession: true)
.addBooleanProperty('checkout_started', true, isSession: true)
.build();
Feature Flags #
Basic Flag Evaluation #
final client = CFClient.getInstance();
// Boolean flags
bool isEnabled = client?.getBoolean('new_feature', false) ?? false;
// String flags
String theme = client?.getString('app_theme', 'light') ?? 'light';
// Number flags
double threshold = client?.getNumber('conversion_threshold', 0.5) ?? 0.5;
// JSON flags
Map<String, dynamic> config = client?.getJson('feature_config', {}) ?? {};
Advanced Flag Usage #
// Using flag values in business logic
final maxRetries = client?.getNumber('api_max_retries', 3)?.toInt() ?? 3;
final timeout = client?.getNumber('api_timeout_ms', 5000)?.toInt() ?? 5000;
// Configure components based on flags
final apiClient = HttpClient(
maxRetries: maxRetries,
timeout: Duration(milliseconds: timeout),
);
// Feature rollout with percentage
final rolloutPercentage = client?.getNumber('feature_rollout', 0.0) ?? 0.0;
final userId = user.userCustomerId ?? '';
final userHash = userId.hashCode.abs() % 100;
final shouldShowFeature = userHash < (rolloutPercentage * 100);
Type-Safe Feature Flags #
The SDK provides a type-safe API for feature flags that eliminates runtime errors from typos and type mismatches:
final client = CFClient.getInstance();
final flags = client?.typed;
// Define strongly-typed flags
final enableNewUI = flags?.boolean(
key: 'enable_new_ui',
defaultValue: false,
description: 'Enables the new dashboard UI',
);
final apiEndpoint = flags?.string(
key: 'api_endpoint',
defaultValue: 'https://api.production.com',
allowedValues: ['https://api.production.com', 'https://api.staging.com'],
);
final maxUploadSize = flags?.number(
key: 'max_upload_size_mb',
defaultValue: 10.0,
min: 1.0,
max: 100.0,
description: 'Maximum file upload size in MB',
);
// Use flags with compile-time safety
if (enableNewUI?.value ?? false) {
print('New UI is enabled');
}
print('API endpoint: ${apiEndpoint?.value}');
print('Max upload: ${maxUploadSize?.value}MB');
Advanced Type-Safe Usage
// Enum flags for A/B testing
enum ExperimentGroup { control, variantA, variantB }
final experimentGroup = flags?.enumFlag<ExperimentGroup>(
key: 'experiment_group',
defaultValue: ExperimentGroup.control,
values: ExperimentGroup.values,
);
// JSON flags with custom types
class UIConfig {
final String primaryColor;
final bool darkMode;
UIConfig({required this.primaryColor, required this.darkMode});
factory UIConfig.fromJson(Map<String, dynamic> json) {
return UIConfig(
primaryColor: json['primaryColor'] ?? '#007AFF',
darkMode: json['darkMode'] ?? false,
);
}
}
final uiConfig = flags?.json<UIConfig>(
key: 'ui_config',
defaultValue: UIConfig(primaryColor: '#007AFF', darkMode: false),
parser: (json) => UIConfig.fromJson(json),
);
// Listen to flag changes
enableNewUI?.onChange((enabled) {
print('New UI flag changed to: $enabled');
});
// Quick flags for simple cases
final quickFlags = QuickFlags(provider);
final debugMode = quickFlags.boolFlag('debug_mode', false);
Benefits of Type-Safe Flags
- Compile-time safety: Catch typos and type mismatches during development
- IDE autocomplete: Full IntelliSense support for flag names and values
- Type validation: Automatic validation and parsing of flag values
- Change listeners: Reactive streams for flag value changes
- Constraints: Built-in support for min/max values, allowed values, etc.
Event Tracking #
Basic Event Tracking #
final client = CFClient.getInstance();
// Simple event
await client?.trackEvent('button_clicked');
// Event with properties
await client?.trackEvent('purchase_completed', properties: {
'product_id': 'abc123',
'price': 29.99,
'currency': 'USD',
'payment_method': 'credit_card',
'timestamp': DateTime.now().millisecondsSinceEpoch,
});
Advanced Analytics #
// Track feature usage
await client?.trackEvent('feature_used', properties: {
'feature_name': 'premium_dashboard',
'usage_duration': 120, // seconds
'interactions': 5,
'user_satisfaction': 4.5,
});
// Track user journey
await client?.trackEvent('user_journey', properties: {
'step': 'onboarding_complete',
'time_to_complete': 300, // seconds
'completion_rate': 0.85,
'drop_off_point': null,
});
Session Management #
Sessions are automatically managed by the SDK with configurable rotation policies:
// Sessions rotate automatically based on:
// - Time intervals (configurable)
// - User authentication changes
// - App state transitions (background/foreground)
// Access current session info
final client = CFClient.getInstance();
final sessionId = client?.getCurrentSessionId();
Listeners & Callbacks #
Feature Flag Change Listeners #
// Listen for specific flag changes
final client = CFClient.getInstance();
client?.addFeatureFlagListener('new_feature', (flagKey, newValue) {
print('Flag $flagKey changed to: $newValue');
// Update UI or trigger actions
setState(() {
_featureEnabled = newValue as bool;
});
});
// Listen for all flag changes
client?.addAllFlagsListener((flags) {
print('Flags updated: ${flags.keys}');
// Batch update multiple features
_updateAllFeatures(flags);
});
Offline Support #
The SDK provides robust offline capabilities:
// Enable offline mode
final config = CFConfig.builder('your-client-key')
.setOfflineMode(true)
.setMaxStoredEvents(1000)
.setLocalStorageEnabled(true)
.build();
// The SDK will:
// - Cache feature flag values locally
// - Queue events for later transmission
// - Use cached configurations when offline
// - Automatically sync when connection is restored
Offline Behavior #
- Feature Flags: Served from local cache with configurable TTL
- Events: Queued locally and transmitted when online
- Configuration: Cached with stale-while-revalidate strategy
- Recovery: Automatic retry with exponential backoff
Event Persistence and Offline Support #
The Flutter SDK provides comprehensive event persistence to ensure no analytics data is lost, even when the device is offline or the app is terminated.
Event Persistence Strategy #
Events are automatically persisted using SharedPreferences with the PersistentEventQueue
class:
- Storage Mechanism: SharedPreferences
- Storage Class:
PersistentEventQueue
with 100ms debounce - Queue Size Limit: Maximum 100 events (configurable)
- Persistence Triggers:
- Network failures
- App background/termination
- Queue size threshold reached
- Automatic persistence with 100ms debounce
Storage Mechanism #
The SDK uses PersistentEventQueue
to ensure no events are lost:
// Events are stored in SharedPreferences
// Key pattern: customfit_events_queue
// The PersistentEventQueue automatically persists events with:
// - 100ms debounce for efficient writes
// - Automatic persistence on network failure
// - Queue size limit triggers persistence
// - Background state triggers persistence
// Events are automatically saved when:
// 1. Network is unavailable
// 2. Queue reaches size limit (default: 100 events)
// 3. App goes to background
// 4. 100ms after any change (debounced)
Configuration Options #
final config = CFConfig.builder('your-client-key')
// Event queue configuration
.setEventsQueueSize(100) // Max events in memory before persistence
.setMaxStoredEvents(1000) // Max events to persist
.setEventsFlushIntervalMs(5000) // Auto-flush interval
// Cache TTL configuration
.setConfigCacheTtlSeconds(300) // Config cache: 5 minutes
.setEventCacheTtlSeconds(3600) // Event cache: 1 hour
// Persistence settings
.setLocalStorageEnabled(true) // Enable local storage
.setPersistCacheAcrossRestarts(true) // Persist cache between app restarts
.build();
Automatic Event Recovery #
Events are automatically recovered and retransmitted when:
- App Launch: Persisted events are loaded from SharedPreferences
- Network Restored: Queued events are sent when connectivity returns
- Foreground Transition: Background events are synced
- SDK Initialization: Any pending events are processed
Cache TTL Values #
The SDK implements different TTL (Time To Live) values for various cached data:
Data Type | TTL | Description |
---|---|---|
Configuration | 5 minutes | Feature flags and SDK settings |
User Data | 24 hours | User properties and context |
Events | Persistent | Never expires until successfully sent |
Session Data | 30 minutes | Active session information |
Best Practices for Event Persistence #
// 1. Configure appropriate queue sizes
final config = CFConfig.builder('your-client-key')
.setEventsQueueSize(100) // Smaller for mobile to save memory
.setMaxStoredEvents(500) // Reasonable limit for storage
.setLocalStorageEnabled(true) // Ensure persistence is enabled
.build();
// 2. Handle critical events
await client?.trackEvent('purchase_completed', properties: {
'amount': 99.99,
'product_id': 'premium_plan',
'timestamp': DateTime.now().millisecondsSinceEpoch,
});
// 3. Flush all queued events
await client?.flushEvents(); // Send all queued events
Offline Event Tracking Flow #
// Events are handled seamlessly offline
await client?.trackEvent('offline_action', properties: {
'network_status': 'offline',
'action': 'button_tap',
'timestamp': DateTime.now().millisecondsSinceEpoch,
});
// The SDK will:
// 1. Add event to PersistentEventQueue
// 2. Detect network failure
// 3. Persist to SharedPreferences with 100ms debounce
// 4. Retry when network is restored
// 5. Remove from storage after successful transmission
Platform-Specific Considerations #
// iOS - Events persist across app terminations
if (Platform.isIOS) {
// SharedPreferences backed by NSUserDefaults
// Data persists until app is uninstalled
}
// Android - Events persist with SharedPreferences
if (Platform.isAndroid) {
// SharedPreferences in private mode
// Data persists until app data is cleared
}
// Web - Events persist in localStorage
if (kIsWeb) {
// localStorage with domain isolation
// Data persists until browser cache is cleared
}
// Configure for optimal persistence
final config = CFConfig.builder('your-client-key')
.setPersistCacheAcrossRestarts(true)
.setUseStaleWhileRevalidate(true) // Use cached data while updating
.setMaxCacheSizeMb(50) // Limit cache size
.build();
Persistence Strategy #
The Flutter SDK implements a multi-layered persistence strategy to ensure data durability and optimal performance:
Cache Layers #
- Memory Cache: Fast in-memory storage with LRU eviction
- Disk Cache: SharedPreferences and file-based storage for larger payloads
- Event Queue: Persistent queue with 100ms debounce writes
TTL Policies #
Data Type | TTL | Storage Layer |
---|---|---|
Config | 5 minutes | Memory + Disk |
User Data | 24 hours | Memory + Disk |
Events | Persistent | Disk only |
Session | 30 minutes | Memory only |
Storage Mechanisms #
- SharedPreferences: Metadata and small configs (<100KB)
- File Storage: Large configurations and event queues
- Memory Cache: Hot data with automatic cleanup
Battery Optimization #
The SDK includes intelligent battery optimization features to reduce power consumption on mobile devices:
Automatic Battery-Aware Polling #
When the device battery is low, the SDK automatically reduces polling frequency to conserve power:
final config = CFConfig.builder('your-client-key')
.setUseReducedPollingWhenBatteryLow(true) // Enable battery optimization
.setReducedPollingIntervalMs(300000) // Poll every 5 minutes when battery is low
.setBackgroundPollingIntervalMs(60000) // Normal background polling: 1 minute
.build();
Battery Optimization Features #
- Adaptive Polling: Automatically switches to reduced polling intervals when battery is low
- Smart Thresholds: Considers both battery level and charging state
- Background Optimization: Further reduces activity when app is in background
- Event Batching: Consolidates network requests to minimize radio usage
Configuration Options #
Option | Description | Default |
---|---|---|
useReducedPollingWhenBatteryLow |
Enable battery-aware polling | true |
reducedPollingIntervalMs |
Polling interval when battery is low | 300000 (5 min) |
backgroundPollingIntervalMs |
Normal background polling interval | 60000 (1 min) |
disableBackgroundPolling |
Completely disable background polling | false |
Battery State Detection #
The SDK automatically detects battery state on supported platforms:
// The SDK monitors:
// - Battery level (low threshold typically < 20%)
// - Charging state (optimization disabled when charging)
// - Power save mode (respects system power saving settings)
Best Practices for Battery Optimization #
-
Use Appropriate Intervals: Balance between data freshness and battery life
.setReducedPollingIntervalMs(300000) // 5 minutes is a good default
-
Consider Disabling Background Polling: For non-critical features
.setDisableBackgroundPolling(true) // Completely stops background polling
-
Leverage Event Batching: Group events to reduce network calls
.setEventsFlushIntervalMs(30000) // Flush events every 30 seconds .setEventsQueueSize(50) // Or when 50 events accumulate
-
Platform Considerations:
- iOS: Integrates with iOS background modes and low power mode
- Android: Respects Doze mode and app standby restrictions
- Web/Desktop: Battery optimization has minimal effect
Monitoring Battery Impact #
The SDK automatically handles battery optimization internally. You can track battery-related events:
// Track battery optimization events
client?.trackEvent('battery_optimization_active', properties: {
'timestamp': DateTime.now().millisecondsSinceEpoch,
});
Advanced Features #
Certificate Pinning #
Implement certificate pinning for enhanced security in production environments:
// Production configuration with certificate pinning
final secureConfig = CFConfig.builder('your-client-key')
.setCertificatePinningEnabled(true)
.setPinnedCertificates([
'sha256/PRIMARY_CERT_FINGERPRINT',
'sha256/BACKUP_CERT_FINGERPRINT'
])
.build();
// The SDK will:
// - Validate server certificates against pinned fingerprints
// - Reject connections with unpinned certificates
// - Log certificate validation failures
// - Provide detailed error information for debugging
Custom Evaluation Contexts #
// Add location context
final locationContext = EvaluationContext(
type: ContextType.location,
properties: {
'country': 'US',
'region': 'California',
'city': 'San Francisco',
},
);
// Add device context
final deviceContext = EvaluationContext(
type: ContextType.device,
properties: {
'platform': 'iOS',
'version': '15.0',
'model': 'iPhone 13',
},
);
final user = CFUser.builder('user123')
.addContext(locationContext)
.addContext(deviceContext)
.build();
Error Handling and Recovery #
try {
final client = await CFClient.initialize(config, user);
// The SDK includes automatic:
// - Session recovery on failures
// - Event retry with exponential backoff
// - Configuration fallback mechanisms
// - Network error handling
} catch (e) {
print('Initialization failed: $e');
// Implement fallback behavior
}
Error Handling #
The SDK provides comprehensive error handling with the CFResult<T>
wrapper for safe operations:
// Initialization errors
try {
final client = await CFClient.initialize(config, user);
} on CFInitializationException catch (e) {
print('SDK initialization failed: ${e.message}');
// Handle initialization failure
} catch (e) {
print('Unexpected error: $e');
}
// Runtime errors are handled gracefully
// Flag evaluations return default values on error
// Events are queued for retry on network errors
// Sessions are recovered automatically
Flutter Integration #
State Management Integration #
// With Provider
class FeatureFlagProvider extends ChangeNotifier {
final CFClient? _client = CFClient.getInstance();
Map<String, dynamic> _flags = {};
Map<String, dynamic> get flags => _flags;
void init() {
_client?.addAllFlagsListener((flags) {
_flags = flags;
notifyListeners();
});
}
bool getFeature(String key, bool defaultValue) {
return _flags[key] as bool? ?? defaultValue;
}
}
// With Bloc
class FeatureFlagBloc extends Bloc<FeatureFlagEvent, FeatureFlagState> {
final CFClient? _client = CFClient.getInstance();
FeatureFlagBloc() : super(FeatureFlagInitial()) {
_client?.addAllFlagsListener((flags) {
add(FlagsUpdated(flags));
});
}
}
Widget Integration #
// Feature flag widget wrapper
class FeatureFlag extends StatelessWidget {
final String flagKey;
final bool defaultValue;
final Widget child;
final Widget? fallback;
const FeatureFlag({
Key? key,
required this.flagKey,
required this.defaultValue,
required this.child,
this.fallback,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final client = CFClient.getInstance();
final isEnabled = client?.getBoolean(flagKey, defaultValue) ?? defaultValue;
return isEnabled ? child : (fallback ?? SizedBox.shrink());
}
}
// Usage
FeatureFlag(
flagKey: 'new_dashboard',
defaultValue: false,
child: NewDashboard(),
fallback: OldDashboard(),
)
Best Practices #
Performance Optimization #
// 1. Initialize SDK early in app lifecycle
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeCustomFitSDK(); // Initialize before runApp
runApp(MyApp());
}
// 2. Cache flag values to avoid repeated lookups
class FeatureCache {
static final Map<String, dynamic> _cache = {};
static bool getFeature(String key, bool defaultValue) {
if (_cache.containsKey(key)) {
return _cache[key] as bool;
}
final client = CFClient.getInstance();
final value = client?.getBoolean(key, defaultValue) ?? defaultValue;
_cache[key] = value;
return value;
}
}
// 3. Use listeners for reactive updates
client?.addFeatureFlagListener('feature_key', (key, value) {
FeatureCache._cache[key] = value; // Update cache
});
Error Resilience #
// Always provide sensible defaults
bool isFeatureEnabled(String key) {
final client = CFClient.getInstance();
return client?.getBoolean(key, false) ?? false;
}
// Graceful degradation
Widget buildFeature() {
try {
if (isFeatureEnabled('new_ui')) {
return NewUIComponent();
}
} catch (e) {
Logger.w('Feature flag evaluation failed: $e');
}
return FallbackUIComponent();
}
Analytics Best Practices #
// Structure event properties consistently
await client?.trackEvent('user_action', properties: {
'action_type': 'click',
'element_id': 'submit_button',
'page_name': 'checkout',
'timestamp': DateTime.now().millisecondsSinceEpoch,
'session_id': client?.getCurrentSessionId(),
'user_id': user.userCustomerId,
});
// Use semantic event names
await client?.trackEvent('checkout_completed'); // Good
await client?.trackEvent('button_click'); // Too generic
API Reference #
CFClient #
Main SDK client providing feature flags, event tracking, and user management.
class CFClient {
// Initialization
static Future<CFClient> initialize(CFConfig config, CFUser user);
static CFClient? getInstance();
// Feature flags
bool? getBoolean(String key, bool defaultValue);
String? getString(String key, String defaultValue);
double? getNumber(String key, double defaultValue);
Map<String, dynamic>? getJson(String key, Map<String, dynamic> defaultValue);
// Event tracking
Future<void> trackEvent(String eventType, {Map<String, dynamic>? properties});
Future<CFResult<void>> flushEvents(); // Returns CFResult wrapper
// User management
void addStringProperty(String key, String value);
void addNumberProperty(String key, num value);
void addBooleanProperty(String key, bool value);
void addJsonProperty(String key, Map<String, dynamic> value);
void addGeoPointProperty(String key, double lat, double lon);
void addDateProperty(String key, DateTime value);
void addUserProperty(String key, dynamic value);
// Listeners
void addFeatureFlagListener(String flagKey, FeatureFlagChangeListener listener);
void addAllFlagsListener(AllFlagsListener listener);
// Note: Connection status listeners are accessed through the listeners component
// client?.listeners.addConnectionStatusListener(listener);
// Session management
String? getCurrentSessionId();
void forceSessionRotation();
// Cleanup
Future<void> shutdown();
}
CFConfig #
Configuration builder for SDK initialization.
class CFConfig {
static CFConfigBuilder builder(String clientKey);
// Core properties
String get clientKey;
bool get debugLoggingEnabled;
bool get offlineMode;
int get eventsFlushIntervalMs;
int get networkConnectionTimeoutMs;
// ... additional properties
}
class CFConfigBuilder {
CFConfigBuilder debugLoggingEnabled(bool enabled);
CFConfigBuilder eventsFlushIntervalMs(int interval);
CFConfigBuilder networkConnectionTimeoutMs(int timeout);
CFConfigBuilder offlineMode(bool offline);
// ... additional builder methods
CFConfig build();
}
CFUser #
User model with builder pattern for properties and contexts.
class CFUser {
static CFUserBuilder builder(String userId);
static CFUserBuilder anonymousBuilder();
String? get userCustomerId;
bool get anonymous;
Map<String, dynamic> get properties;
List<EvaluationContext> get contexts;
}
class CFUserBuilder {
CFUserBuilder addStringProperty(String key, String value);
CFUserBuilder addNumberProperty(String key, num value);
CFUserBuilder addBooleanProperty(String key, bool value);
CFUserBuilder addJsonProperty(String key, Map<String, dynamic> value);
CFUserBuilder addGeoPointProperty(String key, double latitude, double longitude);
CFUserBuilder makeAnonymous(bool anonymous);
CFUserBuilder addContext(EvaluationContext context);
CFUser build();
}
Troubleshooting #
Common Issues #
SDK Initialization Fails
// Check client key
if (config.clientKey.isEmpty) {
throw Exception('Client key is required');
}
// Check user ID
if (user.userCustomerId == null || user.userCustomerId!.isEmpty) {
throw Exception('User ID is required');
}
// Check network connectivity
final client = CFClient.getInstance();
client?.addConnectionStatusListener((status) {
if (status == ConnectionStatus.disconnected) {
print('Network connectivity issues detected');
}
});
Feature Flags Not Updating
// Verify listener registration
client?.addFeatureFlagListener('flag_key', (key, value) {
print('Flag updated: $key = $value');
});
// Check polling configuration
final config = CFConfig.builder('client-key')
.setSdkSettingsCheckIntervalMs(30000) // Check every 30 seconds
.build();
// The SDK automatically handles offline mode
// Feature flags will use cached values when offline
Events Not Being Sent
// Check event queue configuration
final config = CFConfig.builder('client-key')
.setEventsFlushIntervalMs(5000) // Flush every 5 seconds
.setEventsQueueSize(50) // Flush when 50 events queued
.build();
// Verify event structure
await client?.trackEvent('valid_event', properties: {
'string_prop': 'value',
'number_prop': 123,
'boolean_prop': true,
// Avoid complex nested objects
});
Debug Mode #
Enable debug logging to troubleshoot issues:
final config = CFConfig.builder('client-key')
.setDebugLoggingEnabled(true)
.setLogLevel('DEBUG')
.build();
// This will log:
// - SDK initialization steps
// - Network requests and responses
// - Feature flag evaluations
// - Event tracking activities
// - Error conditions and recovery
Performance Optimization #
// Optimize configuration for performance
final config = CFConfig.builder('client-key')
.setLocalStorageEnabled(true) // Enable caching
.setUseStaleWhileRevalidate(true) // Use cache while updating
.setBackgroundPollingIntervalMs(300000) // Reduce polling frequency
.build();
Support #
- 📚 Documentation: https://docs.customfit.ai
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
- 📧 Email: reach@custom.ai
- 🌐 Website: https://customfit.ai
Getting Help #
When reporting issues, please include:
- SDK Version: Check
pubspec.yaml
for version number - Flutter/Dart Version: Run
flutter --version
- Platform: iOS, Android, Web, Desktop
- Configuration: Relevant
CFConfig
settings (remove sensitive keys) - Error Messages: Full stack traces and error logs
- Reproduction Steps: Minimal code example that reproduces the issue
Community #
Join our community for:
- Best practices discussions
- Feature requests and feedback
- Technical support from other developers
- SDK updates and announcements
License #
MIT License - see LICENSE for details.
🔗 Related Projects #
https://customfit.ai