ListView

一个可以滚动的组件列表,按照线性排列

ListView是最常用的滚动组件,它会按照指定的滚动方向一个接一个的排列它的子组件,而在另一个方向上子组件需要填充满ListView的大小。它支持懒加载(只渲染可见区域),因此即使列表有成千上万条数据,也不会一次性全部构建,性能开销很小。

核心特点

  1. 方向可选: Axis.vertical(默认)或Axis.horizontal
  2. 懒加载: 仅构建可视区域内的子项
  3. 多种构造器: 针对不同场景(固定数量、无限列表、分隔线等)
  4. 可组合: 配合RefreshIndicatorScrollControllerAutomaticKeepAliveClientMixin等实现下拉刷新、上拉加载更多、缓存等功能

构造函数

  1. 默认构造函数,接收一个显式的List<Widget>子组件列表,这适用于子组件数量比较少的列表,ListView会将子组件列表中的所有子组件全部渲染出来,而不是仅仅渲染可见区域的子组件
ListView.new({
  Key? key, 
  Axis scrollDirection = Axis.vertical, 
  bool reverse = false, 
  ScrollController? controller, 
  bool? primary, 
  ScrollPhysics? physics, 
  bool shrinkWrap = false, 
  EdgeInsetsGeometry? padding, 
  double? itemExtent, 
  ItemExtentBuilder? itemExtentBuilder, 
  Widget? prototypeItem, 
  bool addAutomaticKeepAlives = true, 
  bool addRepaintBoundaries = true, 
  bool addSemanticIndexes = true, 
  double? cacheExtent, 
  List<Widget> children = const <Widget>[], 
  int? semanticChildCount, 
  DragStartBehavior dragStartBehavior = DragStartBehavior.start, 
  ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior, 
  String? restorationId, 
  Clip clipBehavior = Clip.hardEdge, 
  HitTestBehavior hitTestBehavior = HitTestBehavior.opaque
})
  1. ListView.builder构造函数接收一个IndexedWidgetBuilder,将会根据该builder来创建子组件,此构造函数适用于具有大量(或无限)子项的列表视图,因为builder仅对实际可见的子项调用
ListView.builder({
  Key? key, 
  Axis scrollDirection = Axis.vertical, 
  bool reverse = false, 
  ScrollController? controller, 
  bool? primary, 
  ScrollPhysics? physics, 
  bool shrinkWrap = false, 
  EdgeInsetsGeometry? padding, 
  double? itemExtent, 
  ItemExtentBuilder? itemExtentBuilder, 
  Widget? prototypeItem, 
  required NullableIndexedWidgetBuilder itemBuilder, 
  ChildIndexGetter? findChildIndexCallback, 
  int? itemCount, 
  bool addAutomaticKeepAlives = true, 
  bool addRepaintBoundaries = true, 
  bool addSemanticIndexes = true, 
  double? cacheExtent, 
  int? semanticChildCount, 
  DragStartBehavior dragStartBehavior = DragStartBehavior.start, 
  ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior, 
  String? restorationId, 
  Clip clipBehavior = Clip.hardEdge, 
  HitTestBehavior hitTestBehavior = HitTestBehavior.opaque
})
  1. ListView.custom构造函数接收一个SliverChildDelegate,可自定义子项模型的其他方面。例如SliverChildDelegate可以控制用于估算实际不可见子项尺寸的算法
ListView.custom({
  Key? key, 
  Axis scrollDirection = Axis.vertical, 
  bool reverse = false, 
  ScrollController? controller, 
  bool? primary, 
  ScrollPhysics? physics, 
  bool shrinkWrap = false, 
  EdgeInsetsGeometry? padding, 
  double? itemExtent, 
  Widget? prototypeItem, 
  ItemExtentBuilder? itemExtentBuilder, 
  required SliverChildDelegate childrenDelegate, 
  double? cacheExtent, 
  int? semanticChildCount, 
  DragStartBehavior dragStartBehavior = DragStartBehavior.start, 
  ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior, 
  String? restorationId, 
  Clip clipBehavior = Clip.hardEdge, 
  HitTestBehavior hitTestBehavior = HitTestBehavior.opaque
})
  1. ListView.separated构造函数接收两个IndexedWidgetBuilderitemBulder按需构建子组件,separatorBuilder按需构建子组件之间的分隔线
ListView.separated({
  Key? key, 
  Axis scrollDirection = Axis.vertical, 
  bool reverse = false, 
  ScrollController? controller, 
  bool? primary, 
  ScrollPhysics? physics, 
  bool shrinkWrap = false, 
  EdgeInsetsGeometry? padding, 
  required NullableIndexedWidgetBuilder itemBuilder, 
  ChildIndexGetter? findChildIndexCallback, 
  required IndexedWidgetBuilder separatorBuilder, 
  required int itemCount, 
  bool addAutomaticKeepAlives = true, 
  bool addRepaintBoundaries = true, 
  bool addSemanticIndexes = true, 
  double? cacheExtent, 
  DragStartBehavior dragStartBehavior = DragStartBehavior.start, 
  ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior, 
  String? restorationId, 
  Clip clipBehavior = Clip.hardEdge, 
  HitTestBehavior hitTestBehavior = HitTestBehavior.opaque
})
构造器适用场景关键参数
ListView()少量固定子项(<=10)children:<Widget>[]
ListView.builder()长列表/无限列表itemCount、itemBuilder
ListView.custom()完全自定义子项布局childrenDelegate
ListView.separated()需要分隔线separatorBuilder

示例代码

  1. 固定子项列表
ListView(
  padding: EdgeInsets.all(8),
  children: const [
    ListTile(title: Text('Item 1')),
    ListTile(title: Text('Item 2')),
    ListTile(title: Text('Item 3')),
    ListTile(title: Text('Item 4'))
  ]
)
  1. 动态列表(推荐)
ListView.builder(
  itemCount: 1000,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text('Item $index'),
      onTap: () => print('Clicked $index')
    );
  }
)
  1. 带分隔线的列表
ListView.separated(
  itemCount: 20,
  itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
  separatorBuilder: (context, index) => Divider(height: 1, color: Colors.grey),
)
  1. 自定义ChildrenDelegate实现完全控制子组件的展示
static const colors = [
  Colors.red, Colors.orange, Colors.yellow,
  Colors.green, Colors.blue, Colors.indigo, Colors.purple,
];

ListView.custom(
  childrenDelegate: SliverChildBuilderDelegate(
    (BuildContext context, int index) {
      final Color = colors[index % colors.length]
      return GestureDetector(
        onTap: () => Fluttertoast.showToast(msg: 'Clicked $index', toastLength: Toast.LENGTH_SHORT),
        child: Container(
          height: 80,
          alignment: Alignment.center,
          color: color,
          child: Text(
            'Item $index',
            style: const TextStyle(
              color: Colors.white,
              fontSize: 18,
              fontWeight: FontWeight.bold
            )
          )
        )
      );
    },
    childCount: 100
  ),
  padding: const EdgeInsets.symmetric(vertical: 8),
  physics: const BouncingScrollPhysics(),
  cacheExtent: 200, //预加载区域
)

子组件的生命周期

创建

在布局列表时,可见子组件的ElementStateRenderObject会根据现有Widget(例如使用默认构造函数时)或延迟提供的Widget(例如使用ListView.builder构造函数时)进行惰性创建。

销毁

当某个子组件滚动出视图范围时,其关联的Element子树、StateRenderObject会被销毁。当该位置的子项重新滚动会视图中时,会再次惰性地重新创建新的Element子树、StateRenderObject

解决状态丢失

为了解决当子组件滚动进出视图区域时保留自身状态,有两种方式可以方便完成,一种是使用KeepAlive组件包裹想要保留状态的子组件,另一种是使用AutomaticKeepAlive由子组件来控制是否保留状态

  • KeepAlive: 当使用KeepAlive后,KeepAlive将成为需要保留的列表子项Widget子树的根WidgetKeepAlive将会将其子树顶部的RenderObject标记为需要保留。当关联顶部RenderObject滚动出视图时,列表会将该子组件的RenderObject(及其关联的Element和State)保留在缓存列表中,而不是销毁它们。当重新滚动回视图时,该RenderObject会按原样重绘。此方法仅在addAutomaticKeepAlivesaddRepaintBoundaries均为false时有效,因为这些参数会导致ListView为每个子Widget子树额外包裹其他的Widget
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'KeepAlive Demo',
      theme: ThemeData(useMaterial3: true),
      home: const KeepAlivePage(),
    );
  }
}

class KeepAlivePage extends StatelessWidget {
  const KeepAlivePage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('KeepAlive 组件示例')),
      body: ListView.builder(
        itemCount: 30,
        itemBuilder: (context, index) {
          // 只对 3 的倍数索引保持状态
          final bool shouldKeep = index % 3 == 0;

          // 手动包 KeepAlive
          return KeepAlive(
            keepAlive: shouldKeep,
            child: StatefulTile(index: index),
          );
        },
      ),
    );
  }
}

/// 内部有动画的示例子项
class StatefulTile extends StatefulWidget {
  final int index;
  const StatefulTile({super.key, required this.index});

  
  State<StatefulTile> createState() => _StatefulTileState();
}

class _StatefulTileState extends State<StatefulTile>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    )..repeat();
  }

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Container(
      height: 80,
      margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
      color: Colors.primaries[widget.index % Colors.primaries.length],
      child: Center(
        child: RotationTransition(
          turns: _controller,
          child: Text(
            'Tile ${widget.index}',
            style: const TextStyle(color: Colors.white, fontSize: 18),
          ),
        ),
      ),
    );
  }
}
  • AutomaticKeepAlive: 允许子组件控制是否实际保留状态,当addAutomaticKeepAlives为true时默认插入AutomaticKeepAlive Widget,然后让子组件实现AutomaticKeepAliveClientMixin来达到目的。
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('addAutomaticKeepAlives 示例')),
        body: ListView.builder(
          itemCount: 50,
          // 默认就是 true,这里显式写出来
          addAutomaticKeepAlives: true,
          itemBuilder: (_, index) => KeepAliveItem(index: index),
        ),
      ),
    );
  }
}

/// 子项:混入 AutomaticKeepAliveClientMixin
class KeepAliveItem extends StatefulWidget {
  final int index;
  const KeepAliveItem(this.index, {super.key});

  
  State<KeepAliveItem> createState() => _KeepAliveItemState();
}

class _KeepAliveItemState extends State<KeepAliveItem>
    with AutomaticKeepAliveClientMixin {
  final TextEditingController _ctrl = TextEditingController();

  
  bool get wantKeepAlive => true; // 声明需要保活

  
  void dispose() {
    _ctrl.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    super.build(context); // 必须调用
    return Container(
      height: 80,
      margin: const EdgeInsets.all(8),
      color: Colors.primaries[widget.index % Colors.primaries.length],
      alignment: Alignment.center,
      child: TextField(
        controller: _ctrl,
        decoration: InputDecoration(
          hintText: '输入内容 ${widget.index}',
          border: InputBorder.none,
          contentPadding: const EdgeInsets.symmetric(horizontal: 12),
        ),
      ),
    );
  }
}

比较

特性手动包裹KeepAliveaddAutomaticKeepAlives: true + AutomaticKeepAliveClientMixin
控制方父级(keepAlive)开关子组件自己(wantKeepAlive)
侵入性无需修改子项子项必须混入Mixin
粒度父级统一决定每个子项可以独立决定
默认行为需要手动包ListView.builder 默认为 true

性能优化

使用itemExtent

当所有子项在主轴上的尺寸是固定且已知的情况下,使用itemExtent可以省去一次布局测量,提升滚动流畅度。

ListView.builder(
  itemCount: 1000,
  itemExtent: 72,          // 每个子项高 72
  itemBuilder: (_, i) => ListTile(
    title: Text('Item $i'),
  ),
)

注意点:只有当子项高度/宽度完全一致时可以使用,如果子项大小差距很大或者大部分相同偶有差异的情况下,不建议使用。

使用prototypeItem

prototypeItem介于完全测量和完全固定之间:

  • 只给出一个原型模板子项,通过测量这个子项来拿到主轴尺寸
  • 其余子项全部复用该尺寸,不再逐条测量
  • 因此允许子项内容不同,但是主轴尺寸必须一致
ListView.builder(
  itemCount: 1000,
  prototypeItem: const ListTile(
    title: Text(''),   // 只用来测量一次高度
  ),
  itemBuilder: (_, i) => ListTile(
    leading: const Icon(Icons.person),
    title: Text('User $i'),
    subtitle: Text('Subtitle $i'),
  ),
)

itemExtent对比

维度prototypeItemitemExtent
是否需要手动写死数值不需要需要
子项内容是否可不同可以可以
子项尺寸是否必须严格一致必须一致必须一致
性能≈ itemExtent最优
可读性稍差(多一个样板)简洁

一句话总结:

“如果子项高度/宽度固定但不想手写数值 → 用 prototypeItem;

如果愿意手写数值 → 用 itemExtent 更直观。”

属性

属性名属性类型说明
cacheExtentdouble?视口两端对于视口进行扩展的区域,超出该区域才被视为离开或者进入可视区域
childrenDelegateSliverChildDelegate用于创建子项的委托对象
clipBehaviorClip根据配置进行裁切
controllerScrollController?用于实时控制和监听滚动位置、跳转到指定offset的对象
itemExtentdouble?用于精确指定子组件在主轴方向上的尺寸来优化性能
itemExtentBuilderItemExtentBuilder可以精确控制第n个子组件在主轴方向上的尺寸来优化性能
physicsScrollPhysics?控制滚动行为
prototypeItemWidget?使用一个原型组件来测量子组件在主轴上的尺寸,以此来优化性能
reversebool反向滚动
scrollBehaviorScrollBehavior?决定所有ListView在没有显式指定时的滚动条、回弹、鼠标滚轮、拖拽设备等行为
scrollDirectionAxis滚动方向
shrinkWrapbool让ListView的高度缩小到只包住自己所有子组件的总高度,而不是占满父级剩余空间

itemExtentBuilder示例

ListView.builder(
  itemCount: 100,
  itemExtentBuilder: (index, _) {
    // 奇数行高 60,偶数行高 120
    return index.isOdd ? 60 : 120;
  },
  itemBuilder: (context, index) {
    return Container(
      height: index.isOdd ? 60 : 120, // 实际 widget 也要对应
      color: index.isEven ? Colors.blue : Colors.green,
      child: Text('Item $index'),
    );
  },
)

physics属性举例

效果典型场景
AlwaysScrollableScrollPhysics()永远可滚,即使内容不足下拉刷新
NeverScrollableScrollPhysics()完全禁止滚动嵌套在另一个可滚动组件里
BouncingScrollPhysics()iOS 风格,边界弹性回弹iOS 设备
ClampingScrollPhysics()Android 风格,边界“卡住”Android 设备
PageScrollPhysics()整页滚动(配合 PageView)轮播图
FixedExtentScrollPhysics()固定 item 高度,滚轮效果CupertinoPicker

shrinkWrap属性举例

场景默认 (shrinkWrap=false)shrinkWrap=true
父级给的是 无限高ColumnSingleChildScrollView…)直接抛异常:hasSize正常渲染,高度 = 子项总高
父级给的是 固定高ListView 占满整个高ListView 只占用“刚好”高度
性能只绘制可见子项,必须一次性测量所有子项,

必须打开的场景:

  • ListView放在无界高度的父组件里:Column、Row、ListView、SingleChildScrollView、Stack… 否则会报 “BoxConstraints forces an infinite height”。
Column(
  children: [
    ListView(          // 会报错
      children: [...],
    ),
  ],
)

// 解决
Column(
  children: [
    ListView(
      shrinkWrap: true,   // 告诉 ListView:别占满,有多高算多高
      physics: const NeverScrollableScrollPhysics(), // 通常一起关滚动
      children: [...],
    ),
  ],
)
  • 需要测量实际内容高度做动画、对齐、计算尺寸时。