Unverified Commit f68d03f1 authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

Reland root predictive back (#132249)

Root predictive back (https://github.com/flutter/flutter/pull/120385) was reverted in https://github.com/flutter/flutter/pull/132167.  This PR is an attempt to reland it.

The reversion happened due to failed Google tests (b/295073110).
parent ced3e766
...@@ -48,9 +48,9 @@ class CupertinoNavigationDemo extends StatelessWidget { ...@@ -48,9 +48,9 @@ class CupertinoNavigationDemo extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return WillPopScope( return PopScope(
// Prevent swipe popping of this page. Use explicit exit buttons only. // Prevent swipe popping of this page. Use explicit exit buttons only.
onWillPop: () => Future<bool>.value(true), canPop: false,
child: DefaultTextStyle( child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle, style: CupertinoTheme.of(context).textTheme.textStyle,
child: CupertinoTabScaffold( child: CupertinoTabScaffold(
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
// This demo is based on // This demo is based on
...@@ -109,16 +110,15 @@ class FullScreenDialogDemoState extends State<FullScreenDialogDemo> { ...@@ -109,16 +110,15 @@ class FullScreenDialogDemoState extends State<FullScreenDialogDemo> {
bool _hasName = false; bool _hasName = false;
late String _eventName; late String _eventName;
Future<bool> _onWillPop() async { Future<void> _handlePopInvoked(bool didPop) async {
_saveNeeded = _hasLocation || _hasName || _saveNeeded; if (didPop) {
if (!_saveNeeded) { return;
return true;
} }
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final TextStyle dialogTextStyle = theme.textTheme.titleMedium!.copyWith(color: theme.textTheme.bodySmall!.color); final TextStyle dialogTextStyle = theme.textTheme.titleMedium!.copyWith(color: theme.textTheme.bodySmall!.color);
return showDialog<bool>( final bool? shouldDiscard = await showDialog<bool>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
...@@ -130,19 +130,31 @@ class FullScreenDialogDemoState extends State<FullScreenDialogDemo> { ...@@ -130,19 +130,31 @@ class FullScreenDialogDemoState extends State<FullScreenDialogDemo> {
TextButton( TextButton(
child: const Text('CANCEL'), child: const Text('CANCEL'),
onPressed: () { onPressed: () {
Navigator.of(context).pop(false); // Pops the confirmation dialog but not the page. // Pop the confirmation dialog and indicate that the page should
// not be popped.
Navigator.of(context).pop(false);
}, },
), ),
TextButton( TextButton(
child: const Text('DISCARD'), child: const Text('DISCARD'),
onPressed: () { onPressed: () {
Navigator.of(context).pop(true); // Returning true to _onWillPop will pop again. // Pop the confirmation dialog and indicate that the page should
// be popped, too.
Navigator.of(context).pop(true);
}, },
), ),
], ],
); );
}, },
) as Future<bool>; );
if (shouldDiscard ?? false) {
// Since this is the root route, quit the app where possible by invoking
// the SystemNavigator. If this wasn't the root route, then
// Navigator.maybePop could be used instead.
// See https://github.com/flutter/flutter/issues/11490
SystemNavigator.pop();
}
} }
@override @override
...@@ -162,7 +174,8 @@ class FullScreenDialogDemoState extends State<FullScreenDialogDemo> { ...@@ -162,7 +174,8 @@ class FullScreenDialogDemoState extends State<FullScreenDialogDemo> {
], ],
), ),
body: Form( body: Form(
onWillPop: _onWillPop, canPop: !_saveNeeded && !_hasLocation && !_hasName,
onPopInvoked: _handlePopInvoked,
child: Scrollbar( child: Scrollbar(
child: ListView( child: ListView(
primary: true, primary: true,
......
...@@ -143,10 +143,9 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> { ...@@ -143,10 +143,9 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {
return null; return null;
} }
Future<bool> _warnUserAboutInvalidData() async { Future<void> _handlePopInvoked(bool didPop) async {
final FormState? form = _formKey.currentState; if (didPop) {
if (form == null || !_formWasEdited || form.validate()) { return;
return true;
} }
final bool? result = await showDialog<bool>( final bool? result = await showDialog<bool>(
...@@ -168,7 +167,14 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> { ...@@ -168,7 +167,14 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {
); );
}, },
); );
return result!;
if (result ?? false) {
// Since this is the root route, quit the app where possible by invoking
// the SystemNavigator. If this wasn't the root route, then
// Navigator.maybePop could be used instead.
// See https://github.com/flutter/flutter/issues/11490
SystemNavigator.pop();
}
} }
@override @override
...@@ -185,7 +191,8 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> { ...@@ -185,7 +191,8 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {
child: Form( child: Form(
key: _formKey, key: _formKey,
autovalidateMode: _autovalidateMode, autovalidateMode: _autovalidateMode,
onWillPop: _warnUserAboutInvalidData, canPop: _formKey.currentState == null || !_formWasEdited || _formKey.currentState!.validate(),
onPopInvoked: _handlePopInvoked,
child: Scrollbar( child: Scrollbar(
child: SingleChildScrollView( child: SingleChildScrollView(
primary: true, primary: true,
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:scoped_model/scoped_model.dart'; import 'package:scoped_model/scoped_model.dart';
import 'colors.dart'; import 'colors.dart';
...@@ -361,14 +360,12 @@ class ExpandingBottomSheetState extends State<ExpandingBottomSheet> with TickerP ...@@ -361,14 +360,12 @@ class ExpandingBottomSheetState extends State<ExpandingBottomSheet> with TickerP
// Closes the cart if the cart is open, otherwise exits the app (this should // Closes the cart if the cart is open, otherwise exits the app (this should
// only be relevant for Android). // only be relevant for Android).
Future<bool> _onWillPop() async { void _handlePopInvoked(bool didPop) {
if (!_isOpen) { if (didPop) {
await SystemNavigator.pop(); return;
return true;
} }
close(); close();
return true;
} }
@override @override
...@@ -378,8 +375,9 @@ class ExpandingBottomSheetState extends State<ExpandingBottomSheet> with TickerP ...@@ -378,8 +375,9 @@ class ExpandingBottomSheetState extends State<ExpandingBottomSheet> with TickerP
duration: const Duration(milliseconds: 225), duration: const Duration(milliseconds: 225),
curve: Curves.easeInOut, curve: Curves.easeInOut,
alignment: FractionalOffset.topLeft, alignment: FractionalOffset.topLeft,
child: WillPopScope( child: PopScope(
onWillPop: _onWillPop, canPop: !_isOpen,
onPopInvoked: _handlePopInvoked,
child: AnimatedBuilder( child: AnimatedBuilder(
animation: widget.hideController, animation: widget.hideController,
builder: _buildSlideAnimation, builder: _buildSlideAnimation,
......
...@@ -325,14 +325,14 @@ class _GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStat ...@@ -325,14 +325,14 @@ class _GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStat
backgroundColor: isDark ? _kFlutterBlue : theme.primaryColor, backgroundColor: isDark ? _kFlutterBlue : theme.primaryColor,
body: SafeArea( body: SafeArea(
bottom: false, bottom: false,
child: WillPopScope( child: PopScope(
onWillPop: () { canPop: _category == null,
onPopInvoked: (bool didPop) {
if (didPop) {
return;
}
// Pop the category page if Android back button is pressed. // Pop the category page if Android back button is pressed.
if (_category != null) {
setState(() => _category = null); setState(() => _category = null);
return Future<bool>.value(false);
}
return Future<bool>.value(true);
}, },
child: Backdrop( child: Backdrop(
backTitle: const Text('Options'), backTitle: const Text('Options'),
......
...@@ -71,14 +71,10 @@ class _HomeState extends State<Home> with TickerProviderStateMixin<Home> { ...@@ -71,14 +71,10 @@ class _HomeState extends State<Home> with TickerProviderStateMixin<Home> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return WillPopScope( return NavigatorPopHandler(
onWillPop: () async { onPop: () {
final NavigatorState navigator = navigatorKeys[selectedIndex].currentState!; final NavigatorState navigator = navigatorKeys[selectedIndex].currentState!;
if (!navigator.canPop()) {
return true;
}
navigator.pop(); navigator.pop();
return false;
}, },
child: Scaffold( child: Scaffold(
body: SafeArea( body: SafeArea(
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// This sample demonstrates showing a confirmation dialog when the user
/// attempts to navigate away from a page with unsaved [Form] data.
void main() => runApp(const FormApp());
class FormApp extends StatelessWidget {
const FormApp({
super.key,
});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Confirmation Dialog Example'),
),
body: Center(
child: _SaveableForm(),
),
),
);
}
}
class _SaveableForm extends StatefulWidget {
@override
State<_SaveableForm> createState() => _SaveableFormState();
}
class _SaveableFormState extends State<_SaveableForm> {
final TextEditingController _controller = TextEditingController();
String _savedValue = '';
bool _isDirty = false;
@override
void initState() {
super.initState();
_controller.addListener(_onChanged);
}
@override
void dispose() {
_controller.removeListener(_onChanged);
super.dispose();
}
void _onChanged() {
final bool nextIsDirty = _savedValue != _controller.text;
if (nextIsDirty == _isDirty) {
return;
}
setState(() {
_isDirty = nextIsDirty;
});
}
Future<void> _showDialog() async {
final bool? shouldDiscard = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Are you sure?'),
content: const Text('Any unsaved changes will be lost!'),
actions: <Widget>[
TextButton(
child: const Text('Yes, discard my changes'),
onPressed: () {
Navigator.pop(context, true);
},
),
TextButton(
child: const Text('No, continue editing'),
onPressed: () {
Navigator.pop(context, false);
},
),
],
);
},
);
if (shouldDiscard ?? false) {
// Since this is the root route, quit the app where possible by invoking
// the SystemNavigator. If this wasn't the root route, then
// Navigator.maybePop could be used instead.
// See https://github.com/flutter/flutter/issues/11490
SystemNavigator.pop();
}
}
void _save(String? value) {
setState(() {
_savedValue = value ?? '';
});
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('If the field below is unsaved, a confirmation dialog will be shown on back.'),
const SizedBox(height: 20.0),
Form(
canPop: !_isDirty,
onPopInvoked: (bool didPop) {
if (didPop) {
return;
}
_showDialog();
},
autovalidateMode: AutovalidateMode.always,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextFormField(
controller: _controller,
onFieldSubmitted: (String? value) {
_save(value);
},
),
TextButton(
onPressed: () {
_save(_controller.text);
},
child: Row(
children: <Widget>[
const Text('Save'),
if (_controller.text.isNotEmpty)
Icon(
_isDirty ? Icons.warning : Icons.check,
),
],
),
),
],
),
),
TextButton(
onPressed: () {
if (_isDirty) {
_showDialog();
return;
}
// Since this is the root route, quit the app where possible by
// invoking the SystemNavigator. If this wasn't the root route,
// then Navigator.maybePop could be used instead.
// See https://github.com/flutter/flutter/issues/11490
SystemNavigator.pop();
},
child: const Text('Go back'),
),
],
),
);
}
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
/// This sample demonstrates using [NavigatorPopHandler] to handle system back
/// gestures when there are nested [Navigator] widgets by delegating to the
/// current [Navigator].
void main() => runApp(const NavigatorPopHandlerApp());
class NavigatorPopHandlerApp extends StatelessWidget {
const NavigatorPopHandlerApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: <String, WidgetBuilder>{
'/': (BuildContext context) => _HomePage(),
'/nested_navigators': (BuildContext context) => const NestedNavigatorsPage(),
},
);
}
}
class _HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Nested Navigators Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('Home Page'),
const Text('A system back gesture here will exit the app.'),
const SizedBox(height: 20.0),
ListTile(
title: const Text('Nested Navigator route'),
subtitle: const Text('This route has another Navigator widget in addition to the one inside MaterialApp above.'),
onTap: () {
Navigator.of(context).pushNamed('/nested_navigators');
},
),
],
),
),
);
}
}
class NestedNavigatorsPage extends StatefulWidget {
const NestedNavigatorsPage({super.key});
@override
State<NestedNavigatorsPage> createState() => _NestedNavigatorsPageState();
}
class _NestedNavigatorsPageState extends State<NestedNavigatorsPage> {
final GlobalKey<NavigatorState> _nestedNavigatorKey = GlobalKey<NavigatorState>();
@override
Widget build(BuildContext context) {
return NavigatorPopHandler(
onPop: () {
_nestedNavigatorKey.currentState!.maybePop();
},
child: Navigator(
key: _nestedNavigatorKey,
initialRoute: 'nested_navigators/one',
onGenerateRoute: (RouteSettings settings) {
switch (settings.name) {
case 'nested_navigators/one':
final BuildContext rootContext = context;
return MaterialPageRoute<void>(
builder: (BuildContext context) => NestedNavigatorsPageOne(
onBack: () {
Navigator.of(rootContext).pop();
},
),
);
case 'nested_navigators/one/another_one':
return MaterialPageRoute<void>(
builder: (BuildContext context) => const NestedNavigatorsPageTwo(
),
);
default:
throw Exception('Invalid route: ${settings.name}');
}
},
),
);
}
}
class NestedNavigatorsPageOne extends StatelessWidget {
const NestedNavigatorsPageOne({
required this.onBack,
super.key,
});
final VoidCallback onBack;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('Nested Navigators Page One'),
const Text('A system back here returns to the home page.'),
TextButton(
onPressed: () {
Navigator.of(context).pushNamed('nested_navigators/one/another_one');
},
child: const Text('Go to another route in this nested Navigator'),
),
TextButton(
// Can't use Navigator.of(context).pop() because this is the root
// route, so it can't be popped. The Navigator above this needs to
// be popped.
onPressed: onBack,
child: const Text('Go back'),
),
],
),
),
);
}
}
class NestedNavigatorsPageTwo extends StatelessWidget {
const NestedNavigatorsPageTwo({
super.key,
});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.withBlue(180),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('Nested Navigators Page Two'),
const Text('A system back here will go back to Nested Navigators Page One'),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Go back'),
),
],
),
),
);
}
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This sample demonstrates nested navigation in a bottom navigation bar.
import 'package:flutter/material.dart';
// There are three possible tabs.
enum _Tab {
home,
one,
two,
}
// Each tab has two possible pages.
enum _TabPage {
home,
one,
}
typedef _TabPageCallback = void Function(List<_TabPage> pages);
void main() => runApp(const NavigatorPopHandlerApp());
class NavigatorPopHandlerApp extends StatelessWidget {
const NavigatorPopHandlerApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/home',
routes: <String, WidgetBuilder>{
'/home': (BuildContext context) => const _BottomNavPage(
),
},
);
}
}
class _BottomNavPage extends StatefulWidget {
const _BottomNavPage();
@override
State<_BottomNavPage> createState() => _BottomNavPageState();
}
class _BottomNavPageState extends State<_BottomNavPage> {
_Tab _tab = _Tab.home;
final GlobalKey _tabHomeKey = GlobalKey();
final GlobalKey _tabOneKey = GlobalKey();
final GlobalKey _tabTwoKey = GlobalKey();
List<_TabPage> _tabHomePages = <_TabPage>[_TabPage.home];
List<_TabPage> _tabOnePages = <_TabPage>[_TabPage.home];
List<_TabPage> _tabTwoPages = <_TabPage>[_TabPage.home];
BottomNavigationBarItem _itemForPage(_Tab page) {
switch (page) {
case _Tab.home:
return const BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Go to Home',
);
case _Tab.one:
return const BottomNavigationBarItem(
icon: Icon(Icons.one_k),
label: 'Go to One',
);
case _Tab.two:
return const BottomNavigationBarItem(
icon: Icon(Icons.two_k),
label: 'Go to Two',
);
}
}
Widget _getPage(_Tab page) {
switch (page) {
case _Tab.home:
return _BottomNavTab(
key: _tabHomeKey,
title: 'Home Tab',
color: Colors.grey,
pages: _tabHomePages,
onChangedPages: (List<_TabPage> pages) {
setState(() {
_tabHomePages = pages;
});
},
);
case _Tab.one:
return _BottomNavTab(
key: _tabOneKey,
title: 'Tab One',
color: Colors.amber,
pages: _tabOnePages,
onChangedPages: (List<_TabPage> pages) {
setState(() {
_tabOnePages = pages;
});
},
);
case _Tab.two:
return _BottomNavTab(
key: _tabTwoKey,
title: 'Tab Two',
color: Colors.blueGrey,
pages: _tabTwoPages,
onChangedPages: (List<_TabPage> pages) {
setState(() {
_tabTwoPages = pages;
});
},
);
}
}
void _onItemTapped(int index) {
setState(() {
_tab = _Tab.values.elementAt(index);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: _getPage(_tab),
),
bottomNavigationBar: BottomNavigationBar(
items: _Tab.values.map(_itemForPage).toList(),
currentIndex: _Tab.values.indexOf(_tab),
selectedItemColor: Colors.amber[800],
onTap: _onItemTapped,
),
);
}
}
class _BottomNavTab extends StatefulWidget {
const _BottomNavTab({
super.key,
required this.color,
required this.onChangedPages,
required this.pages,
required this.title,
});
final Color color;
final _TabPageCallback onChangedPages;
final List<_TabPage> pages;
final String title;
@override
State<_BottomNavTab> createState() => _BottomNavTabState();
}
class _BottomNavTabState extends State<_BottomNavTab> {
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
@override
Widget build(BuildContext context) {
return NavigatorPopHandler(
onPop: () {
_navigatorKey.currentState?.maybePop();
},
child: Navigator(
key: _navigatorKey,
onPopPage: (Route<void> route, void result) {
if (!route.didPop(null)) {
return false;
}
widget.onChangedPages(<_TabPage>[
...widget.pages,
]..removeLast());
return true;
},
pages: widget.pages.map((_TabPage page) {
switch (page) {
case _TabPage.home:
return MaterialPage<void>(
child: _LinksPage(
title: 'Bottom nav - tab ${widget.title} - route $page',
backgroundColor: widget.color,
buttons: <Widget>[
TextButton(
onPressed: () {
widget.onChangedPages(<_TabPage>[
...widget.pages,
_TabPage.one,
]);
},
child: const Text('Go to another route in this nested Navigator'),
),
],
),
);
case _TabPage.one:
return MaterialPage<void>(
child: _LinksPage(
backgroundColor: widget.color,
title: 'Bottom nav - tab ${widget.title} - route $page',
buttons: <Widget>[
TextButton(
onPressed: () {
widget.onChangedPages(<_TabPage>[
...widget.pages,
]..removeLast());
},
child: const Text('Go back'),
),
],
),
);
}
}).toList(),
),
);
}
}
class _LinksPage extends StatelessWidget {
const _LinksPage ({
required this.backgroundColor,
this.buttons = const <Widget>[],
required this.title,
});
final Color backgroundColor;
final List<Widget> buttons;
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: backgroundColor,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(title),
...buttons,
],
),
),
);
}
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This sample demonstrates showing a confirmation dialog before navigating
// away from a page.
import 'package:flutter/material.dart';
void main() => runApp(const NavigatorPopHandlerApp());
class NavigatorPopHandlerApp extends StatelessWidget {
const NavigatorPopHandlerApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/home',
routes: <String, WidgetBuilder>{
'/home': (BuildContext context) => const _HomePage(),
'/two': (BuildContext context) => const _PageTwo(),
},
);
}
}
class _HomePage extends StatefulWidget {
const _HomePage();
@override
State<_HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<_HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('Page One'),
TextButton(
onPressed: () {
Navigator.of(context).pushNamed('/two');
},
child: const Text('Next page'),
),
],
),
),
);
}
}
class _PageTwo extends StatefulWidget {
const _PageTwo();
@override
State<_PageTwo> createState() => _PageTwoState();
}
class _PageTwoState extends State<_PageTwo> {
void _showBackDialog() {
showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Are you sure?'),
content: const Text(
'Are you sure you want to leave this page?',
),
actions: <Widget>[
TextButton(
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.labelLarge,
),
child: const Text('Nevermind'),
onPressed: () {
Navigator.pop(context);
},
),
TextButton(
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.labelLarge,
),
child: const Text('Leave'),
onPressed: () {
Navigator.pop(context);
Navigator.pop(context);
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('Page Two'),
PopScope(
canPop: false,
onPopInvoked: (bool didPop) {
if (didPop) {
return;
}
_showBackDialog();
},
child: TextButton(
onPressed: () {
_showBackDialog();
},
child: const Text('Go back'),
),
),
],
),
),
);
}
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
/// Flutter code sample for [WillPopScope].
void main() => runApp(const WillPopScopeExampleApp());
class WillPopScopeExampleApp extends StatelessWidget {
const WillPopScopeExampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: WillPopScopeExample(),
);
}
}
class WillPopScopeExample extends StatefulWidget {
const WillPopScopeExample({super.key});
@override
State<WillPopScopeExample> createState() => _WillPopScopeExampleState();
}
class _WillPopScopeExampleState extends State<WillPopScopeExample> {
bool shouldPop = true;
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
return shouldPop;
},
child: Scaffold(
appBar: AppBar(
title: const Text('Flutter WillPopScope demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
OutlinedButton(
child: const Text('Push'),
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return const WillPopScopeExample();
},
),
);
},
),
OutlinedButton(
child: Text('shouldPop: $shouldPop'),
onPressed: () {
setState(
() {
shouldPop = !shouldPop;
},
);
},
),
const Text('Push to a new screen, then tap on shouldPop '
'button to toggle its value. Press the back '
'button in the appBar to check its behavior '
'for different values of shouldPop'),
],
),
),
),
);
}
}
...@@ -2,31 +2,36 @@ ...@@ -2,31 +2,36 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter_api_samples/widgets/will_pop_scope/will_pop_scope.0.dart' as example; import 'package:flutter/material.dart';
import 'package:flutter_api_samples/widgets/form/form.1.dart' as example;
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
testWidgets('pressing shouldPop button changes shouldPop', (WidgetTester tester) async { testWidgets('Can go back when form is clean', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const example.WillPopScopeExampleApp(), const example.FormApp(),
); );
final Finder buttonFinder = find.text('shouldPop: true'); expect(find.text('Are you sure?'), findsNothing);
expect(buttonFinder, findsOneWidget);
await tester.tap(buttonFinder); await tester.tap(find.text('Go back'));
await tester.pump(); await tester.pumpAndSettle();
expect(find.text('shouldPop: false'), findsOneWidget);
expect(find.text('Are you sure?'), findsNothing);
}); });
testWidgets('pressing Push button pushes route', (WidgetTester tester) async {
testWidgets('Cannot go back when form is dirty', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const example.WillPopScopeExampleApp(), const example.FormApp(),
); );
final Finder buttonFinder = find.text('Push'); expect(find.text('Are you sure?'), findsNothing);
expect(buttonFinder, findsOneWidget);
expect(find.byType(example.WillPopScopeExample), findsOneWidget); await tester.enterText(find.byType(TextFormField), 'some new text');
await tester.tap(buttonFinder);
await tester.tap(find.text('Go back'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(example.WillPopScopeExample, skipOffstage: false), findsNWidgets(2));
expect(find.text('Are you sure?'), findsOneWidget);
}); });
} }
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_api_samples/widgets/navigator_pop_handler/navigator_pop_handler.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';
import '../navigator_utils.dart';
void main() {
testWidgets('Can go back with system back gesture', (WidgetTester tester) async {
await tester.pumpWidget(
const example.NavigatorPopHandlerApp(),
);
expect(find.text('Nested Navigators Example'), findsOneWidget);
expect(find.text('Nested Navigators Page One'), findsNothing);
expect(find.text('Nested Navigators Page Two'), findsNothing);
await tester.tap(find.text('Nested Navigator route'));
await tester.pumpAndSettle();
expect(find.text('Nested Navigators Example'), findsNothing);
expect(find.text('Nested Navigators Page One'), findsOneWidget);
expect(find.text('Nested Navigators Page Two'), findsNothing);
await tester.tap(find.text('Go to another route in this nested Navigator'));
await tester.pumpAndSettle();
expect(find.text('Nested Navigators Example'), findsNothing);
expect(find.text('Nested Navigators Page One'), findsNothing);
expect(find.text('Nested Navigators Page Two'), findsOneWidget);
await simulateSystemBack();
await tester.pumpAndSettle();
expect(find.text('Nested Navigators Example'), findsNothing);
expect(find.text('Nested Navigators Page One'), findsOneWidget);
expect(find.text('Nested Navigators Page Two'), findsNothing);
await simulateSystemBack();
await tester.pumpAndSettle();
expect(find.text('Nested Navigators Example'), findsOneWidget);
expect(find.text('Nested Navigators Page One'), findsNothing);
expect(find.text('Nested Navigators Page Two'), findsNothing);
});
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_api_samples/widgets/navigator_pop_handler/navigator_pop_handler.1.dart' as example;
import 'package:flutter_test/flutter_test.dart';
import '../navigator_utils.dart';
void main() {
testWidgets("System back gesture operates on current tab's nested Navigator", (WidgetTester tester) async {
await tester.pumpWidget(
const example.NavigatorPopHandlerApp(),
);
expect(find.text('Bottom nav - tab Home Tab - route _TabPage.home'), findsOneWidget);
// Go to the next route in this tab.
await tester.tap(find.text('Go to another route in this nested Navigator'));
await tester.pumpAndSettle();
expect(find.text('Bottom nav - tab Home Tab - route _TabPage.one'), findsOneWidget);
// Go to another tab.
await tester.tap(find.text('Go to One'));
await tester.pumpAndSettle();
expect(find.text('Bottom nav - tab Tab One - route _TabPage.home'), findsOneWidget);
// Return to the home tab. The navigation state is preserved.
await tester.tap(find.text('Go to Home'));
await tester.pumpAndSettle();
expect(find.text('Bottom nav - tab Home Tab - route _TabPage.one'), findsOneWidget);
// A back pops the navigation stack of the current tab's nested Navigator.
await simulateSystemBack();
await tester.pumpAndSettle();
expect(find.text('Bottom nav - tab Home Tab - route _TabPage.home'), findsOneWidget);
});
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
/// Simulates a system back, like a back gesture on Android.
///
/// Sends the same platform channel message that the engine sends when it
/// receives a system back.
Future<void> simulateSystemBack() {
return TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/navigation',
const JSONMessageCodec().encodeMessage(<String, dynamic>{
'method': 'popRoute',
}),
(ByteData? _) {},
);
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_api_samples/widgets/pop_scope/pop_scope.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';
import '../navigator_utils.dart';
void main() {
testWidgets('Can choose to stay on page', (WidgetTester tester) async {
await tester.pumpWidget(
const example.NavigatorPopHandlerApp(),
);
expect(find.text('Page One'), findsOneWidget);
expect(find.text('Page Two'), findsNothing);
expect(find.text('Are you sure?'), findsNothing);
await tester.tap(find.text('Next page'));
await tester.pumpAndSettle();
expect(find.text('Page One'), findsNothing);
expect(find.text('Page Two'), findsOneWidget);
expect(find.text('Are you sure?'), findsNothing);
await simulateSystemBack();
await tester.pumpAndSettle();
expect(find.text('Page One'), findsNothing);
expect(find.text('Page Two'), findsOneWidget);
expect(find.text('Are you sure?'), findsOneWidget);
await tester.tap(find.text('Nevermind'));
await tester.pumpAndSettle();
expect(find.text('Page One'), findsNothing);
expect(find.text('Page Two'), findsOneWidget);
expect(find.text('Are you sure?'), findsNothing);
});
testWidgets('Can choose to go back', (WidgetTester tester) async {
await tester.pumpWidget(
const example.NavigatorPopHandlerApp(),
);
expect(find.text('Page One'), findsOneWidget);
expect(find.text('Page Two'), findsNothing);
expect(find.text('Are you sure?'), findsNothing);
await tester.tap(find.text('Next page'));
await tester.pumpAndSettle();
expect(find.text('Page One'), findsNothing);
expect(find.text('Page Two'), findsOneWidget);
expect(find.text('Are you sure?'), findsNothing);
await simulateSystemBack();
await tester.pumpAndSettle();
expect(find.text('Page One'), findsNothing);
expect(find.text('Page Two'), findsOneWidget);
expect(find.text('Are you sure?'), findsOneWidget);
await tester.tap(find.text('Leave'));
await tester.pumpAndSettle();
expect(find.text('Page One'), findsOneWidget);
expect(find.text('Page Two'), findsNothing);
expect(find.text('Are you sure?'), findsNothing);
});
}
...@@ -157,6 +157,7 @@ class CupertinoApp extends StatefulWidget { ...@@ -157,6 +157,7 @@ class CupertinoApp extends StatefulWidget {
this.onGenerateRoute, this.onGenerateRoute,
this.onGenerateInitialRoutes, this.onGenerateInitialRoutes,
this.onUnknownRoute, this.onUnknownRoute,
this.onNavigationNotification,
List<NavigatorObserver> this.navigatorObservers = const <NavigatorObserver>[], List<NavigatorObserver> this.navigatorObservers = const <NavigatorObserver>[],
this.builder, this.builder,
this.title = '', this.title = '',
...@@ -202,6 +203,7 @@ class CupertinoApp extends StatefulWidget { ...@@ -202,6 +203,7 @@ class CupertinoApp extends StatefulWidget {
this.builder, this.builder,
this.title = '', this.title = '',
this.onGenerateTitle, this.onGenerateTitle,
this.onNavigationNotification,
this.color, this.color,
this.locale, this.locale,
this.localizationsDelegates, this.localizationsDelegates,
...@@ -268,6 +270,9 @@ class CupertinoApp extends StatefulWidget { ...@@ -268,6 +270,9 @@ class CupertinoApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.onUnknownRoute} /// {@macro flutter.widgets.widgetsApp.onUnknownRoute}
final RouteFactory? onUnknownRoute; final RouteFactory? onUnknownRoute;
/// {@macro flutter.widgets.widgetsApp.onNavigationNotification}
final NotificationListenerCallback<NavigationNotification>? onNavigationNotification;
/// {@macro flutter.widgets.widgetsApp.navigatorObservers} /// {@macro flutter.widgets.widgetsApp.navigatorObservers}
final List<NavigatorObserver>? navigatorObservers; final List<NavigatorObserver>? navigatorObservers;
...@@ -573,6 +578,7 @@ class _CupertinoAppState extends State<CupertinoApp> { ...@@ -573,6 +578,7 @@ class _CupertinoAppState extends State<CupertinoApp> {
onGenerateRoute: widget.onGenerateRoute, onGenerateRoute: widget.onGenerateRoute,
onGenerateInitialRoutes: widget.onGenerateInitialRoutes, onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
onUnknownRoute: widget.onUnknownRoute, onUnknownRoute: widget.onUnknownRoute,
onNavigationNotification: widget.onNavigationNotification,
builder: widget.builder, builder: widget.builder,
title: widget.title, title: widget.title,
onGenerateTitle: widget.onGenerateTitle, onGenerateTitle: widget.onGenerateTitle,
......
...@@ -196,7 +196,8 @@ mixin CupertinoRouteTransitionMixin<T> on PageRoute<T> { ...@@ -196,7 +196,8 @@ mixin CupertinoRouteTransitionMixin<T> on PageRoute<T> {
} }
// If attempts to dismiss this route might be vetoed such as in a page // If attempts to dismiss this route might be vetoed such as in a page
// with forms, then do not allow the user to dismiss the route with a swipe. // with forms, then do not allow the user to dismiss the route with a swipe.
if (route.hasScopedWillPopCallback) { if (route.hasScopedWillPopCallback
|| route.popDisposition == RoutePopDisposition.doNotPop) {
return false; return false;
} }
// Fullscreen dialogs aren't dismissible by back swipe. // Fullscreen dialogs aren't dismissible by back swipe.
......
...@@ -162,15 +162,39 @@ class _CupertinoTabViewState extends State<CupertinoTabView> { ...@@ -162,15 +162,39 @@ class _CupertinoTabViewState extends State<CupertinoTabView> {
..add(_heroController); ..add(_heroController);
} }
GlobalKey<NavigatorState>? _ownedNavigatorKey;
GlobalKey<NavigatorState> get _navigatorKey {
if (widget.navigatorKey != null) {
return widget.navigatorKey!;
}
_ownedNavigatorKey ??= GlobalKey<NavigatorState>();
return _ownedNavigatorKey!;
}
// Whether this tab is currently the active tab.
bool get _isActive => TickerMode.of(context);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Navigator( final Widget child = Navigator(
key: widget.navigatorKey, key: _navigatorKey,
onGenerateRoute: _onGenerateRoute, onGenerateRoute: _onGenerateRoute,
onUnknownRoute: _onUnknownRoute, onUnknownRoute: _onUnknownRoute,
observers: _navigatorObservers, observers: _navigatorObservers,
restorationScopeId: widget.restorationScopeId, restorationScopeId: widget.restorationScopeId,
); );
// Handle system back gestures only if the tab is currently active.
return NavigatorPopHandler(
enabled: _isActive,
onPop: () {
if (!_isActive) {
return;
}
_navigatorKey.currentState!.pop();
},
child: child,
);
} }
Route<dynamic>? _onGenerateRoute(RouteSettings settings) { Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
......
...@@ -1179,9 +1179,10 @@ class _MasterDetailFlowState extends State<_MasterDetailFlow> implements _PageOp ...@@ -1179,9 +1179,10 @@ class _MasterDetailFlowState extends State<_MasterDetailFlow> implements _PageOp
_builtLayout = _LayoutMode.nested; _builtLayout = _LayoutMode.nested;
final MaterialPageRoute<void> masterPageRoute = _masterPageRoute(context); final MaterialPageRoute<void> masterPageRoute = _masterPageRoute(context);
return WillPopScope( return NavigatorPopHandler(
// Push pop check into nested navigator. onPop: () {
onWillPop: () async => !(await _navigatorKey.currentState!.maybePop()), _navigatorKey.currentState!.maybePop();
},
child: Navigator( child: Navigator(
key: _navigatorKey, key: _navigatorKey,
initialRoute: 'initial', initialRoute: 'initial',
...@@ -1234,12 +1235,10 @@ class _MasterDetailFlowState extends State<_MasterDetailFlow> implements _PageOp ...@@ -1234,12 +1235,10 @@ class _MasterDetailFlowState extends State<_MasterDetailFlow> implements _PageOp
MaterialPageRoute<void> _detailPageRoute(Object? arguments) { MaterialPageRoute<void> _detailPageRoute(Object? arguments) {
return MaterialPageRoute<dynamic>(builder: (BuildContext context) { return MaterialPageRoute<dynamic>(builder: (BuildContext context) {
return WillPopScope( return PopScope(
onWillPop: () async { onPopInvoked: (bool didPop) {
// No need for setState() as rebuild happens on navigation pop. // No need for setState() as rebuild happens on navigation pop.
focus = _Focus.master; focus = _Focus.master;
Navigator.of(context).pop();
return false;
}, },
child: BlockSemantics(child: widget.detailPageBuilder(context, arguments, null)), child: BlockSemantics(child: widget.detailPageBuilder(context, arguments, null)),
); );
......
...@@ -214,6 +214,7 @@ class MaterialApp extends StatefulWidget { ...@@ -214,6 +214,7 @@ class MaterialApp extends StatefulWidget {
this.onGenerateRoute, this.onGenerateRoute,
this.onGenerateInitialRoutes, this.onGenerateInitialRoutes,
this.onUnknownRoute, this.onUnknownRoute,
this.onNavigationNotification,
List<NavigatorObserver> this.navigatorObservers = const <NavigatorObserver>[], List<NavigatorObserver> this.navigatorObservers = const <NavigatorObserver>[],
this.builder, this.builder,
this.title = '', this.title = '',
...@@ -267,6 +268,7 @@ class MaterialApp extends StatefulWidget { ...@@ -267,6 +268,7 @@ class MaterialApp extends StatefulWidget {
this.builder, this.builder,
this.title = '', this.title = '',
this.onGenerateTitle, this.onGenerateTitle,
this.onNavigationNotification,
this.color, this.color,
this.theme, this.theme,
this.darkTheme, this.darkTheme,
...@@ -343,6 +345,9 @@ class MaterialApp extends StatefulWidget { ...@@ -343,6 +345,9 @@ class MaterialApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.onUnknownRoute} /// {@macro flutter.widgets.widgetsApp.onUnknownRoute}
final RouteFactory? onUnknownRoute; final RouteFactory? onUnknownRoute;
/// {@macro flutter.widgets.widgetsApp.onNavigationNotification}
final NotificationListenerCallback<NavigationNotification>? onNavigationNotification;
/// {@macro flutter.widgets.widgetsApp.navigatorObservers} /// {@macro flutter.widgets.widgetsApp.navigatorObservers}
final List<NavigatorObserver>? navigatorObservers; final List<NavigatorObserver>? navigatorObservers;
...@@ -1019,6 +1024,7 @@ class _MaterialAppState extends State<MaterialApp> { ...@@ -1019,6 +1024,7 @@ class _MaterialAppState extends State<MaterialApp> {
onGenerateRoute: widget.onGenerateRoute, onGenerateRoute: widget.onGenerateRoute,
onGenerateInitialRoutes: widget.onGenerateInitialRoutes, onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
onUnknownRoute: widget.onUnknownRoute, onUnknownRoute: widget.onUnknownRoute,
onNavigationNotification: widget.onNavigationNotification,
builder: _materialBuilder, builder: _materialBuilder,
title: widget.title, title: widget.title,
onGenerateTitle: widget.onGenerateTitle, onGenerateTitle: widget.onGenerateTitle,
......
...@@ -2,10 +2,44 @@ ...@@ -2,10 +2,44 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'system_channels.dart'; import 'system_channels.dart';
/// Controls specific aspects of the system navigation stack. /// Controls specific aspects of the system navigation stack.
abstract final class SystemNavigator { abstract final class SystemNavigator {
/// Informs the platform of whether or not the Flutter framework will handle
/// back events.
///
/// Currently, this is used only on Android to inform its use of the
/// predictive back gesture when exiting the app. When true, predictive back
/// is disabled.
///
/// See also:
///
/// * The
/// [migration guide](https://developer.android.com/guide/navigation/predictive-back-gesture)
/// for predictive back in native Android apps.
static Future<void> setFrameworkHandlesBack(bool frameworkHandlesBack) async {
// Currently, this method call is only relevant on Android.
if (kIsWeb) {
return;
}
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return;
case TargetPlatform.android:
return SystemChannels.platform.invokeMethod<void>(
'SystemNavigator.setFrameworkHandlesBack',
frameworkHandlesBack,
);
}
}
/// Removes the topmost Flutter instance, presenting what was before /// Removes the topmost Flutter instance, presenting what was before
/// it. /// it.
/// ///
......
...@@ -19,6 +19,7 @@ import 'framework.dart'; ...@@ -19,6 +19,7 @@ import 'framework.dart';
import 'localizations.dart'; import 'localizations.dart';
import 'media_query.dart'; import 'media_query.dart';
import 'navigator.dart'; import 'navigator.dart';
import 'notification_listener.dart';
import 'pages.dart'; import 'pages.dart';
import 'performance_overlay.dart'; import 'performance_overlay.dart';
import 'restoration.dart'; import 'restoration.dart';
...@@ -313,6 +314,7 @@ class WidgetsApp extends StatefulWidget { ...@@ -313,6 +314,7 @@ class WidgetsApp extends StatefulWidget {
this.onGenerateRoute, this.onGenerateRoute,
this.onGenerateInitialRoutes, this.onGenerateInitialRoutes,
this.onUnknownRoute, this.onUnknownRoute,
this.onNavigationNotification,
List<NavigatorObserver> this.navigatorObservers = const <NavigatorObserver>[], List<NavigatorObserver> this.navigatorObservers = const <NavigatorObserver>[],
this.initialRoute, this.initialRoute,
this.pageRouteBuilder, this.pageRouteBuilder,
...@@ -420,6 +422,7 @@ class WidgetsApp extends StatefulWidget { ...@@ -420,6 +422,7 @@ class WidgetsApp extends StatefulWidget {
this.builder, this.builder,
this.title = '', this.title = '',
this.onGenerateTitle, this.onGenerateTitle,
this.onNavigationNotification,
this.textStyle, this.textStyle,
required this.color, required this.color,
this.locale, this.locale,
...@@ -701,6 +704,13 @@ class WidgetsApp extends StatefulWidget { ...@@ -701,6 +704,13 @@ class WidgetsApp extends StatefulWidget {
/// {@endtemplate} /// {@endtemplate}
final RouteFactory? onUnknownRoute; final RouteFactory? onUnknownRoute;
/// {@template flutter.widgets.widgetsApp.onNavigationNotification}
/// The callback to use when receiving a [NavigationNotification].
///
/// By default this updates the engine with the navigation status.
/// {@endtemplate}
final NotificationListenerCallback<NavigationNotification>? onNavigationNotification;
/// {@template flutter.widgets.widgetsApp.initialRoute} /// {@template flutter.widgets.widgetsApp.initialRoute}
/// The name of the first route to show, if a [Navigator] is built. /// The name of the first route to show, if a [Navigator] is built.
/// ///
...@@ -1328,6 +1338,28 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver { ...@@ -1328,6 +1338,28 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
? WidgetsBinding.instance.platformDispatcher.defaultRouteName ? WidgetsBinding.instance.platformDispatcher.defaultRouteName
: widget.initialRoute ?? WidgetsBinding.instance.platformDispatcher.defaultRouteName; : widget.initialRoute ?? WidgetsBinding.instance.platformDispatcher.defaultRouteName;
AppLifecycleState? _appLifecycleState;
/// The default value for [onNavigationNotification].
///
/// Does nothing and stops bubbling if the app is detached. Otherwise, updates
/// the platform with [NavigationNotification.canHandlePop] and stops
/// bubbling.
bool _defaultOnNavigationNotification(NavigationNotification notification) {
// Don't do anything with navigation notifications if there is no engine
// attached.
if (_appLifecycleState != AppLifecycleState.detached) {
SystemNavigator.setFrameworkHandlesBack(notification.canHandlePop);
}
return true;
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
_appLifecycleState = state;
super.didChangeAppLifecycleState(state);
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
...@@ -1751,6 +1783,8 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver { ...@@ -1751,6 +1783,8 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
return RootRestorationScope( return RootRestorationScope(
restorationId: widget.restorationScopeId, restorationId: widget.restorationScopeId,
child: SharedAppData( child: SharedAppData(
child: NotificationListener<NavigationNotification>(
onNotification: widget.onNavigationNotification ?? _defaultOnNavigationNotification,
child: Shortcuts( child: Shortcuts(
debugLabel: '<Default WidgetsApp Shortcuts>', debugLabel: '<Default WidgetsApp Shortcuts>',
shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts, shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts,
...@@ -1778,6 +1812,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver { ...@@ -1778,6 +1812,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
), ),
), ),
), ),
),
); );
} }
} }
...@@ -54,9 +54,8 @@ export 'dart:ui' show AppLifecycleState, Locale; ...@@ -54,9 +54,8 @@ export 'dart:ui' show AppLifecycleState, Locale;
/// ** See code in examples/api/lib/widgets/binding/widget_binding_observer.0.dart ** /// ** See code in examples/api/lib/widgets/binding/widget_binding_observer.0.dart **
/// {@end-tool} /// {@end-tool}
abstract mixin class WidgetsBindingObserver { abstract mixin class WidgetsBindingObserver {
/// Called when the system tells the app to pop the current route. /// Called when the system tells the app to pop the current route, such as
/// For example, on Android, this is called when the user presses /// after a system back button press or back gesture.
/// the back button.
/// ///
/// Observers are notified in registration order until one returns /// Observers are notified in registration order until one returns
/// true. If none return true, the application quits. /// true. If none return true, the application quits.
...@@ -69,6 +68,8 @@ abstract mixin class WidgetsBindingObserver { ...@@ -69,6 +68,8 @@ abstract mixin class WidgetsBindingObserver {
/// ///
/// This method exposes the `popRoute` notification from /// This method exposes the `popRoute` notification from
/// [SystemChannels.navigation]. /// [SystemChannels.navigation].
///
/// {@macro flutter.widgets.AndroidPredictiveBack}
Future<bool> didPopRoute() => Future<bool>.value(false); Future<bool> didPopRoute() => Future<bool>.value(false);
/// Called when the host tells the application to push a new route onto the /// Called when the host tells the application to push a new route onto the
...@@ -703,6 +704,27 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -703,6 +704,27 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
/// ///
/// This method exposes the `popRoute` notification from /// This method exposes the `popRoute` notification from
/// [SystemChannels.navigation]. /// [SystemChannels.navigation].
///
/// {@template flutter.widgets.AndroidPredictiveBack}
/// ## Handling backs ahead of time
///
/// Not all system backs will result in a call to this method. Some are
/// handled entirely by the system without informing the Flutter framework.
///
/// Android API 33+ introduced a feature called predictive back, which allows
/// the user to peek behind the current app or route during a back gesture and
/// then decide to cancel or commit the back. Flutter enables or disables this
/// feature ahead of time, before a back gesture occurs, and back gestures
/// that trigger predictive back are handled entirely by the system and do not
/// trigger this method here in the framework.
///
/// By default, the framework communicates when it would like to handle system
/// back gestures using [SystemNavigator.setFrameworkHandlesBack] in
/// [WidgetsApp]. This is done automatically based on the status of the
/// [Navigator] stack and the state of any [PopScope] widgets present.
/// Developers can manually set this by calling the method directly or by
/// using [NavigationNotification].
/// {@endtemplate}
@protected @protected
@visibleForTesting @visibleForTesting
Future<void> handlePopRoute() async { Future<void> handlePopRoute() async {
......
...@@ -10,8 +10,10 @@ import 'package:flutter/rendering.dart'; ...@@ -10,8 +10,10 @@ import 'package:flutter/rendering.dart';
import 'basic.dart'; import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'navigator.dart'; import 'navigator.dart';
import 'pop_scope.dart';
import 'restoration.dart'; import 'restoration.dart';
import 'restoration_properties.dart'; import 'restoration_properties.dart';
import 'routes.dart';
import 'will_pop_scope.dart'; import 'will_pop_scope.dart';
// Duration for delay before announcement in IOS so that the announcement won't be interrupted. // Duration for delay before announcement in IOS so that the announcement won't be interrupted.
...@@ -52,10 +54,17 @@ class Form extends StatefulWidget { ...@@ -52,10 +54,17 @@ class Form extends StatefulWidget {
const Form({ const Form({
super.key, super.key,
required this.child, required this.child,
this.canPop,
this.onPopInvoked,
@Deprecated(
'Use canPop and/or onPopInvoked instead. '
'This feature was deprecated after v3.12.0-1.0.pre.',
)
this.onWillPop, this.onWillPop,
this.onChanged, this.onChanged,
AutovalidateMode? autovalidateMode, AutovalidateMode? autovalidateMode,
}) : autovalidateMode = autovalidateMode ?? AutovalidateMode.disabled; }) : autovalidateMode = autovalidateMode ?? AutovalidateMode.disabled,
assert((onPopInvoked == null && canPop == null) || onWillPop == null, 'onWillPop is deprecated; use canPop and/or onPopInvoked.');
/// Returns the [FormState] of the closest [Form] widget which encloses the /// Returns the [FormState] of the closest [Form] widget which encloses the
/// given context, or null if none is found. /// given context, or null if none is found.
...@@ -134,8 +143,44 @@ class Form extends StatefulWidget { ...@@ -134,8 +143,44 @@ class Form extends StatefulWidget {
/// ///
/// * [WillPopScope], another widget that provides a way to intercept the /// * [WillPopScope], another widget that provides a way to intercept the
/// back button. /// back button.
@Deprecated(
'Use canPop and/or onPopInvoked instead. '
'This feature was deprecated after v3.12.0-1.0.pre.',
)
final WillPopCallback? onWillPop; final WillPopCallback? onWillPop;
/// {@macro flutter.widgets.PopScope.canPop}
///
/// {@tool dartpad}
/// This sample demonstrates how to use this parameter to show a confirmation
/// dialog when a navigation pop would cause form data to be lost.
///
/// ** See code in examples/api/lib/widgets/form/form.1.dart **
/// {@end-tool}
///
/// See also:
///
/// * [onPopInvoked], which also comes from [PopScope] and is often used in
/// conjunction with this parameter.
/// * [PopScope.canPop], which is what [Form] delegates to internally.
final bool? canPop;
/// {@macro flutter.widgets.navigator.onPopInvoked}
///
/// {@tool dartpad}
/// This sample demonstrates how to use this parameter to show a confirmation
/// dialog when a navigation pop would cause form data to be lost.
///
/// ** See code in examples/api/lib/widgets/form/form.1.dart **
/// {@end-tool}
///
/// See also:
///
/// * [canPop], which also comes from [PopScope] and is often used in
/// conjunction with this parameter.
/// * [PopScope.onPopInvoked], which is what [Form] delegates to internally.
final PopInvokedCallback? onPopInvoked;
/// Called when one of the form fields changes. /// Called when one of the form fields changes.
/// ///
/// In addition to this callback being invoked, all the form fields themselves /// In addition to this callback being invoked, all the form fields themselves
...@@ -200,6 +245,18 @@ class FormState extends State<Form> { ...@@ -200,6 +245,18 @@ class FormState extends State<Form> {
break; break;
} }
if (widget.canPop != null || widget.onPopInvoked != null) {
return PopScope(
canPop: widget.canPop ?? true,
onPopInvoked: widget.onPopInvoked,
child: _FormScope(
formState: this,
generation: _generation,
child: widget.child,
),
);
}
return WillPopScope( return WillPopScope(
onWillPop: widget.onWillPop, onWillPop: widget.onWillPop,
child: _FormScope( child: _FormScope(
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'framework.dart';
import 'navigator.dart';
import 'notification_listener.dart';
import 'pop_scope.dart';
/// Enables the handling of system back gestures.
///
/// Typically wraps a nested [Navigator] widget and allows it to handle system
/// back gestures in the [onPop] callback.
///
/// {@tool dartpad}
/// This sample demonstrates how to use this widget to properly handle system
/// back gestures when using nested [Navigator]s.
///
/// ** See code in examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.0.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This sample demonstrates how to use this widget to properly handle system
/// back gestures with a bottom navigation bar whose tabs each have their own
/// nested [Navigator]s.
///
/// ** See code in examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.1.dart **
/// {@end-tool}
///
/// See also:
///
/// * [PopScope], which allows toggling the ability of a [Navigator] to
/// handle pops.
/// * [NavigationNotification], which indicates whether a [Navigator] in a
/// subtree can handle pops.
class NavigatorPopHandler extends StatefulWidget {
/// Creates an instance of [NavigatorPopHandler].
const NavigatorPopHandler({
super.key,
this.onPop,
this.enabled = true,
required this.child,
});
/// The widget to place below this in the widget tree.
///
/// Typically this is a [Navigator] that will handle the pop when [onPop] is
/// called.
final Widget child;
/// Whether this widget's ability to handle system back gestures is enabled or
/// disabled.
///
/// When false, there will be no effect on system back gestures. If provided,
/// [onPop] will still be called.
///
/// This can be used, for example, when the nested [Navigator] is no longer
/// active but remains in the widget tree, such as in an inactive tab.
///
/// Defaults to true.
final bool enabled;
/// Called when a handleable pop event happens.
///
/// For example, a pop is handleable when a [Navigator] in [child] has
/// multiple routes on its stack. It's not handleable when it has only a
/// single route, and so [onPop] will not be called.
///
/// Typically this is used to pop the [Navigator] in [child]. See the sample
/// code on [NavigatorPopHandler] for a full example of this.
final VoidCallback? onPop;
@override
State<NavigatorPopHandler> createState() => _NavigatorPopHandlerState();
}
class _NavigatorPopHandlerState extends State<NavigatorPopHandler> {
bool _canPop = true;
@override
Widget build(BuildContext context) {
// When the widget subtree indicates it can handle a pop, disable popping
// here, so that it can be manually handled in canPop.
return PopScope(
canPop: !widget.enabled || _canPop,
onPopInvoked: (bool didPop) {
if (didPop) {
return;
}
widget.onPop?.call();
},
// Listen to changes in the navigation stack in the widget subtree.
child: NotificationListener<NavigationNotification>(
onNotification: (NavigationNotification notification) {
// If this subtree cannot handle pop, then set canPop to true so
// that our PopScope will allow the Navigator higher in the tree to
// handle the pop instead.
final bool nextCanPop = !notification.canHandlePop;
if (nextCanPop != _canPop) {
setState(() {
_canPop = nextCanPop;
});
}
return false;
},
child: widget.child,
),
);
}
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'framework.dart';
import 'navigator.dart';
import 'routes.dart';
/// Manages system back gestures.
///
/// The [canPop] parameter can be used to disable system back gestures. Defaults
/// to true, meaning that back gestures happen as usual.
///
/// The [onPopInvoked] parameter reports when system back gestures occur,
/// regardless of whether or not they were successful.
///
/// If [canPop] is false, then a system back gesture will not pop the route off
/// of the enclosing [Navigator]. [onPopInvoked] will still be called, and
/// `didPop` will be `false`.
///
/// If [canPop] is true, then a system back gesture will cause the enclosing
/// [Navigator] to receive a pop as usual. [onPopInvoked] will be called with
/// `didPop` as `true`, unless the pop failed for reasons unrelated to
/// [PopScope], in which case it will be `false`.
///
/// {@tool dartpad}
/// This sample demonstrates how to use this widget to handle nested navigation
/// in a bottom navigation bar.
///
/// ** See code in examples/api/lib/widgets/pop_scope/pop_scope.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [NavigatorPopHandler], which is a less verbose way to handle system back
/// gestures in simple cases of nested [Navigator]s.
/// * [Form.canPop] and [Form.onPopInvoked], which can be used to handle system
/// back gestures in the case of a form with unsaved data.
/// * [ModalRoute.registerPopEntry] and [ModalRoute.unregisterPopEntry],
/// which this widget uses to integrate with Flutter's navigation system.
class PopScope extends StatefulWidget {
/// Creates a widget that registers a callback to veto attempts by the user to
/// dismiss the enclosing [ModalRoute].
const PopScope({
super.key,
required this.child,
this.canPop = true,
this.onPopInvoked,
});
/// The widget below this widget in the tree.
///
/// {@macro flutter.widgets.ProxyWidget.child}
final Widget child;
/// {@template flutter.widgets.PopScope.onPopInvoked}
/// Called after a route pop was handled.
/// {@endtemplate}
///
/// It's not possible to prevent the pop from happening at the time that this
/// method is called; the pop has already happened. Use [canPop] to
/// disable pops in advance.
///
/// This will still be called even when the pop is canceled. A pop is canceled
/// when the relevant [Route.popDisposition] returns false, such as when
/// [canPop] is set to false on a [PopScope]. The `didPop` parameter
/// indicates whether or not the back navigation actually happened
/// successfully.
///
/// See also:
///
/// * [Route.onPopInvoked], which is similar.
final PopInvokedCallback? onPopInvoked;
/// {@template flutter.widgets.PopScope.canPop}
/// When false, blocks the current route from being popped.
///
/// This includes the root route, where upon popping, the Flutter app would
/// exit.
///
/// If multiple [PopScope] widgets appear in a route's widget subtree, then
/// each and every `canPop` must be `true` in order for the route to be
/// able to pop.
///
/// [Android's predictive back](https://developer.android.com/guide/navigation/predictive-back-gesture)
/// feature will not animate when this boolean is false.
/// {@endtemplate}
final bool canPop;
@override
State<PopScope> createState() => _PopScopeState();
}
class _PopScopeState extends State<PopScope> implements PopEntry {
ModalRoute<dynamic>? _route;
@override
PopInvokedCallback? get onPopInvoked => widget.onPopInvoked;
@override
late final ValueNotifier<bool> canPopNotifier;
@override
void initState() {
super.initState();
canPopNotifier = ValueNotifier<bool>(widget.canPop);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final ModalRoute<dynamic>? nextRoute = ModalRoute.of(context);
if (nextRoute != _route) {
_route?.unregisterPopEntry(this);
_route = nextRoute;
_route?.registerPopEntry(this);
}
}
@override
void didUpdateWidget(PopScope oldWidget) {
super.didUpdateWidget(oldWidget);
canPopNotifier.value = widget.canPop;
}
@override
void dispose() {
_route?.unregisterPopEntry(this);
canPopNotifier.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => widget.child;
}
...@@ -9,26 +9,25 @@ import 'routes.dart'; ...@@ -9,26 +9,25 @@ import 'routes.dart';
/// Registers a callback to veto attempts by the user to dismiss the enclosing /// Registers a callback to veto attempts by the user to dismiss the enclosing
/// [ModalRoute]. /// [ModalRoute].
/// ///
/// {@tool dartpad}
/// Whenever the back button is pressed, you will get a callback at [onWillPop],
/// which returns a [Future]. If the [Future] returns true, the screen is
/// popped.
///
/// ** See code in examples/api/lib/widgets/will_pop_scope/will_pop_scope.0.dart **
/// {@end-tool}
///
/// See also: /// See also:
/// ///
/// * [ModalRoute.addScopedWillPopCallback] and [ModalRoute.removeScopedWillPopCallback], /// * [ModalRoute.addScopedWillPopCallback] and [ModalRoute.removeScopedWillPopCallback],
/// which this widget uses to register and unregister [onWillPop]. /// which this widget uses to register and unregister [onWillPop].
/// * [Form], which provides an `onWillPop` callback that enables the form /// * [Form], which provides an `onWillPop` callback that enables the form
/// to veto a `pop` initiated by the app's back button. /// to veto a `pop` initiated by the app's back button.
/// @Deprecated(
'Use PopScope instead. '
'This feature was deprecated after v3.12.0-1.0.pre.',
)
class WillPopScope extends StatefulWidget { class WillPopScope extends StatefulWidget {
/// Creates a widget that registers a callback to veto attempts by the user to /// Creates a widget that registers a callback to veto attempts by the user to
/// dismiss the enclosing [ModalRoute]. /// dismiss the enclosing [ModalRoute].
/// ///
/// The [child] argument must not be null. /// The [child] argument must not be null.
@Deprecated(
'Use PopScope instead. '
'This feature was deprecated after v3.12.0-1.0.pre.',
)
const WillPopScope({ const WillPopScope({
super.key, super.key,
required this.child, required this.child,
......
...@@ -81,6 +81,7 @@ export 'src/widgets/media_query.dart'; ...@@ -81,6 +81,7 @@ export 'src/widgets/media_query.dart';
export 'src/widgets/modal_barrier.dart'; export 'src/widgets/modal_barrier.dart';
export 'src/widgets/navigation_toolbar.dart'; export 'src/widgets/navigation_toolbar.dart';
export 'src/widgets/navigator.dart'; export 'src/widgets/navigator.dart';
export 'src/widgets/navigator_pop_handler.dart';
export 'src/widgets/nested_scroll_view.dart'; export 'src/widgets/nested_scroll_view.dart';
export 'src/widgets/notification_listener.dart'; export 'src/widgets/notification_listener.dart';
export 'src/widgets/orientation_builder.dart'; export 'src/widgets/orientation_builder.dart';
...@@ -95,6 +96,7 @@ export 'src/widgets/placeholder.dart'; ...@@ -95,6 +96,7 @@ export 'src/widgets/placeholder.dart';
export 'src/widgets/platform_menu_bar.dart'; export 'src/widgets/platform_menu_bar.dart';
export 'src/widgets/platform_selectable_region_context_menu.dart'; export 'src/widgets/platform_selectable_region_context_menu.dart';
export 'src/widgets/platform_view.dart'; export 'src/widgets/platform_view.dart';
export 'src/widgets/pop_scope.dart';
export 'src/widgets/preferred_size.dart'; export 'src/widgets/preferred_size.dart';
export 'src/widgets/primary_scroll_controller.dart'; export 'src/widgets/primary_scroll_controller.dart';
export 'src/widgets/raw_keyboard_listener.dart'; export 'src/widgets/raw_keyboard_listener.dart';
......
...@@ -2,13 +2,14 @@ ...@@ -2,13 +2,14 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:typed_data';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../image_data.dart'; import '../image_data.dart';
import '../rendering/rendering_tester.dart' show TestCallbackPainter; import '../rendering/rendering_tester.dart' show TestCallbackPainter;
import '../widgets/navigator_utils.dart';
late List<int> selectedTabs; late List<int> selectedTabs;
...@@ -1215,6 +1216,132 @@ void main() { ...@@ -1215,6 +1216,132 @@ void main() {
expect(find.text('Content 2'), findsNothing); expect(find.text('Content 2'), findsNothing);
expect(find.text('Content 3'), findsNothing); expect(find.text('Content 3'), findsNothing);
}); });
group('Android Predictive Back', () {
bool? lastFrameworkHandlesBack;
setUp(() {
// Initialize to false. Because this uses a static boolean internally, it
// is not reset between tests or calls to pumpWidget. Explicitly setting
// it to false before each test makes them behave deterministically.
SystemNavigator.setFrameworkHandlesBack(false);
lastFrameworkHandlesBack = null;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
if (methodCall.method == 'SystemNavigator.setFrameworkHandlesBack') {
expect(methodCall.arguments, isA<bool>());
lastFrameworkHandlesBack = methodCall.arguments as bool;
}
return;
});
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, null);
SystemNavigator.setFrameworkHandlesBack(true);
});
testWidgets('System back navigation inside of tabs', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: MediaQuery(
data: const MediaQueryData(
viewInsets: EdgeInsets.only(bottom: 200),
),
child: CupertinoTabScaffold(
tabBar: _buildTabBar(),
tabBuilder: (BuildContext context, int index) {
return CupertinoTabView(
builder: (BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('Page 1 of tab ${index + 1}'),
),
child: Center(
child: CupertinoButton(
child: const Text('Next page'),
onPressed: () {
Navigator.of(context).push(
CupertinoPageRoute<void>(
builder: (BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('Page 2 of tab ${index + 1}'),
),
child: Center(
child: CupertinoButton(
child: const Text('Back'),
onPressed: () {
Navigator.of(context).pop();
},
),
),
);
},
),
);
},
),
),
);
},
);
},
),
),
),
);
expect(find.text('Page 1 of tab 1'), findsOneWidget);
expect(find.text('Page 2 of tab 1'), findsNothing);
expect(lastFrameworkHandlesBack, isFalse);
await tester.tap(find.text('Next page'));
await tester.pumpAndSettle();
expect(find.text('Page 1 of tab 1'), findsNothing);
expect(find.text('Page 2 of tab 1'), findsOneWidget);
expect(lastFrameworkHandlesBack, isTrue);
await simulateSystemBack();
await tester.pumpAndSettle();
expect(find.text('Page 1 of tab 1'), findsOneWidget);
expect(find.text('Page 2 of tab 1'), findsNothing);
expect(lastFrameworkHandlesBack, isFalse);
await tester.tap(find.text('Next page'));
await tester.pumpAndSettle();
expect(find.text('Page 1 of tab 1'), findsNothing);
expect(find.text('Page 2 of tab 1'), findsOneWidget);
expect(lastFrameworkHandlesBack, isTrue);
await tester.tap(find.text('Tab 2'));
await tester.pumpAndSettle();
expect(find.text('Page 1 of tab 2'), findsOneWidget);
expect(find.text('Page 2 of tab 2'), findsNothing);
expect(lastFrameworkHandlesBack, isFalse);
await tester.tap(find.text('Tab 1'));
await tester.pumpAndSettle();
expect(find.text('Page 1 of tab 1'), findsNothing);
expect(find.text('Page 2 of tab 1'), findsOneWidget);
expect(lastFrameworkHandlesBack, isTrue);
await simulateSystemBack();
await tester.pumpAndSettle();
expect(find.text('Page 1 of tab 1'), findsOneWidget);
expect(find.text('Page 2 of tab 1'), findsNothing);
expect(lastFrameworkHandlesBack, isFalse);
await tester.tap(find.text('Tab 2'));
await tester.pumpAndSettle();
expect(find.text('Page 1 of tab 2'), findsOneWidget);
expect(find.text('Page 2 of tab 2'), findsNothing);
expect(lastFrameworkHandlesBack, isFalse);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }),
skip: kIsWeb, // [intended] frameworkHandlesBack not used on web.
);
});
} }
CupertinoTabBar _buildTabBar({ int selectedTab = 0 }) { CupertinoTabBar _buildTabBar({ int selectedTab = 0 }) {
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
/// Simulates a system back, like a back gesture on Android.
///
/// Sends the same platform channel message that the engine sends when it
/// receives a system back.
Future<void> simulateSystemBack() {
return TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/navigation',
const JSONMessageCodec().encodeMessage(<String, dynamic>{
'method': 'popRoute',
}),
(ByteData? _) {},
);
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment