deantawonezvi
deantawonezvi

Reputation: 1087

How to execute a function after a period of inactivity in Flutter

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

Answers (5)

vivek
vivek

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

Blackd
Blackd

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:

  • 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
    }

Upvotes: 10

Bill Foote
Bill Foote

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

Osama FelFel
Osama FelFel

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

Igor Kharakhordin
Igor Kharakhordin

Reputation: 9903

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 via GlobalKey;
  • GestureDetector behavior is set to HitTestBehavior.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'),
                  ),
                );
              }
            );
          },
        ),
      ),
    );
  }
}

Upvotes: 13

Related Questions