Flutter可滚动组件(2):ListView

fengMisaka / 2024-10-12 / 原文

一、ListView基础

移动端数据量比较大时,我们都是通过列表来进行展示的,比如商品数据、聊天列表、通信录、朋友圈等。

在 Android 中,我们可以使用 ListView 或 RecyclerView 来实现,在 iOS 中,我们可以通过 UITableView 来实现。

在 Flutter 中,我们也有对应的列表 Widget,就是 ListView。


1.1 ListView基本使用

ListView 可以沿一个方向(垂直或水平方向,默认是垂直方向)来排列其所有子 Widget。

一种最简单的使用方式是直接将所有需要排列的子 Widget 放在 ListView 的 children 属性中即可。

我们来看一下直接使用 ListView 的代码演练:

  • 为了让文字之间有一些间距,我使用了 Padding Widget
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';

main(List<String> args) {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: MyHomeBody(),
      ),
    );
  }
}

class MyHomeBody extends StatelessWidget {
  MyHomeBody({Key? key}) : super(key: key);
  final TextStyle textStyle = TextStyle(fontSize: 20, color: Colors.redAccent);

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: <Widget>[
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text("人的一切痛苦,本质上都是对自己无能的愤怒。", style: textStyle),
        ),
        Padding(
          padding: const EdgeInsets.all(8.0),
          child:
              Text("人活在世界上,不可以有偏差;而且多少要费点劲儿,才能把自己保持到理性的轨道上。", style: textStyle),
        ),
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text("我活在世上,无非想要明白些道理,遇见些有趣的事。", style: textStyle),
        )
      ],
    );
  }
}

效果图如下所示:

Flutter_view_A.png


1.2 ListTile的使用

在开发中,我们经常见到一种列表,有一个图标或图片(Icon),有一个标题(Title),有一个子标题(Subtitle),还有尾部一个图标(Icon)。

这个时候,我们可以使用ListTile来实现:

class MyHomeBody extends StatelessWidget {
  const MyHomeBody({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: <Widget>[
        ListTile(
          leading: Icon(
            Icons.people,
            size: 36,
          ),
          title: Text("联系人"),
          subtitle: Text("联系人信息"),
          trailing: Icon(Icons.arrow_forward_ios),
        ),
        ListTile(
          leading: Icon(
            Icons.email,
            size: 36,
          ),
          title: Text("邮箱"),
          subtitle: Text("邮箱地址信息"),
          trailing: Icon(Icons.arrow_forward_ios),
        ),
        ListTile(
          leading: Icon(
            Icons.message,
            size: 36,
          ),
          title: Text("消息"),
          subtitle: Text("消息详情信息"),
          trailing: Icon(Icons.arrow_forward_ios),
        ),
        ListTile(
          leading: Icon(
            Icons.map,
            size: 36,
          ),
          title: Text("地址"),
          subtitle: Text("地址详情信息"),
          trailing: Icon(Icons.arrow_forward_ios),
        )
      ],
    );
  }
}

效果图如下所示:

Flutter_view_B.png


二、ListView.build

通过构造函数中的 children 传入所有的子 Widget 有一个问题:默认会创建出所有的子 Widget。

但是对于用户来说,一次性构建出所有的 Widget 并不会有什么差异,但是对于我们的程序来说会产生性能问题,而且会增加首屏的渲染时间。

我们可以 ListView.build 来构建子 Widget,提供性能。


2.1 ListView.build基本使用

ListView.build 适用于子 Widget 比较多的场景,该构造函数将创建子 Widget 交给了一个抽象的方法,交给 ListView 进行管理,ListView 会在真正需要的时候去创建子 Widget,而不是一开始就全部初始化好。

该方法有两个重要参数:

  • itemBuilder:列表项创建的方法。当列表滚动到对应位置的时候,ListView 会自动调用该方法来创建对应的子 Widget。类型是 IndexedWidgetBuilder,是一个函数类型。
  • itemCount:表示列表项的数量,如果为空,则表示 ListView 为无限列表。

我们还是通过一个简单的案例来认识它:

class MyHomeBody extends StatelessWidget {
  const MyHomeBody({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
        itemCount: 100,
        itemExtent: 80,
        itemBuilder: (BuildContext context, int index) {
          return ListTile(
              title: Text("标题$index"), subtitle: Text("详情内容$index"));
        });
  }
}

效果图如下所示:

Flutter_view_C.png


2.2 ListView.build动态数据

我们现在动态的来通过 JSON 数据展示一个列表。

思考:这个时候是否依然可以使用StatelessWidget:

答案:不可以,因为当前我们的数据是异步加载的,刚开始界面并不会展示数据(没有数据),后面从 JSON 中加载出来数据(有数据)后,再次展示加载的数据。

  • 这里是有状态的变化的,从无数据,到有数据的变化。
  • 这个时候,我们需要使用StatefulWidget来管理组件。

展示代码如下:

import'model/anchor.dart';

...省略中间代码
  class MyHomeBody extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return MyHomeBodyState();
  }
}

class MyHomeBodyState extends State<MyHomeBody> {
  List<Anchor> anchors = [];

  // 在初始化状态的方法中加载数据
  @override
  void initState() {
    getAnchors().then((anchors) {
      setState(() {
        this.anchors = anchors;
      });
    });

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemBuilder: (BuildContext context, int index) {
        return Padding(
          padding: EdgeInsets.all(8),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Image.network(
                anchors[index].imageUrl,
                fit: BoxFit.fitWidth,
                width: MediaQuery.of(context).size.width,
              ),
              SizedBox(height: 8),
              Text(anchors[index].nickname, style: TextStyle(fontSize: 20),),
              SizedBox(height: 5),
              Text(anchors[index].roomName)
            ],
          ),
        );
      },
    );
  }
}

2.3 ListView.separated

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

下面我们看一个例子:奇数行添加一条蓝色下划线,偶数行添加一条红色下划线:

class MyHomeBody extends StatelessWidget {
  Divider blueColor = Divider(color: Colors.blue);
  Divider redColor = Divider(color: Colors.red);

  @override
  Widget build(BuildContext context) {
    return ListView.separated(
        itemBuilder: (BuildContext context, int index) {
          return ListTile(
            leading: Icon(Icons.people),
            title: Text("联系人${index + 1}"),
            subtitle: Text("联系人电话${index + 1}"),
          );
        },
        separatorBuilder: (BuildContext context, int index) {
          return index % 2 == 0 ? redColor : blueColor;
        },
        itemCount: 100);
  }
}

效果图如下所示:

Flutter_view_D.png