列表与滚动:ListView、GridView 与 Sliver

列表是移动端最核心的交互载体,也最容易出性能问题。Flutter 提供了从简到繁的四层工具:SingleChildScrollViewListViewGridViewCustomScrollView + Sliver。选错层次就会出现"一次性构建上千个 Widget"或"整页只有列表能滚动"两类典型故障。

先搞清性能差异

组件构建方式适用数据量内存特征
SingleChildScrollView + Column一次性构建全部子级10 项以内数据越多内存越高
ListView(children: [...])一次性构建全部子级20 项以内同上
ListView.builder按需构建可见项千项以上恒定
ListView.separated按需构建 + 分隔线需分割线的长列表恒定
GridView.builder按需构建网格项图片墙、商品宫格恒定

只要列表长度不确定或可能超过一屏,就用 builder 写法(ListView.builder/ListView.separated);SingleChildScrollView 只留给"表单 + 少量内容",它在构建时就创建全部子级,1000 条数据必然掉帧。

ListView 的三种常用形态

itemExtent 用于固定行高,框架无需逐项测量即可算出滚动位置,长列表建议给上。

// 1. 数据少可直接列子级
ListView(children: const [ListTile(title: Text('第一项'))])
// 2. 长列表标准写法
ListView.builder(itemCount: 200, itemBuilder: (context, i) => ListTile(title: Text('第 $i 条')))
// 3. 带分隔线
ListView.separated(
  itemCount: 50,
  separatorBuilder: (context, i) => const Divider(height: 1),
  itemBuilder: (context, i) => ListTile(title: Text('示范 $i')),
)

GridView

GridView.count 直接指定列数,适合固定数量的宫格入口;数据来自接口时用 GridView.builderSliverGridDelegateWithFixedCrossAxisCount,按需构建。

GridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2, mainAxisSpacing: 12, crossAxisSpacing: 12,
    childAspectRatio: 3 / 4, // 宽高比算错会让格子内部溢出
  ),
  itemCount: 40,
  itemBuilder: (context, index) => Image.network(
    'https://picsum.photos/seed/$index/300/400',
    fit: BoxFit.cover,
  ),
)

CustomScrollView 与 Sliver

需要"可折叠头部 + 列表 + 网格"参与同一套滚动时,用 CustomScrollView,它的子级必须是 Sliver。

Sliver 组件对应普通组件说明
SliverAppBarAppBar可折叠,支持 floating/pinned/snap
SliverListListViewSliverChildBuilderDelegate 按需构建
SliverGridGridView同上,需给 gridDelegate
CustomScrollView(
  slivers: [
    const SliverAppBar(
      pinned: true,        // 滚到顶后固定在顶部
      expandedHeight: 200, // 展开高度
      flexibleSpace: FlexibleSpaceBar(title: Text('商品列表')),
    ),
    const SliverToBoxAdapter(child: Padding(padding: EdgeInsets.all(16), child: Text('分类筛选'))),
    SliverGrid(
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
      delegate: SliverChildBuilderDelegate(
        (context, i) => Center(child: Text('$i')),
        childCount: 12,
      ),
    ),
  ],
)

下拉刷新与上拉加载

class _FeedPageState extends State<FeedPage> {
  // 监听滚动位置:距底 200 像素就预加载,避免用户看到"卡住"
  final _controller = ScrollController()
    ..addListener(() {
      final p = _controller.position;
      if (p.pixels >= p.maxScrollExtent - 200 && !_loading) _loadMore();
    });
  final List<String> _items = List.generate(20, (i) => '数据 $i');
  bool _loading = false;

  Future<void> _loadMore() async {
    setState(() => _loading = true);
    await Future<void>.delayed(const Duration(seconds: 1)); // 换成真实分页请求
    if (!mounted) return; // 页面已销毁就不再 setState
    setState(() {
      _items.addAll(List.generate(10, (i) => '新增 ${_items.length + i}'));
      _loading = false;
    });
  }

  @override
  void dispose() {
    _controller.dispose(); // 不释放会内存泄漏
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Scaffold(
        body: RefreshIndicator(
          onRefresh: () async => setState(() => _items.shuffle()), // 下拉刷新
          child: ListView.builder(
            controller: _controller,
            physics: const AlwaysScrollableScrollPhysics(), // 不满一屏也能下拉
            itemCount: _items.length + (_loading ? 1 : 0),
            itemBuilder: (context, i) => i >= _items.length
                ? const Center(child: CircularProgressIndicator())
                : ListTile(title: Text(_items[i])),
          ),
        ),
      );
}

列表性能要点

要点原因做法
不在 itemBuilder 里做重计算每帧都可能被调用提前算好放进模型
图片限制解码尺寸大图解码吃内存cacheWidth/cacheHeight 或缩略图
静态部分用 const减少重建图标、间距、固定行
flutter run --profile     # 用 DevTools 的 Performance 看滚动帧率

小结:长列表一律用 ListView.builderSliverList,网格用 GridView.builder;需要折叠头部、多段混排时上 CustomScrollView + Sliver;下拉刷新用 RefreshIndicator、上拉加载用 ScrollController 阈值判断,并记得在 dispose 里释放 Controller。

笔记加载中…