ListView

ListView是最常用的可滚动widget,它可以沿一个方向线性排布所有子widget。我们看看ListView的默认构造函数定义:

  1. ListView({
  2. ...
  3. //可滚动widget公共参数
  4. Axis scrollDirection = Axis.vertical,
  5. bool reverse = false,
  6. ScrollController controller,
  7. bool primary,
  8. ScrollPhysics physics,
  9. EdgeInsetsGeometry padding,
  10. //ListView各个构造函数的共同参数
  11. double itemExtent,
  12. bool shrinkWrap = false,
  13. bool addAutomaticKeepAlives = true,
  14. bool addRepaintBoundaries = true,
  15. double cacheExtent,
  16. //子widget列表
  17. List<Widget> children = const <Widget>[],
  18. })

上面参数分为两组:第一组是可滚动widget公共参数,前面已经介绍过,不再赘述;第二组是ListView各个构造函数(ListView有多个构造函数)的共同参数,我们重点来看看这些参数,:

  • itemExtent:该参数如果不为null,则会强制children的”长度”为itemExtent的值;这里的”长度”时指滚动方向上子widget的长度,即如果滚动方向时垂直方向,则itemExtent代表子widget的高度,如果滚动方向为水平方向,则itemExtent代表子widget的长度。在ListView中,指定itemExtent比让子widget自己决定自身长度会更高效,这是因为指定itemExtent后,滚动系统可以提前知道列表的长度,而不是总是动态去计算,尤其是在滚动位置频繁变化时(滚动系统需要频繁去计算列表高度)。
  • shrinkWrap:该属性表示是否根据子widget的总长度来设置ListView的长度,默认值为false 。默认情况下,ListView的会在滚动方向尽可能多的占用空间。当ListView在一个无边界(滚动方向上)的容器中时,shrinkWrap必须为true
  • addAutomaticKeepAlives:该属性表示是否将列表项(子widget)包裹在AutomaticKeepAlive widget中;典型地,在一个懒加载列表中,如果将列表项包裹在AutomaticKeepAlive中,在该列表项滑出视口时该列表项不会被GC,它会使用KeepAliveNotification来保存其状态。如果列表项自己维护其KeepAlive状态,那么此参数必须置为false
  • addRepaintBoundaries:该属性表示是否将列表项(子widget)包裹在RepaintBoundary中。当可滚动widget滚动时,将列表项包裹在RepaintBoundary中可以避免列表项重绘,但是当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加RepaintBoundary反而会更高效。和addAutomaticKeepAlive一样,如果列表项自己维护其KeepAlive状态,那么此参数必须置为false

注意:上面这些参数并非ListView特有,在本章后面介绍的其它可滚动widget也可能会拥有这些参数,它们的含义是相同的。

默认构造函数

默认构造函数有一个children参数,它接受一个Widget列表(List)。这种方式适合只有少量的子widget的情况,因为这种方式需要将所有children都提前创建好(这需要做大量工作),而不是等到子widget真正显示的时候再创建。实际上通过此方式创建的ListView和使用SingleChildScrollView+Column的方式没有本质的区别。下面是一个例子:

  1. ListView(
  2. shrinkWrap: true,
  3. padding: const EdgeInsets.all(20.0),
  4. children: <Widget>[
  5. const Text('I\'m dedicating every day to you'),
  6. const Text('Domestic life was never quite my style'),
  7. const Text('When you smile, you knock me out, I fall apart'),
  8. const Text('And I thought I was so smart'),
  9. ],
  10. );

注意:可滚动widget通过一个List来作为其children属性时,只适用于子widget较少的情况,这是一个通用规律,并非ListView自己的特性,像GridView也是如此。

ListView.builder

ListView.builder适合列表项比较多(或者无限)的情况,因为只有当子Widget真正显示的时候才会被创建。下面看一下ListView.builder的核心参数列表:

  1. ListView.builder({
  2. // ListView公共参数已省略
  3. ...
  4. @required IndexedWidgetBuilder itemBuilder,
  5. int itemCount,
  6. ...
  7. })
  • itemBuilder:它是列表项的构建器,类型为IndexedWidgetBuilder,返回值为一个widget。当列表滚动到具体的index位置时,会调用该构建器构建列表项。
  • itemCount:列表项的数量,如果为null,则为无限列表。

看一个例子:

  1. ListView.builder(
  2. itemCount: 100,
  3. itemExtent: 50.0, //强制高度为50.0
  4. itemBuilder: (BuildContext context, int index) {
  5. return ListTile(title: Text("$index"));
  6. }
  7. );

image-20180911171508400

ListView.separated

ListView.separated可以生成列表项之间的分割器,它除了比ListView.builder多了一个separatorBuilder参数,该参数是一个分割器生成器。下面我们看一个例子:奇数行添加一条蓝色下划线,偶数行添加一条绿色下划线。

  1. class ListView3 extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. //下划线widget预定义以供复用。
  5. Widget divider1=Divider(color: Colors.blue,);
  6. Widget divider2=Divider(color: Colors.green);
  7. return ListView.separated(
  8. itemCount: 100,
  9. //列表项构造器
  10. itemBuilder: (BuildContext context, int index) {
  11. return ListTile(title: Text("$index"));
  12. },
  13. //分割器构造器
  14. separatorBuilder: (BuildContext context, int index) {
  15. return index%2==0?divider1:divider2;
  16. },
  17. );
  18. }
  19. }

image-20180911172832687

实例:无限加载列表

假设我们要从数据源异步分批拉取一些数据,然后用ListView显示,当我们滑动到列表末尾时,判断是否需要再去拉取数据,如果是,则去拉取,拉取过程在表尾显示一个loading,拉取成功后将数据插入列表;如果不需要再去拉取,则在表尾提示”没有更多”。代码如下:

  1. class InfiniteListView extends StatefulWidget {
  2. @override
  3. _InfiniteListViewState createState() => new _InfiniteListViewState();
  4. }
  5. class _InfiniteListViewState extends State<InfiniteListView> {
  6. static const loadingTag = "##loading##"; //表尾标记
  7. var _words = <String>[loadingTag];
  8. @override
  9. void initState() {
  10. _retrieveData();
  11. }
  12. @override
  13. Widget build(BuildContext context) {
  14. return ListView.separated(
  15. itemCount: _words.length,
  16. itemBuilder: (context, index) {
  17. //如果到了表尾
  18. if (_words[index] == loadingTag) {
  19. //不足100条,继续获取数据
  20. if (_words.length - 1 < 100) {
  21. //获取数据
  22. _retrieveData();
  23. //加载时显示loading
  24. return Container(
  25. padding: const EdgeInsets.all(16.0),
  26. alignment: Alignment.center,
  27. child: SizedBox(
  28. width: 24.0,
  29. height: 24.0,
  30. child: CircularProgressIndicator(strokeWidth: 2.0)
  31. ),
  32. );
  33. } else {
  34. //已经加载了100条数据,不再获取数据。
  35. return Container(
  36. alignment: Alignment.center,
  37. padding: EdgeInsets.all(16.0),
  38. child: Text("没有更多了", style: TextStyle(color: Colors.grey),)
  39. );
  40. }
  41. }
  42. //显示单词列表项
  43. return ListTile(title: Text(_words[index]));
  44. },
  45. separatorBuilder: (context, index) => Divider(height: .0),
  46. );
  47. }
  48. void _retrieveData() {
  49. Future.delayed(Duration(seconds: 2)).then((e) {
  50. _words.insertAll(_words.length - 1,
  51. //每次生成20个单词
  52. generateWordPairs().take(20).map((e) => e.asPascalCase).toList()
  53. );
  54. setState(() {
  55. //重新构建列表
  56. });
  57. });
  58. }
  59. }

image-20180912152627343image-20180912153208597

代码比较简单,读者可以参照代码中的注释理解,故不再赘述。需要说明的是,_retrieveData()的功能是模拟从数据源异步获取数据,我们使用english_words包的generateWordPairs()方法每次生成20个单词。

总结

本节主要介绍了ListView的一些公共参数以及常用的构造函数。不同的构造函数对应了不同的列表项生成模型,如果需要自定义列表项生成模型,可以通过ListView.custom来自定义,它需要实现一个SliverChildDelegate用来给ListView生成列表项widget,更多详情请参考API文档。