Flutter日历项目的优化记录

FlutterCalendarWidget

Flutter上的一个日历控件,能够定制成本身想要的样子。git

实现日历并不难,主要是以前实现的性能有点差,进行了下优化。这篇文章主要记录一下本身对这个日历项目的优化过程。github

截图

项目地址web

日历支持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,达到指望,还能够继续优化下。bash

  • 图一:AndroidStudio自带的工具栏Flutter Performance,能够查看相关的性能数据
  • 图二:打开Show Performance Overlay的开关,就能够在app上显示性能统计数据的浮窗。
  • 图三:Flutter提供的Devtools工具,在浏览器中能够查看各类性能的数据。(具体使用百度一下)

代码优化

以前日历的配置信息全都是放在controller里面的,并且日历可配置的信息会有不少,感受有点乱。此次将配置的信息都抽放到一个CalendarConfiguration对象中。而且这个CalendarConfiguration对象存放在顶层的provider状态类中,可让子Widget直接获取到配置信息。网络

引进provider状态管理框架

引进provider状态管理框架,一方面是能够避免数据各类嵌套传递,一方面是实现局部刷新提升性能。app

关于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的一些原理。

相关文章
相关标签/搜索