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.
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:
class UserActivityDetector extends StatefulWidget {
final Widget child;
const UserActivityDetector({required this.child, super.key});
@override
State createState() => _UserActivityDetectorState();
}
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,
);
}
}
}
initState
method and triggers the _logout
method after 5 minutes of inactivity.Listener
widget detects user interactions such as taps or pointer movements and resets the timer accordingly._logout
method clears user data and navigates to the login page.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
.
Navigator
, especially if the UserActivityDetector
is positioned outside the MaterialApp
or CupertinoApp
.BuildContext
in a state variable can lead to invalid references if the widget tree changes.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,
);
},
);
}
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);
});
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,
);
}
}
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(),
),
),
);
}
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.
When performing operations that depend on the widget tree's structure, such as navigation, encapsulate them within a Builder
to obtain the correct context.
For scenarios where navigation needs to occur outside the widget hierarchy, such as background services, GlobalKey<NavigatorState>
provides a reliable reference to the Navigator
.
Ensure that the widget is still mounted before performing navigation to prevent errors related to disposed contexts.
if (mounted) {
Navigator.of(context).pushNamed('/login');
}
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();
}
}
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:
GlobalKey<NavigatorState>
is used to manage navigation globally.UserActivityDetector
is wrapped around the MyHomePage
, ensuring that any user interaction resets the inactivity timer.onTimeout
callback utilizes the navigatorKey
to navigate to the login page upon timeout.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();
}
}
}
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!')),
),
)
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.