Chat
Search
Ithy Logo

Implementing User Inactivity Logout in Flutter

ux field - Is it a good idea to hide logout button with error/warning ...

Introduction

In many applications, especially those handling sensitive user data, it's crucial to implement a mechanism that detects user inactivity and automatically logs the user out after a specified period. This enhances security by preventing unauthorized access when the user is away from the device. In Flutter, creating a UserActivityDetector widget can effectively manage this functionality. This guide provides a comprehensive breakdown of implementing such a widget, addressing common issues like context-related navigation errors and offering best practices for robust and error-free code.

Understanding the UserActivityDetector Widget

Code Breakdown

The UserActivityDetector is a StatefulWidget that wraps around child widgets to monitor user interactions. If no interaction is detected within a specified timeout duration, it triggers a logout action. Below is the core structure of the widget:

UserActivityDetector Class

class UserActivityDetector extends StatefulWidget {
  final Widget child;

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

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

_UserActivityDetectorState Class

class _UserActivityDetectorState extends State {
  Timer? _timer;

  @override
  void initState() {
    super.initState();
    _startTimer();
  }

  void _startTimer() {
    _timer?.cancel();
    _timer = Timer(const Duration(minutes: 5), _logout);
  }

  void _resetTimer() {
    _startTimer();
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      behavior: HitTestBehavior.translucent,
      onPointerDown: (_) => _resetTimer(),
      onPointerMove: (_) => _resetTimer(),
      child: widget.child,
    );
  }

  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }

  void _logout() async {
    EasyLoading.show(status: 'Logging out...');
    bool isSuccess = await UserManager.removeUserInfo();
    EasyLoading.dismiss();
    if (isSuccess) {
      Navigator.of(context).pushNamedAndRemoveUntil(
        '/login', // Replace with your login route
        (route) => false,
      );
    }
  }
}

Functionality Overview

  • Timer Initialization: The timer starts in the initState method and triggers the _logout method after 5 minutes of inactivity.
  • User Interaction: The Listener widget detects user interactions such as taps or pointer movements and resets the timer accordingly.
  • Logout Mechanism: Upon timeout, the _logout method clears user data and navigates to the login page.

Common Issue: Navigator Context Error

Understanding the Error

When attempting to navigate using the BuildContext, you might encounter the following error:

"Navigator operation requested with a context that does not include a Navigator."

This error typically arises when the BuildContext used for navigation isn't associated with a Navigator widget. In the context of the UserActivityDetector, this can occur if the context captured during the widget's build phase isn't a descendant of a Navigator.

Why Does This Happen?

  • Context Misplacement: The context used for navigation might not be under a Navigator, especially if the UserActivityDetector is positioned outside the MaterialApp or CupertinoApp.
  • Storing BuildContext: Retaining a BuildContext in a state variable can lead to invalid references if the widget tree changes.
  • Asynchronous Operations: Delayed navigation calls might execute after the widget has been disposed, rendering the context invalid.

Solutions to Navigator Context Error

1. Use a Builder Widget

Utilize a Builder widget to obtain a context that is guaranteed to be under a Navigator. This ensures that navigation operations have access to the appropriate Navigator.

@override
Widget build(BuildContext context) {
  return Builder(
    builder: (BuildContext builderContext) {
      return Listener(
        behavior: HitTestBehavior.translucent,
        onPointerDown: (_) => _resetTimer(),
        onPointerMove: (_) => _resetTimer(),
        child: widget.child,
      );
    },
  );
}

2. Pass Context Dynamically

Instead of storing the BuildContext, pass it dynamically to the logout method. This approach ensures that the context used is always current and valid.

void _logout(BuildContext context) async {
  EasyLoading.show(status: 'Logging out...');
  bool isSuccess = await UserManager.removeUserInfo();
  EasyLoading.dismiss();
  if (isSuccess) {
    Navigator.of(context).pushNamedAndRemoveUntil(
      '/login',
      (route) => false,
    );
  }
}

Modify the timer callback to pass the context:

Timer(const Duration(minutes: 5), () {
  _logout(context);
});

3. Use a GlobalKey for Navigation

Implement a GlobalKey<NavigatorState> to manage navigation without relying on the widget's context. This method is especially useful for navigating outside the widget tree.

final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

MaterialApp(
  navigatorKey: navigatorKey,
  home: UserActivityDetector(
    child: MyHomePage(),
  ),
);

Use the key for navigation:

void _logout() async {
  EasyLoading.show(status: 'Logging out...');
  bool isSuccess = await UserManager.removeUserInfo();
  EasyLoading.dismiss();
  if (isSuccess) {
    navigatorKey.currentState?.pushNamedAndRemoveUntil(
      '/login',
      (route) => false,
    );
  }
}

4. Ensure Proper Placement in the Widget Tree

Verify that the UserActivityDetector is placed within a widget tree that includes a Navigator. Typically, wrapping it inside the MaterialApp ensures access to the Navigator.

void main() {
  runApp(
    MaterialApp(
      home: UserActivityDetector(
        child: MyHomePage(),
      ),
    ),
  );
}

Best Practices for Managing BuildContext in Flutter

1. Avoid Storing BuildContext

Never store a BuildContext in a state variable for later use. Instead, pass it directly to methods that require it to ensure the context remains valid.

2. Use Builders for Context-Sensitive Operations

When performing operations that depend on the widget tree's structure, such as navigation, encapsulate them within a Builder to obtain the correct context.

3. Utilize GlobalKeys for Global Navigation

For scenarios where navigation needs to occur outside the widget hierarchy, such as background services, GlobalKey<NavigatorState> provides a reliable reference to the Navigator.

4. Check Widget Lifecycle Before Navigation

Ensure that the widget is still mounted before performing navigation to prevent errors related to disposed contexts.

if (mounted) {
  Navigator.of(context).pushNamed('/login');
}

Complete Example Implementation

Corrected UserActivityDetector Widget

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

class UserActivityDetector extends StatefulWidget {
  final Widget child;
  final Duration timeoutDuration;
  final VoidCallback onTimeout;

  const UserActivityDetector({
    required this.child,
    required this.onTimeout,
    this.timeoutDuration = const Duration(minutes: 5),
    Key? key,
  }) : super(key: key);

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

class _UserActivityDetectorState extends State {
  Timer? _inactivityTimer;

  @override
  void initState() {
    super.initState();
    _startInactivityTimer();
  }

  void _startInactivityTimer() {
    _inactivityTimer?.cancel();
    _inactivityTimer = Timer(widget.timeoutDuration, widget.onTimeout);
  }

  void _resetInactivityTimer() {
    _startInactivityTimer();
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      behavior: HitTestBehavior.translucent,
      onPointerDown: (_) => _resetInactivityTimer(),
      onPointerMove: (_) => _resetInactivityTimer(),
      child: widget.child,
    );
  }

  @override
  void dispose() {
    _inactivityTimer?.cancel();
    super.dispose();
  }
}

Integrating into the App

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // Define a GlobalKey for Navigator
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: navigatorKey,
      builder: EasyLoading.init(),
      home: UserActivityDetector(
        timeoutDuration: Duration(minutes: 5),
        onTimeout: () => _logout(navigatorKey),
        child: MyHomePage(),
      ),
    );
  }

  void _logout(GlobalKey<NavigatorState> navigatorKey) async {
    EasyLoading.show(status: 'Logging out...');
    bool isSuccess = await UserManager.removeUserInfo();
    EasyLoading.dismiss();
    if (isSuccess) {
      navigatorKey.currentState?.pushNamedAndRemoveUntil(
        '/login',
        (route) => false,
      );
    }
  }
}

In this example:

  • A GlobalKey<NavigatorState> is used to manage navigation globally.
  • The UserActivityDetector is wrapped around the MyHomePage, ensuring that any user interaction resets the inactivity timer.
  • The onTimeout callback utilizes the navigatorKey to navigate to the login page upon timeout.

Additional Considerations

Handling App Lifecycle

To ensure that the inactivity timer behaves correctly during app lifecycle changes (e.g., when the app moves to the background), implement the WidgetsBindingObserver to listen for lifecycle events.

class _UserActivityDetectorState extends State
    with WidgetsBindingObserver {
  
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _startInactivityTimer();
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _timer?.cancel();
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      _resetInactivityTimer();
    } else if (state == AppLifecycleState.paused) {
      _timer?.cancel();
    }
  }
}

Using Additional Packages for Enhanced Detection

For more granular activity detection, consider integrating packages like idle_detector_wrapper or flutter_activity_recognition. These packages offer advanced features such as background activity monitoring and can enhance the reliability of inactivity detection.

For example, using idle_detector_wrapper:

IdleDetector(
  idleTime: const Duration(minutes: 5),
  onIdle: () => _logout(navigatorKey),
  child: Scaffold(
    appBar: AppBar(title: Text('Home')),
    body: Center(child: Text('Welcome to the app!')),
  ),
)

Conclusion

Implementing a UserActivityDetector in Flutter is an effective way to enhance application security by automatically logging out inactive users. Addressing common issues like Navigator context errors is essential for ensuring seamless navigation and user experience. By following best practices such as avoiding the storage of BuildContext, utilizing GlobalKey for navigation, and carefully managing widget lifecycle events, developers can create robust and reliable inactivity detection mechanisms in their Flutter applications.

References


Last updated January 3, 2025
Ask Ithy AI
Export Article
Delete Article