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

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)

  • 出色的解释和易于理解的最小示例!谢谢。赏金是你的:)。但有一个问题:您将您的解决方案标记为“快速解决方法”。这是否意味着有更清洁的解决方案? (7认同)
  • @WSBT如果我在一些 onPressed 回调中需要 2 个异步调用怎么办,例如我需要一个接一个地创建 2 个对话框:所以我有两次 `await showDialog(..)` ,而在第二个中我还需要 BuildContext 来创建对话。我只需要上下文来构建第二个对话框小部件,并且我有异步间隙。那么该怎么办? (5认同)
  • @Schnodderbalken哈哈,我主要关注“快速”这个词 - 在你的情况下(获取导航器),这已经是一个非常干净的解决方案。然而我们可能并不总是这么幸运。例如,如果您有一个接受 BuildContext 作为参数的自定义函数,则此“解决方法”将不起作用,因此您可能必须进行一些不太快速的重构或考虑其他解决方法。顺便说一句,这是一个很好的问题,这是我第一次听说这个 lint 规则。希望一旦此 lint 作为默认规则发布,您的问题可以帮助更多的人。 (4认同)
  • 我想补充一点,linter 可能无法捕获在 Future 的 .then() 或 .whenComplete() 块中异步使用上下文的实例,但在这些块中使用上下文仍然可能会导致错误。 (3认同)
  • 很好的答案!我根据您的示例代码制作了一个飞镖板。https://dartpad.dev/?id=0215a7e3c67347c84450697c824842ef (3认同)

nde*_*nou 13

颤动\xe2\x89\xa5 3.7答案:

\n

您现在可以mounted在 StatelessWidget 上使用。此解决方案不会显示 linter 警告:

\n
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 。

\n

  • 今天,您无法使用语法“if (!context.mounted) return;”而不会出现 linter 警告。这是在这里跟踪的:https://github.com/dart-lang/linter/issues/4007 (6认同)