概述

#

從 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