持续创作,加快生长!这是我参加「日新计划 10 月更文挑战」的第32天,点击查看活动概况

ListView是最常用的可翻滚组件之一,它能够沿一个方向线性排布一切子组件,而且它也支撑列表项懒加载(在需求时才会创立)。咱们看看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<Widget> children = const <Widget>[],
})

上面参数分为两组:第一组是可翻滚组件的公共参数;第二组是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
  • addRepaintBoundaries:该属性表明是否将列表项(子组件)包裹在RepaintBoundary组件中。RepaintBoundary 读者能够先简单理解为它是一个”制作鸿沟“,将列表项包裹在RepaintBoundary中能够避免列表项不必要的重绘,可是当列表项重绘的开支十分小(如一个颜色块,或者一个较短的文本)时,不增加RepaintBoundary反而会更高效。假如列表项本身来保护是否需求增加制作鸿沟组件,则此参数应该指定为 false。

留意:上面这些参数并非ListView特有,其它可翻滚组件也可能会具有这些参数,它们的含义是相同的。

默许结构函数

默许结构函数有一个children参数,它承受一个Widget列表(List)。这种办法适合只有少数的子组件数量已知且比较少的状况,反之则应该运用ListView.builder 按需动态构建列表项。

留意,尽管这种办法将一切children一次性传递给 ListView,但子组件)仍然是在需求时才会加载(build(如有)、布局、制作),也便是说经过默许结构函数构建的 ListView 也是基于 Sliver 的列表懒加载模型。

下面是一个比方:

能够看到,尽管运用默许结构函数创立的列表也是懒加载的,但咱们仍是需求提早将 Widget 创立好,等到真实需求加载的时分才会对 Widget 进行布局和制作。

shrinkWrap: true 作用,ListView依据子视图核算高度:

32、Flutter之 ListView组件详解

shrinkWrap: false的作用,ListView的会在翻滚方向尽可能多的占用空间。

32、Flutter之 ListView组件详解

ListView.builder

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

    ListView.builder({
  // ListView公共参数已省掉  
  ...
  required IndexedWidgetBuilder itemBuilder,
  int itemCount,
  ...
})
  • itemBuilder:它是列表项的构建器,类型为IndexedWidgetBuilder,返回值为一个widget。当列表翻滚到详细的index位置时,会调用该构建器构建列表项。

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

下面看一个比方:

      return ListView.builder(
        itemCount: 100,
        itemExtent: 50,//强制高度为50.0
        itemBuilder: (BuildContext context,int index){
      return ListTile(
        leading: const Icon(Icons.person),
        title: Text('$index'),
      );
    });

运转作用“

32、Flutter之 ListView组件详解

ListView.separated

ListView.separated能够在生成的列表项之间增加一个切割组件,它比ListView.builder多了一个separatorBuilder参数,该参数是一个切割组件生成器。
下面咱们看一个比方:奇数行增加一条蓝色下划线,偶数行增加一条绿色下划线。

 //下划线widget预定义以供复用。
    Widget divider1=Divider(color: Colors.blue,);
    Widget divider2=Divider(color: Colors.green);
    return ListView.separated(
      //列表项结构器
        itemBuilder: (BuildContext context, int index) {
          return ListTile(title: Text("$index"));
        },
        //切割器结构器
        separatorBuilder: (BuildContext context, int index) {
          return index%2==0?divider1:divider2;
        },
        itemCount: 100);
  }    

运转作用:

32、Flutter之 ListView组件详解

固定高度列表

前面说过,给列表指定 itemExtentprototypeItem 会有更高的功能,所以当咱们知道列表项的高度都相一起,强烈建议指定 itemExtentprototypeItem 。下面看一个示例:

ListView.builder(
        prototypeItem: const ListTile(
          title: Text('1'),
        ),
        itemBuilder: (BuildContext context, int index) {
          return Center(child: Text('$index'),);
        }); 

由于列表项都是一个 ListTile,高度相同,可是咱们不知道 ListTile 的高度是多少,所以指定了prototypeItem ,每个item高度依据prototypeItem来定。

ListView 原理

ListView 内部组合了 Scrollable、Viewport 和 Sliver,需求留意:

  • ListView 中的列表项组件都是 RenderBox,并不是 Sliver, 这个一定要留意。
  • 一个 ListView 中只有一个Sliver,对列表项进行按需加载的逻辑是 Sliver 中完成的。
  • ListView 的 Sliver 默许是 SliverList,假如指定了 itemExtent ,则会运用 SliverFixedExtentList;假如 prototypeItem 属性不为空,则会运用 SliverPrototypeExtentList,无论是是哪个,都完成了子组件的按需加载模型。

实例:无限加载列表

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


class MyListViewPage extends StatefulWidget {
  const MyListViewPage({Key? key}) : super(key: key);
  @override
  _MyListViewPageState createState() => _MyListViewPageState();
}
class _MyListViewPageState extends State<MyListViewPage> {
  static const loadingTag = "##loading##"; //表尾符号
  final _words = <String>[loadingTag];
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _retrieveData();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: getAppBar('ListView'),
      body: Container(
        color: Colors.black.withOpacity(0.2),
        child: _buildInfinite(),
      ),
    );
  }
  _buildDefault() {
    return ListView(
      shrinkWrap: false,
      padding: const EdgeInsets.all(20.0),
      children: const <Widget>[
        Text('I\'m dedicating every day to you'),
        Text('Domestic life was never quite my style'),
        Text('When you smile, you knock me out, I fall apart'),
        Text('And I thought I was so smart'),
      ],
    );
  }
  _buildBuilder() {
    return ListView.builder(
        itemCount: 100,
        itemExtent: 50, //强制高度为50.0
        itemBuilder: (BuildContext context, int index) {
          return ListTile(
            leading: const Icon(Icons.person),
            title: Text('$index'),
          );
        });
  }
  _buildSeparated() {
    //下划线widget预定义以供复用。
    Widget divider1 = Divider(
      color: Colors.blue,
    );
    Widget divider2 = Divider(color: Colors.green);
    return ListView.separated(
        scrollDirection: Axis.vertical,
        //列表项结构器
        itemBuilder: (BuildContext context, int index) {
          return ListTile(title: Text("$index"));
        },
        //切割器结构器
        separatorBuilder: (BuildContext context, int index) {
          return index % 2 == 0 ? divider1 : divider2;
        },
        itemCount: 100);
  }
  _buildExtent() {
    return ListView.builder(
        prototypeItem: const ListTile(
          title: Text('1'),
        ),
        itemBuilder: (BuildContext context, int index) {
          //LayoutLogPrint是一个自定义组件,在布局时能够打印当时上下文中父组件给子组件的束缚信息
          return Center(child: Text('$index'),);
        });
  }
   //无限加载列表
  _buildInfinite(){
     return ListView.separated(
         itemBuilder: (context,index){
           //假如到了表尾
           if(_words[index] ==loadingTag) {
             //假如数据不足100条
             if (_words.length <= 100) {
               //拉去数据
               _retrieveData();
               //加载显现loading
               return Container(
                 padding: const EdgeInsets.all(16),
                 alignment: Alignment.center,
                 child: const SizedBox(
                   width: 24,
                   height: 24,
                   child: CircularProgressIndicator(strokeWidth: 2,),
                 ),
               );
             } else {
               //已经加载100不再获取数据
               return Container(
                 alignment: Alignment.center,
                 padding: const EdgeInsets.all(16),
                 child: const Text('没有更多了',
                   style: TextStyle(color: Colors.grey),),
               );
             }
           }
           return ListTile(title: Text(_words[index]),);
         },
         separatorBuilder:(context,index)=>Divider(height:1,color: Colors.black,),
         itemCount: _words.length);
  }
  void _retrieveData(){
    Future.delayed(Duration(seconds: 5)).then((value){
      setState(() {
        _words.insertAll(_words.length-1,
            //每次生成20个单词
            List.generate(20, (index){
              return 'words $index';
            }));
      });
    });
  }
}    

运营作用:

32、Flutter之 ListView组件详解

增加固定列表头

很多时分咱们需求给列表增加一个固定表头,比方咱们想完成一个产品列表,需求在列表顶部增加一个“产品列表”标题,希望的作用如图 6-6 所示:

32、Flutter之 ListView组件详解
咱们按照之前经验,写出如下代码:

@override
Widget build(BuildContext context) {
  return Column(children: <Widget>[
    ListTile(title:Text("产品列表")),
    ListView.builder(itemBuilder: (BuildContext context, int index) {
        return ListTile(title: Text("$index"));
    }),
  ]);
}    

然后运转,发现并没有出现咱们希望的作用,相反触发了一个反常;

Vertical viewport was given unbounded height.

======== Exception caught by rendering library =====================================================
The following assertion was thrown during performResize():
Vertical viewport was given unbounded height.
Viewports expand in the scrolling direction to fill their container. In this case, a vertical viewport was given an unlimited amount of vertical space in which to expand. This situation typically happens when a scrollable widget is nested inside another scrollable widget.
If this widget is always nested in a scrollable widget there is no need to use a viewport because there will always be enough vertical space for the children. In this case, consider using a Column instead. Otherwise, consider using the "shrinkWrap" property (or a ShrinkWrappingViewport) to size the height of the viewport to the sum of the heights of its children.    

从反常信息中咱们能够看到是由于ListView高度鸿沟无法确定引起,所以处理的办法也很明显,咱们需求给ListView指定鸿沟,咱们经过SizedBox指定一个列表高度看看是否生效:

Column(
      children: [
        ListTile(title: Text('产品列表'),),
        SizedBox(height: 400,//指定高度
        child: ListView.builder(itemBuilder: (BuildContext context,int index){
          return ListTile(title: Text('$index'),);
        }),
        )
      ],
    )    

32、Flutter之 ListView组件详解

能够看到,现在没有触发反常而且列表已经显现出来了,可是咱们的手机屏幕高度要大于 400,所以底部会有一些空白。那假如咱们要完成列表铺满除表头以外的屏幕空间应该怎么做?直观的办法是咱们去动态核算,用屏幕高度减去状态栏、导航栏、表头的高度即为剩余屏幕高度,代码如下:

... //省掉无关代码
SizedBox(
  //Material设计规范中状态栏、导航栏、ListTile高度分别为245656 
  height: MediaQuery.of(context).size.height-24-56-56,
  child: ListView.builder(itemBuilder: (BuildContext context, int index) {
    return ListTile(title: Text("$index"));
  }),
)
...      

32、Flutter之 ListView组件详解
能够看到,咱们希望的作用完成了,可是这种办法并不优雅,假如页面布局发生变化,比方表头布局调整导致表头高度改动,那么剩余空间的高度就得从头核算。那么有什么办法能够主动拉伸ListView以填充屏幕剩余空间的办法吗?当然有!答案便是Flex。在弹性布局中,能够运用Expanded主动拉伸组件巨细,而且咱们也说过Column是承继自Flex的,所以咱们能够直接运用Column + Expanded来完成,代码如下:

 @override
Widget build(BuildContext context) {
  return Column(children: <Widget>[
    ListTile(title:Text("产品列表")),
    Expanded(
      child: ListView.builder(itemBuilder: (BuildContext context, int index) {
        return ListTile(title: Text("$index"));
      }),
    ),
  ]);
}   

运转后,和上图相同,完美完成了!

总结

本节主要介绍了ListView 常用的的运用办法和要点,但并没有介绍ListView.custom办法,它需求完成一个SliverChildDelegate 用来给 ListView 生成列表项组件,更多概况请参阅 API 文档。

demo完好代码:gitee.com/wywinstonwy…