Flutter 3.10 适配之单例 Window 弃用,一起来了解 View.of 和 PlatformDispatcher
Flutter 3.10 发布之后,大家可能注意到,在它的 release note 里提了一句: Window singleton 相关将被弃用,并且这个改动是为了支持未来多窗口的相关实现。
所以这是一个为了支持多窗口的相关改进,多窗口更多是在 PC 场景下更常见,但是又需要兼容 Mobile 场景,故而有此次改动作为提前铺垫。
如下图所示,如果具体到对应的 API 场景,主要就是涉及 WidgetsBinding.instance.window
和 MediaQueryData.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
可以通过PlatformDispatcher
的views
对象去获取
这里注意到没有,现在用的是 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
对应的是 Map
的 values
,它是一个 Iterable
对象,那么对于 3.10 我们需要如何使用 PlatformDispatcher.views
来适配没有 BuildContext
的 WidgetsBinding.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
。
整体上解释那么多,主要还是给大家对这次变动有一个背景认知,同时也对未来多窗口实现进展有进一步的了解,相信下一个版本多窗口应该就可以和大家见面了。
更多讨论可见: