Ask me anything
Ithy Logo
Sign in to save

Implementing a User Activity Detector in Flutter

Azure AD – You can now enable suspicious activities reporting (preview)

Overview

Implementing a user activity detector in Flutter is essential for applications that require session management, ensuring security by logging out users after a period of inactivity. The provided UserActivityDetector widget serves this purpose by monitoring user interactions and managing the logout process accordingly.

Key Components of the Implementation

1. UserActivityDetector Widget

The UserActivityDetector is a StatefulWidget that wraps around child widgets to monitor user activity. It leverages Flutter's Listener widget to detect pointer events, which are indicative of user interactions.

2. InactivityTimer Class

The InactivityTimer class manages the inactivity timeout logic. It initializes a timer that counts down from a specified duration (e.g., 10 minutes). If the timer expires without being reset (indicating inactivity), it triggers the logout process.

3. Logout Mechanism

The logout mechanism involves removing user information, dismissing any loading indicators, and navigating the user back to the login screen. This ensures that inactive sessions do not pose a security risk.

Detailed Breakdown of the Implementation

A. UserActivityDetector Widget


class UserActivityDetector extends StatefulWidget {
  final Widget child;

  const UserActivityDetector({required this.child, super.key});

  @override
  State<StatefulWidget> createState() => _UserActivityDetectorState();
}
  

This widget accepts a child widget and monitors user interactions within its subtree.

B. _UserActivityDetectorState Class


class _UserActivityDetectorState extends State<UserActivityDetector> {
  late BuildContext _buildContext;

  @override
  void initState() {
    super.initState();
    // Initialize the inactivity timer with a callback to logout
    InactivityTimer().init(() {
      // Handle logout logic
      EasyLoading.show();
      _logout();
    });
  }

  @override
  Widget build(BuildContext context) {
    _buildContext = context;
    return Listener(
      behavior: HitTestBehavior.translucent,
      onPointerDown: (_) {
        debugPrint("UserActivityDetector onPointerDown");
        InactivityTimer().resetTimer();
      },
      /*onPointerMove: (_) {
        print("UserActivityDetector onPointerMove");
        InactivityTimer().resetTimer();
      },*/
      child: widget.child,
    );
  }

  @override
  void dispose() {
    InactivityTimer().dispose();
    super.dispose();
  }

  void _logout() async {
    var isSuccess = await UserManager.removeUserInfo();
    EasyLoading.dismiss();
    if (isSuccess) {
      var user = UserManager.getUser();
      debugPrint("UserActivityDetector cleanUserInfo() after user = $user");
      InactivityTimer().resetTimer();
      NavigatorUtils.push(_buildContext, CommonRouter.aJLoginpage, clearStack: true);
    }
  }
}
  

The _UserActivityDetectorState manages the lifecycle of the inactivity timer. Upon initialization (initState()), it sets up the timer with a callback to handle user logout. The Listener widget detects pointer events such as touches or clicks, resetting the timer to prevent unintended logouts during active use.

C. InactivityTimer Class Implementation


import 'dart:async';

class InactivityTimer {
  static Timer? _timer;

  static void init(VoidCallback callback) {
    _timer?.cancel();
    _timer = Timer(Duration(minutes: 10), callback);
  }

  static void resetTimer() {
    _timer?.cancel();
    _timer = Timer(Duration(minutes: 10), () {
      _logout();
    });
  }

  static void dispose() {
    _timer?.cancel();
  }

  static void logout() async {
    var isSuccess = await UserManager.removeUserInfo();
    EasyLoading.dismiss();
    if (isSuccess) {
      var user = UserManager.getUser();
      debugPrint('UserActivityDetector cleanUserInfo() after user = $user');
      NavigatorUtils.push(buildContext, CommonRouter.aJLoginpage, clearStack: true);
    }
  }
}
  

The InactivityTimer class uses Dart's Timer from the dart:async library to manage the inactivity countdown. It provides methods to initialize, reset, and dispose of the timer. Upon timer expiration, it invokes the logout process.

Strengths of the Implementation

  • Clear Purpose: The widget's intent to monitor user inactivity and handle logout is straightforward and easily understandable.
  • Use of Listener: Leveraging the Listener widget with HitTestBehavior.translucent ensures that pointer events are captured even if they occur on underlying widgets.
  • Proper Lifecycle Management: Initialization and disposal of the inactivity timer within the initState() and dispose() methods prevent memory leaks and ensure resources are appropriately managed.
  • Separation of Concerns: The separation between the UI component (UserActivityDetector) and the timer logic (InactivityTimer) adheres to good software design principles.
  • Error Handling: The logout method checks for the success of user information removal, providing a layer of reliability.

Areas for Improvement

1. Definition and Implementation of Supporting Classes

Classes such as InactivityTimer, UserManager, and NavigatorUtils are referenced but not defined within the provided code. Providing implementations or ensuring these classes are well-documented is crucial for understanding and maintaining the codebase.

2. Enhanced Error Handling

While the _logout() function checks if the user information removal was successful, it lacks comprehensive error handling. Implementing try-catch blocks can help manage unexpected exceptions and provide more robust error management.

3. Configurable Timeout Duration

The inactivity timeout duration is hardcoded (e.g., 10 minutes). Making this duration configurable allows for greater flexibility and adaptability based on different requirements or user preferences.

4. Use of Named Routes for Navigation

Instead of hardcoding the route CommonRouter.aJLoginpage, utilizing named routes (e.g., Navigator.pushReplacementNamed(context, '/login');) enhances maintainability and readability. It also aligns with Flutter's recommended navigation practices.

5. Dependency Management for External Libraries

The code uses external libraries such as EasyLoading. Ensuring these dependencies are correctly listed in the pubspec.yaml file and properly initialized within the application is essential to prevent runtime issues.

6. Redundancy in Pointer Event Handlers

The onPointerMove handler is commented out, suggesting potential redundancy or uncertainty about its necessity. Evaluating whether continuous pointer movements should reset the timer is important to balance user experience and functionality.

Suggested Revised Implementation

A. Enhanced InactivityTimer Class


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

class InactivityTimer {
  static Timer? _timer;
  static final Duration timeoutDuration = Duration(minutes: 10);
  static BuildContext? _context;

  static void init(BuildContext context, VoidCallback callback) {
    _context = context;
    _timer?.cancel();
    _timer = Timer(timeoutDuration, callback);
  }

  static void resetTimer() {
    _timer?.cancel();
    _timer = Timer(timeoutDuration, () {
      _logout();
    });
  }

  static void dispose() {
    _timer?.cancel();
  }

  static void _logout() async {
    try {
      // Show loading indicator
      EasyLoading.show(status: 'Logging out...');
      
      // Remove user information
      bool isSuccess = await UserManager.removeUserInfo();
      
      // Dismiss loading indicator
      EasyLoading.dismiss();
      
      if (isSuccess) {
        debugPrint('UserActivityDetector cleanUserInfo() after user = ${UserManager.getUser()}');
        
        // Navigate to login page using named route
        Navigator.pushReplacementNamed(_context!, '/login');
      } else {
        // Handle unsuccessful logout attempt
        EasyLoading.showError('Logout failed. Please try again.');
      }
    } catch (e) {
      // Handle exceptions during logout
      EasyLoading.showError('An error occurred during logout.');
      debugPrint('Logout error: $e');
    }
  }
}
  

The revised InactivityTimer class introduces the following improvements:

  • Configurable Timeout: The timeout duration is defined as a static constant, making it easier to adjust as needed.
  • Context Management: Accepts BuildContext during initialization to facilitate navigation within the logout process.
  • Comprehensive Error Handling: Implements try-catch blocks to manage potential exceptions during the logout process.
  • Use of Named Routes: Utilizes named routes for navigation, enhancing maintainability.

B. Updated UserActivityDetector Widget


class UserActivityDetector extends StatefulWidget {
  final Widget child;

  const UserActivityDetector({required this.child, super.key});

  @override
  State<StatefulWidget> createState() => _UserActivityDetectorState();
}

class _UserActivityDetectorState extends State<UserActivityDetector> {
  @override
  void initState() {
    super.initState();
    // Initialize the inactivity timer with the current context
    InactivityTimer.init(context, _handleLogout);
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      behavior: HitTestBehavior.translucent,
      onPointerDown: (_) {
        debugPrint("UserActivityDetector onPointerDown");
        InactivityTimer.resetTimer();
      },
      onPointerMove: (_) {
        debugPrint("UserActivityDetector onPointerMove");
        InactivityTimer.resetTimer();
      },
      child: widget.child,
    );
  }

  @override
  void dispose() {
    InactivityTimer.dispose();
    super.dispose();
  }

  void _handleLogout() {
    // The actual logout process is handled within the InactivityTimer
  }
}
  

Key enhancements in the UserActivityDetector widget include:

  • Context Initialization: Passes the current BuildContext to the InactivityTimer during initialization.
  • Active Pointer Movement Handling: Uncommented and retained the onPointerMove handler to reset the timer on pointer movements, ensuring comprehensive activity tracking.
  • Centralized Logout Handling: Delegates the logout logic to the InactivityTimer, keeping the widget focused on detecting interactions.

C. Managing External Dependencies

Ensure that external libraries such as EasyLoading are properly included and configured in your project. Add the following to your pubspec.yaml:


dependencies:
  flutter:
    sdk: flutter
  flutter_easyloading: ^3.0.0
  # Add other dependencies as needed
  

Initialize EasyLoading in your main.dart:


import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'user_activity_detector.dart'; // Import the UserActivityDetector widget

void main() {
  runApp(MyApp());
  
  // Configure EasyLoading
  configLoading();
}

void configLoading() {
  EasyLoading.instance
    ..loadingStyle = EasyLoadingStyle.light
    ..indicatorType = EasyLoadingIndicatorType.circle
    ..maskType = EasyLoadingMaskType.black
    ..dismissOnTap = false;
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Inactivity Detector',
      builder: EasyLoading.init(),
      routes: {
        '/login': (context) => LoginPage(),
        // Define other routes here
      },
      home: UserActivityDetector(
        child: HomePage(),
      ),
    );
  }
}
  

This setup ensures that EasyLoading is properly initialized and integrated within your application, providing visual feedback during the logout process.

Best Practices and Recommendations

1. Comprehensive Documentation

Documenting the purpose and functionality of each class and method enhances code readability and maintainability. Clear comments and documentation help future developers understand the implementation nuances.

2. Modular Code Structure

Organizing code into separate files and directories based on functionality promotes a cleaner codebase. For instance, placing the UserActivityDetector and InactivityTimer in dedicated folders or files can streamline development workflows.

3. Configurable Parameters

Allowing configurability for parameters such as timeout durations or routes enhances the flexibility of the implementation. This enables adjustments without modifying the core logic.

4. Robust Error Handling

Implementing thorough error handling mechanisms ensures that the application can gracefully handle unexpected scenarios, improving user experience and application stability.

5. Testing and Validation

Conducting unit and integration tests for the inactivity detection and logout functionalities ensures that the implementation behaves as expected under various conditions. Automated tests can catch regressions and validate enhancements.

Conclusion

The provided implementation of the UserActivityDetector widget effectively monitors user inactivity and manages the logout process in a Flutter application. By addressing the highlighted areas for improvement and adhering to best practices, the robustness and maintainability of the inactivity detection mechanism can be significantly enhanced.

Implementing such security measures not only safeguards user data but also aligns with industry standards for user session management. Continuous refinement and adherence to software development best practices will further ensure the reliability and efficiency of the application.


Last updated January 3, 2025

AI-assisted research. Sources, methodology and corrections · Source-checked guides and tools · Explore published answers

Ask Ithy AI