异步等待 Navigator.push() - 出现 linter 警告:use_build_context_synchronously
Waiting asynchronously for Navigator.push() - linter warning appears: use_build_context_synchronously
在 Flutter 中,所有 Navigator
将新元素推送到导航堆栈的函数 return a Future
因为调用者可以等待执行并处理结果.
我大量使用它 e。 G。将用户(通过 push()
)重定向到新页面时。当用户完成与该页面的交互时,我有时希望原始页面也 pop()
:
onTap: () async {
await Navigator.of(context).pushNamed(
RoomAddPage.routeName,
arguments: room,
);
Navigator.of(context).pop();
},
一个常见的例子是底部 sheet 与具有敏感操作(如删除实体)的按钮的用法。当用户单击该按钮时,另一个 底部sheet 将打开,要求确认。当用户确认时,将关闭确认对话框,以及打开确认底部 sheet.
的第一个底部 sheet
所以基本上 onTap
属性 底部 sheet 内的删除按钮看起来像这样:
onTap: () async {
bool deleteConfirmed = await showModalBottomSheet<bool>(/* open the confirm dialog */);
if (deleteConfirmed) {
Navigator.of(context).pop();
}
},
这种方法一切都很好。我遇到的唯一问题是 linter 发出警告:use_build_context_synchronously 因为我在完成 async
函数后使用相同的 BuildContext
。
忽略/暂停此警告对我来说安全吗?但是我如何使用相同的 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 毫秒刷新一次,因此构建函数被频繁调用。然后我做了 2 个按钮,都打开一个对话框然后尝试弹出当前页面(就像你在问题中描述的那样)。其中一个在打开对话框之前存储 Navigator ,另一个在异步调用之后危险地使用 BuildContext (就像您在问题中描述的那样)。单击按钮后,如果您在警告对话框上等待几秒钟,然后退出(通过单击对话框外的任意位置),安全按钮会按预期工作并弹出当前页面,而另一个按钮则不会。
它打印出来的错误是:
[VERBOSE-2:ui_dart_state.cc(209)] Unhandled Exception: Looking up a
deactivated widget's ancestor is unsafe. At this point the state of
the widget's element tree is no longer stable. To safely refer to a
widget's ancestor in its dispose() method, save a reference to the
ancestor by calling dependOnInheritedWidgetOfExactType() in the
widget's didChangeDependencies() method.
#0 Element._debugCheckStateIsActiveForAncestorLookup. (package:flutter/src/widgets/framework.dart:4032:9)
#1 Element._debugCheckStateIsActiveForAncestorLookup (package:flutter/src/widgets/framework.dart:4046:6)
#2 Element.findAncestorStateOfType (package:flutter/src/widgets/framework.dart:4093:12)
#3 Navigator.of (package:flutter/src/widgets/navigator.dart:2736:40)
#4 MyDangerousButton.build. (package: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();
},
);
}
}
在 Flutter 中,所有 Navigator
将新元素推送到导航堆栈的函数 return a Future
因为调用者可以等待执行并处理结果.
我大量使用它 e。 G。将用户(通过 push()
)重定向到新页面时。当用户完成与该页面的交互时,我有时希望原始页面也 pop()
:
onTap: () async {
await Navigator.of(context).pushNamed(
RoomAddPage.routeName,
arguments: room,
);
Navigator.of(context).pop();
},
一个常见的例子是底部 sheet 与具有敏感操作(如删除实体)的按钮的用法。当用户单击该按钮时,另一个 底部sheet 将打开,要求确认。当用户确认时,将关闭确认对话框,以及打开确认底部 sheet.
的第一个底部 sheet所以基本上 onTap
属性 底部 sheet 内的删除按钮看起来像这样:
onTap: () async {
bool deleteConfirmed = await showModalBottomSheet<bool>(/* open the confirm dialog */);
if (deleteConfirmed) {
Navigator.of(context).pop();
}
},
这种方法一切都很好。我遇到的唯一问题是 linter 发出警告:use_build_context_synchronously 因为我在完成 async
函数后使用相同的 BuildContext
。
忽略/暂停此警告对我来说安全吗?但是我如何使用相同的 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 毫秒刷新一次,因此构建函数被频繁调用。然后我做了 2 个按钮,都打开一个对话框然后尝试弹出当前页面(就像你在问题中描述的那样)。其中一个在打开对话框之前存储 Navigator ,另一个在异步调用之后危险地使用 BuildContext (就像您在问题中描述的那样)。单击按钮后,如果您在警告对话框上等待几秒钟,然后退出(通过单击对话框外的任意位置),安全按钮会按预期工作并弹出当前页面,而另一个按钮则不会。
它打印出来的错误是:
[VERBOSE-2:ui_dart_state.cc(209)] Unhandled Exception: Looking up a deactivated widget's ancestor is unsafe. At this point the state of the widget's element tree is no longer stable. To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method. #0 Element._debugCheckStateIsActiveForAncestorLookup. (package:flutter/src/widgets/framework.dart:4032:9) #1 Element._debugCheckStateIsActiveForAncestorLookup (package:flutter/src/widgets/framework.dart:4046:6) #2 Element.findAncestorStateOfType (package:flutter/src/widgets/framework.dart:4093:12) #3 Navigator.of (package:flutter/src/widgets/navigator.dart:2736:40) #4 MyDangerousButton.build. (package: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();
},
);
}
}