在 Flutter 中,所有将新元素推送到导航堆栈的
Navigator
函数都会返回 Future
,因为调用者可以等待执行并处理结果。
我大量使用它。 G。将用户重定向(通过
push()
)到新页面时。当用户完成与该页面的交互时,我有时希望原始页面也pop()
:
onTap: () async {
await Navigator.of(context).pushNamed(
RoomAddPage.routeName,
arguments: room,
);
Navigator.of(context).pop();
},
一个常见的例子是使用带有敏感操作按钮的底部工作表(例如删除实体)。当用户单击该按钮时,会打开“另一个”底部表单,要求确认。当用户确认时,确认对话框以及打开确认底部表单的第一个底部表单将被关闭。 所以基本上底部工作表内“删除”按钮的
onTap
属性如下所示:
onTap: () async {
bool deleteConfirmed = await showModalBottomSheet<bool>(/* open the confirm dialog */);
if (deleteConfirmed) {
Navigator.of(context).pop();
}
},
这种方法一切都很好。我遇到的唯一问题是 linter 发出警告:use_build_context_synchronously
,因为我在完成 BuildContext
函数后使用相同的
async
。我忽略/暂停此警告是否安全?但是,我如何使用相同的 BuildContext
的后续代码等待导航堆栈上的推送操作?有合适的替代方案吗?一定有可能做到这一点,对吧?
PS:我不能也不想检查mounted
属性,因为我没有使用
StatefulWidget
。即使在无状态小部件中,始终忽略此警告也是不安全的。
这种情况下的解决方法是在异步调用之前使用
context
。例如,找到
Navigator
并将其存储为变量。这样你就可以传递 Navigator
,而不是传递 BuildContext
,如下所示:onPressed: () async {
final navigator = Navigator.of(context); // store the Navigator
await showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('Dialog Title'),
),
);
navigator.pop(); // use the Navigator, not the BuildContext
},
长答案:此警告本质上是提醒您,在异步调用之后,BuildContext
可能不再有效。 BuildContext 无效的原因有多种,例如,原始小部件在等待期间被破坏可能是(主要)原因之一。这就是为什么检查您的有状态小部件是否仍然安装是一个好主意。 但是,我们无法在无状态小部件上检查
mounted
,但这绝对并不意味着它们在等待期间不能被卸载。
如果满足条件,它们也可以被卸载!例如,如果它们的父窗口小部件是有状态的,并且如果它们的父窗口在等待期间触发了重建,并且如果无状态窗口小部件的参数以某种方式更改,或者它的键不同,它将被销毁并重新创建。这将使旧的 BuildContext 无效,如果您尝试使用旧的上下文,将会导致崩溃。 为了演示危险,我创建了一个小项目。在 TestPage(Stateful Widget)中,我每 500 毫秒刷新一次,因此构建函数被频繁调用。然后我制作了两个按钮,两个按钮都打开一个对话框,然后尝试弹出当前页面(就像您在问题中描述的那样)。其中之一在打开对话框之前存储导航器,另一个在异步调用之后危险地使用 BuildContext(就像您在问题中所描述的那样)。单击按钮后,如果您在警报对话框上等待几秒钟,然后退出它(通过单击对话框外的任意位置),则更安全的按钮将按预期工作并弹出当前页面,而另一个按钮则不会。
打印出的错误是:
[VERBOSE-2:ui_dart_state.cc(209)] 未处理的异常:查找 停用的小部件的祖先是不安全的。此时的状态为 小部件的元素树不再稳定。为了安全地参考 widget 的祖先在其 dispose() 方法中,保存对 通过调用 dependentOnInheritedWidgetOfExactType() 中的祖先 小部件的 didChangeDependency() 方法。 #0 元素._debugCheckStateIsActiveForAncestorLookup。 (包:flutter/src/widgets/framework.dart:4032:9) #1 Element._debugCheckStateIsActiveForAncestorLookup (包:flutter/src/widgets/framework.dart:4046:6) #2 Element.findAncestorStateOfType(包:flutter/src/widgets/framework.dart:4093:12) #3 Navigator.of (包:flutter/src/widgets/navigator.dart:2736:40) #4 MyDangerousButton.build。 (包:helloworld/main.dart:114:19)
演示问题的完整源代码:
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home Page')),
body: Center(
child: ElevatedButton(
child: Text('Open Test Page'),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => TestPage()),
);
},
),
),
);
}
}
class TestPage extends StatefulWidget {
@override
State<TestPage> createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
late final Timer timer;
@override
void initState() {
super.initState();
timer = Timer.periodic(Duration(milliseconds: 500), (timer) {
setState(() {});
});
}
@override
void dispose() {
timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final time = DateTime.now().millisecondsSinceEpoch;
return Scaffold(
appBar: AppBar(title: Text('Test Page')),
body: Center(
child: Column(
children: [
Text('Current Time: $time'),
MySafeButton(key: UniqueKey()),
MyDangerousButton(key: UniqueKey()),
],
),
),
);
}
}
class MySafeButton extends StatelessWidget {
const MySafeButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ElevatedButton(
child: Text('Open Dialog Then Pop Safely'),
onPressed: () async {
final navigator = Navigator.of(context);
await showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('Dialog Title'),
),
);
navigator.pop();
},
);
}
}
class MyDangerousButton extends StatelessWidget {
const MyDangerousButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ElevatedButton(
child: Text('Open Dialog Then Pop Dangerously'),
onPressed: () async {
await showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('Dialog Title'),
),
);
Navigator.of(context).pop();
},
);
}
}
您现在可以在 StatelessWidget 上使用
mounted
。此解决方案不会显示 linter 警告:
onTap: () async {
bool deleteConfirmed = await showModalBottomSheet<bool>(/* open the confirm dialog */);
if (mounted && deleteConfirmed) {
Navigator.of(context).pop();
}
},
或者,如果在小部件之外,您可以使用
context.mounted
。
onTap: () async {
final navigatorContext = Navigator.of(context);
await Navigator.of(context).pushNamed(
RoomAddPage.routeName,
arguments: room,
);
navigatorContext.pop();
},
或者
onTap: () async {
await Navigator.of(context).pushNamed(
RoomAddPage.routeName,
arguments: room,
);
if (!context.mounted) return;
Navigator.of(context).pop();
},
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
final scaffoldKey = GlobalKey<ScaffoldState>(); // initialize globalkey
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
key: scaffoldKey, // use key property of scaffold widget and declare initialized key in it.
appBar: AppBar(title: Text('Home Page')),
body: Center(
child: ElevatedButton(
child: Text('Open Test Page'),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => TestPage()),
);
}
),),);
}
}
class TestPage extends StatefulWidget {
@override
State<TestPage> createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
late final Timer timer;
@override
void initState() {
super.initState();
timer = Timer.periodic(Duration(milliseconds: 500), (timer) {
setState(() {});
});
}
@override
void dispose() {
timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final time = DateTime.now().millisecondsSinceEpoch;
return Scaffold(
appBar: AppBar(title: Text('Test Page')),
body: Center(
child: Column(
children: [
Text('Current Time: $time'),
MySafeButton(),
],), ),);
}}
class MySafeButton extends StatelessWidget {
const MySafeButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ElevatedButton(
child: Text('Open Dialog Then Pop Safely'),
onPressed: () async {
await showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('Dialog Title'),),);
Navigator.pop(scaffoldKey.currentContext!) // using of globalkey context instead of BuildContext
},); }