Skip to main content
Imagine you’ve built a mobile app — users install it, sign in, browse content, and eventually make a purchase or complete a key action. This guide walks through tracking the full app experience using the Zixflow Flutter SDK: from first install and screen navigation to feature usage, push notifications, and offline sessions.

SDK Integration

Flutter SDK Setup

pubspec.yaml:
dependencies:
  zixflow: ^1.0.0
Initialize on app launch:
// lib/main.dart
import 'package:zixflow/zixflow.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Configure SDK
  const config = ZixflowConfig(
    siteId: 'your_workspace_id',
    apiKey: 'your_api_key',
    region: Region.US,
    autoTrackScreenViews: true,        // Auto-track screen events
    autoTrackDeviceAttributes: true,   // Capture OS, app version, etc.
    backgroundQueueMinNumberOfTasks: 10,  // Batch events
    backgroundQueueSecondsDelay: 30,      // Flush every 30s
  );
  
  await Zixflow.instance.initialize(config);
  
  runApp(MyApp());
}
SDK behavior:
  • Events batched in memory (max 20 events or 30 seconds)
  • On app background: Immediate flush
  • On network failure: Persist to SQLite, retry with exponential backoff
  • Battery-conscious: Batching reduces network calls by 90%

App Lifecycle Events

App Installed

When: First app launch after install (SDK auto-tracks) What’s tracked automatically:
{
  "event": "App Installed",
  "properties": {
    "app_version": "2.1.0",
    "build_number": "403",
    "install_date": "2026-05-04T10:00:00Z",
    "install_source": "App Store"  // iOS: App Store, Android: Play Store, APK
  },
  "context": {
    "os": { "name": "iOS", "version": "17.4.1" },
    "device": { "model": "iPhone 14 Pro", "manufacturer": "Apple" },
    "locale": "en-US",
    "timezone": "America/New_York"
  }
}
Manual tracking (if you need attribution):
// After retrieving install attribution from AppsFlyer/Branch/Adjust
track('App Installed', {
  'install_source': 'App Store',
  'attribution_campaign': 'spring_2026_ios',
  'attribution_channel': 'google_uac',
  'attribution_ad_group': 'sneakers_lookalike',
  'install_cost': 2.45  // CPI from attribution provider
});

App Opened

When: App moves from background to foreground Auto-tracked by SDK (if autoTrackAppLifecycle: true):
{
  "event": "App Opened",
  "properties": {
    "cold_start": true,  // vs warm start (from background)
    "push_notification_triggered": false,  // Did user tap notification?
    "deeplink_url": null,  // If opened via deep link
    "session_id": "sess_abc-123",
    "time_since_last_open_seconds": 3600
  }
}
Manual tracking (if you want custom logic):
class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
  DateTime? _lastBackgroundTime;
  
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }
  
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      // App opened
      final sessionGapMinutes = _lastBackgroundTime != null
          ? DateTime.now().difference(_lastBackgroundTime!).inMinutes
          : null;
      
      Zixflow.instance.track('App Opened', {
        'cold_start': _lastBackgroundTime == null,
        'session_gap_minutes': sessionGapMinutes,
        'previous_screen': _lastScreen,  // Where user was before backgrounding
      });
    } else if (state == AppLifecycleState.paused) {
      _lastBackgroundTime = DateTime.now();
    }
  }
}

App Backgrounded

When: User switches to another app or locks screen
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
  if (state == AppLifecycleState.paused) {
    // Calculate session duration
    final sessionDuration = DateTime.now().difference(_sessionStartTime);
    
    Zixflow.instance.track('App Backgrounded', {
      'session_duration_seconds': sessionDuration.inSeconds,
      'screens_viewed': _screensViewedThisSession,
      'events_tracked': _eventsThisSession,
      'last_screen': _currentScreen,
      'actions_completed': _actionsCompletedThisSession,  // Purchases, signups, etc.
    });
    
    // Flush event queue immediately (don't wait for batch)
    Zixflow.instance.flush();
  }
}
Analytics use cases:
  • Session duration distribution
  • Screens per session
  • Features used per session
  • Session → Conversion rate

Screen Tracking

Enable in SDK config:
const config = ZixflowConfig(
  autoTrackScreenViews: true,  // Auto-track on Navigator route changes
  // ...
);
What’s tracked:
{
  "type": "screen",
  "name": "ProductDetailScreen",
  "properties": {
    "route": "/products/nike-air-max-90",
    "previous_screen": "HomeScreen",
    "screen_index": 3,  // 3rd screen in this session
    "session_id": "sess_abc-123"
  },
  "timestamp": "2026-05-04T10:15:30.123Z"
}

Manual Screen Tracking (Granular Control)

Use case: You want to add screen-specific properties (product ID, category, etc.)
// lib/screens/product_detail_screen.dart

class ProductDetailScreen extends StatefulWidget {
  final String productId;
  
  const ProductDetailScreen({required this.productId});
  
  @override
  _ProductDetailScreenState createState() => _ProductDetailScreenState();
}

class _ProductDetailScreenState extends State<ProductDetailScreen> {
  late Product _product;
  DateTime? _screenEnteredAt;
  
  @override
  void initState() {
    super.initState();
    _screenEnteredAt = DateTime.now();
    _loadProduct();
  }
  
  Future<void> _loadProduct() async {
    _product = await ProductService.getProduct(widget.productId);
    
    // Track screen view with product context
    Zixflow.instance.screen(
      name: 'Product Detail',
      properties: {
        'product_id': _product.id,
        'product_name': _product.name,
        'product_category': _product.category,
        'product_price': _product.price,
        'inventory_status': _product.inStock ? 'in_stock' : 'out_of_stock',
      },
    );
  }
  
  @override
  void dispose() {
    // Track time spent on screen
    if (_screenEnteredAt != null) {
      final timeOnScreen = DateTime.now().difference(_screenEnteredAt!);
      Zixflow.instance.track('Screen Exited', {
        'screen_name': 'Product Detail',
        'product_id': _product.id,
        'time_on_screen_seconds': timeOnScreen.inSeconds,
        'interaction_count': _interactionCount,  // Taps, swipes, etc.
      });
    }
    super.dispose();
  }
}

Feature Usage Events

Feature Discovered

When: User views feature for first time (onboarding, tutorial, tooltip)
void _showFeatureTutorial() {
  setState(() => _tutorialVisible = true);
  
  Zixflow.instance.track('Feature Discovered', {
    'feature_name': 'ar_try_on',
    'discovery_method': 'tooltip',  // vs 'onboarding', 'organic', 'search'
    'days_since_signup': _daysSinceSignup,
    'onboarding_step': 3,
  });
}
Analytics: Feature discovery rate, time to discovery

Feature Used

When: User actively engages with feature
void _launchARTryOn() {
  Zixflow.instance.track('Feature Used', {
    'feature_name': 'ar_try_on',
    'product_id': widget.productId,
    'is_first_use': !_userProfile.hasUsedAR,
    'days_since_discovery': _daysSinceDiscovered,
  });
  
  // Launch AR experience
  Navigator.push(context, MaterialPageRoute(
    builder: (context) => ARTryOnScreen(product: _product),
  ));
  
  // Update profile attribute
  Zixflow.instance.setProfileAttributes({
    'has_used_ar': true,
    'ar_first_used_at': DateTime.now().toIso8601String(),
  });
}

Search Performed

When: User submits search query
class SearchScreen extends StatefulWidget {
  @override
  _SearchScreenState createState() => _SearchScreenState();
}

class _SearchScreenState extends State<SearchScreen> {
  final TextEditingController _searchController = TextEditingController();
  List<Product> _results = [];
  
  Future<void> _performSearch(String query) async {
    final results = await ProductService.search(query);
    
    setState(() => _results = results);
    
    Zixflow.instance.track('Search Performed', {
      'query': query,
      'results_count': results.length,
      'search_type': 'full_text',  // vs 'barcode_scan', 'voice', 'visual'
      'filters_applied': _activeFilters,
      'sort_by': _sortOrder,
    });
    
    // Track zero-results searches (product gap opportunity)
    if (results.isEmpty) {
      Zixflow.instance.track('Search No Results', {
        'query': query,
        'suggestions_shown': _suggestionEngine.getSuggestions(query),
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _searchController,
      onSubmitted: _performSearch,
      decoration: InputDecoration(hintText: 'Search products...'),
    );
  }
}

Content Shared

When: User shares via native share sheet
void _shareProduct() async {
  Zixflow.instance.track('Content Shared', {
    'content_type': 'product',
    'product_id': _product.id,
    'product_name': _product.name,
    'share_method': 'native_sheet',  // vs 'copy_link', 'qr_code'
  });
  
  final result = await Share.share(
    'Check out ${_product.name}!\n${_product.url}',
    subject: _product.name,
  );
  
  // Track share outcome
  if (result.status == ShareResultStatus.success) {
    Zixflow.instance.track('Content Shared Success', {
      'product_id': _product.id,
      'platform': result.raw,  // 'WhatsApp', 'Messages', 'Twitter', etc.
    });
  }
}
Analytics: Viral coefficient, most-shared products

Push Notification Events

Push Permission Requested

When: App asks for notification permission
void _requestPushPermission() async {
  Zixflow.instance.track('Push Permission Requested', {
    'prompt_type': 'soft_ask',  // Pre-permission priming
    'days_since_install': _daysSinceInstall,
    'screen': 'onboarding_step_3',
  });
  
  final granted = await _showPermissionDialog();
  
  if (granted) {
    _requestSystemPermission();
  } else {
    Zixflow.instance.track('Push Permission Soft Declined', {
      'reason': _userDeclineReason,  // From survey
    });
  }
}

Push Permission Granted/Denied

When: User responds to system permission prompt
void _requestSystemPermission() async {
  final settings = await FirebaseMessaging.instance.requestPermission(
    alert: true,
    badge: true,
    sound: true,
  );
  
  if (settings.authorizationStatus == AuthorizationStatus.authorized) {
    // Permission granted
    Zixflow.instance.track('Push Permission Granted', {
      'prompt_number': _promptAttemptCount,
      'days_since_install': _daysSinceInstall,
    });
    
    // Get device token and register
    final token = await FirebaseMessaging.instance.getToken();
    Zixflow.instance.registerDeviceToken(token!);
    
    // Update profile
    Zixflow.instance.setProfileAttributes({
      'push_enabled': true,
      'push_enabled_at': DateTime.now().toIso8601String(),
    });
    
    // Note: This call works correctly whether the user is identified or still anonymous.
    // If the user hasn't signed in yet, the device token is stored under their anonymousId
    // and automatically linked to their user profile when they call identify() at sign-up.
    // No extra re-registration step is needed after sign-up.
    
  } else {
    // Permission denied
    Zixflow.instance.track('Push Permission Denied', {
      'prompt_number': _promptAttemptCount,
      'days_since_install': _daysSinceInstall,
      'status': settings.authorizationStatus.toString(),
    });
  }
}
Analytics: Permission grant rate, optimal timing for prompt

Push Received

When: Push notification arrives (app in foreground or background)
// Configure FCM listener
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  // App in foreground
  Zixflow.instance.track('Push Received', {
    'campaign_id': message.data['campaign_id'],
    'message_id': message.messageId,
    'title': message.notification?.title,
    'app_state': 'foreground',
  });
  
  // Show in-app notification (optional)
  _showInAppNotification(message);
});

Push Opened

When: User taps notification (SDK auto-tracks) Auto-tracked:
{
  "event": "Push Opened",
  "properties": {
    "campaign_id": "camp_spring_sale",
    "message_id": "msg_abc-123",
    "title": "Your cart is waiting!",
    "deep_link": "app://cart",
    "time_to_open_seconds": 120,  // Time since delivery
    "app_state": "background"  // App was backgrounded when tapped
  }
}
Custom handling:
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
  Zixflow.instance.track('Push Opened', {
    'campaign_id': message.data['campaign_id'],
    'deep_link': message.data['deep_link'],
  });
  
  // Navigate to deep link destination
  _navigateToDeepLink(message.data['deep_link']);
});

Offline Behavior

Event Queueing

SDK handles offline automatically:
  1. Network unavailable: Events persisted to SQLite
  2. Battery optimization: Batch 20 events per network call
  3. Retry logic: Exponential backoff (1s, 2s, 4s, 8s, 16s)
  4. Max retries: 10 attempts, then discard (prevents infinite queue growth)
Developer control:
// Force immediate flush (critical events)
Zixflow.instance.track('Purchased', {...});
Zixflow.instance.flush();  // Send immediately, don't batch

// Check network status
final networkAvailable = await Connectivity().checkConnectivity();
if (networkAvailable == ConnectivityResult.none) {
  // Show "Offline mode" indicator
  _showOfflineBanner();
}

Offline Analytics

Track offline sessions separately:
class ConnectivityService {
  bool _isOnline = true;
  DateTime? _offlineSince;
  
  void _onConnectivityChange(ConnectivityResult result) {
    final wasOffline = !_isOnline;
    _isOnline = result != ConnectivityResult.none;
    
    if (_isOnline && wasOffline) {
      // Back online
      final offlineDuration = DateTime.now().difference(_offlineSince!);
      
      Zixflow.instance.track('Back Online', {
        'offline_duration_seconds': offlineDuration.inSeconds,
        'events_queued': Zixflow.instance.getQueueSize(),
      });
      
      // Flush queued events
      Zixflow.instance.flush();
      
    } else if (!_isOnline) {
      // Went offline
      _offlineSince = DateTime.now();
      Zixflow.instance.track('Went Offline', {
        'network_type': result.toString(),
        'screen': _currentScreen,
      });
    }
  }
}
Analytics: Offline usage patterns, feature availability gaps

Performance Monitoring

Screen Load Time

class ProductDetailScreen extends StatefulWidget {
  @override
  _ProductDetailScreenState createState() => _ProductDetailScreenState();
}

class _ProductDetailScreenState extends State<ProductDetailScreen> {
  DateTime? _loadStartTime;
  
  @override
  void initState() {
    super.initState();
    _loadStartTime = DateTime.now();
    _loadProduct();
  }
  
  Future<void> _loadProduct() async {
    final product = await ProductService.getProduct(widget.productId);
    
    final loadTime = DateTime.now().difference(_loadStartTime!);
    
    // Track performance
    Zixflow.instance.track('Screen Loaded', {
      'screen_name': 'Product Detail',
      'product_id': widget.productId,
      'load_time_ms': loadTime.inMilliseconds,
      'data_source': product.cachedLocally ? 'cache' : 'api',
      'image_count': product.images.length,
    });
    
    setState(() => _product = product);
  }
}

API Call Performance

class ApiClient {
  Future<Response> get(String endpoint) async {
    final startTime = DateTime.now();
    
    try {
      final response = await http.get(Uri.parse(endpoint));
      final duration = DateTime.now().difference(startTime);
      
      Zixflow.instance.track('API Call Completed', {
        'endpoint': endpoint,
        'status_code': response.statusCode,
        'duration_ms': duration.inMilliseconds,
        'response_size_bytes': response.body.length,
        'success': response.statusCode \< 400,
      });
      
      return response;
      
    } catch (e) {
      final duration = DateTime.now().difference(startTime);
      
      Zixflow.instance.track('API Call Failed', {
        'endpoint': endpoint,
        'duration_ms': duration.inMilliseconds,
        'error_type': e.runtimeType.toString(),
        'error_message': e.toString(),
      });
      
      rethrow;
    }
  }
}
Analytics: P50/P95/P99 latency by endpoint, error rates

Implementation Checklist

Phase 1: Core Tracking (Week 1)

  • SDK integration (initialization, config)
  • App Installed, Opened, Backgrounded
  • Screen tracking (auto or manual)
  • User identification on signup/login
  • Device token registration

Phase 2: Feature Instrumentation (Week 2)

  • Feature Used events (top 5 features)
  • Search Performed
  • Content Shared
  • Purchase flow events (Product Viewed, Added to Cart, Purchased)
  • Profile attribute updates (plan type, feature usage)

Phase 3: Push Notifications (Week 3)

  • Permission request flow (soft ask + system prompt)
  • Push Received, Opened tracking
  • Deep link handling
  • Campaign: Onboarding push sequence
  • Campaign: Cart abandonment push

Phase 4: Optimization (Week 4)

  • Performance monitoring (screen load, API calls)
  • Offline behavior handling
  • Event batching tuning
  • Retention analysis (DAU/MAU, cohorts)
  • Push notification A/B testing

Next: SaaS product tracking patterns → SaaS Product Tracking