FlutterCalendarWidget
Flutter上的一个日历控件,可以定制成自己想要的样子。
实现日历并不难,主要是之前实现的性能有点差,进行了下优化。这篇文章主要记录一下自己对这个日历项目的优化过程。
截图
日历支持web预览:点击此处进入预览
项目结构
下图就是项目的整体结构,没啥特殊的,就是写一个日历需要的一些数据。
- constants:存放一些常量
- model:自定义日历用的实体类DateModel
- style:自定义的一些style
- utils:工具类
- widget:显示月视图、周视图的widget。
- calendar_provider:使用provider创建的共享状态类
- configuration:定义配置信息
- controller:日历控制器,可以对日历进行一些操作或者配置
- flutter_custom_calendar: 日历Widget
Widget层级
- 整体是一个Column,顶部固定一个自定义的weekbar。
- 下面是一个AnimatedContainer,用来实现周视图和月视图切换的高度变化的动画效果。
- IndexedStack用来存放周视图和月视图的widget。与Stack相同的地方是都是实现层叠的布局,与Stack不同的地方是Stack显示的时候会把children都绘制出来。而IndexedStack只会绘制指定index的那个child。所以这里利用切换index来切换显示周视图和月视图。具体文章:Exploring Stack and IndexedStack in Flutter
优化记录
优化后的性能
优化前,真tm是一片红。
进行各种优化后,最终的性能如下面几个图所示。整体性能还好,页面间切换也没那么卡来。每帧的耗时大部分小于16ms,达到期望,还可以继续优化下。
- 图一:AndroidStudio自带的工具栏Flutter Performance,可以查看相关的性能数据
- 图二:打开Show Performance Overlay的开关,就可以在app上显示性能统计数据的浮窗。
- 图三:Flutter提供的Devtools工具,在浏览器中可以查看各种性能的数据。(具体使用百度一下)
代码优化
之前日历的配置信息全都是放在controller里面的,而且日历可配置的信息会有很多,感觉有点乱。这次将配置的信息都抽放到一个CalendarConfiguration对象中。并且这个CalendarConfiguration对象存放在顶层的provider状态类中,可以让子Widget直接获取到配置信息。
引进provider状态管理框架
引进provider状态管理框架,一方面是可以避免数据各种嵌套传递,一方面是实现局部刷新提高性能。
关于provider的使用:Flutter | 状态管理指南篇——Provider
- 引进provider前的代码
各种数据和状态都需要一层一层往下传递给子Widget,有点恶心。
- 引进provider后的代码
创建状态类CalendarProvider,用于共享日历的数据和状态,子widget可以直接获取到CalendarProvider中的数据。代码比之前好看一点,不过后面还是得继续优化。
没有了代码嵌套,构造方法简单了很多。在子组件的build方法里,也可以获取到各种状态数据。使用compute加载数据
相关文章:Flutter 异步编程:Future、Isolate 和事件循环
-
Dart是一种单线程语言,Isolate就是Dart中的线程。
-
默认的Flutter代码是运行在同一个isolate里面的。每个Isolate都有着自己的事件循环EventLoop和两个队列(MicroTask和Event)。如下图所示,MicroTask队列的优先级优先于Event队列,当没有MicroTask事件的时候,才会去执行Event队列中的第一项。
-
注意:Future操作也是通过Event队列处理。Future和async并非并行执行,而是遵循事件循环处理事件的顺序规则执行。 如果繁重的处理可能需要一些时间才能完成,并且可能影响应用的性能,考虑使用 Isolate。 所以,可以利用多个Isolate来实现真正的并行处理。
-
Flutter提供了一个compute的方法,让我们可用来直接创建一个isolate。Compute函数对isolate的创建和底层的消息传递进行了封装,使得我们不必关系底层的实现,只需要关注功能实现。 使用Compute写isolates
-
Isolate的使用场景
所以一些比较耗时的操作,我们可以放在另一个isolate中进行并行执行。
- JSON 解码:解码 JSON(HttpRequest 的响应)可能需要一些时间 => 使用 compute
- 加密:加密可能非常耗时 => Isolate
- 图像处理:处理图像(比如:剪裁)确实需要一些时间来完成 => Isolate
-
在日历项目中的应用
比如我准备显示月视图,需要进行数据的加载拿到42个item。就需要去计算这个月对应的是42个item,以及去计算出每个item所对应的DateModel,以及各种所需要的信息。这个过程是比较耗时的,所以我就使用compute将这个操作放在另一个isolate里面进行操作。
使用getter实现数据的懒加载
Dart中的某个变量,默认都有setter和getter方法,getter方法就是用来获取变量的值。
如果这个变量的值是需要一系列计算(可能比较耗时)后才能得到结果。 如果在创建这个对象的时候就去计算结果并赋值给这个变量,就会花费额外的一些时间。也有可能计算了,但是这个对象后面也不会用到,那就很没必要了。
所以可以使用类似下面的写法,实现类似懒加载的功能。调用getter方法的时候,才去判断值是否为空,为空的话才开始进行数据计算。
每个DateModel都包含了一大堆属性,需要我去计算,比如农历、传统节日、24节气。这些的计算是比较复杂和耗时的,所以我就将部分属性的计算操作放到对应getter方法中,这样的话,就不用在一开始加载数据的时候,将所有的属性都进行计算。
点击item后进行刷新日历
呵呵,之前点击item后,然后不管三七二之一,调用setState方法去刷新整个日历的状态,搞定。可想而知,性能肯定会差点。
提高Build效率的一种方法就是降低遍历的出发点。直接在日历Widget内调用它的setState的话,那rebuild的时候,就需要将整个日历Widget树进行遍历刷新。
所以这里的做法是将日历的item抽成一个StatefulWidget,这样的话,如果调用日历item的setState方法的话,就只会刷新这个item,实现将刷新范围缩小到item级别。
- 这里写了一个refreshItem的方法,可以给item自身调用或者外部调用。
注意:这里要判断mounted后才去调用setState方法。因为有可能这个节点已经从element树移除了,这个时候如果调用setState的话,就会报错。在平常的开发中,也要注意这种问题。比如在获取网络数据的时候,如果当前页面被dispose了,等接口的数据返回后,直接调用setState的话就会报错。
Exception caught by gesture
The following assertion was thrown while handling a gesture:
setState() called after dispose()
- 多选模式:只刷新当前的item就行了。
多选模式就很简单,每个item,都利用GestureDetector监听日历item的点击事件,然后调用setState方法刷新自身就行。
- 单选模式:只刷新两个item,当前item和上一个item。
单选模式,比如我们选中某一个item,需要刷新这个item,并且将上一个选中的item的Widget进行刷新。所以这里定义了一个lastClickItemState变量来保存上一个点击的item的State对象,每次点击item的时候,调用这个lastClickItemState的refreshItem方法。
ItemContainerState lastClickItemState;//上一个点击的item
使用AutomaticKeepAliveClientMixin,使PageView保存内部item状态
这个相信搞过PageView的朋友,都想切换到其他页面的时候,需要实现页面保持状态。 AutomaticKeepAliveClientMixin 这个 Mixin 是 Flutter 为了保持页面设置的。哪个页面需要保持页面状态,就在这个页面进行混入。
只有两个组件才能保持页面状态:PageView 和 IndexedStack。
所以这里利用AutomaticKeepAliveClientMixin实现切换月份的时候,会维持上一个月或者下一个月的页面。
加入 AutomaticKeepAliveClientMixin 混入,并重写 wantKeepAlive 方法。
使用IndexStack实现切换功能:
周视图和月视图的切换功能的实现,这个暂时是用AnimatedContainer+IndexStack来实现的。不清楚还有没有其他更好的实现方案,还请大佬们给给意见。
- 一个是动画效果。两个视图之间的切换,高度变化会有个动画效果,可以使用现成的AnimatedContainer来实现。比自己用AnimationController会方便很多。
- 用什么widget来放周视图和月视图这两个Widget,现在是用IndexStack。
一开始是用Stack来放周视图和月视图两个widget,也是可以实现效果的。后面看到有IndexStack这个东西,就拿来使用了。
与Stack相同的地方是都是实现层叠的布局,与Stack不同的地方是Stack显示的时候会把children都绘制出来。而IndexedStack只会绘制指定index的那个child。所以这里利用切换index来切换显示周视图和月视图。
Stack对应的renderObject是RenderStack,可以看到,paint方法,最后是会将所有的child的给绘制出来。
IndexedStack其实是继承Stack,相应的renderObject是RenderIndexedStack,也是继承于RenderStack。重写了paintStack方法,只会绘制指定index的child。
总结
自己写一个开源库,虽然写得很辣鸡,不过还是有挺多收获的。通过这个项目,把Flutter性能优化的方法进行了实践,更深入地去了解Flutter的一些原理。