简介
PopScope 是 Flutter 中用于管理页面导航返回行为的一个组件,替代了旧的 WillPopScope。它可以拦截页面返回操作(例如用户点击系统的返回按钮或调用 Navigator.pop),能在页面退出前执行自定义逻辑,可以用于返回前显示确认对话框、返回前保存数据、处理复杂的导航逻辑(如注册步骤)等。
PopScope
PopScope的构造函数有4个参数
-
key标识 Widget 的唯一性 -
child为包裹的子 Widget(必填) -
canPop控制当前路由是否允许被弹出(即是否允许页面返回),默认为true,当canPop为false时,阻止页面直接弹出,触发onPopInvokedWithResult回调。
当为true时,允许页面正常弹出 -
onPopInvokedWithResult当路由的返回操作被触发时调用的回调函数
canPop
当canPop为true时,可以通过如 AppBar 的返回箭头,系统的返回按钮或返回手势来返回,当canPop为false时这些都会被禁用。
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(home: const HomePage());
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PopScope的CanPop示例'), centerTitle: true),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("这是主页", style: TextStyle(fontSize: 40)),
SizedBox(height: 16),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const Num1Page()),
);
},
child: const Text('前往页面1', style: TextStyle(fontSize: 25)),
),
],
),
),
);
}
}
class Num1Page extends StatefulWidget {
const Num1Page({super.key});
State<Num1Page> createState() => _Num1PageState();
}
class _Num1PageState extends State<Num1Page> {
bool _canPopState = true;
Widget build(BuildContext context) {
return PopScope<String>(
canPop: _canPopState,
onPopInvokedWithResult: (didPop, result) {
print("onPopInvokedWithResult回调函数调用");
},
child: Scaffold(
appBar: AppBar(title: const Text('页面1'), centerTitle: true),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("这是页面1", style: TextStyle(fontSize: 40)),
Text("现在CanPop的属性为$_canPopState", style: TextStyle(fontSize: 20)),
ElevatedButton(
onPressed: () {
setState(() {
_canPopState = !_canPopState;
});
},
child: const Text('切换CanPop属性', style: TextStyle(fontSize: 25)),
),
SizedBox(height: 16),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('前往主页', style: TextStyle(fontSize: 25)),
),
],
),
),
),
);
}
}
但是会发现两个问题:
- 无论
canPop的值为什么onPopInvokedWithResult的回调依旧会触发。
onPopInvokedWithResult属性的类型为
PopInvokedWithResultCallback<T>? onPopInvokedWithResult
它有几个特点:
- 事后回调: 这个回调在返回操作已经发生之后被调用,无法阻止返回操作的发生。
- 无法阻止返回: 在这个方法被调用时,返回操作已经完成,无法再阻止。如果需要阻止返回,应该使用 canPop 属性。
- 总是被调用: 即使返回操作被取消,这个回调仍然会被调用。
- 包含结果数据: 携带返回的结果数据(泛型类型 T)
- 返回主页按钮也可以正常返回。
canPop确实可以控制页面是否能够被弹出,但它只影响 系统导航事件(如 Android 的返回键或 iOS 的滑动返回手势)。而Navigator.pop是一个 程序性导航调用,它直接触发页面弹出,且不会直接受canPop的约束。二者的目的不同,PopScope为的是控制非预期的页面退出,而Navigator等方法是开发者主动调用的,开发者已经明确知道需要弹出页面。
onPopInvokedWithResult
onPopInvokedWithResult在触发回调时会传递两个参数
bool didPop和 T? result
void _callPopInvoked(bool didPop, T? result) {
if (onPopInvokedWithResult != null) {
onPopInvokedWithResult!(didPop, result);
return;
}
onPopInvoked?.call(didPop);
}
didPop: 表示页面是否真的被弹出(true 表示已弹出,false 表示未弹出,比如被拦截)
result: 表示弹出页面时可能返回的结果(比如通过 Navigator.pop(context, result) 返回的数据)
下面是Demo例子
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(home: const HomePage());
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PopScope 的 canPop 示例'), centerTitle: true),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("这是主页", style: TextStyle(fontSize: 40)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const Num1Page()),
);
},
child: const Text('前往页面1', style: TextStyle(fontSize: 25)),
),
],
),
),
);
}
}
class Num1Page extends StatefulWidget {
const Num1Page({super.key});
State<Num1Page> createState() => _Num1PageState();
}
class _Num1PageState extends State<Num1Page> {
bool _canPopState = true;
// 自定义返回方法,检查 canPop 逻辑
void _handlePop(BuildContext context) {
print('处理返回请求,canPopState: $_canPopState');
if (_canPopState) {
print('canPopState 为 true,直接返回');
Navigator.pop(context, '页面已关闭');
} else {
print('canPopState 为 false,显示确认对话框');
_showConfirmationDialog(context);
}
}
// 显示确认对话框
Future<void> _showConfirmationDialog(BuildContext context) async {
if (!context.mounted) return;
final shouldPop = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('确认返回'),
content: const Text('您确定要返回主页吗?'),
actions: [
TextButton(
onPressed: () {
print('用户选择取消');
Navigator.pop(dialogContext, false);
},
child: const Text('取消'),
),
TextButton(
onPressed: () {
print('用户选择返回');
Navigator.pop(dialogContext, true);
},
child: const Text('返回'),
),
],
),
);
print('对话框结果: $shouldPop');
if (shouldPop == true && context.mounted) {
print('手动返回页面并附带结果');
Navigator.pop(context, '页面已放弃');
}
}
Widget build(BuildContext context) {
return PopScope<String>(
canPop: _canPopState,
onPopInvokedWithResult: (bool didPop, String? result) async {
print('onPopInvokedWithResult 调用,canPop: $_canPopState, didPop: $didPop, result: $result');
if (didPop) {
print('页面已返回,无需进一步操作');
return;
}
// 当 canPop = false 时,显示对话框
if (!_canPopState && context.mounted) {
await _showConfirmationDialog(context);
}
},
child: Scaffold(
appBar: AppBar(title: const Text('页面1'), centerTitle: true),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("这是页面1", style: TextStyle(fontSize: 40)),
Text("当前 canPop 属性为: $_canPopState", style: const TextStyle(fontSize: 20)),
ElevatedButton(
onPressed: () {
setState(() {
_canPopState = !_canPopState;
print('切换 canPopState 为: $_canPopState');
});
},
child: const Text('切换 canPop 属性', style: TextStyle(fontSize: 25)),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
_handlePop(context); // 使用自定义返回逻辑
},
child: const Text('返回主页', style: TextStyle(fontSize: 25)),
),
],
),
),
),
);
}
}
我们对canPop的不同值进行测试并观察didPop的值,结果如下:
- 当
canPop=true时,使用AppBar的箭头或系统手势返回时,didPop为true - 当
canPop=false时,使用AppBar的箭头或系统手势返回中,didPop为false,点击对话框返回后,didPop为true - 当
canPop=true时,使用Navigator.pop返回时,didPop为true - 当
canPop=false时,使用Navigator.pop返回时,didPop为true
一般情况下当
canPop=true时,didPop应该也为true,canPop=false时,didPop应该也为false。但是为什么2、4反而为true呢
2的原因是
onPopInvokedWithResult的特性有始终调用的特性,打印语句在他调用时确实还没弹出,但是调用Navigator.pop使页面弹出了,didPop就为false了。
4的原因是
didPop是指页面是否真的弹出,而调用Navigator.pop的页面弹出,canPop管不了,所以页面是真的弹出了,didPop就为false了。
我们注意到在测试结果2时,
onPopInvokedWithResult实际上调用了2次,这是因为onPopInvokedWithResult在任何尝试离开当前页面的操作发生后都会被调用,无论这个操作是否成功完成了页面返回。因此Navigator.pop的返回也会触发。
canPop 和 didPop 的异常情况
-
问题:当
canPop = false时,Navigator.pop仍会弹出页面,导致didPop =true,绕过canPop。 -
原因:
canPop仅控制系统导航(返回键、AppBar 箭头),Navigator.pop是程序性调用,不受约束。 -
解决方案:
- 使用自定义方法(如
_handlePop)检查canPop状态,拦截Navigator.pop。 - 在
onPopInvokedWithResult中记录手动pop的情况,执行额外逻辑。
- 使用自定义方法(如
result
result 来自 Navigator.pop(context, result)
可以将当前页面的数据或状态传递给调用页面或其他监听返回的组件。
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(home: const HomePage());
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
String _result = '暂无返回结果'; // 存储 Num1Page 的返回结果
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PopScope的CanPop示例'), centerTitle: true),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("这是主页", style: TextStyle(fontSize: 40)),
const SizedBox(height: 16),
Text("页面1返回结果: $_result", style: const TextStyle(fontSize: 20)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () async {
final result = await Navigator.push<String>(
context,
MaterialPageRoute(builder: (context) => const Num1Page()),
);
if (context.mounted) {
setState(() {
_result = result ?? '无结果';
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('页面1返回结果: ${result ?? '无结果'}')),
);
}
},
child: const Text('前往页面1', style: TextStyle(fontSize: 25)),
),
],
),
),
);
}
}
class Num1Page extends StatefulWidget {
const Num1Page({super.key});
State<Num1Page> createState() => _Num1PageState();
}
class _Num1PageState extends State<Num1Page> {
bool _canPopState = true;
bool _isShowingDialog = false;
String _inputText = ''; // 存储 TextField 输入
// 自定义返回方法
void _handlePop(BuildContext context) {
print('处理返回请求,canPopState: $_canPopState, inputText: $_inputText');
if (_canPopState) {
print('canPopState 为 true,直接返回');
Navigator.pop(context, _inputText.isEmpty ? '无输入' : _inputText);
} else if (!_isShowingDialog) {
print('canPopState 为 false,显示确认对话框');
_showConfirmationDialog(context);
}
}
// 显示确认对话框
Future<void> _showConfirmationDialog(BuildContext context) async {
if (!context.mounted || _isShowingDialog) return;
_isShowingDialog = true;
final shouldPop = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('确认返回'),
content: const Text('您有未保存的输入,确定要返回主页吗?'),
actions: [
TextButton(
onPressed: () {
print('用户选择取消');
Navigator.pop(dialogContext, false);
},
child: const Text('取消'),
),
TextButton(
onPressed: () {
print('用户选择返回');
Navigator.pop(dialogContext, true);
},
child: const Text('返回'),
),
],
),
);
_isShowingDialog = false;
print('对话框结果: $shouldPop');
if (shouldPop == true && context.mounted) {
print('手动返回页面并附带结果');
Navigator.pop(context, _inputText.isEmpty ? '无输入(放弃)' : '放弃: $_inputText');
}
}
Widget build(BuildContext context) {
return PopScope<String>(
canPop: _canPopState,
onPopInvokedWithResult: (bool didPop, String? result) async {
print('onPopInvokedWithResult 调用,canPop: $_canPopState, didPop: $didPop, result: $result');
if (didPop) {
print('页面已返回,无需进一步操作');
return;
}
if (!_canPopState && context.mounted && !_isShowingDialog) {
await _showConfirmationDialog(context);
}
},
child: Scaffold(
appBar: AppBar(title: const Text('页面1'), centerTitle: true),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("这是页面1", style: TextStyle(fontSize: 40)),
Text("当前 canPop 属性为: $_canPopState", style: const TextStyle(fontSize: 20)),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextField(
decoration: const InputDecoration(
labelText: '请输入内容',
border: OutlineInputBorder(),
),
onChanged: (value) {
setState(() {
_inputText = value;
_canPopState = value.isEmpty; // 非空时阻止返回
print('输入变化,inputText: $_inputText, canPopState: $_canPopState');
});
},
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
setState(() {
_canPopState = !_canPopState;
print('手动切换 canPopState 为: $_canPopState');
});
},
child: const Text('切换 canPop 属性', style: TextStyle(fontSize: 25)),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
_handlePop(context);
},
child: const Text('返回主页', style: TextStyle(fontSize: 25)),
),
],
),
),
),
);
}
}
context.mounted
context.mounted 是 BuildContext 的一个bool属性,用于检查当前 Widget 是否仍挂载在 Widget 树中。
一般使用上下文相关方法前,使用context.mounted 属性检查 Widget 是否仍有效(防止某些代码可能在页面关闭后仍在执行),避免错误。
在异步操作(如 await showDialog、网络请求或 Future.delayed)后,Widget 可能已被销毁(如页面关闭)。
如果此时使用无效的 BuildContext 调用方法(如 Navigator.pop 或 ScaffoldMessenger.of(context).showSnackBar),会导致运行时错误。
总结
PopScope 的适用场景
- 表单页面:用户输入数据后,防止意外返回导致数据丢失。
- 向导流程:在多步骤导航中,确保用户完成必要步骤。
- 游戏或交互页面:退出前提示保存进度或确认退出。