一「表」走天下,Flutter瀑布流及通用列表解决方案

简介: 目前闲鱼业务中无论是首页还是搜索页都有大量可以落地瀑布流的场景,而在Flutter原生中只提供了ListView, GridView,无法提供自定义布局的能力。而在社区中,一般瀑布流的解决方案都是基于SliverMultiBoxAdaptor对其performLayout进行定制,主要存在的问题是缺乏复用机制,并且在很多情形下容易出现重复布局,在线上业务的复杂场景下容易出现帧数偏低的问题, 闪屏的问题。同时对于Child生命周期,打点曝光等一系列基础功能的支持还是一片空白的状态。所以,我们迫切需要一个更为通用的可以解决复杂布局过程同时能够对基础能力进行扩充的列表视图解决方案。

vcg_VCG211268528756_RF.jpg

作者|董家建(夜澜)
编辑|橙子君
出品|阿里巴巴新零售淘系技术

目前闲鱼业务中无论是首页还是搜索页都有大量可以落地瀑布流的场景,而在Flutter原生中只提供了ListView, GridView,无法提供自定义布局的能力。

而在社区中,一般瀑布流的解决方案都是基于SliverMultiBoxAdaptor对其performLayout进行定制,主要存在的问题是缺乏复用机制,并且在很多情形下容易出现重复布局,在线上业务的复杂场景下容易出现帧数偏低的问题, 闪屏的问题。同时对于Child生命周期,打点曝光等一系列基础功能的支持还是一片空白的状态。

所以,我们迫切需要一个更为通用的可以解决复杂布局过程同时能够对基础能力进行扩充的列表视图解决方案。

Flutter中的列表视图介绍

屏幕快照 2020-08-21 下午2.19.55.png

▐ Scrollable

Scrollable是一个StatefulWidget, 职责是监听用户的手势输入。其State的build方法会返回一个含有Listener和RawGestureDetector的Viewport。ScrollPosition用于描述其位置信息,并在其内部定义了 onStart, onUpdate, onEnd等回调。Scrollable中的每一次滑动的开始到结束都对应于一个Darg对象,并且会发送滑动的通知。而Viewport则负责对通知进行监听。

▐ Sliver

Flutter有两种布局体系 Box, Sliver。在layout的过程中,每个Sliver 都接收 SliverConstraints 计算返回一个 SliverGeometry,可以类比于RenderBox 接收 BoxConstraints 返回一个 Size。Sliver由Viewport统一来负责进行管理。

▐ Viewport

A widget that is bigger on the inside.

Viewport持有一个或多个Sliver。Scrollable将offset传递给Viewport, 由Viewport决定哪些Sliver应该是Visible。Viewport本质上是一个MultiChildRenderObjectWidget,也就是整个滚动视图的主要渲染逻辑都在Viewport中完成。

而在performLayout中,_attemptLayout会以center为中心,先布局leading方向的child,再布局trailing方向的child。其中只有dirty的child会被布局。

do {
  correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels + centerOffsetAdjustment);
  if (correction != 0.0) {
    offset.correctBy(correction);
  } else {
    if (offset.applyContentDimensions(
          math.min(0.0, _minScrollExtent + mainAxisExtent * anchor),
          math.max(0.0, _maxScrollExtent - mainAxisExtent * (1.0 - anchor)),
       ))
      break;
  }
  count += 1;
} while (count < _maxLayoutCycles);

如果attemptLayout返回了一个非0的correction, 就会打断当前布局的过程,需要对offset进行调整后重新开始布局,最多只能连续打断10次(maxLayoutCycles)。

correction用于调整,举个🌰,比如targetScrollOffset很远,而在scroll的过程中child用完了,就需要让Sliver通知Viewport, 同时进行修正。但是Flutter并不是通过不断对child进行layout来改变child位置实现的滑动效果,这样的重绘过程显然效率太低,显然RenderObject不需要被改变,是可以复用的。

但是布局一般只发生在添加新child的过程中,而滑动效果则发生在paint过程中。

void _paintWithContext(PaintingContext context, Offset offset) {
  // 重新布局就不需要调整offset了.
  if (_needsLayout)
    return;
  _needsPaint = false;
  paint(context, offset);
}

Viewport通过PaintingContext间接持有Canvas进行绘制。Offset指笛卡尔坐标系下的坐标,与Axis方向无关。绘制时只需改变对应RenderObject的Offset即可实现滚动的效果, 这样就不必重新创建RenderObject。所以我们如果想实现性能较高的列表视图,就要尝试去减少重新布局Child。在对Flutter的列表布局有了基本了解后,我们再来看瀑布流的实现过程。

瀑布流的实现逻辑

WatetfallFlow的布局过程中需要指定Child的Offset,然后对其进行布局。所以需要继承SliverMultiBoxAtaptor,依赖于其将SliverConstraints转换为BoxConstraints的能力。我们也可以使用其SliverBoxChildManager, 方便控制Child的懒加载过程。

▐ 核心逻辑

在瀑布流中由于同一行(列)的child(大多)具有先后关系,需要按照顺序来进行布局,所以瀑布流相比于GridView更类似于ListView,而瀑布流的布局过程也借鉴了ListView。整个瀑布流的布局逻辑围绕三个核心展开:

在滑动的过程中找到其边缘最近的child,在其后(前)进行添加child,并对child进行layout。
在child离开一定距离后进行GC。
保证layout方法被尽可能少的调用. 上文有提过layout会调用performLayout而不能直接进行paint。

其中核心的数据结构是ParentData。

ParentData位于Child中,Child将其传递给Sliver,Sliver又将其传递至上层,其中储存了全部的布局信息(在笛卡尔坐标系下)。在performLayout中,child在调用layout时所使用的布局信息就来自ParentData。在Child的添加过程中,用一个Manager存储前后边缘所有Child的ParentData,在添加时寻找边缘最靠近可见区域的Child,对其ParentData进行设置并替换当前Child。

布局的核心逻辑为对从最开始的Child(对应firstIndex)到最末的Child(对应targetLastIndex)进行布局。如果_layoutedChilds中已经有记录,则跳过其布局过程。

for (int index = firstIndex; index <= targetLastIndex; ++index) {
  final SliverGeometry gridGeometry = layout.getGeometryForChildIndex(index);
  final BoxConstraints childConstraints = gridGeometry.getBoxConstraints(constraints);
  RenderBox child = childAfter(trailingChildWithLayout);
  if (child == null || indexOf(child) != index) {
    // 重新获取Child.
    child = _createAndLayoutChildIfNeeded(childConstraints, after: trailingChildWithLayout);
    if (child != null && indexOf(child) == index) {
      _layoutedChilds.add(index);
    }else if (child == null) {
      // Child已经用尽.
      break;
    }
  } else {
    if (!_layoutedChilds.contains(index)) {
      _layoutChildIfNeeded(child, parentUsesSize: true);
      _layoutedChilds.add(index);
    }
  }
  trailingChildWithLayout = child;
}

对离开视图的child进行GC,同时记得将数组中的child清除。

if (firstChild != null) {
  // 上一次的最先最末Child.
  final int oldFirstIndex = indexOf(firstChild);
  final int oldLastIndex = indexOf(lastChild);
  
  // 前后需要GC的child数量
  final int leadingGarbage = (firstIndex - oldFirstIndex).clamp(0, childCount);
  final int trailingGarbage = targetLastIndex == null 
    ? 0 : (oldLastIndex - targetLastIndex).clamp(0, childCount);
  
  // GC
  collectGarbage(leadingGarbage, trailingGarbage);
  _layoutedChilds.sort();
  _layoutedChilds.removeRange(0, leadingGarbage);
  _layoutedChilds.removeRange(layoutedChilds.length - 1 - trailingGarbage, 
                              layoutedChilds.length - 1);
} else {
  collectGarbage(0, 0);
}

在开发过程中出现了帧数偏低的问题,发现是Child在performLayout的过程中会出现重复布局。解决方法是我们不仅记录leading, trailing边缘的child。而且用对已经layout过的child进行记录,粗暴直接但是有效,这样做也可以提供单独update单个child的Layout能力。在更新Child的布局时也只需从记录中将对应child移除。

相比于原生视图,我们可以通过获取所有Child的ParentData信息,可以为上层接口提供实时并且有效的回调.。这样就可以根据每个Child的实时位置来提供生命周期,曝光打点的能力。所以可以对每个child的坐标进行监听,从而获得精准的曝光信息。

从瀑布流到容器

在瀑布流的开发过程中也暴露出了一些设计上的问题。比如瀑布流的具体渲染逻辑都在RenderObject中进行,太过底层显然是不利于业务方根据业务进行定制。又比如由于没有复用的机制,在视图层级较为复杂时帧数会由于重复渲染而不可避免的降低。

屏幕快照 2020-08-21 下午2.21.14.png

借鉴native思路重新设计后将整体容器分为3个部分进行设计。

▐ delegate

主要管理child生命周期并响应手势,由于我们可以得到每个可见Child的parentData属性,所以可在滚动时进行实时的通知。从而对每个Child的位置监听,从开始创建到进入缓冲区,到从缓冲区进入可见区域。手势则来自于顶层的Scrollable。

▐ layout

主要负责布局所有的Child。将具体的布局逻辑抽离出,类似于iOS中的UICollectionViewLayout。但是在开发过程中也出现了一些问题,原因主要来自于Flutter特殊的信息传递方式,就是我们不能采用native的方式一次性计算出所有child的布局。因为RenderBox需要接收一个BoxConstraints才能返回一个size。

▐ reuser

reuser则在RenderObject层面,对Child进行基于类型的复用并实现局部更新的操作。需要将SliverMultiBoxAdaptor和其Element拷贝一份进行重写,改变其mount的逻辑,方案还在探索和调研之中,希望能在后续的文章中和大家见面!

性能数据

应用于主搜索页进行自动化测试,先前在54.7帧左右,换用瀑布流后为56.2,大概提升了1.5帧。

屏幕快照 2020-08-21 下午2.21.40.png

内存上则有略微的升高情况。

屏幕快照 2020-08-21 下午2.22.05.png

后续计划

目前Flutter的列表视图中仍然有很多问题需要处理,比如瀑布流中scrollTo(int index)的能力还无法实现,内存的使用情况等和原生相比仍然有不小的差距, 对于Flutter侧的复用的稳定性和兼容性上还存在问题,闲鱼在Flutter化上还有很多路要走。

关注「淘系技术」微信公众号,一个有温度有内容的技术社区~

屏幕快照 2020-08-21 下午2.23.37.png

相关文章
|
6月前
|
安全 数据安全/隐私保护 Android开发
Flutter应用程序加固的问题及解决方案
Flutter应用程序加固的问题及解决方案
124 0
|
23天前
|
开发工具
Flutter&鸿蒙next中封装一个列表组件
Flutter&鸿蒙next中封装一个列表组件
37 0
|
1月前
|
开发框架 移动开发 Android开发
安卓与iOS开发中的跨平台解决方案:Flutter入门
【9月更文挑战第30天】在移动应用开发的广阔舞台上,安卓和iOS两大操作系统各自占据半壁江山。开发者们常常面临着选择:是专注于单一平台深耕细作,还是寻找一种能够横跨两大系统的开发方案?Flutter,作为一种新兴的跨平台UI工具包,正以其现代、响应式的特点赢得开发者的青睐。本文将带你一探究竟,从Flutter的基础概念到实战应用,深入浅出地介绍这一技术的魅力所在。
82 7
|
3月前
|
存储 缓存 搜索推荐
Flutter开发者必读:sp_util - SharedPreferences的终极解决方案
Flutter开发者必读:sp_util - SharedPreferences的终极解决方案
82 0
|
4月前
Flutter 列表学习(listview,gridview,ExpansionTile,ScrollController,RefreshIndicator)
Flutter 列表学习(listview,gridview,ExpansionTile,ScrollController,RefreshIndicator)
|
6月前
|
前端开发 UED 开发者
【Flutter前端技术开发专栏】Flutter中的列表与滚动视图优化
【4月更文挑战第30天】Flutter开发中,优化列表和滚动视图至关重要。本文介绍了几种优化方法:1) 使用`ListView.builder`和`GridView.builder`实现懒加载;2) 复用子组件以减少实例创建;3) 利用`CustomScrollView`和`Slivers`提升滚动性能;4) 通过`NotificationListener`监听滚动事件;5) 使用`KeepAlive`保持列表项状态。掌握这些技巧能提升应用性能和用户体验。
104 1
【Flutter前端技术开发专栏】Flutter中的列表与滚动视图优化
|
5月前
|
Dart 前端开发 JavaScript
探索移动应用开发中的跨平台解决方案:Flutter与React Native的比较
在移动应用开发领域,选择合适的跨平台解决方案是关键。本文将深入分析Flutter和React Native这两大主流框架,从性能、开发效率、社区支持等方面进行比较,帮助开发者做出明智的选择。
79 0
|
6月前
|
缓存
使用Riverpod在Flutter中创建Todo列表
学习如何使用Riverpod在Flutter中构建一个功能完整的Todo列表应用。通过Consumer组件、ConsumerStatefulWidget类、ref.read方法和provider build重写,了解Riverpod的状态管理和更新状态机制。
336 7
使用Riverpod在Flutter中创建Todo列表
|
6月前
|
存储 缓存 监控
【Flutter前端技术开发专栏】Flutter中的列表滚动性能优化
【4月更文挑战第30天】本文探讨了Flutter中优化列表滚动性能的策略。建议使用`ListView.builder`以节省内存,避免一次性渲染所有列表项。为防止列表项重建,可使用`UniqueKey`或`ObjectKey`。缓存已渲染项、减少不必要的重绘和异步加载大数据集也是关键。此外,选择轻量级组件,如`StatelessWidget`,并利用Flutter DevTools监控性能以识别和解决瓶颈。持续测试和调整以提升用户体验。
211 0
【Flutter前端技术开发专栏】Flutter中的列表滚动性能优化
|
6月前
|
前端开发 数据处理
【Flutter 前端技术开发专栏】Flutter 中的滚动性能优化与无限列表实现
【4月更文挑战第30天】本文探讨了 Flutter 中的滚动性能优化和无限列表实现。关键点包括:1) 滚动性能直接影响用户满意度,优化可提升响应速度;2) 影响因素有布局复杂度、频繁重绘和数据处理;3) 优化措施包括懒加载、简化布局、减少不必要的重绘和高效处理数据;4) 无限列表通过监听滚动位置,动态加载新数据;5) 实现时注意加载策略、数据处理效率和内存管理。案例分析和总结强调了优化在实际开发中的重要性。
129 0
【Flutter 前端技术开发专栏】Flutter 中的滚动性能优化与无限列表实现
下一篇
无影云桌面