在这里插入图片描述

Flutter for OpenHarmony 实战之基础组件:第六篇 ListView 列表组件详解

前言

在 Flutter 中,ListView 是最常用的滚动组件。初学者常犯的错误是:直接把所有数据塞进一个 Column 里放进 SingleChildScrollView,或者在数据量很大时使用默认的 ListView() 构造函数。

这些做法在数据少时没问题,一旦数据超过 100 条,性能就会急剧下降,甚至导致 App 卡顿(Jank)。

本文你将学到

  • ListView、ListView.builder、ListView.separated 的选择策略
  • 如何实现各种各样的分割线
  • 下拉刷新 (Pull to Refresh) 的原生实现
  • 监听滚动位置与“一键回到顶部”
  • 列表性能优化的 3 个关键点

一、ListView 的三种构建方式

在这里插入图片描述

1.1 默认构造函数 (少量静态数据)

适用于数据量少且确定的场景(如设置页面)。

ListView(
  padding: const EdgeInsets.all(16),
  children: const [
    ListTile(title: Text('设置')),
    ListTile(title: Text('关于')),
    ListTile(title: Text('退出登录')),
  ],
)

缺点:它会一次性创建所有子组件,数据多了会导致内存暴涨。

import 'package:flutter/material.dart';

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

  
  Widget build(BuildContext context) {
    // 使用浅灰色背景,营造现代 App 的层次感
    return Scaffold(
      backgroundColor: const Color(0xFFF2F2F7), // iOS/HarmonyOS 风格背景色
      appBar: AppBar(
        title: const Text('设置', style: TextStyle(fontWeight: FontWeight.w600)),
        backgroundColor: const Color(0xFFF2F2F7),
        elevation: 0,
        scrolledUnderElevation: 0,
      ),
      body: ListView(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
        children: [
          // 分组 1: 账号相关
          const _SectionHeader('账号中心'),
          _SettingsGroup(
            children: [
              _SettingsItem(
                icon: Icons.person_outline,
                iconColor: Colors.white,
                iconBgColor: Colors.blueAccent,
                title: '个人资料',
                subtitle: '完善信息可提升账户安全',
                onTap: () {},
              ),
              _SettingsItem(
                icon: Icons.security,
                iconColor: Colors.white,
                iconBgColor: Colors.green,
                title: '账号安全',
                onTap: () {},
              ),
            ],
          ),

          const SizedBox(height: 24),

          // 分组 2: 通用设置
          const _SectionHeader('通用'),
          _SettingsGroup(
            children: [
              _SettingsItem(
                icon: Icons.notifications_none,
                iconColor: Colors.white,
                iconBgColor: Colors.orange,
                title: '消息通知',
                trailing: Switch(
                    value: true,
                    onChanged: (v) {},
                    activeColor: Colors.blueAccent),
              ),
              _SettingsItem(
                icon: Icons.language,
                iconColor: Colors.white,
                iconBgColor: Colors.purple,
                title: '多语言',
                trailingText: '简体中文',
                onTap: () {},
              ),
              _SettingsItem(
                icon: Icons.dark_mode_outlined,
                iconColor: Colors.white,
                iconBgColor: Colors.black87,
                title: '深色模式',
                trailingText: '跟随系统',
                onTap: () {},
              ),
            ],
          ),

          const SizedBox(height: 24),

          // 分组 3: 其他
          const _SectionHeader('关于'),
          _SettingsGroup(
            children: [
              _SettingsItem(
                icon: Icons.help_outline,
                iconColor: Colors.white,
                iconBgColor: Colors.blueGrey,
                title: '帮助与反馈',
                onTap: () {},
              ),
              _SettingsItem(
                icon: Icons.info_outline,
                iconColor: Colors.white,
                iconBgColor: Colors.blueGrey,
                title: '关于我们',
                trailingText: 'v1.0.0',
                onTap: () {},
              ),
            ],
          ),

          const SizedBox(height: 32),

          // 退出按钮
          InkWell(
            onTap: () {},
            borderRadius: BorderRadius.circular(12),
            child: Container(
              height: 50,
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.circular(12),
              ),
              child: const Center(
                child: Text(
                  '退出登录',
                  style: TextStyle(
                    color: Colors.red,
                    fontSize: 16,
                    fontWeight: FontWeight.w600,
                  ),
                ),
              ),
            ),
          ),
          const SizedBox(height: 40),
        ],
      ),
    );
  }
}

/// 分组标题组件
class _SectionHeader extends StatelessWidget {
  final String title;
  const _SectionHeader(this.title);

  
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(left: 12, bottom: 8),
      child: Text(
        title,
        style: TextStyle(
          fontSize: 13,
          color: Colors.grey[600],
          fontWeight: FontWeight.w500,
        ),
      ),
    );
  }
}

/// 设置分组容器 (圆角卡片)
class _SettingsGroup extends StatelessWidget {
  final List<Widget> children;
  const _SettingsGroup({required this.children});

  
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12),
      ),
      child: Column(
        children: List.generate(children.length, (index) {
          final isLast = index == children.length - 1;
          return Column(
            children: [
              children[index],
              if (!isLast)
                Padding(
                  padding: const EdgeInsets.only(left: 56), // 留出图标位置的缩进
                  child: Divider(height: 1, color: Colors.grey[200]),
                ),
            ],
          );
        }),
      ),
    );
  }
}

/// 单个设置项组件
class _SettingsItem extends StatelessWidget {
  final IconData icon;
  final Color iconColor;
  final Color iconBgColor;
  final String title;
  final String? subtitle;
  final Widget? trailing;
  final String? trailingText;
  final VoidCallback? onTap;

  const _SettingsItem({
    required this.icon,
    required this.iconColor,
    required this.iconBgColor,
    required this.title,
    this.subtitle,
    this.trailing,
    this.trailingText,
    this.onTap,
  });

  
  Widget build(BuildContext context) {
    return Material(
      color: Colors.transparent,
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(12),
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
          child: Row(
            children: [
              // 图标容器
              Container(
                width: 32,
                height: 32,
                decoration: BoxDecoration(
                  color: iconBgColor,
                  borderRadius: BorderRadius.circular(8),
                ),
                child: Icon(icon, color: iconColor, size: 20),
              ),
              const SizedBox(width: 12),
              // 无标题和子标题
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      title,
                      style: const TextStyle(
                        fontSize: 16,
                        color: Colors.black87,
                        fontWeight: FontWeight.w500,
                      ),
                    ),
                    if (subtitle != null) ...[
                      const SizedBox(height: 2),
                      Text(
                        subtitle!,
                        style: TextStyle(
                          fontSize: 12,
                          color: Colors.grey[500],
                        ),
                      ),
                    ],
                  ],
                ),
              ),
              // 尾部内容
              if (trailing != null)
                trailing!
              else if (trailingText != null)
                Row(
                  children: [
                    Text(
                      trailingText!,
                      style: TextStyle(fontSize: 15, color: Colors.grey[500]),
                    ),
                    const SizedBox(width: 4),
                    Icon(Icons.chevron_right,
                        color: Colors.grey[400], size: 20),
                  ],
                )
              else if (onTap != null)
                Icon(Icons.chevron_right, color: Colors.grey[400], size: 20),
            ],
          ),
        ),
      ),
    );
  }
}

在这里插入图片描述

1.2 ListView.builder (大量/动态数据)

强烈推荐。它采用“懒加载”机制,只有当子组件滚动到屏幕可见区域时才会被创建,滑出屏幕后会被回收。

import 'package:flutter/material.dart';

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ListView.builder (懒加载)'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      // 使用 builder 构造函数,适合长列表/无限列表
      body: ListView.builder(
        itemCount: 1000, // 列表总数
        itemBuilder: (context, index) {
          // 回调函数:返回第 index 个位置的组件
          // print('Build Item: $index'); // 可打开此注释观察懒加载行为(仅构建可见区域)
          return ListTile(
            leading: CircleAvatar(child: Text('${index + 1}')),
            title: Text('第 $index 条数据'),
            subtitle: const Text('这是动态生成的超长列表'),
            trailing: const Icon(Icons.arrow_forward_ios,
                size: 16, color: Colors.grey),
          );
        },
      ),
    );
  }
}

在这里插入图片描述

1.3 ListView.separated (带分割线)

需要在每个列表项之间添加分割线时使用,比在 builder 里手动判断 index 更优雅。

import 'package:flutter/material.dart';

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ListView.separated (带分割线)'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: ListView.separated(
        itemCount: 20,
        // 列表项构建器
        itemBuilder: (context, index) => ListTile(
          title: Text('Item $index'),
          leading: const Icon(Icons.list),
        ),
        // 分割线构建器
        separatorBuilder: (context, index) {
          // 每隔 5 项显示一个广告,否则显示普通分割线
          if ((index + 1) % 5 == 0) {
            return Container(
              height: 60,
              color: Colors.blue[50],
              alignment: Alignment.center,
              child: const Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.ad_units, color: Colors.blue, size: 16),
                  SizedBox(width: 8),
                  Text('--- 广告位 ---', style: TextStyle(color: Colors.blue)),
                ],
              ),
            );
          }
          return const Divider(height: 1, indent: 16, endIndent: 16);
        },
      ),
    );
  }
}

二、滚动控制与监听

想要获取滑动的距离,或者控制列表滚动,需要使用 ScrollController

在这里插入图片描述

2.1 监听滚动 (实现“回到顶部”按钮)

import 'package:flutter/material.dart';

class ScrollControllerDemoPage extends StatefulWidget {
  const ScrollControllerDemoPage({super.key});

  
  State<ScrollControllerDemoPage> createState() =>
      _ScrollControllerDemoPageState();
}

class _ScrollControllerDemoPageState extends State<ScrollControllerDemoPage> {
  // 1. 创建 Controller
  final ScrollController _controller = ScrollController();
  bool _showBackTop = false;

  
  void initState() {
    super.initState();
    // 2. 添加监听器
    _controller.addListener(() {
      // 当滑动距离 > 200 时显示按钮
      if (_controller.offset > 200 && !_showBackTop) {
        setState(() => _showBackTop = true);
      } else if (_controller.offset <= 200 && _showBackTop) {
        setState(() => _showBackTop = false);
      }
    });
  }

  
  void dispose() {
    // 3. 销毁 Controller
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('滚动监听与控制')),
      body: ListView.builder(
        controller: _controller, // 4. 绑定 Controller
        itemCount: 50,
        itemBuilder: (_, i) => ListTile(
          leading: CircleAvatar(child: Text('${i + 1}')),
          title: Text('Item $i (滑动查看效果)'),
        ),
      ),
      floatingActionButton: _showBackTop
          ? FloatingActionButton(
              child: const Icon(Icons.arrow_upward),
              onPressed: () {
                // 5. 控制滚动
                _controller.animateTo(
                  0, // 回到顶部
                  duration: const Duration(milliseconds: 500),
                  curve: Curves.easeInOut,
                );
              },
            )
          : null,
    );
  }
}

三、实战:下拉刷新与上拉加载

在这里插入图片描述

这是最常用的列表业务场景。

  • 下拉刷新:使用自带的 RefreshIndicator
  • 上拉加载:通常通过监听 ScrollController 是否滑动到底部来实现。
import 'package:flutter/material.dart';

class NewsListPage extends StatefulWidget {
  const NewsListPage({super.key});

  
  State<NewsListPage> createState() => _NewsListPageState();
}

class _NewsListPageState extends State<NewsListPage> {
  final List<String> _data = List.generate(15, (i) => '初始新闻标题 $i');
  final ScrollController _scrollController = ScrollController();
  bool _isLoading = false;

  
  void initState() {
    super.initState();
    _scrollController.addListener(_onScroll);
  }

  void _onScroll() {
    // 判断是否滑动到底部 (保留 50 像素边距触发更自然)
    if (_scrollController.position.pixels >=
        _scrollController.position.maxScrollExtent - 50) {
      _loadMore();
    }
  }

  // 模拟上拉加载
  Future<void> _loadMore() async {
    if (_isLoading) return;
    setState(() => _isLoading = true);

    await Future.delayed(const Duration(seconds: 2)); // 模拟网络请求

    // 模拟没有更多数据的情况
    if (_data.length >= 40) {
      _isLoading = false;
      if (mounted) setState(() {});
      ScaffoldMessenger.of(context)
          .showSnackBar(const SnackBar(content: Text('没有更多数据了')));
      return;
    }

    setState(() {
      _data.addAll(
          List.generate(5, (i) => '新增新闻 ${DateTime.now().second} - $i'));
      _isLoading = false;
    });
  }

  // 模拟下拉刷新
  Future<void> _onRefresh() async {
    await Future.delayed(const Duration(seconds: 1));
    setState(() {
      _data.clear();
      _data.addAll(List.generate(15, (i) => '刷新后的新闻 $i (New)'));
    });
  }

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('实战:下拉刷新与加载')),
      body: RefreshIndicator(
        onRefresh: _onRefresh, // 绑定下拉刷新事件
        child: ListView.builder(
          controller: _scrollController,
          itemCount: _data.length + 1, // 多一项用于显示 Loading
          itemBuilder: (context, index) {
            // 如果是最后一项,显示加载指示器
            if (index == _data.length) {
              return _isLoading
                  ? const Padding(
                      padding: EdgeInsets.all(16.0),
                      child: Center(child: CircularProgressIndicator()),
                    )
                  : const SizedBox(height: 20); // 留白或者显示底部状态
            }

            return ListTile(
              leading: const Icon(Icons.newspaper, color: Colors.blueAccent),
              title: Text(_data[index]),
              subtitle: const Text('2026-05-20'),
              trailing: const Icon(Icons.arrow_forward_ios,
                  size: 14, color: Colors.grey),
            );
          },
        ),
      ),
    );
  }
}

四、鸿蒙开发性能优化技巧

在 OpenHarmony 这样可能运行在不同性能水平设备上的平台,列表优化尤为重要。

在这里插入图片描述

4.1 固定高度优化 (itemExtent)

如果你的列表项高度是固定的(例如都是 50px),务必设置 itemExtent
这会让 Flutter 跳过高度计算过程,大幅提升长列表滚动的流畅度。

ListView.builder(
  itemExtent: 50.0, // 强制指定每个 item 高度为 50
  itemCount: 10000,
  itemBuilder: (ctx, index) => Container(alignment: Alignment.center, child: Text('$index')),
)

4.2 避免过度绘制 (RepaintBoundary)

如果列表项非常复杂(例如包含复杂的 Stack、Canvas 绘图),可以给 Item 包裹一个 RepaintBoundary,这样该 Item 的重绘不会影响其他 Item。

4.3 保持状态 (AutomaticKeepAlive)

如果列表项中包含 TextField 输入框或者 TabView,滑动出屏幕后状态会丢失。
解决方案:让 Item 的 State 混入 AutomaticKeepAliveClientMixin

class MyListItem extends StatefulWidget {
  // ...
}

class _MyListItemState extends State<MyListItem> with AutomaticKeepAliveClientMixin {
  
  bool get wantKeepAlive => true; // 保持存活,不被回收

  
  Widget build(BuildContext context) {
    super.build(context); // 必须调用
    return TextField();
  }
}

完整代码

import 'package:flutter/material.dart';

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

  
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('ListView 性能优化'),
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          bottom: const TabBar(
            tabs: [
              Tab(text: '固定高度 (性能)'),
              Tab(text: '状态保持 (KeepAlive)'),
            ],
          ),
        ),
        body: const TabBarView(
          children: [
            _FixedExtentList(),
            _KeepAliveList(),
          ],
        ),
      ),
    );
  }
}

/// 4.1 固定高度优化
/// 适用于 Item 高度确定的场景,大幅减少布局计算量
class _FixedExtentList extends StatelessWidget {
  const _FixedExtentList();

  
  Widget build(BuildContext context) {
    return ListView.builder(
      itemExtent: 60.0, // 强制指定每个 item 高度为 60
      itemCount: 10000, // 这里的万级数据也能极致流畅
      itemBuilder: (ctx, index) => Container(
        alignment: Alignment.centerLeft,
        padding: const EdgeInsets.symmetric(horizontal: 16),
        decoration: BoxDecoration(
          border: Border(bottom: BorderSide(color: Colors.grey[200]!)),
        ),
        child: Row(
          children: [
            Text('Index $index',
                style: const TextStyle(fontWeight: FontWeight.bold)),
            const Spacer(),
            const Text('固定高度 60px', style: TextStyle(color: Colors.grey)),
          ],
        ),
      ),
    );
  }
}

/// 4.3 状态保持演示
class _KeepAliveList extends StatelessWidget {
  const _KeepAliveList();

  
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 100,
      itemBuilder: (context, index) => _MyListItem(index: index),
    );
  }
}

class _MyListItem extends StatefulWidget {
  final int index;
  const _MyListItem({required this.index});

  
  State<_MyListItem> createState() => _MyListItemState();
}

// 混入 AutomaticKeepAliveClientMixin
class _MyListItemState extends State<_MyListItem>
    with AutomaticKeepAliveClientMixin {
  
  bool get wantKeepAlive => true; // 返回 true 表示保持存活,不被回收

  
  Widget build(BuildContext context) {
    super.build(context); // 必须调用 super.build
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        border: Border(bottom: BorderSide(color: Colors.grey[200]!)),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('Item ${widget.index} (输入内容测试保持状态)'),
          const SizedBox(height: 8),
          const TextField(
            decoration: InputDecoration(
              border: OutlineInputBorder(),
              hintText: '输入字后滑出屏幕再滑回来...',
              isDense: true,
            ),
          ),
        ],
      ),
    );
  }
}

五、总结

ListView 是处理流式内容的核心。

核心要点

  1. 选对构造器:数据多了一定要用 builderseparated
  2. 交互三剑客RefreshIndicator (下拉) + ScrollController (监听) + CircularProgressIndicator (上拉加载)。
  3. 性能第一:固定高度用 itemExtent,复杂 Item 用 RepaintBoundary

下一篇预告

列表中的内容需要用户去点击、长按甚至拖拽。
《Flutter for OpenHarmony 实战之基础组件:第七篇 Button 按钮与手势交互》
我们将学习各种 Button (点击)、InkWell (水波纹) 以及 GestureDetector (全能手势) 的使用。


🌐 欢迎加入开源鸿蒙跨平台社区开源鸿蒙跨平台开发者社区

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐