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 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'),
                  ),
                );
              }
            );
          },
        ),
      ),
    );
  }
}

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
    }