How to execute a function after a period of inactivity in Flutter
Here's my solution. Some details:
- I added
Navigator
in home widget of the app, so it's possible to access navigator outside of MaterialApp viaGlobalKey
; GestureDetector
behavior is set toHitTestBehavior.translucent
to propagate taps to other widgets;- You don't need
Timer.periodic
for this purpose. Periodic timer is used to execute callback repeatedly (e.g., every 10 seconds); - Timer sets when the widget initializes and when any tap happens. Any following tap will cancel the old timer and create a new one. After
_logOutUser
callback is called, timer gets cancelled (if there was any), every route is getting popped and new route is pushed.
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _navigatorKey = GlobalKey<NavigatorState>();
Timer _timer;
@override
void initState() {
super.initState();
_initializeTimer();
}
void _initializeTimer() {
if (_timer != null) {
_timer.cancel();
}
_timer = Timer(const Duration(seconds: 3), _logOutUser);
}
void _logOutUser() {
_timer?.cancel();
_timer = null;
// Popping all routes and pushing welcome screen
_navigatorKey.currentState.pushNamedAndRemoveUntil('welcome', (_) => false);
}
void _handleUserInteraction([_]) {
_initializeTimer();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: _handleUserInteraction,
onPanDown: _handleUserInteraction,
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Navigator(
initialRoute: 'welcome',
key: _navigatorKey,
onGenerateRoute: (settings) {
return MaterialPageRoute(
builder: (context) {
return Scaffold(
appBar: AppBar(),
body: SafeArea(
child: Text(settings.name)
),
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.of(context).pushNamed('test'),
),
);
}
);
},
),
),
);
}
}
FWIW, I experimented with using a GestureDetector
as suggested, but it didn't work as expected. The problem was that I received a continuous stream of gestures when there was no activity. This includes when I tried the more restrictive onTap: callback.
I saw this in debug mode on an emulator. I didn't experiment further to see if real phones manifest the same behavior, because even if they don't now, they might in the future: There's clearly no spec guarantee that a GestureDetector won't receive spurious events. For something security-related like an inactivity timeout, that's not acceptable.
For my use case, I decided that it was OK to instead detect when the application is invisible for more than a set amount of time. My reasoning for my expected usage is that the real danger is when the app is invisible, and they forget it's there.
Setting this kind of inactivity timeout is pretty easy. I arrange to call startKeepAlive()
at the moment the app gains access to sensitive information (e.g. after a password is entered). For my usage, just crashing out of the app after the timeout is fine; obviously one could get more sophisticated, if needed. Anyhoo, here's the relevant code:
const _inactivityTimeout = Duration(seconds: 10);
Timer _keepAliveTimer;
void _keepAlive(bool visible) {
_keepAliveTimer?.cancel();
if (visible) {
_keepAliveTimer = null;
} else {
_keepAliveTimer = Timer(_inactivityTimeout, () => exit(0));
}
}
class _KeepAliveObserver extends WidgetsBindingObserver {
@override didChangeAppLifecycleState(AppLifecycleState state) {
switch(state) {
case AppLifecycleState.resumed:
_keepAlive(true);
break;
case AppLifecycleState.inactive:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
_keepAlive(false); // Conservatively set a timer on all three
break;
}
}
}
/// Must be called only when app is visible, and exactly once
void startKeepAlive() {
assert(_keepAliveTimer == null);
_keepAlive(true);
WidgetsBinding.instance.addObserver(_KeepAliveObserver());
}
In production, I'll probably extend the timeout :-)
For people who want the strict answer to the question of the title ("How to execute a function after a period of inactivity in Flutter"), this is the complete solution:
- Wrap your MaterialApp inside a GestureDetector so you can detect taps and pans.
- In GestureDetector set following property to avoid messing with the standard gesture system:
- behavior: HitTestBehavior.translucent
- In GestureDetector set callbacks to restart timer when tap/pan activity happens:
- onTap: (_) => _initializeTimer()
- onPanDown: (_) => _initializeTimer()
- onPanUpdate: (_) => _initializeTimer()
- We should not forget that a typing user is also an active user, so we also need to setup callbacks in every TextField of the widget:
- onChanged: (_) => _initializeTimer()
- Add Timer _timer; to your class_SomethingState.
- Finally, init _timer at initState(), write _initializeTimer() and write _handleInactivity() including your desired actions when enough inactivity happens:
@override void initState() { super.initState(); _initializeTimer(); } // start/restart timer void _initializeTimer() { if (_timer != null) { _timer.cancel(); } // setup action after 5 minutes _timer = Timer(const Duration(minutes: 5), () => _handleInactivity()); } void _handleInactivity() { _timer?.cancel(); _timer = null; // TODO: type your desired code here }