概述

#

从 Flutter 3.10 开始,框架会检测使用 Zones 时出现的失配,并在调试构建中向控制台报告。

背景

#

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

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

过去,Flutter 并没有检测到这种失配。这有时会导致晦涩难懂且难以调试的问题。例如,键盘输入的事件回调可能使用一个无法访问其期望的 zoneValues 的 Zone 来调用。根据我们的经验,绝大多数(如果不是全部)不保证 Flutter 框架所有部分都在同一个 Zone 中工作的 Zones 使用方式的代码都存在一些潜在的 bug。通常这些 bug 与 Zones 的使用无关。

为了帮助那些意外违反此约束的开发者,从 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

迁移指南

#

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

如果移除 Zones 不可行(例如,因为应用程序依赖于一个依赖 Zones 进行配置的第三方库),那么对 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 调用中,以提供适当的值。

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

时间线

#

已合并版本:3.9.0-9.0.pre
稳定版本:3.10.0

参考资料

#

API 文档

相关问题

  • Issue 94123: Flutter framework does not warn when ensureInitialized is called in a different zone than runApp

相关 PR

  • PR 122836: Assert that runApp is called in the same zone as binding.ensureInitialized