在使用 Flutter 开发应用的过程当中咱们常常遇到须要展现一组连续元素的情景。这时咱们一般会选择使用 ListView 组件。在电商场景中,被展现的元素一般是一组商品、一组店铺又或是一组优惠券信息。把这些信息正确的展现出来仅仅是第一步,一般业务同窗为了统计用户的浏览习惯、活动的展现效果还会让咱们上报列表元素的曝光信息。html
什么是曝光是信息呢?简单来讲就是用户实际看到了一个列表中的哪些元素?实际展现给用户的这部分元素用户浏览了多少次?git
让咱们经过一个简单示例应用来讲明:github
import 'package:flutter/material.dart';
class Card extends StatelessWidget {
final String text;
Card({
@required this.text,
});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(bottom: 10.0),
color: Colors.greenAccent,
height: 300.0,
child: Center(
child: Text(
text,
style: TextStyle(fontSize: 40.0),
),
),
);
}
}
class HelloFlutter extends StatelessWidget {
final items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (BuildContext context, int index) {
return Card(text: '$index');
},
);
}
}
void main() {
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(title: Text('hello flutter')),
body: HelloFlutter())));
}
复制代码
上面这段代码建立了一个卡片列表。假设咱们像下面这样操做:bash
应用启动时默认展现了第 0、一、2 张卡片,接着咱们向下浏览到第 3 张卡片,这时第 0 张卡片已经离开屏幕可视区域。最后咱们从新回到顶部,第 0 张卡片再次进入可视区域。app
此时的曝光数据就是:less
0 -> 1 -> 2 -> 3 -> 0
复制代码
在了解了什么是曝光信息之后,让咱们来看看如何统计这类信息。在讲解具体方案以前,先让咱们看看 ListView 组件的工做原理。ide
因为 ListView 组件的具体实现原理有不少细节,这里咱们只从宏观上介绍和曝光逻辑相关的部分。函数
读过 ListView 组件文档的小伙伴应该都知道 ListView 组件的子元素都是按需加载的。换句话说,只有在可视区域的元素才会被初始化。这样作能够保证不论列表中有多少子元素,ListView 组件对系统资源的占用始终能够保持在一个比较低的水平。post
按需加载的子元素是如何动态建立的呢?先让咱们看看 ListView 的构造函数。ui
一般咱们有 3 种方式建立一个 ListView (注:为方便阅读,三种建立方式中共同的参数已被省去):
ListView({
List<Widget> children,
})
ListView.builder({
int: itemCount,
IndexedWidgetBuilder itemBuilder,
})
ListView.custom({
SliverChildDelegate childrenDelegate,
})
复制代码
你们可能对前两种比较熟悉,分别是传入一个子元素列表或是传入一个根据索引建立子元素的函数。其实前两种方式都是第三种方式的“快捷方式”。由于 ListView 内部是靠这个 childrenDelegate
属性动态初始化子元素的。
以 ListView({List<Widget> children})
为例,其构造函数以下:
ListView({
...
List<Widget> children: const <Widget>[],
}) : childrenDelegate = new SliverChildListDelegate(
children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
), super(
key: key,
...
);
复制代码
可见,这里自动帮咱们建立了一个 SliverChildListDelegate
的实例。而SliverChildListDelegate
是抽象类 SliverChildDelegate
的子类。SliverChildListDelegate
中主要逻辑就是实现了 SliverChildDelegate
中定义的 build
方法:
@override
Widget build(BuildContext context, int index) {
assert(children != null);
if (index < 0 || index >= children.length)
return null;
Widget child = children[index];
assert(child != null);
if (addRepaintBoundaries)
child = new RepaintBoundary.wrap(child, index);
if (addAutomaticKeepAlives)
child = new AutomaticKeepAlive(child: child);
return child;
}
复制代码
逻辑很简单,根据传入的索引返回 children
列表中对应的元素。
每当 ListView 的底层实现须要加载一个元素时,就会把该元素的索引传递给 SliverChildDelegate
的 build
方法,由该方法返回具体的元素。当经过 ListView.builder
方式建立 ListView 时,构造函数自动帮咱们建立的是 SliverChildBuilderDelegate
实例(点此查看相关代码)。
看到这里你可能会问,说了这么多,和曝光统计有什么关系呢?
在 SliverChildDelegate
内部,除了定义了 build
方法外,还定义了一个名为 didFinishLayout
的方法:
void didFinishLayout(int firstIndex, int lastIndex) {}
复制代码
每当 ListView 完成一次 layout 以后都会调用该方法。同时传入两个索引值。这两个值分别是这次 layout 中第一个元素和最后一个元素在 ListView 全部子元素中的索引值。也就是可视区域内的元素在子元素列表中的位置。咱们只要比较两次 layout 之间这些索引值的差别就能够推断出有哪些元素曝光了,哪些元素隐藏了。
然而不管是 SliverChildListDelegate
仍是 SliverChildBuilderDelegate
的代码中,都没有 didFinishLayout
的具体实现。因此咱们须要编写一个它们的子类。
首先让咱们定义一个实现了 didFinishLayout
方法的 SliverChildBuilderDelegate
的子类:
class MySliverChildBuilderDelegate extends SliverChildBuilderDelegate {
MySliverChildBuilderDelegate(
Widget Function(BuildContext, int) builder, {
int childCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
}) : super(builder,
childCount: childCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries);
@override
void didFinishLayout(int firstIndex, int lastIndex) {
print('firstIndex: $firstIndex, lastIndex: $lastIndex');
}
}
复制代码
而后将咱们示例应用中建立 ListView 的代码改成使用咱们新建立的类:
Widget build(BuildContext context) {
return ListView.custom(
childrenDelegate: MySliverChildBuilderDelegate(
(BuildContext context, int index) {
return Card(text: '$index');
}, childCount: items.length,
),
);
}
复制代码
从新在模拟器中启动咱们的实例程序能够看到:
首先咱们能够看到调试终端中输出了咱们打印的调试信息。可是仔细观察会发现输出的信息和咱们指望的并不彻底一致。首先咱们打开首屏时,但是区域内只展现了 3 张卡片,但终端中输出的 lastIndex
倒是 3,这意味着 ListVivew 组件实际渲染了 4 张卡片。其次,随着咱们划动屏幕将第 1 张卡片划出可视区域后,firstIndex
并无当即从 0 变成 1,而是在咱们继续划动一段距离后才改变。
通过查阅文档并阅读相关源码,咱们了解到 ListView 中还有一个 cacheExtent
的概念。能够简单理解成一个“预加载”的区域。也就是说出如今可视区域上下各 cacheExtent
大小区域内的元素会被提早加载。虽然咱们建立 ListView 时并无指定该值,但因为该属性有一个默认值,因此仍是影响咱们的曝光统计。
如今让咱们更新示例应用的代码,明确把 cacheExtent
设置为 0.0
:
return ListView.custom(
childrenDelegate: MySliverChildBuilderDelegate(
(BuildContext context, int index) {
return Card(text: '$index');
}, childCount: items.length,
),
cacheExtent: 0.0,
);
复制代码
重启示例应用:
能够看到此次咱们已经能够正确获取当前渲染元素的索引值了。
剩下的逻辑就很简单了,咱们只须要在 MySliverChildBuilderDelegate
中记录并比较每次 didFinishLayout
收到的参数就能够正确的获取曝光元素的索引了。具体的代码就不贴在这里了,文末会给出实例应用的代码库地址。
让咱们看看完成后的效果吧:
因为强制把 cacheExtent
强制设置为了 0.0
,从而关闭了“预加载”。在复杂页面中快速划动时有可能会有延迟加载的状况,这须要你们根据本身具体的场景评估。本文中介绍的方案也不是实现曝光统计逻辑的惟一方式,只是为你们提供一个思路。欢迎一块儿讨论 :)。
本文中示例应用的完整代码能够在这里找到。