极端很容易,平衡才是最难的。
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
会有更好的性能。注意,itemExtent
和prototypeItem
互斥,不能同时指定它们。shrinkWrap
:该属性表示是否根据子组件的总长度来设置ListView
的长度,默认值为false
。默认情况下,ListView
会在滚动方向尽可能多的占用空间。当ListView
在一个无边界(滚动方向上)的容器中时,shrinkWrap
必须为true
。addAutomaticKeepAlives
:该属性比较复杂,以后再说。addRepaintBoundaries
:该属性表示是否将列表项(子组件)包裹在RepaintBoundary
组件中。RepaintBoundary
读者可以先简单理解为它是一个”绘制边界“,将列表项包裹在RepaintBoundary
中可以避免列表项不必要的重绘,但是当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加RepaintBoundary
反而会更高效(具体原因会在本书后面 Flutter 绘制原理相关章节中介绍)。如果列表项自身来维护是否需要添加绘制边界组件,则此参数应该指定为false
。
默认构造函数有一个children
参数,它接受一个Widget
列表(ListListView.builder
按需动态构建列表项。
注意:虽然这种方式将所有
children
一次性传递给ListView
,但子组件仍然是在需要时才会加载(build(如有)、布局、绘制),也就是说通过默认构造函数构建的ListView
也是基于Sliver
的列表懒加载模型。
举个栗子:
_listView() {
return ListView(
shrinkWrap: true,
padding: const EdgeInsets.all(20.0),
children: const [
Text('张三'),
Text('李四'),
Text('王五'),
Text('赵六'),
],
);
}
效果如下:
可以看到,虽然使用默认构造函数创建的列表也是懒加载的,但我们还是需要提前将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"));
});
}
效果如下:
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;
},
);
}
效果如下:
当然还有一个场景就是,每个item
之间有一定的间距,也可以用这个实现。
总结
- 我在开发中一般使用
ListView.builder
,里面的itemBuilder
对应的原生的cell
,然后自定义cell
就行了,用起来还是比较舒服的。 - 如果
item
之间有分割线或是间距的,ListView.separated
当首选。
后记
常见的列表页面ListView
一般都能搞定,其对应iOS 原生的UITableView
。那原生的UICollectionView
呢,在Flutter中要使用GridView
,这个以后再写。