Flutter中有两个常用的状态Widget分为StatefulWidget和StatelessWidget,分别为动态视图和静态视图,视图的更新需要调用StatefulWidget的setState方法,这会遍历调用子Widget的build方法。如果一个页面内容比较复杂时,会包含多个widget,如果直接调用setState,会遍历所有子Widget的build,这样会造成很多不必要的开销,所以非常有必要了解Flutter中局部刷新的方式:
专注于为中小企业提供成都网站设计、成都网站制作服务,电脑端+手机端+微信端的三站合一,更高效的管理,为中小企业宽城免费做网站提供优质的服务。我们立足成都,凝聚了一批互联网行业人才,有力地推动了1000多家企业的稳健成长,帮助中小企业通过网站建设实现规模扩充和转变。
globalkey唯一定义了某个element,它使你能够访问与element相关联的其他对象,例如buildContext、state等。应用场景:跨widget访问状态。
例如:可以通过key.currentState拿到它的状态对象,然后就可以调用其中的onPressed方法。
Flutter框架内部提供了一个非常小巧精致的组件,专门用于局部组件的刷新。适用于值改动的刷新。
实现原理:在 initState 中对传入的可监听对象进行监听,执行 _valueChanged 方法,_valueChanged 中进行了 setState 来触发当前状态的刷新。触发 build 方法,从而触发 widget.builder 回调,这样就实现了局部刷新。可以看到这里回调的 child 是组件传入的 child,所以直接使用,这就是对 child 的优化的根源。
可以看到 ValueListenableBuilder 实现局部刷新的本质,也是进行组件的抽离,让组件状态的改变框定在状态内部,并通过 builder 回调控制局部刷新,暴露给用户使用。
通过这个可以创建一个支持局部刷新的widget树,比如你可以在StatelessWidget里面刷新某个布局,但是不需要改变成StatefulWidget;也可以在StatefulWidget中使用做部分刷新而不需要刷新整个页面,这个刷新是不会调用Widget build(BuildContext context)刷新整个布局树的。
异步UI更新:
很多时候我们会依赖一些异步数据来动态更新UI,比如在打开一个页面时我们需要先从互联网上获取数据,在获取数据的过程中显示一个加载框,等获取到数据时我们再渲染页面;又比如我们想展示Stream(比如文件流、互联网数据接收流)的进度。当然StatefulWidget我们完全可以实现以上功能。但由于在实际开发中依赖异步数据更新UI的这种场景非常常见,并且当StatefulWidget中控件树较大时,更新一个属性导致整个树重建,消耗性能,因此Flutter专门提供了FutureBuilder和SteamBuilder两个组件来快速实现这种功能。
通常情况下,子Widget无法单独感知父Widget的变化,当父state变化时,通过其build重建所有子widget;
InheriteddWidget可以避免这种全局创建,实现局部子Widget更新。InheritedWidget提供了一种在Widget树中从上到下传递、共享数据的方式。Flutter SDK正是通过InheritedWidget来共享应用主题和Locale等信息。
InheritedWidgetData
TestData
InheritedTest1Page
provider是Google I/O 2019大会上宣布的现在官方推荐的管理方式,而ChangeNotifierProvider可以说是Provider的一种:
yaml文件需要引入provider: ^3.1.0
顶层嵌套ChangeNotifierProvider
创建共享数据类DataInfo:
数据类需要with ChangeNotifier 以使用 notifyListeners()函数通知监听者更新界面。
使用Provider.of(context)获取DataInfo
nextPage:
使用Consumer包住需要使用共享数据的Widget
RepaintBoundary就是重绘边界,用于重绘时独立于父视图。页面需要更新的页面结构可以用 RepaintBoundary组件嵌套,flutter 会将包含的组件独立出一层"画布",去绘制。官方很多组件 外层也包了层 RepaintBoundary 标签。如果你的自定义view比较复杂,应该尽可能的避免重绘。
以上总结了几种Flutter的局部刷新的方式,可根据实际需要使用不同的方式,最适合的才是最好的。
注意: 滚动组件添加: physics: ClampingScrollPhysics() 可以处理IOS系统的物理滚动的效果(即橡皮筋效果)
ListView 是最常用的可滚动组件之一,可以沿一个方向线性排布所有子组件,并且它也支持基于Sliver的延迟构建模型
默认构造函数:
ListView.builder:
ListView.separated:
ListView.separated 可以在生成的列表项之间添加一个分割组件,它比 ListView.builder 多了一个 separatorBuilder 参数,该参数是一个分割组件生成器。
RefreshIndicator 下拉刷新:
RefreshIndicator 是 Material 风格的下拉刷新组件。
CupertinoSliverRefreshControl 下拉刷新:
CupertinoSliverRefreshControl 是 ios 风格的下拉刷新控件。
上拉加载的功能,需要用到 ScrollController + ListView组件:
其中,参数 image 类型为抽象类 ImageProvider ,定义了图片数据获取和加载的相关接口。
根据不同的数据来源,派生出不同的 ImageProvider :
抽象类 ImageProvider 提供了一个用于加载数据源的抽象方法 @protected ImageStreamCompleter load(T key, DecoderCallback decode); 接口,不同的数据源定义各自的实现。
子类 NetworkImage 实现如下:
load 方法返回类型为抽象类 ImageStreamCompleter ,其中定义了一些管理图片加载过程的接口,比如 addListener 、 removeListener 、 addOnLastListenerRemovedCallback 等, MultiFrameImageStreamCompleter 为其子类。
MultiFrameImageStreamCompleter 第一个参数 codec 类型为 Futureui.Codec ,用来对突破进行解码,当 codec 准备好的时候,就会立即对图片第一帧进行解码操作。
codec 为 _loadAsync 方法返回值,
_loadAsync 方法实现:
decode 方法的类型:
其中解码传入的回调方法 image_provider.DecoderCallback decode ,
传入 Uint8List ,返回 Futureui.Codec 。
而对 decode 回调方法的具体定义,在 ImageProvider 的 resolveStreamForKey 方法中做了定义, resolveStreamForKey 方法在 ImageProvider 的 resolve 方法中有调用, resolve 方法则为 ImageProvider 类层级结构的公共入口点。
resolveStreamForKey 和 resolve 实现如下:
decode 方法,即 PaintingBinding.instance!.instantiateImageCodec ,即为具体图片解码的方法实现。
ui.instantiateImageCodec 实现:
descriptor.instantiateCodec 方法实现:
_instantiateCodec 方法的实现,最终到了 native 的实现:
其中返回值类型 Codec 里定义了一些属性:
obtainKey 方法:
ImageProvider 定义了一个抽象方法 FutureT obtainKey(ImageConfiguration configuration); ,供子类来实现,其中 NetworkImage 的实现为:
obtainKey 作用:
配合实现图片缓存, ImageProvider 从数据源加载完数据后,会在 ImageCache 中缓存图片数据,图片数据缓存时一个 Map ,其中 Map 中的 key 便是 obtainKey 。
resolve 作为 ImageProvider 提供给 Image 的主入口方法,参数为 ImageConfiguration ,
resolve 其中调用了 _createErrorHandlerAndKey 方法,设置了成功回调和失败回调:
其中 _createErrorHandlerAndKey 方法的实现,便调用了 obtainKey 来设置 key 。
在成功回调里,调用了方法 resolveStreamForKey ,里面有具体的缓存实现 PaintingBinding.instance!.imageCache!.putIfAbsent :
PaintingBinding.instance!.imageCache 是ImageCache的一个实例,是 PaintingBinding 的一个属性,是一个单例,图片缓存是全局的。
如上述判断:
ImageCache 定义:
ImageCache 缓存池:
在 NetworkImage 中,对 ImageProvider 的抽象方法 obtainKey 进行了实现,将自己创建了一个同步 Future 进行返回:
同时,自身又重写了 ImageProvider 定义的 == 比较操作符,通过图片 url 和图片的缩放比例 scale 进行比较:
通过ImageCache提供的方法来设置:
在耗时操作的时候,一般都要弹出一个加载框,然后在完成的时候再把加载框关掉,在Flutter中可以直接用showDialog()来弹出一个对话框。
这是一个简单的提示对话框,包含了关闭按钮,点击就能关闭。但一般的耗时操作完成,就需要我们自己把dialog关闭掉。
首先,开启dialog的时机。由于我们需要获取到BuildContext,所以就得等build()方法走完,这里可以用Future.delayed()来等创建好BuildContext再进行创建,或者用Timer来延迟操作,我选择了前者。
其中delayed()在initState()结尾来做就行,这里参考网友封装了一个LoadingDialog。
那么接下来要在什么时机关闭呢?
一开始,我理所当然的以为,是在异步方法结束后,去更新界面的时候关闭,也就是setState(() {})的时候,可是不管怎么尝试,用Navigator.pop()不行,用Navigator.of(context, rootNavigator: true).pop(result)也不行,用FlutterBoost.singleton.close(id)也不行,用FlutterBoost.singleton.closeCurrent()也不行,都会直接把非Dialog的页面也关闭掉,这让我百思不得其解,因为showDialog()的本质也是新建了一个Route出来,也就是最顶层的页面是弹出的Dialog,可是为什么关不掉呢。
一番思前想后,把showDialog的逻辑移到和异步逻辑同级,也就是setState(() {})外面,然后把showDialog()自身创建的BuildContext传进去就能正常关闭了。也就是,在setState(() {})的时候,其实用的context还是非Dialog页面的,所以关闭的当然就不是Dialog了。
持有Dialog自己的BuildContext,然后在异步以后调用就行了。
Future.delayed(Duration.zero, () {
showDialog(
context: context,
barrierDismissible: false,
builder: (_) {
return LoadingDialog();
});
});
记录下坑
一开始我就使用Future、async、await去做异步操作,以为这样能解决问题,经过一天研究发现他们都还在同一个线程里面,也就是UI线程,导致卡顿,这明显不是我们想要的异步加载数据。
Dart真正的线程叫隔离(Isolate)
难受香菇
有点心累,记录下吧。