import 'dart:async'; import 'package:flutter/material.dart'; /// Allows the user to close the app by double tapping the back-button. /// /// You must specify a [SnackBar], so it can be shown when the user taps the /// back-button. Notice that the value you set for [SnackBar.duration] is going /// to be considered to decide whether the snack-bar is currently visible or /// not. /// /// Since the back-button is an Android feature, this Widget is going to be /// nothing but the own [child] if the current platform is anything but Android. class DoubleBackToCloseApp extends StatefulWidget { /// The [SnackBar] shown when the user taps the back-button. final SnackBar snackBar; /// The widget below this widget in the tree. final Widget child; /// Creates a widget that allows the user to close the app by double tapping /// the back-button. const DoubleBackToCloseApp({ Key key, @required this.snackBar, @required this.child, }) : assert(snackBar != null), assert(child != null), super(key: key); @override _DoubleBackToCloseAppState createState() => _DoubleBackToCloseAppState(); } class _DoubleBackToCloseAppState extends State { /// The last time the user tapped Android's back-button. DateTime _lastTimeBackButtonWasTapped; /// Returns whether the current platform is Android. bool get _isAndroid => Theme.of(context).platform == TargetPlatform.android; /// Returns whether the [DoubleBackToCloseApp.snackBar] is currently visible. /// /// The snack-bar is going to be considered visible if the duration of the /// snack-bar is greater than the difference from now to the /// [_lastTimeBackButtonWasTapped]. /// /// This is not quite accurate since the snack-bar could've been dismissed by /// the user, so this algorithm needs to be improved, as described in #2. bool get _isSnackBarVisible => (_lastTimeBackButtonWasTapped != null) && (widget.snackBar.duration > DateTime.now().difference(_lastTimeBackButtonWasTapped)); /// Returns whether the next back navigation of this route will be handled /// internally. /// /// Returns true when there's a widget that inserted an entry into the /// local-history of the current route, in order to handle pop. This is done /// by [Drawer], for example, so it can close on pop. bool get _willHandlePopInternally => ModalRoute.of(context).willHandlePopInternally; @override Widget build(BuildContext context) { _ensureThatContextContainsScaffold(); if (_isAndroid) { return WillPopScope( onWillPop: _handleWillPop, child: widget.child, ); } else { return widget.child; } } /// Handles [WillPopScope.onWillPop]. Future _handleWillPop() async { if (_isSnackBarVisible || _willHandlePopInternally) { return true; } else { _lastTimeBackButtonWasTapped = DateTime.now(); // Scaffold.of(context).showSnackBar(widget.snackBar); ScaffoldMessenger.of(context).showSnackBar(widget.snackBar); return false; } } /// Throws a [FlutterError] if this widget was not wrapped in a [Scaffold]. void _ensureThatContextContainsScaffold() { if (Scaffold.maybeOf(context) == null) { throw FlutterError( '`DoubleBackToCloseApp` must be wrapped in a `Scaffold`.', ); } } }