概述

#

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

背景

#

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

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

从历史上看,Flutter 并未检测到此类不匹配。这有时会导致隐晦且难以调试的问题。例如,键盘输入的 callback 可能会使用一个无法访问其预期 zoneValues 的 Zone 来调用。根据我们的经验,大多数(如果不是全部)以不保证 Flutter 框架的所有部分都在同一个 Zone 中运行的方式使用 Zone 的代码都存在一些潜在错误。这些错误通常看起来与 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:当 ensureInitialized 在与 runApp 不同的 Zone 中调用时,Flutter 框架不发出警告

相关 PR

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