概述

#

为支持多视图和多窗口,window 单例已被弃用。之前依赖 window 单例的代码需要通过 View.of API 查找要操作的特定视图,或直接与 PlatformDispatcher 交互。

背景

#

最初,Flutter 假定一个应用程序只包含一个视图(即 window),内容可以绘制到其中。在多视图世界中,这个假设不再合理,因此编码了此假设的 API 已被弃用。应用程序和库中依赖这些 API 的部分必须选择要操作的特定视图,并按照本迁移指南中概述的方式迁移到新的多视图兼容 API。

变更说明

#

此次更改中被弃用的 API 如下:

  • dart:ui 中公开的全局 window 属性。
  • BaseBinding 类上的 window 属性,通常通过以下方式访问:
    • GestureBinding.instance.window,
    • SchedulerBinding.instance.window,
    • ServicesBinding.instance.window,
    • PaintingBinding.instance.window,
    • SemanticsBinding.instance.window,
    • RendererBinding.instance.window,
    • WidgetsBinding.instance.window,或
    • WidgetTester.binding.window.
  • dart:ui 中的 SingletonFlutterView 类。
  • flutter_test 中的 TestWindow、其构造函数,以及其所有属性和方法。

对于依赖这些已弃用 API 的应用程序和库代码,存在以下迁移选项:

如果 BuildContext 可用,请考虑通过 View.of 查找当前的 FlutterView。它返回与给定上下文关联的 build 方法所构建的 widget 将被绘制到的 FlutterViewFlutterView 提供了与之前已弃用的 window 属性返回的 SingletonFlutterView 类相同的功能。然而,一些平台特定的功能已移至 PlatformDispatcher,可以通过 View.of 返回的 FlutterView 经由 FlutterView.platformDispatcher 访问。使用 View.of 是从上述已弃用属性进行迁移的首选方式。

如果无法通过 BuildContext 查找 FlutterView,可以直接查询 PlatformDispatcher 来访问平台特定功能。它还通过 PlatformDispatcher.views 维护所有可用 FlutterView 的列表,以访问视图特定功能。如果可能,应通过 binding(例如 WidgetsBinding.instance.platformDispatcher)访问 PlatformDispatcher,而不是使用静态 PlatformDispatcher.instance 属性。这确保了 PlatformDispatcher 的功能可以在测试中被正确地模拟。

测试

#

对于在测试中访问 WidgetTester.binding.window 属性以更改窗口属性的测试,以下迁移可用:

在用 testWidgets 编写的测试中,增加了两个新属性,它们共同取代了 TestWindow 的功能。

  • WidgetTester.view 将提供一个 TestFlutterView,可以类似于 WidgetTester.binding.window 进行修改,但仅限于视图特定的属性,例如视图的大小、其显示像素比等。
    • WidgetTester.viewOf 适用于某些多视图用例,但不应作为从 WidgetTester.binding.window 迁移的必需项。
  • WidgetTester.platformDispatcher 将提供对 TestPlatformDispatcher 的访问,可用于修改平台特定属性,例如平台的区域设置、某些系统功能是否可用等。

迁移指南

#

应用程序和库代码(如果可访问 BuildContext)不应再访问静态 window 属性,而应使用 View.of 来查找与上下文关联的 FlutterView。某些属性已通过 platformDispatcher getter 移动到视图中可访问的 PlatformDispatcher

迁移前的代码

dart
Widget build(BuildContext context) {
  final double dpr = WidgetsBinding.instance.window.devicePixelRatio;
  final Locale locale = WidgetsBinding.instance.window.locale;
  return Text('The device pixel ratio is $dpr and the locale is $locale.');
}

迁移后的代码

dart
Widget build(BuildContext context) {
  final double dpr = View.of(context).devicePixelRatio;
  final Locale locale = View.of(context).platformDispatcher.locale;
  return Text('The device pixel ratio is $dpr and the locale is $locale.');
}

如果没有 BuildContext 可用,可以直接查阅 binding 公开的 PlatformDispatcher

迁移前的代码

dart
double getTextScaleFactor() {
  return WidgetsBinding.instance.window.textScaleFactor;
}

迁移后的代码

dart
double getTextScaleFactor() {
  // View.of(context).platformDispatcher.textScaleFactor if a BuildContext is available, otherwise:
  return WidgetsBinding.instance.platformDispatcher.textScaleFactor;
}

测试

#

在用 testWidget 编写的测试中,应使用新的 viewplatformDispatcher 访问器。

设置视图特定属性

#

TestFlutterView 还通过使用与相关 getter 同名的 setter 而不是带有 TestValue 后缀的 setter,努力使测试 API 更清晰、更简洁。

迁移前的代码

dart
testWidget('test name', (WidgetTester tester) async {
  tester.binding.window.devicePixelRatioTestValue = 2.0;
  tester.binding.window.displayFeaturesTestValue = <DisplayFeatures>[];
  tester.binding.window.gestureSettingsTestValue = const GestureSettings(physicalTouchSlop: 100);
  tester.binding.window.paddingTestValue = FakeViewPadding.zero;
  tester.binding.window.physicalGeometryTestValue = const Rect.fromLTRB(0,0, 500, 800);
  tester.binding.window.physicalSizeTestValue = const Size(300, 400);
  tester.binding.window.systemGestureInsetsTestValue = FakeViewPadding.zero;
  tester.binding.window.viewInsetsTestValue = FakeViewPadding.zero;
  tester.binding.window.viewPaddingTestValue = FakeViewPadding.zero;
});

迁移后的代码

dart
testWidget('test name', (WidgetTester tester) async {
  tester.view.devicePixelRatio = 2.0;
  tester.view.displayFeatures = <DisplayFeatures>[];
  tester.view.gestureSettings = const GestureSettings(physicalTouchSlop: 100);
  tester.view.padding = FakeViewPadding.zero;
  tester.view.physicalGeometry = const Rect.fromLTRB(0,0, 500, 800);
  tester.view.physicalSize = const Size(300, 400);
  tester.view.systemGestureInsets = FakeViewPadding.zero;
  tester.view.viewInsets = FakeViewPadding.zero;
  tester.view.viewPadding = FakeViewPadding.zero;
});

重置视图特定属性

#

TestFlutterView 保留了重置单个属性或整个视图的功能,但为了更清晰和一致,这些方法的命名已从 clear<property>TestValueclearAllTestValues 分别更改为 reset<property>reset

重置单个属性
#

迁移前的代码

dart
testWidget('test name', (WidgetTester tester) async {
  addTearDown(tester.binding.window.clearDevicePixelRatioTestValue);
  addTearDown(tester.binding.window.clearDisplayFeaturesTestValue);
  addTearDown(tester.binding.window.clearGestureSettingsTestValue);
  addTearDown(tester.binding.window.clearPaddingTestValue);
  addTearDown(tester.binding.window.clearPhysicalGeometryTestValue);
  addTearDown(tester.binding.window.clearPhysicalSizeTestValue);
  addTearDown(tester.binding.window.clearSystemGestureInsetsTestValue);
  addTearDown(tester.binding.window.clearViewInsetsTestValue);
  addTearDown(tester.binding.window.clearViewPaddingTestValue);
});

迁移后的代码

dart
testWidget('test name', (WidgetTester tester) async {
  addTearDown(tester.view.resetDevicePixelRatio);
  addTearDown(tester.view.resetDisplayFeatures);
  addTearDown(tester.view.resetGestureSettings);
  addTearDown(tester.view.resetPadding);
  addTearDown(tester.view.resetPhysicalGeometry);
  addTearDown(tester.view.resetPhysicalSize);
  addTearDown(tester.view.resetSystemGestureInsets);
  addTearDown(tester.view.resetViewInsets);
  addTearDown(tester.view.resetViewPadding);
});
一次性重置所有属性
#

迁移前的代码

dart
testWidget('test name', (WidgetTester tester) async {
  addTearDown(tester.binding.window.clearAllTestValues);
});

迁移后的代码

dart
testWidget('test name', (WidgetTester tester) async {
  addTearDown(tester.view.reset);
});

设置平台特定属性

#

TestPlatformDispatcher 保持了与 TestWindow 相同的测试 setter 功能和命名方案,因此平台特定属性的迁移主要包括在新 WidgetTester.platformDispatcher 访问器上调用相同的 setter。

迁移前的代码

dart
testWidgets('test name', (WidgetTester tester) async {
  tester.binding.window.accessibilityFeaturesTestValue = FakeAccessibilityFeatures.allOn;
  tester.binding.window.alwaysUse24HourFormatTestValue = false;
  tester.binding.window.brieflyShowPasswordTestValue = true;
  tester.binding.window.defaultRouteNameTestValue = '/test';
  tester.binding.window.initialLifecycleStateTestValue = 'painting';
  tester.binding.window.localesTestValue = <Locale>[const Locale('en-us'), const Locale('ar-jo')];
  tester.binding.window.localeTestValue = const Locale('ar-jo');
  tester.binding.window.nativeSpellCheckServiceDefinedTestValue = false;
  tester.binding.window.platformBrightnessTestValue = Brightness.dark;
  tester.binding.window.semanticsEnabledTestValue = true;
  tester.binding.window.textScaleFactorTestValue = 2.0;
});

迁移后的代码

dart
testWidgets('test name', (WidgetTester tester) async {
  tester.platformDispatcher.accessibilityFeaturesTestValue = FakeAccessibilityFeatures.allOn;
  tester.platformDispatcher.alwaysUse24HourFormatTestValue = false;
  tester.platformDispatcher.brieflyShowPasswordTestValue = true;
  tester.platformDispatcher.defaultRouteNameTestValue = '/test';
  tester.platformDispatcher.initialLifecycleStateTestValue = 'painting';
  tester.platformDispatcher.localesTestValue = <Locale>[const Locale('en-us'), const Locale('ar-jo')];
  tester.platformDispatcher.localeTestValue = const Locale('ar-jo');
  tester.platformDispatcher.nativeSpellCheckServiceDefinedTestValue = false;
  tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark;
  tester.platformDispatcher.semanticsEnabledTestValue = true;
  tester.platformDispatcher.textScaleFactorTestValue = 2.0;
});

重置平台特定属性

#

与设置属性类似,重置平台特定属性主要包括将 binding.window 访问器更改为 platformDispatcher 访问器。

重置单个属性
#

迁移前的代码

dart
testWidgets('test name', (WidgetTester tester) async {
  addTeardown(tester.binding.window.clearAccessibilityFeaturesTestValue);
  addTeardown(tester.binding.window.clearAlwaysUse24HourFormatTestValue);
  addTeardown(tester.binding.window.clearBrieflyShowPasswordTestValue);
  addTeardown(tester.binding.window.clearDefaultRouteNameTestValue);
  addTeardown(tester.binding.window.clearInitialLifecycleStateTestValue);
  addTeardown(tester.binding.window.clearLocalesTestValue);
  addTeardown(tester.binding.window.clearLocaleTestValue);
  addTeardown(tester.binding.window.clearNativeSpellCheckServiceDefinedTestValue);
  addTeardown(tester.binding.window.clearPlatformBrightnessTestValue);
  addTeardown(tester.binding.window.clearSemanticsEnabledTestValue);
  addTeardown(tester.binding.window.clearTextScaleFactorTestValue);
});

迁移后的代码

dart
testWidgets('test name', (WidgetTester tester) async {
  addTeardown(tester.platformDispatcher.clearAccessibilityFeaturesTestValue);
  addTeardown(tester.platformDispatcher.clearAlwaysUse24HourFormatTestValue);
  addTeardown(tester.platformDispatcher.clearBrieflyShowPasswordTestValue);
  addTeardown(tester.platformDispatcher.clearDefaultRouteNameTestValue);
  addTeardown(tester.platformDispatcher.clearInitialLifecycleStateTestValue);
  addTeardown(tester.platformDispatcher.clearLocalesTestValue);
  addTeardown(tester.platformDispatcher.clearLocaleTestValue);
  addTeardown(tester.platformDispatcher.clearNativeSpellCheckServiceDefinedTestValue);
  addTeardown(tester.platformDispatcher.clearPlatformBrightnessTestValue);
  addTeardown(tester.platformDispatcher.clearSemanticsEnabledTestValue);
  addTeardown(tester.platformDispatcher.clearTextScaleFactorTestValue);
});
一次性重置所有属性
#

迁移前的代码

dart
testWidgets('test name', (WidgetTester tester) async {
  addTeardown(tester.binding.window.clearAllTestValues);
});

迁移后的代码

dart
testWidgets('test name', (WidgetTester tester) async {
  addTeardown(tester.platformDispatcher.clearAllTestValues);
});

时间线

#

发布版本:3.9.0-13.0.pre.20
稳定版本:3.10.0

参考资料

#

API 文档

相关问题

相关 PR