Flutter 3.10 适配之单例 Window 弃用,一起来了解 View.of 和 PlatformDispatcher

Flutter 3.10 发布之后,大家可能注意到,在它的 release note 里提了一句: Window singleton 相关将被弃用,并且这个改动是为了支持未来多窗口的相关实现

所以这是一个为了支持多窗口的相关改进,多窗口更多是在 PC 场景下更常见,但是又需要兼容 Mobile 场景,故而有此次改动作为提前铺垫。

如下图所示,如果具体到对应的 API 场景,主要就是涉及 WidgetsBinding.instance.windowMediaQueryData.fromWindow 等接口的适配,因为 WidgetsBinding.instance.window 即将被弃用。

你可以不适配,还能跑,只是升级的技术债务往后累计而已。

那首先可能就有一个疑问,为什么会有需要直接使用 WidgetsBinding.instance.window 的使用场景?简单来说,具体可以总结为:

  • 没有 BuildContext ,不想引入 BuildContext
  • 不希望获取到的 MediaQueryData 受到所在 BuildContext 的影响,例如键盘弹起导致 padding 变化重构和受到 Scaffold 下的参数影响等

这部分详细可见:《MediaQuery 和 build 优化你不知道的秘密》

那么从 3.10 开始,针对 WidgetsBinding.instance.window 可以通过新的 API 方式进行兼容:

  • 如果存在 BuildContex , 可以通过 View.of 获取 FlutterView,这是官方最推荐的替代方式
  • 如果没有 BuildContex 可以通过 PlatformDispatcherviews 对象去获取

这里注意到没有,现在用的是 View.of ,获取的是 FlutterView ,对象都称呼为 View 而不是 「Window」,对应的 MediaQueryData.fromWindow API 也被弃用,修改为 MediaQueryData.fromView ,这个修改的依据在于:

起初 Flutter 假定了它只支持一个 Window 的场景,所以会有 SingletonFlutterWindow 这样的 instance window 对象存在,同时 window 属性又提供了许多和窗口本身无关的功能,在多窗口逻辑下会显得很另类。

那么接下来就让我们用「长篇大论」来简单介绍下这两个场景的特别之处。

存在 BuildContext

回归到本次的调整,首先是存在 BuildContext 的场景,如下代码所示,对于存在 BuildContex 的场景, View.of 相关的调整为:

/// 3.10 之前
double dpr = WidgetsBinding.instance.window.devicePixelRatio;
Locale locale = WidgetsBinding.instance.window.locale;
double width =
    MediaQueryData.fromWindow(WidgetsBinding.instance.window).size.width;


/// 3.10 之后
double dpr = View.of(context).devicePixelRatio;
Locale locale = View.of(context).platformDispatcher.locale;
double width =
    MediaQueryData.fromView(View.of(context)).size.width;

可以看到,这里的 View 内部实现肯定是有一个 InheritedWidget ,它将 FlutterView 通过 BuildContext 往下共享,从而提供类似 「window」 的参数能力,而通过 View.of 获取的参数:

  • FlutterView 本身的属性值发生变化时,是不会通知绑定的 context 更新,这个行为类似于之前的 WidgetsBinding.instance.window
  • 只有当 FlutterView 本身发生变化时,比如 context 绘制到不同的 FlutterView 时,才会触发对应绑定的 context 更新

可以看到 View.of 这个行为考虑的是「多 FlutterView」 下的更新场景,如果是需要绑定到具体对应参数的变动更新,如 size 等,还是要通过以前的 MediaQuery.of / MediaQuery.maybeOf 来实现。

而对于 View 来说,每个 FlutterView 都必须是独立且唯一的,在一个 Widget Tree 里,一个 FlutterView 只能和一个 View 相关联,这个主要体现在 FlutterView 标识 GlobalObjectKey 的实现上。

简单总结一下:在存在 BuildContex 的场景,可以简单将 WidgetsBinding.instance.window 替换为 View.of(context) ,不用担心绑定了 context 导致重构,因为 View.of 只对 FlutterView 切换的场景生效

不存在 BuildContext

对于不存在或者不方便使用 BuildContext 的场景,官方提供了 PlatformDispatcher.views API 来进行支持,不过因为 get views 对应的是 Mapvalues ,它是一个 Iterable 对象,那么对于 3.10 我们需要如何使用 PlatformDispatcher.views 来适配没有 BuildContextWidgetsBinding.instance.window 场面

PlatformDispatcher 内部的views 维护了中所有可用 FlutterView 的列表,用于提供在没有 BuildContext 的情况下访问视图的支持。

你说什么情况下会有没有 BuildContext ?比如 Flutter 里 的 runApp ,如下图所示,3.10 目前在 runApp 时会通过 platformDispatcher.implicitView 来塞进去一个默认的 FlutterView

implicitView 又是什么?其实 implicitView 就是 PlatformDispatcher._views 里 id 为 0 的 FlutterView ,默认也是 views 这个 Iterable 里的 first 对象。

也就是在没有 BuildContext 的场景, 可以通过 platformDispatcher.views.first 的实现迁移对应的 instance.window 实现。

/// 3.10 之前
MediaQueryData.fromWindow(WidgetsBinding.instance.window)
/// 3.10 之后
MediaQueryData.fromView(WidgetsBinding.instance.platformDispatcher.views.first)

为什么不直接使用 implicitView 对象? 因为 implicitView 目前是一个过渡性方案,官方希望在多视图的场景下不应该始终存在 implicit view 的概念,而是应用自己应该主动请求创建一个窗口,去提供一个视图进行绘制。

所以对于 implicitView 目前官方提供了 _implicitViewEnabled 函数,后续可以通过可配置位来控制引擎是否支持 implicitView ,也就是 implicitView 在后续更新随时可能为 null ,这也是我们不应该在外部去使用它的理由,同时它是在 runApp 时配置的,所以它在应用启动运行后永远不会改变,如果它在启动时为空,则它永远都会是 null。

PlatformDispatcher.instance.views[0] 在之前的单视图场景中,无论是否有窗口存在,类似的 implicitView 会始终存在;而在多窗口场景下,PlatformDispatcher.instance.views 将会跟随窗口变化。

另外我们是通过 WidgetsBinding.instance.platformDispatcher.views 去访问 views ,而不是直接通过 PlatformDispatcher.instance.views ,因为通常官方更建议在 Binding 的依赖关系下去访问 PlatformDispatcher

除了需要在 runApp() 或者 ensureInitialized() 之前访问 PlatformDispatcher 的场景。

另外,如下图所示,通过 Engine 里对于 window 部分代码的实现,可以看到我们所需的默认FlutterView 是 id 为 0 的相关依据,所以这也是我们通过 WidgetsBinding.instance.platformDispatcher.views 去兼容支持的逻辑所在。

最后

最后总结一下,说了那么多,其实不外乎就是将 WidgetsBinding.instance.window 替换为 View.of(context) ,如果还有一些骚操作场景,可以使用 WidgetsBinding.instance.platformDispatcher.views ,如果不怕后续又坑,甚至可以直接使用 WidgetsBinding.instance.platformDispatcher.implicitView

整体上解释那么多,主要还是给大家对这次变动有一个背景认知,同时也对未来多窗口实现进展有进一步的了解,相信下一个版本多窗口应该就可以和大家见面了。

更多讨论可见:

results matching ""

    No results matching ""