Sch*_*ken 53 asynchronous lint dart flutter flutter-navigation
在 Flutter 中,所有Navigator
将新元素推送到导航堆栈的函数都会返回 a Future
,因为调用者可以等待执行并处理结果。
我大量使用它,例如将用户(通过push()
)重定向到新页面时。当用户完成与该页面的交互时,我有时还希望原始页面pop()
:
onTap: () async {
await Navigator.of(context).pushNamed(
RoomAddPage.routeName,
arguments: room,
);
Navigator.of(context).pop();
},
Run Code Online (Sandbox Code Playgroud)
一个常见的示例是使用带有敏感操作(例如删除实体)的按钮的底部工作表。当用户单击该按钮时,将打开另一个底部工作表,要求确认。当用户确认时,确认对话框以及打开确认底部表单的第一个底部表单将被关闭。
所以基本上onTap
底部工作表内的“删除”按钮的属性如下所示:
onTap: () async {
bool deleteConfirmed = await showModalBottomSheet<bool>(/* open the confirm dialog */);
if (deleteConfirmed) {
Navigator.of(context).pop();
}
},
Run Code Online (Sandbox Code Playgroud)
这种方法一切都很好。我遇到的唯一问题是 linter 发出警告:use_build_context_synchronouslyBuildContext
,因为我在函数完成后使用相同的警告async
。
我忽略/暂停此警告是否安全?但是,我如何使用相同的后续代码等待导航堆栈上的推送操作BuildContext
?有合适的替代方案吗?一定有可能做到这一点,对吧?
PS:我不能也不想检查该mounted
属性,因为我没有使用StatefulWidget
.
use*_*613 133
简短回答:
即使在无状态小部件中,始终忽略此警告也是不安全的。
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
},
Run Code Online (Sandbox Code Playgroud)
长答案:
此警告本质上是提醒您,在异步调用之后,BuildContext可能不再有效。BuildContext 无效的原因有多种,例如,原始小部件在等待期间被破坏可能是(主要)原因之一。这就是为什么检查有状态小部件是否仍然安装是一个好主意。
但是,我们无法检查mounted
无状态小部件,但这绝对并不意味着它们在等待期间不能被卸载。如果满足条件,它们也可以被卸载!例如,如果它们的父窗口小部件是有状态的,并且如果它们的父窗口在等待期间触发了重建,并且如果无状态窗口小部件的参数以某种方式发生更改,或者其密钥不同,则它将被销毁并重新创建。这将使旧的 BuildContext 无效,并且如果您尝试使用旧的上下文,将会导致崩溃。
为了演示这种危险,我创建了一个小项目。在 TestPage(Stateful Widget)中,我每 500 毫秒刷新一次,因此构建函数被频繁调用。然后我制作了两个按钮,两个按钮都打开一个对话框,然后尝试弹出当前页面(就像您在问题中描述的那样)。其中之一在打开对话框之前存储导航器,另一个在异步调用之后危险地使用 BuildContext(就像您在问题中所描述的那样)。单击按钮后,如果您在警报对话框上等待几秒钟,然后退出它(通过单击对话框外的任意位置),则更安全的按钮将按预期工作并弹出当前页面,而另一个按钮则不会。
它打印出来的错误是:
[VERBOSE-2:ui_dart_state.cc(209)] 未处理的异常:查找已停用小部件的祖先是不安全的。此时小部件的元素树的状态不再稳定。要在 widget 的 dispose() 方法中安全地引用它的祖先,请通过在 widget 的 didChangeDependency() 方法中调用 dependentOnInheritedWidgetOfExactType() 来保存对祖先的引用。#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 (package: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();
},
);
}
}
Run Code Online (Sandbox Code Playgroud)
nde*_*nou 13
颤动\xe2\x89\xa5 3.7答案:
\n您现在可以mounted
在 StatelessWidget 上使用。此解决方案不会显示 linter 警告:
onTap: () async {\n bool deleteConfirmed = await showModalBottomSheet<bool>(/* open the confirm dialog */);\n if (mounted && deleteConfirmed) {\n Navigator.of(context).pop();\n }\n},\n
Run Code Online (Sandbox Code Playgroud)\n或者,您可以context.mounted
在小部件之外使用 if 。