Flutter学习之九 ListView

极端很容易,平衡才是最难的。

Flutter学习之八 Container

前言

Flutter中的ListView的地位,就好比于iOS中的UITableView,算是最常用的可滚动组件之一,它可以沿一个方向线性排布所有子组件,并且它也支持列表项懒加载(在需要时才会创建)。

默认构造函数

我们看看ListView的默认构造函数定义:

ListView({
  ...  
  //可滚动widget公共参数
  Axis scrollDirection = Axis.vertical,
  bool reverse = false,
  ScrollController? controller,
  bool? primary,
  ScrollPhysics? physics,
  EdgeInsetsGeometry? padding,
  
  //ListView各个构造函数的共同参数  
  double? itemExtent,
  Widget? prototypeItem, //列表项原型,后面解释
  bool shrinkWrap = false,
  bool addAutomaticKeepAlives = true,
  bool addRepaintBoundaries = true,
  double? cacheExtent, // 预渲染区域长度
    
  //子widget列表
  List children = const [],
})

上面的可滑动公共参数就不再赘述了,下面是ListView各个构造函数(ListView有多个构造函数)的共同参数,我们重点来看看这些参数:

  • itemExtent:该参数如果不为null,则会强制children的“长度”为itemExtent的值;这里的“长度”是指滚动方向上子组件的长度,也就是说如果滚动方向是垂直方向,则itemExtent代表子组件的高度;如果滚动方向为水平方向,则itemExtent就代表子组件的宽度。在ListView中,指定itemExtent比让子组件自己决定自身长度会有更好的性能,这是因为指定itemExtent后,滚动系统可以提前知道列表的长度,而无需每次构建子组件时都去再计算一下,尤其是在滚动位置频繁变化时(滚动系统需要频繁去计算列表高度),和原生很像。

  • prototypeItem:如果我们知道列表中的所有列表项长度都相同但不知道具体是多少,这时我们可以指定一个列表项,该列表项被称为prototypeItem(列表项原型)。指定 prototypeItem后,可滚动组件会在 layout 时计算一次它延主轴方向的长度,这样也就预先知道了所有列表项的延主轴方向的长度,所以和指定 itemExtent一样,指定prototypeItem会有更好的性能。注意,itemExtentprototypeItem互斥,不能同时指定它们。

  • shrinkWrap:该属性表示是否根据子组件的总长度来设置ListView的长度,默认值为false 。默认情况下,ListView会在滚动方向尽可能多的占用空间。当ListView在一个无边界(滚动方向上)的容器中时,shrinkWrap必须为true

  • addAutomaticKeepAlives:该属性比较复杂,以后再说。

  • addRepaintBoundaries:该属性表示是否将列表项(子组件)包裹在RepaintBoundary组件中。RepaintBoundary 读者可以先简单理解为它是一个”绘制边界“,将列表项包裹在RepaintBoundary中可以避免列表项不必要的重绘,但是当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加RepaintBoundary反而会更高效(具体原因会在本书后面 Flutter 绘制原理相关章节中介绍)。如果列表项自身来维护是否需要添加绘制边界组件,则此参数应该指定为false

默认构造函数有一个children参数,它接受一个Widget列表(List)。这种方式适合只有少量的子组件数量已知且比较少的情况,反之则应该使用ListView.builder 按需动态构建列表项。

注意:虽然这种方式将所有children一次性传递给 ListView,但子组件仍然是在需要时才会加载(build(如有)、布局、绘制),也就是说通过默认构造函数构建的ListView 也是基于 Sliver的列表懒加载模型。

举个栗子:

_listView() {
  return ListView(
    shrinkWrap: true,
    padding: const EdgeInsets.all(20.0),
    children: const [
      Text('张三'),
      Text('李四'),
      Text('王五'),
      Text('赵六'),
    ],
  );
}

效果如下:


ListView.png

可以看到,虽然使用默认构造函数创建的列表也是懒加载的,但我们还是需要提前将Widget创建好,等到真正需要加载的时候才会对 Widget 进行布局和绘制。

ListView.builder

ListView.builder适合列表项比较多或者列表项不确定的情况,下面看一下ListView.builder的核心参数列表:

ListView.builder({
  // ListView公共参数已省略  
  ...
  required IndexedWidgetBuilder itemBuilder,
  int itemCount,
  ...
})

+itemBuilder:它是列表项的构建器,类型为IndexedWidgetBuilder,返回值为一个widget。当列表滚动到具体的index位置时,会调用该构建器构建列表项。

  • itemCount:列表项的数量,如果为null,则为无限列表

举个栗子:

_listViewBuild() {
  return ListView.builder(
      itemCount: 100,
      itemExtent: 50.0, //强制高度为50.0
      itemBuilder: (BuildContext context, int index) {
        return ListTile(title: Text("_listViewBuild $index"));
      });
}

效果如下:


ListViewBuilder.gif

ListView.separated

ListView.separated可以在生成的列表项之间添加一个分割组件,它比ListView.builder多了一个separatorBuilder参数,该参数是一个分割组件生成器。

举个栗子:

_listViewSeparated() {
  // widget预定义以供复用。
  Widget blue = Container(color: Colors.blue, height: 10);
  Widget red = Container(color: Colors.red, height: 10);
  return ListView.separated(
    itemCount: 100,
    //列表项构造器
    itemBuilder: (BuildContext context, int index) {
      return ListTile(title: Text("$index"));
    },
    //分割器构造器
    separatorBuilder: (BuildContext context, int index) {
      return index % 2 == 0 ? blue : red;
    },
  );
}

效果如下:


_listViewSeparated.gif

当然还有一个场景就是,每个item之间有一定的间距,也可以用这个实现。

总结

  • 我在开发中一般使用ListView.builder,里面的itemBuilder对应的原生的cell,然后自定义cell就行了,用起来还是比较舒服的。
  • 如果item之间有分割线或是间距的,ListView.separated当首选。

后记

常见的列表页面ListView一般都能搞定,其对应iOS 原生的UITableView。那原生的UICollectionView呢,在Flutter中要使用GridView,这个以后再写。

你可能感兴趣的:(Flutter学习之九 ListView)