Reputation: 1087
I need to create an app that navigates a user to after a period of inactivity.
I tried wrapping my app in GestureDetector and run a timer after a specified period as shown below but it is not working:
class _MyAppState extends State<MyApp> {
Timer _timer;
@override
void initState() {
super.initState();
_initializeTimer();
}
void _initializeTimer() {
_timer = Timer.periodic(const Duration(seconds: 20), (_) => _logOutUser);
}
void _logOutUser() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WelcomePage()));
_timer.cancel();
}
void _handleUserInteraction([_]) {
if (!_timer.isActive) {
return;
}
_timer.cancel();
_initializeTimer();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleUserInteraction,
onPanDown: _handleUserInteraction,
child: MaterialApp(
title: 'Hello',
home: WelcomePage(),
routes: <String, WidgetBuilder>{
'/welcome': (BuildContext context) => new WelcomePage(),
'/login': (BuildContext context) => new LoginPage(),
},
debugShowCheckedModeBanner: false,
),
);
}
}
My question is how best should I implement running a function after a period of inactivity within my flutter app?
Upvotes: 19
Views: 23106
Reputation: 1112
You can use Listener for achieving inactivity session timeout. Listener’s onPointerDown method calls when you tap on screen.
MyApp class in main.dart
@override
Widget build(BuildContext context) {
return Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (val) {
startSessionTimer();
},
child: MaterialApp() // Your material app.
);
}
}
Create startSessionTimer method in helper class or wherever you want.
void startSessionTimer({bool initialized}) async {
if (_sessionTimer != null) {
_sessionTimer.cancel();
}
// <save last interaction time here>
_sessionTimer = Timer(Duration(minutes: 15), () {
_sessionTimer?.cancel();
_sessionTimer = null;
// code here for your requirements.
// This block will execute when timer finish.
});
}
Listener’s onPointerDown method will not be called for TextField typing, so override your TextField’s onChanged method throughout app (or can create custom) to start timer session.
onChanged: (text){
startSessionTimer();
},
But timers can gets paused by the OS when the app is in the background, so timer will not be complete at specific time, it can take some more time to finish.
If you need just logout after inactivity session expired, so you can monitor background/foreground activity when app goes to background and foreground.
You can save last interaction time in shared preferences in startSessionTimer
method where I wrote <save last interaction time here>
.
And when app goes to background, stop timer.
And when app comes to foreground, check last saved interaction time.
You can get app background and foreground state by below code:
Can create a base class and extend with `WidgetsBindingObserver`class BaseScreenState<T extends StatefulWidget> extends State<T> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) async {
if (state == AppLifecycleState.paused) {
// background
}
if (state == AppLifecycleState.resumed) {
// foreground
}
super.didChangeAppLifecycleState(state);
}
Upvotes: 1
Reputation: 903
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:
@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 }
Upvotes: 10
Reputation: 421
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 :-)
Upvotes: 14
Reputation: 1571
You are on the right track. You need to wrap the whole app in a GestureDetector
.
You can add a behavior
to the GestureDetector
. The desired behaviour should be HitTestBehavior.translucent. This will allow you to receive touch events while allowing widgets in your app to receive the events also.
Then you will need a timer that will be reset with each tap. Once a it times out, navigate the user to the desired screen.
Here is a high level example
Timer timer;
@override
Widget build(BuildContext context)
{
return GestureDetector(
child: Scaffold(
appBar: AppBar(
title: Text("title"),
),
body: Container(),
),
behavior: HitTestBehavior.translucent,
onTapDown: (tapDown)
{
if (timer != null)
{
timer.cancel();
}
timer = Timer(Duration(minutes: 5), timeOutCallBack);
},
);
}
void timeOutCallBack() {
Navigator.push(context, MaterialPageRoute(builder: (context) => DesiredScreen),);
}
Upvotes: 1
Reputation: 9903
Here's my solution. Some details:
Navigator
in home widget of the app, so it's possible to access navigator outside of MaterialApp via GlobalKey
;GestureDetector
behavior is set to HitTestBehavior.translucent
to propagate taps to other widgets;Timer.periodic
for this purpose. Periodic timer is used to execute callback repeatedly (e.g., every 10 seconds);_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'),
),
);
}
);
},
),
),
);
}
}
Upvotes: 13