How to detect user inactivity in Flutter

6.9k views Asked by At

I developed my app for Android using Java and now want to migrate to Flutter.

For detecting user inactivity I override this method in my Activity: onUserInteraction and reset a Timer, if there is no user interaction, I show a Screen Saver like Animation.

Is there Any clean way in Flutter to do this? I don't want to use platform channels. I want it pure Dart and flutter. I know I can set timer and reset it on user touch, but like in Android I want system to inform me about the user interaction.

4

There are 4 answers

0
Oluwaseyi Fatunmole On

You can handle this by creating a sessionActivityListener class

class SessionInactivity extends StatefulWidget {
VoidCallback onSessionTimeout;
Duration duration;
Widget child;
SessionInactivity(
  {super.key,
  required this.onSessionTimeout,
  required this.duration,
  required this.child});

 @override
 State<SessionInactivity> createState() => _SessionInactivityState();
 }

 class _SessionInactivityState extends State<SessionInactivity> {
 Timer? _timer;

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

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

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

Your timer will be used to countdown once the onSessionTimeout function is triggered

 _startTimer() {
_checkTimer();
_timer = Timer(widget.duration, () {
  print("session timedout");
  widget.onSessionTimeout();
 });
}
 _checkTimer() {
  if (_timer != null) {
   print("starting timer");
   _timer?.cancel();
   _timer = null;
 }
  }

After these, just wrap your material app with the sessionactivity listener and pass the required arguments.

void main() {runApp(const UserInactivity());
  }

  class UserInactivity extends StatefulWidget {
  const UserInactivity({super.key});

   @override
    State<UserInactivity> createState() => _UserInactivity();
    }

   class _UserInactivity extends State<UserInactivity> {
   static final navKey = GlobalKey<NavigatorState>();

   @override
   void initState() {
   loggedInStatus = SessionNotifier.notLoggedIn;
    super.initState();
   }
   @override
   Widget build(BuildContext context) {
    return SessionInactivity(
    duration: Duration(minutes: 3),
    onSessionTimeout: () {
    loggedInStatus != SessionNotifier.notLoggedIn
        ? _timeoutFunction()
        : null;
      },
      child: MaterialApp(
    navigatorKey: _UserInactivity.navKey,
    theme: ThemeData(
      colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      useMaterial3: true,
    ),
    debugShowCheckedModeBanner: false,
    home: const LoginScreen(),
    ),
   );
  }
 _timeoutFunction() {
    Navigator.push(navKey.currentState!.context,
    MaterialPageRoute(builder: (context) => const LoginScreen()));
    customAlertWidget(
  navKey.currentState!.context,
  "Your session expired or timedout, please login to continue ",
   );
 }
}

Please note

the loggedInStatus is an enum that notifies the event of a successful login, and this is because your session inactivity callback function should ideally only run when the user has an active session i.e is logged in

i created the enum this way

enum SessionNotifier {
loggedIn,
notLoggedIn,
authenticating,
}

SessionNotifier loggedInStatus = SessionNotifier.notLoggedIn;

and the _timeout function handles the callback. I hope this helps.

1
Vineet On

You can use wrap your MaterialApp in Listener. And reset a timer on interaction. Similar to what you are doing in android.

It only listens for gestures such as tap, drag, then released or canceled. It does not however listen to mouse events like hovering a region without pressing any buttons. For such events, use MouseRegion.

Sample code:

import 'package:flutter/material.dart';

final Color darkBlue = Color.fromARGB(255, 18, 32, 47);

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerDown: (_) => print('down'), // best place to reset timer imo
      onPointerMove: (_) => print('move'),
      onPointerUp: (_) => print('up'),
      child: MaterialApp(
        theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
        debugShowCheckedModeBanner: false,
        home: Scaffold(
          body: Center(
            child: MyWidget(),
          ),
        ),
      ),
    );
  }
}

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return TextFormField(
      maxLength: 10,
      maxLengthEnforced: true,
      decoration: InputDecoration(
        border: OutlineInputBorder(),
        labelText: 'Details',
      ),
    );
  }
}
1
IdrisAde On

All you need to do is to have access to the app's lifecycle. And a good way to do this is to use the mixin WidgetsBindingObserver on your LandingPage of the app.

The mixin provides you with an enum AppLifecycleState which can have 4 values, detached, inactive, paused and resumed which depicts the current state of the app.

You could, with this create a function e.g. didAppLifecycleChange which takes the state of the app. i.e. didAppLifecycleChange(AppLifecycleState state). You could then use these states to perform actions on the app.

0
Bilol On

I have a similar logic behind my app and here is what i came up with.

class _MyHomePageState extends State<MyHomePage> {
InActivityTimer timer = InActivityTimer();
int _counter = 0;

void _incrementCounter() {
timer.handleUserInteraction(context);
setState(() {
  _counter++;
});
}

@override
void initState() {
// TODO: implement initState
super.initState();
timer.startTimer(context);
}

@override
Widget build(BuildContext context) {
return Listener(
  onPointerHover: (_) {
    timer.handleUserInteraction(context);
  },
  onPointerMove: (_) {
    timer.handleUserInteraction(context);
  },
  child: RawKeyboardListener(
  focusNode: FocusNode(),
  onKey: (RawKeyEvent event) {
    if (event is RawKeyDownEvent) {
      timer.handleUserInteraction(context);
      }
  child: Scaffold(
    appBar: AppBar(
      backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      title: Text(widget.title),
    ),
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          const Text(
            'You have pushed the button this many times:',
          ),
          Text(
            '$_counter',
            style: Theme.of(context).textTheme.headlineMedium,
          ),
        ],
      ),
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: _incrementCounter,
      tooltip: 'Increment',
      child: const Icon(Icons.add),
    ), 
    ),
    );
    }
    }

    class InActivityTimer {
    Timer? _timer;
    DateTime? lastInteractionTime;

    void startTimer(BuildContext context) {
     lastInteractionTime = DateTime.now();
    _startInactivityTimer(context);
  }

  void _startInactivityTimer(BuildContext context) {
    print("start Inactivity timer is triggered");
    if (_timer != null && _timer!.isActive) {
      _timer!.cancel();
    }

    _timer = Timer(Duration(seconds: 10), () {
      _restartApp(context);
    });
  }

  void _restartApp(BuildContext context) {
    print("restarting the app");
    // You can use platform-specific code to restart the app here.
    // This example uses Flutter's `runApp` to restart the app.
    RestartWidget.restartApp(context);
  }

  void handleUserInteraction(BuildContext context) {
    print("Handling user interaction");
    lastInteractionTime = DateTime.now();
    _startInactivityTimer(context);
  }
}
}