Kodokon kodokon.com

用 ListView.builder 打造高性能列表

借助 ListView.builder、ListTile 以及被提取为独立 widget 的列表项,流畅地显示成千上万条数据而不卡顿。

8 分钟 · 3 题

在 Kodokon 中打开本课

真实的应用几乎总要显示列表:消息、商品、联系人。你已经认识的 ListView(children: ...) 构造函数会一次性构建它的所有子项 - 十来个项目没问题,上千个就是灾难。ListView.builder 只在滚动时按需构建可见区域附近的项目。

DART
import 'package:flutter/material.dart';

class ContactsScreen extends StatelessWidget {
  const ContactsScreen({super.key, required this.names});

  final List<String> names;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Contacts')),
      body: ListView.builder(
        itemCount: names.length,
        itemBuilder: (context, index) {
          return ListTile(
            leading: const Icon(Icons.person),
            title: Text(names[index]),
          );
        },
      ),
    );
  }
}
即使有 10,000 个名字,也只构建可见的项目。

两个参数撑起了整套机制:itemCount 声明项目的总数,itemBuilder 则在某个索引接近屏幕时才生成对应的项目。对于每一行的内容,ListTile 是你在 Material 中的好帮手:leadingtitlesubtitletrailing 负责组织这一行的结构,而 onTap 会在触摸时提供墨水波纹效果。但一旦某个项目变得复杂,就别让它在 itemBuilder 里膨胀:把它提取成独立的 widget

DART
import 'package:flutter/material.dart';

class ContactTile extends StatelessWidget {
  const ContactTile({
    super.key,
    required this.name,
    required this.onTap,
  });

  final String name;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: CircleAvatar(child: Text(name[0])),
      title: Text(name),
      trailing: const Icon(Icons.chevron_right),
      onTap: onTap,
    );
  }
}
一个被改造成可复用、可测试 widget 的列表项。

被提取出来的项目可以单独测试、在其他界面中复用,并让 itemBuilder 变得非常简单。还剩一个常见需求:分隔线。ListView.separated 的用法和 builder 一样,只是多了第三个参数 separatorBuilder,它会在每两个项目之间被调用 - 最后一个元素后面永远不会跟着分隔线。

DART
import 'package:flutter/material.dart';

class ContactList extends StatelessWidget {
  const ContactList({super.key, required this.names});

  final List<String> names;

  @override
  Widget build(BuildContext context) {
    return ListView.separated(
      itemCount: names.length,
      separatorBuilder: (context, index) {
        return const Divider(height: 1);
      },
      itemBuilder: (context, index) {
        return ContactTile(
          name: names[index],
          onTap: () => debugPrint(names[index]),
        );
      },
    );
  }
}
separatorBuilder 会在每两个项目之间插入一条 Divider。

知识检测

确认你已牢记本课的重点内容。

  1. 对于一个有 500 个项目的列表,为什么要优先选择 ListView.builder 而不是 ListView(children: ...)?
    • 它会自动给项目排序
    • 它只按需构建可见的项目
    • 它会缓存图片
    • 它会自动添加分隔线
  2. 哪个构造函数会在每两个项目之间插入一个 widget?
    • ListView.divided
    • ListView.builder
    • ListView.separated
  3. 把列表项提取成独立的 widget 类有什么好处?
    • 这是 ListView.builder 能够编译的必要条件
    • 它让 const 成为可能、隔离重建,并让复用更容易
    • 它让列表可以滚动
    • 它让你不用再写 itemCount