异步等待 Navigator.push() - 出现 linter 警告:use_build_context_synchronously

问题描述 投票:0回答:4

在 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
    

flutter dart asynchronous lint flutter-navigation
4个回答
128
投票
简短回答:

即使在无状态小部件中,始终忽略此警告也是不安全的。

这种情况下的解决方法是在异步调用之前使用

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(); }, ); } }



12
投票
颤动≥3.7答案:

您现在可以在 StatelessWidget 上使用

mounted

。此解决方案不会显示 linter 警告:

onTap: () async {
  bool deleteConfirmed = await showModalBottomSheet<bool>(/* open the confirm dialog */);
  if (mounted && deleteConfirmed) {
    Navigator.of(context).pop();
  }
},

或者,如果在小部件之外,您可以使用 
context.mounted

    


1
投票

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(); },



0
投票

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

},);
}

© www.soinside.com 2019 - 2024. All rights reserved.