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

Posted

技术标签:

【中文标题】异步等待 Navigator.push() - 出现 linter 警告:use_build_context_synchronously【英文标题】:Waiting asynchronously for Navigator.push() - linter warning appears: use_build_context_synchronously 【发布时间】:2021-11-26 16:17:26 【问题描述】:

在 Flutter 中,所有将新元素推送到导航堆栈的 Navigator 函数都会返回 Future,因为调用者可以等待执行并处理结果。

我大量使用它 e. G。将用户(通过push())重定向到新页面时。当用户完成与该页面的交互时,我有时希望原始页面也可以pop()

onTap: () async 
  await Navigator.of(context).pushNamed(
    RoomAddPage.routeName,
    arguments: room,
  );

  Navigator.of(context).pop();
,

一个常见的例子是使用带有敏感操作按钮的底部工作表(如删除实体)。当用户点击按钮时,另一个底部工作表被打开,要求确认。当用户确认时,确认对话框以及打开确认底页的第一个底页将被关闭。

所以基本上底部表格内的 DELETE 按钮的onTap 属性如下所示:

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

【问题讨论】:

【参考方案1】:

简答:

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

在这种情况下,一种解决方法是在异步调用之前使用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毫秒刷新一次,所以build函数被频繁调用。然后我做了2个按钮,都打开一个对话框然后尝试弹出当前页面(就像你在问题中描述的那样)。其中一个在打开对话框之前存储导航器,另一个在异步调用之后危险地使用 BuildContext (就像您在问题中描述的那样)。单击按钮后,如果您在警报对话框上等待几秒钟,然后退出它(通过单击对话框外的任何位置),更安全的按钮将按预期工作并弹出当前页面,而另一个按钮则不会。

它打印出来的错误是:

[VERBOSE-2:ui_dart_state.cc(209)] 未处理的异常:查找 停用的小部件的祖先是不安全的。此时的状态 小部件的元素树不再稳定。为了安全地参考 小部件的祖先在其 dispose() 方法中,保存对 祖先通过调用dependOnInheritedWidgetOfExactType() 小部件的 didChangeDependencies() 方法。 #0 元素._debugCheckStateIsActiveForAncestorLookup。 (包: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。 (包: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();
      ,
    );
  

【讨论】:

出色的解释和易于理解的最小示例!谢谢你。赏金是你的:)。但是有一个问题:您将解决方案标记为“快速解决方法”。这是否意味着有更清洁的解决方案? @Schnodderbalken 哈哈,我主要关注的是“快速”这个词——在你的情况下(获取导航器),这已经是一个非常干净的解决方案。然而,我们可能并不总是这么幸运。例如,如果您有一个将 BuildContext 作为参数的自定义函数,则此“解决方法”将不起作用,因此您可能必须进行一些不那么快速的重构或考虑另一种解决方法。顺便说一句,很好的问题,这是我第一次听说这个 lint 规则。希望此 lint 作为默认规则发布后,您的问题可以帮助更多人。 我正在使用名为lint: pub.dev/packages/lint 的社区驱动的lint 包。这个包比官方的包更严格,所以我遇到了这个警告。无论如何,再次感谢你。现在有赏金;)

以上是关于异步等待 Navigator.push() - 出现 linter 警告:use_build_context_synchronously的主要内容,如果未能解决你的问题,请参考以下文章

在 Flutter 中使用 Navigator.push() 时出现黑屏

Dart / Flutter:在没有上下文的页面之间导航(使用 Navigator)(所以没有 Navigator.push?)

每次Navigator.push到下一页时都会调用的回调

FLUTTER:-我需要从Navigator.push()方法之后的方法返回小部件

在 ExpressJS 响应中返回异步等待 try/catch 抛出错误 [重复]

InheritedWidget - 在 navigator.push 之后在 null 上调用 getter