跳至主要内容

"区域不匹配" 消息

摘要

#

从 Flutter 3.10 开始,框架会在使用 Zone 时检测不匹配的情况,并在调试版本中将其报告到控制台。

背景

#

Zone 是 Dart 中用于管理回调的机制。虽然主要用于在测试中覆盖 printTimer 逻辑,以及捕获测试中的错误,但有时也用于将全局变量的作用域限制到应用程序的某些部分。

Flutter 要求(并且一直要求)所有框架代码都在同一个 Zone 中运行。值得注意的是,这意味着对 WidgetsFlutterBinding.ensureInitialized() 的调用应该与对 runApp() 的调用在同一个 Zone 中进行。

从历史上看,Flutter 并没有检测到这种不匹配。这有时会导致难以理解和调试的问题。例如,键盘输入的回调可能会使用一个 Zone 调用,而该 Zone 无法访问它期望的 zoneValues。根据我们的经验,大多数(如果不是全部)以不保证 Flutter 框架的所有部分都在同一个 Zone 中运行的方式使用 Zone 的代码都存在一些潜在的 bug。通常,这些 bug 看起来与 Zone 的使用无关。

为了帮助意外违反此不变性的开发者,从 Flutter 3.10 开始,在检测到不匹配时,调试版本中会打印一条非致命警告。警告如下所示

════════ Exception caught by Flutter framework ════════════════════════════════════
The following assertion was thrown during runApp:
Zone mismatch.

The Flutter bindings were initialized in a different zone than is now being used.
This will likely cause confusion and bugs as any zone-specific configuration will
inconsistently use the configuration of the original binding initialization zone or
this zone based on hard-to-predict factors such as which zone was active when a
particular callback was set.
It is important to use the same zone when calling `ensureInitialized` on the
binding as when calling `runApp` later.
To make this warning fatal, set BindingBase.debugZoneErrorsAreFatal to true before
the bindings are initialized (i.e. as the first statement in `void main() { }`).
[...]
═══════════════════════════════════════════════════════════════════════════════════

可以通过将 BindingBase.debugZoneErrorsAreFatal 设置为 true 来将此警告设为致命警告。此标志可能会在 Flutter 的未来版本中更改为默认值为 true

迁移指南

#

消除此消息的最佳方法是从应用程序中移除 Zone 的使用。Zone 非常难以调试,因为它们本质上是全局变量,并且破坏了封装性。最佳实践是避免使用全局变量和 Zone。

如果移除 Zone 不是一种选择(例如,因为应用程序依赖于一个依赖于 Zone 进行配置的第三方库),那么应该将各种对 Flutter 框架的调用移动到同一个 Zone 中。通常,这意味着将对 WidgetsFlutterBinding.ensureInitialized() 的调用移动到与对 runApp() 的调用相同的闭包中。

当运行 runApp 的 Zone 使用从插件获得的 zoneValues 进行初始化时(这需要调用 WidgetsFlutterBinding.ensureInitialized()),这可能会很麻烦。

在这种情况下,一种选择是在 zoneValues 中放置一个可变对象,并在值可用后使用该值更新该对象。

dart
import 'dart:async';
import 'package:flutter/material.dart';

class Mutable<T> {
  Mutable(this.value);
  T value;
}

void main() {
  var myValue = Mutable<double>(0.0);
  Zone.current.fork(
    zoneValues: {
      'myKey': myValue,
    }
  ).run(() {
    WidgetsFlutterBinding.ensureInitialized();
    var newValue = ...; // obtain value from plugin
    myValue.value = newValue; // update value in Zone
    runApp(...);
  });
}

在需要使用 myKey 的代码中,可以通过 Zone.current['myKey'].value 间接获取它。

当此类解决方案不起作用,因为第三方依赖项需要为特定 zoneValues 密钥使用特定类型时,可以将对依赖项的所有调用包装在提供合适值的 Zone 调用中。

强烈建议以这种方式使用 Zone 的包迁移到更易于维护的解决方案。

时间线

#

包含在版本中:3.9.0-9.0.pre
稳定版本:3.10.0

参考文献

#

API 文档

相关问题

  • 问题 94123:Flutter 框架在 ensureInitialized 在与 runApp 不同的 Zone 中调用时不会发出警告

相关 PR

  • PR 122836:断言 runApp 在与 binding.ensureInitialized 相同的 Zone 中调用