关于 MediaQuery
我们介绍过不少,比如在之前的《MediaQuery 和 build 优化你不知道的秘密》里就介绍过,要慎重在 Scaffold
之外使用 MediaQuery.of(context)
,这是因为 MediaQuery.of
对 BuildContext
的绑定可能会导致一些不必要的性能开销,例如键盘弹起时,会导致相关的 MediaQuery.of(context)
绑定的页面出现重构。
比如下面这个例子,我们在 MyHomePage
里使用了 MediaQuery.of(context).size
并打印输出,然后跳转到 EditPage
页面,弹出键盘 ,这时候会发生什么情况?
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("######### MyHomePage ${MediaQuery.of(context).size}");
return Scaffold(
body: Container(
alignment: Alignment.center,
child: InkWell(
onTap: () {
Navigator.of(context).push(CupertinoPageRoute(builder: (context) {
return EditPage();
}));
},
child: new Text(
"Click",
style: TextStyle(fontSize: 50),
),
),
),
);
}
}
class EditPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text("ControllerDemoPage"),
),
extendBody: true,
body: Column(
children: [
new Spacer(),
new Container(
margin: EdgeInsets.all(10),
child: new Center(
child: new TextField(),
),
),
new Spacer(),
],
),
);
}
}
如下图 log 所示 , 可以看到在键盘弹起来的过程,因为 bottom 发生改变,所以 MediaQueryData
发生了改变,从而导致上一级的 MyHomePage
虽然不可见,但是在键盘弹起的过程里也被不断 build 。
虽然在之前的小技巧里我们介绍了解决方式,但是 3.10 开始有更优雅的做法,同时也更方便我们自足控制更细的颗粒度地去管理 InheritedWidget
里的绑定关系,那就是使用 InheritedModel
。
在 3.10 里 MediaQuery
增加了需要针对特定参数的 ****of
方式,例如 MediaQuery.platformBrightnessOf(context);
,这些方法对应在 _MediaQueryAspect
里都有一个枚举类型,而在 Flutter Framework 里,这些参数的调用都修改成了新的 ****of
类型方法。
例如一开始的例子,只需要将 MediaQuery.of(context).size
修改为 MediaQuery.sizeOf(context)
,那么跳转到 EditPage
页面,弹出键盘 ,在键盘弹起来的过程中不会再导致 MyHomePage
rebuild 输出 log。
而之所以会这样的原因,其实是因为这些 MediaQuery.******Of(context);
内部调用的是 InheritedModel.inheritFrom
实现。
是的,3.10 开始 MediaQuery
继承从 InheritedWidget
变成 InheritedModel
,而 InheritedModel
的 inheritFrom
方法可以让开发者可以通过 aspect
来决定数据改变时是否调用对应更新。
小技巧一:3.10 现在可以通过将
MediaQuery.of
获取参数的方式替换成MediaQuery.******Of(context);
来减少不必要的 rebuild 。
使用 InheritedModel
只需要继承它就可以,之后需要重点关注的是 updateShouldNotifyDependent
方法,它用于决定应该什么时候 rebuild 。
如下图所示是 MediaQuery
的实现,在 updateShouldNotifyDependent
里我们可以通过 dependencies
里的类型来进行区分,比如调用时是通过 InheritedModel.inheritFrom<MediaQuery>(context, aspect: _MediaQueryAspect.size)
输入,那么判断时就会进入到 _MediaQueryAspect. size
这个 case
。
如果此时 size
参数发生了改变,就返回 true
,从而产生 rebuild,反之返回放 false,这就是 InheritedModel
可以根据绑定具体变量来更新页面的原因。
当然这里你不一定要传枚举,你喜欢的话传 String 也可以,具体可以根据你的爱好来设定。
那为什么 InheritedModel
的 inheritFrom
方法可以达到这样的效果?
我们简单看一下 inheritFrom
的源码实现,如下图所示:
- 在没有
aspect
的时候直接调用dependOnInheritedWidgetOfExactType()
那就是和之前普通的of(context)
没什么区别 - 在有
aspect
的时候,会先通过_findModels
找到对应的InheritedElement
,然后调用dependOnInheritedElement()
绑定
可能这时候你有些疑问,不要急慢慢来,我们先看 dependOnInheritedWidgetOfExactType()
和 dependOnInheritedElement()
, 其实 dependOnInheritedWidgetOfExactType()
内部也是调用 dependOnInheritedElement()
来完成绑定,那么这里前后的区别是什么?
如果你仔细看,这里的区别在于 dependOnInheritedElement()
多使用了 aspect
,对应到 MediaQuery
里就是如 _MediaQueryAspect.size
这个 aspect
,这样在后续 updateShouldNotifyDependent
时就会被用上。
如下图所示是 InheritedModel
的 InheritedModelElement
,可以看到 inheritFrom
传入的 aspect
会变成 dependencies
,而这个 dependencies
就是我们在 updateShouldNotifyDependent
里用来判断的类型依据。
最后,在 notifyDependent
方法里可以看到,只有 updateShouldNotifyDependent
返回 true
时,才会调用 didChangeDependencies
去更新。
所以 inheritFrom
的特殊之处在于:当存在 aspect
时,该 aspect
会变成 dependencies
集合,然后通过 updateShouldNotifyDependent
来决定是否触发更新。
至于 _findModels
方法其实你无需纠结,虽然它是传入一个 List,但是一般情况下你只会获取到一个。什么时候会可能有多个,就是你 override 了 InheritedModel
的 isSupportedAspect
方法,并且会根据 aspect
条件有不同判断返回时可能会有多个。
例如都是继承 InheritedModel
,但是 isSupportedAspect
可以根据条件来决定你这个实例是否支持 Aspect
绑定。
@override
bool isSupportedAspect(Object aspect) {
return aspects == null || aspects!.contains(aspect);
}
另外 _findModels
里用的是 getElementForInheritedWidgetOfExactType()
,它和 dependOnInheritedWidgetOfExactType()
的区别就是前者会注册依赖关系,而后者不会,所以 _findModels
顾名思义只是找出符合条件的 InheritedModel
。
最后总结一下,今天的小技巧其实很简单,就是更新你的 MediaQuery.of
到对应参数的 MediaQuery.*****of
从而提升应用性能,并且了解到 InheritedModel
的实现逻辑和自定义支持,从而学会优化你现在的 InheritedWidget
的使用。
如果你还有什么问题,欢迎留言评论交流。