98 lines
3.3 KiB
Dart
98 lines
3.3 KiB
Dart
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<DoubleBackToCloseApp> {
|
|
/// 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<bool> _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`.',
|
|
);
|
|
}
|
|
}
|
|
} |