Question #93MediumMonitoring & Analytics

How to monitor the app state like running in background or forground ?

#state#lifecycle#background#foreground#app-state#monitoring

Answer

Overview

Monitoring app lifecycle states (background/foreground) is crucial for managing resources, saving user progress, pausing/resuming tasks, and optimizing battery consumption. Flutter provides multiple approaches to detect when your app transitions between these states.

App Lifecycle States

StateDescriptionWhen It Occurs
ResumedApp is visible and interactiveUser is actively using the app
InactiveApp is visible but not receiving inputIncoming call, system dialog, app switcher
PausedApp is not visible but runningApp moved to background
DetachedApp is still in memory but not visibleAbout to terminate or suspended
HiddenApp is hidden from viewiOS specific - app in background

Method 1: WidgetsBindingObserver (Recommended)

Basic Implementation

dart
import 'package:flutter/material.dart';

class AppLifecycleObserver extends StatefulWidget {
  final Widget child;
  
  const AppLifecycleObserver({required this.child});
  
  
  _AppLifecycleObserverState createState() => _AppLifecycleObserverState();
}

class _AppLifecycleObserverState extends State<AppLifecycleObserver>
    with WidgetsBindingObserver {
  AppLifecycleState? _currentState;
  
  
  void initState() {
    super.initState();
    // Register observer
    WidgetsBinding.instance.addObserver(this);
    _currentState = WidgetsBinding.instance.lifecycleState;
  }
  
  
  void dispose() {
    // Unregister observer
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }
  
  
  void didChangeAppLifecycleState(AppLifecycleState state) {
    setState(() {
      _currentState = state;
    });
    
    switch (state) {
      case AppLifecycleState.resumed:
        print('App is in foreground');
        _onAppResumed();
        break;
      case AppLifecycleState.inactive:
        print('App is inactive');
        _onAppInactive();
        break;
      case AppLifecycleState.paused:
        print('App is in background');
        _onAppPaused();
        break;
      case AppLifecycleState.detached:
        print('App is detached');
        _onAppDetached();
        break;
      case AppLifecycleState.hidden:
        print('App is hidden');
        break;
    }
  }
  
  void _onAppResumed() {
    // Resume video playback
    // Refresh data
    // Resume animations
  }
  
  void _onAppInactive() {
    // Pause ongoing operations
  }
  
  void _onAppPaused() {
    // Save user progress
    // Pause music/video
    // Cancel network requests
    // Stop location updates
  }
  
  void _onAppDetached() {
    // Clean up resources
  }
  
  
  Widget build(BuildContext context) {
    return widget.child;
  }
}

// Usage in main app
void main() {
  runApp(
    AppLifecycleObserver(
      child: MyApp(),
    ),
  );
}

Advanced Example with Use Cases

dart
class VideoPlayerScreen extends StatefulWidget {
  
  _VideoPlayerScreenState createState() => _VideoPlayerScreenState();
}

class _VideoPlayerScreenState extends State<VideoPlayerScreen>
    with WidgetsBindingObserver {
  VideoPlayerController? _controller;
  bool _isPlaying = false;
  
  
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _initializePlayer();
  }
  
  
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _controller?.dispose();
    super.dispose();
  }
  
  
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused) {
      // App moved to background - pause video
      if (_isPlaying) {
        _controller?.pause();
        _isPlaying = false;
      }
    } else if (state == AppLifecycleState.resumed) {
      // App returned to foreground - resume if was playing
      if (!_isPlaying && _controller != null) {
        _controller?.play();
        _isPlaying = true;
      }
    }
  }
  
  void _initializePlayer() {
    _controller = VideoPlayerController.network(
      'https://example.com/video.mp4
    )..initialize().then((_) {
      setState(() {});
      _controller?.play();
      _isPlaying = true;
    });
  }
  
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: _controller != null && _controller!.value.isInitialized
          ? VideoPlayer(_controller!)
          : CircularProgressIndicator(),
    );
  }
}

Method 2: AppLifecycleListener (Flutter 3.13+)

dart
import 'package:flutter/widgets.dart';

class ModernLifecycleObserver extends StatefulWidget {
  
  State<ModernLifecycleObserver> createState() =>
      _ModernLifecycleObserverState();
}

class _ModernLifecycleObserverState extends State<ModernLifecycleObserver> {
  late final AppLifecycleListener _listener;
  
  
  void initState() {
    super.initState();
    
    _listener = AppLifecycleListener(
      onStateChange: _onStateChanged,
      onResume: _onResume,
      onInactive: _onInactive,
      onHide: _onHide,
      onShow: _onShow,
      onPause: _onPause,
      onRestart: _onRestart,
      onDetach: _onDetach,
    );
  }
  
  
  void dispose() {
    _listener.dispose();
    super.dispose();
  }
  
  void _onStateChanged(AppLifecycleState state) {
    print('State changed to: $state');
  }
  
  void _onResume() {
    print('App resumed (foreground)');
  }
  
  void _onInactive() {
    print('App inactive');
  }
  
  void _onHide() {
    print('App hidden');
  }
  
  void _onShow() {
    print('App shown');
  }
  
  void _onPause() {
    print('App paused (background)');
  }
  
  void _onRestart() {
    print('App restarted');
  }
  
  void _onDetach() {
    print('App detached');
  }
  
  
  Widget build(BuildContext context) {
    return Container();
  }
}

Method 3: Using flutter_fgbg Package

Installation

yaml
dependencies:
  flutter_fgbg: ^0.3.0

Implementation

dart
import 'package:flutter_fgbg/flutter_fgbg.dart';

class FGBGExample extends StatefulWidget {
  
  _FGBGExampleState createState() => _FGBGExampleState();
}

class _FGBGExampleState extends State<FGBGExample> {
  StreamSubscription? _subscription;
  String _state = 'Unknown';
  
  
  void initState() {
    super.initState();
    
    _subscription = FGBGEvents.stream.listen((event) {
      setState(() {
        _state = event == FGBGType.foreground ? 'Foreground' : 'Background';
      });
      
      if (event == FGBGType.foreground) {
        print('App came to foreground');
        _handleForeground();
      } else {
        print('App went to background');
        _handleBackground();
      }
    });
  }
  
  
  void dispose() {
    _subscription?.cancel();
    super.dispose();
  }
  
  void _handleForeground() {
    // Refresh data
    // Resume operations
  }
  
  void _handleBackground() {
    // Save state
    // Pause operations
  }
  
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('App State: $_state')),
      body: Center(child: Text('Current state: $_state')),
    );
  }
}

Practical Use Cases

1. Auto-Save User Progress

dart
class AutoSaveManager with WidgetsBindingObserver {
  final UserDataService _dataService = UserDataService();
  
  void initialize() {
    WidgetsBinding.instance.addObserver(this);
  }
  
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
  }
  
  
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused) {
      // Save user data when app goes to background
      _saveUserProgress();
    }
  }
  
  Future<void> _saveUserProgress() async {
    try {
      await _dataService.saveProgress();
      print('User progress saved');
    } catch (e) {
      print('Failed to save progress: $e');
    }
  }
}

2. Pause/Resume Audio

dart
class AudioManager with WidgetsBindingObserver {
  final AudioPlayer _audioPlayer = AudioPlayer();
  bool _wasPlaying = false;
  
  
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused) {
      _wasPlaying = _audioPlayer.playing;
      if (_wasPlaying) {
        _audioPlayer.pause();
      }
    } else if (state == AppLifecycleState.resumed) {
      if (_wasPlaying) {
        _audioPlayer.play();
      }
    }
  }
}

3. Stop Location Tracking

dart
class LocationManager with WidgetsBindingObserver {
  StreamSubscription<Position>? _positionStream;
  
  
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused) {
      // Stop location updates to save battery
      _positionStream?.pause();
    } else if (state == AppLifecycleState.resumed) {
      // Resume location updates
      _positionStream?.resume();
    }
  }
}

4. Refresh Data on Foreground

dart
class DataRefreshManager with WidgetsBindingObserver {
  final ApiService _apiService = ApiService();
  DateTime? _lastRefresh;
  
  
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      _refreshDataIfNeeded();
    }
  }
  
  Future<void> _refreshDataIfNeeded() async {
    final now = DateTime.now();
    
    // Refresh if more than 5 minutes since last refresh
    if (_lastRefresh == null ||
        now.difference(_lastRefresh!).inMinutes > 5) {
      await _apiService.fetchLatestData();
      _lastRefresh = now;
      print('Data refreshed');
    }
  }
}

5. Analytics Tracking

dart
import 'package:firebase_analytics/firebase_analytics.dart';

class AnalyticsManager with WidgetsBindingObserver {
  final FirebaseAnalytics _analytics = FirebaseAnalytics.instance;
  DateTime? _backgroundTime;
  
  
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused) {
      _backgroundTime = DateTime.now();
      _analytics.logEvent(
        name: 'app_backgrounded',
        parameters: {'timestamp': _backgroundTime.toString()},
      );
    } else if (state == AppLifecycleState.resumed) {
      if (_backgroundTime != null) {
        final duration = DateTime.now().difference(_backgroundTime!);
        _analytics.logEvent(
          name: 'app_resumed',
          parameters: {
            'background_duration_seconds': duration.inSeconds,
          },
        );
      }
    }
  }
}

Comparison of Methods

MethodProsConsBest For
WidgetsBindingObserverBuilt-in, no dependenciesManual registration/disposalMost apps
AppLifecycleListenerModern API, cleaner codeRequires Flutter 3.13+New projects
flutter_fgbgSimple foreground/background onlyExternal dependencySimple use cases

Best Practices

Resource Management:

  • Pause heavy operations when app goes to background
  • Stop timers, animations, and network requests
  • Cancel location tracking to save battery
  • Clear sensitive data from memory

User Experience:

  • Save user progress before backgrounding
  • Resume seamlessly when returning to foreground
  • Refresh data if it might be stale
  • Show loading indicators during refresh

Performance:

  • Avoid unnecessary work in background
  • Use background tasks for long-running operations
  • Implement debouncing for state changes
  • Clean up resources in dispose()

Testing:

  • Test all lifecycle transitions
  • Verify state persistence
  • Check memory leaks with DevTools
  • Test on both iOS and Android

Common Pitfalls

Don't forget to unregister:

dart
// ❌ BAD: Memory leak

void initState() {
  super.initState();
  WidgetsBinding.instance.addObserver(this);
}
// Forgot dispose!

// ✅ GOOD

void dispose() {
  WidgetsBinding.instance.removeObserver(this);
  super.dispose();
}

Handle null states:

dart
// ❌ BAD
void didChangeAppLifecycleState(AppLifecycleState state) {
  _controller.pause(); // Might be null!
}

// ✅ GOOD
void didChangeAppLifecycleState(AppLifecycleState state) {
  _controller?.pause();
}

Learning Resources

Pro Tip: Always test lifecycle transitions thoroughly, especially on iOS where apps can be terminated more aggressively than on Android. Use background tasks for operations that must complete even when the app is backgrounded, and always clean up resources properly to avoid memory leaks.