#flutter#analytics#mobile#data science

Properly Implement Screenview and App Close Events in Flutter Apps

Sajith Lal M K
Sajith Lal M K
May 5, 202410 min read

In mobile app development, tracking user interactions is crucial for understanding user behavior and improving the app experience. Two fundamental events to track are screen views (when users navigate to different screens) and app close events (when users exit or minimize the app). This post will guide you through implementing these events in Flutter applications.

Why Track These Events?

  • Screen Views: Help understand user navigation patterns and identify popular or problematic screens
  • App Close Events: Provide insights into user exit points and session durations
  • Implementation Overview

    We'll cover five main components:

    1. Setting up an analytics service

    2. Creating a custom navigator observer for screen tracking

    3. Implementing app lifecycle handling for app close events

    4. Handling custom screens like bottom sheets and dialogs

    5. Preventing unnecessary screen view events

    Let's dive in!

    1. Setting Up the Analytics Service

    First, create a service class to handle analytics events. This example uses Firebase Analytics, but the concepts apply to any analytics provider.

    import 'package:firebase_analytics/firebase_analytics.dart';
    import 'package:flutter/material.dart';
    
    class AnalyticsService {
      AnalyticsService._();  // Private constructor for singleton pattern
    
      static final instance = AnalyticsService._();
    
      final _analytics = FirebaseAnalytics.instance;
    
      // Log custom events
      void logEvent(String name, Map<String, Object> parameters) {
        _analytics
            .logEvent(name: name, parameters: parameters)
            .then((value) => debugPrint('Event logged: $name'))
            .catchError((dynamic e) => debugPrint('Error logging event: $e'));
      }
    
      // Log screen view events
      void logScreenView(String screenName) {
        _analytics.logScreenView(
          screenName: screenName,
        );
      }
    }

    2. Creating a Custom Navigator Observer for Screen Tracking

    Flutter's NavigatorObserver allows you to listen for navigation events. By extending it, you can automatically track screen views:

    import 'package:firebase_analytics/firebase_analytics.dart';
    import 'package:flutter/material.dart';
    
    /// A custom implementation of NavigatorObserver that handles screen view tracking
    class CustomAnalyticsObserver extends NavigatorObserver {
      CustomAnalyticsObserver({
        required this.analytics,
        this.nameExtractor,
        this.routeFilter,
      });
    
      final FirebaseAnalytics analytics;
      final RouteFilter? routeFilter;
      final NameExtractor? nameExtractor;
    
      // Keep track of the current tab name to avoid duplicate events
      String? _currentTabName;
    
      /// Log a screen_view event when a new route is pushed
      @override
      void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
        super.didPush(route, previousRoute);
        _sendScreenView(route);
      }
    
      /// Log a screen_view event when a route is replaced
      @override
      void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
        super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
        if (newRoute != null) {
          _sendScreenView(newRoute);
        }
      }
    
      /// Log a screen_view event when returning to a previous route
      @override
      void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
        super.didPop(route, previousRoute);
        if (previousRoute != null) {
          _sendScreenView(previousRoute);
        }
      }
    
      /// Handles the screen view tracking
      void _sendScreenView(Route<dynamic> route) {
        if (routeFilter != null && !routeFilter!(route)) return;
    
        final screenName =
            nameExtractor != null ? nameExtractor!(route) : route.settings.name;
    
        if (screenName != null) {
          // Check if screen view events should be paused
          if (pauseScreenView) {
            pauseScreenView = false;
            return;
          }
          analytics.logScreenView(screenName: screenName);
        }
      }
    
      /// Track bottom navigation tab changes
      void trackTabChange(String tabName) {
        // Avoid logging duplicate events for the same tab
        if (_currentTabName == tabName) return;
    
        _currentTabName = tabName;
        analytics.logScreenView(screenName: tabName);
      }
    }
    
    /// Signature for a function that extracts a screen name from a Route
    typedef NameExtractor = String? Function(Route<dynamic> route);
    
    /// Signature for a function that determines whether a Route should be tracked
    typedef RouteFilter = bool Function(Route<dynamic> route);

    3. Implementing App Lifecycle Handling for App Close Events

    Flutter provides the AppLifecycleState enum to track the app's lifecycle. You can use this to detect when the app is being closed or minimized:

    import 'dart:developer';
    import 'package:flutter/material.dart';
    
    class MyApp extends StatefulWidget {
      @override
      _MyAppState createState() => _MyAppState();
    }
    
    class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
      @override
      void initState() {
        super.initState();
        WidgetsBinding.instance.addObserver(this);
      }
    
      @override
      void dispose() {
        WidgetsBinding.instance.removeObserver(this);
        super.dispose();
      }
    
      Future<String?> getCurrentScreen() async {
        String? screenName;
        //implement fetching current screen name from local storage
        return screenName;
      }
    
      @override
      void didChangeAppLifecycleState(AppLifecycleState state) async {
        // App is minimized or closed
        if (state == AppLifecycleState.inactive || 
            state == AppLifecycleState.paused) {
          final screenName = await getCurrentScreen();
          log('App lifecycle changed: $screenName, state: $state');
    
          if (screenName != null) {
            AnalyticsService.instance.logEvent(
              'app_close',
              {
                'screen_name': screenName,
                'type': 'user_minimised',
              },
            );
          }
        }
    
        // App is resumed from background
        if (state == AppLifecycleState.resumed) {
          final screenName = await getCurrentScreen();
          log('App resumed: $screenName');
    
          if (screenName != null) {
            AnalyticsService.instance.logScreenView(screenName);
          }
        }
      }
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          // Your app configuration
        );
      }
    }

    4. Handling Custom Screens Like Bottom Sheets and Dialogs

    Bottom sheets, dialogs, and other modal interfaces aren't automatically tracked by the navigator observer since they don't always create new routes. A robust approach is to store the current screen name in local storage:

    GestureDetector(
      onTap: () {
        // Save the current screen name before showing the bottom sheet
        LocalStoreService.instance.saveUserPref(
          'current_screen',
          ScreenNames.filterScreen,
        );
    
        showModalBottomSheet(
          isScrollControlled: true,
          useSafeArea: true,
          context: context,
          useRootNavigator: true,
          routeSettings: const RouteSettings(
            name: FilterBottomSheet.routeName,
          ),
          builder: (_) => FilterBottomSheet(
            searchId: searchId,
          ),
        ).then((_) {
          // When bottom sheet is closed, restore the previous screen
          LocalStoreService.instance.saveUserPref(
            'current_screen',
            ScreenNames.resultScreen,
          );
        });
      },
      child: YourButtonWidget(),
    )

    5. Preventing Unnecessary Screen View Events

    In complex navigation scenarios, you might encounter situations where multiple screen view events are triggered unnecessarily. To prevent this, implement a flag to temporarily pause screen view tracking:

    // Define a global flag to control screen view tracking
    bool pauseScreenView = false;
    
    // In your custom analytics observer
    void _sendScreenView(Route<dynamic> route) {
      if (screenName != null) {
        // Check if screen view events should be paused
        if (pauseScreenView) {
          pauseScreenView = false;
          return;
        }
        analytics.logScreenView(screenName: screenName);
      }
    }

    Best Practices

    1. Define Constants: Create a constants file for event names and screen names

    2. Debug Logging: Use log statements to debug your analytics implementation

    3. Avoid Duplicate Events: Implement logic to prevent duplicate screen view events

    4. Handle Deep Links: Make sure your analytics tracking works correctly with deep links

    5. Test Thoroughly: Verify events using Firebase Analytics DebugView

    Conclusion

    Properly implementing screen view and app close events in Flutter provides valuable insights into user behavior. By following this guide, you can set up a robust analytics system that tracks these important events.

    Remember that the implementation may vary slightly depending on your navigation solution (Navigator 1.0, Navigator 2.0, GoRouter, etc.), but the core principles remain the same.

    Happy coding! ❤️ from Sajith Lal

    Sajith Lal M K
    Sajith Lal M K

    Senior Flutter Developer with 5+ years of experience building high-performance cross-platform applications. Specialized in Flutter, Dart, Firebase & clean architecture.