Flutter中常用的列表

基于ListView实现水平和垂直方式滚动的列表

垂直列表

核心就在于 ListView,它负责把一组子 Widget 按垂直方向排列并提供滚动能力。

  • 使用 ListView 组件
    ListView 是 Flutter 提供的可滚动列表视图,默认滚动方向为 垂直 (scrollDirection: Axis.vertical 默认值)。只要其内容超过父布局可视范围,就会自动启用滚动。

  • 通过 children 传入多条子组件

    示例中这里用 ListView(children: _buildList()) 的方式一次性把所有列表项以数组形式交给 ListView

    • _buildList()cityNames 中的每个字符串转换成一个 _item 组件(Container + Text),形成一个 List<Widget>
    • ListView 会按顺序把这些子组件从 上到下 依次布局,超出视口的部分由内部的 Scrollable 机制负责滑动显示。
  • 滚动行为由 Scrollable & Viewport 自动管理
    ListView 内部组合了 ScrollableViewport

    • 当手指在屏幕垂直拖动时,Scrollable 捕获手势并改变内部 ScrollPosition
    • Viewport 根据新的滚动偏移(offset)决定哪些子组件渲染到屏幕、哪些保持在缓存区,从而实现流畅、高效的惰性渲染。
  • 垂直方向声明式特性

    因为未显式指定 scrollDirectionListView 会使用默认的垂直方向。在需要水平列表时才需显式写 scrollDirection: Axis.horizontal

dart 复制代码
import 'package:flutter/material.dart';

/// 需要在列表中展示的城市名称集合
const cityNames = [
  '北京', '上海', '广州', '深圳', '杭州', '苏州',
  '成都', '武汉', '郑州', '洛阳', '厦门', '青岛', '拉萨'
];

/// 垂直滚动列表页面
class VerticalListPage extends StatelessWidget {
  const VerticalListPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    const title = 'list';        // AppBar 标题
    return MaterialApp(          // 整个应用的根组件
      title: title,
      home: Scaffold(            // 提供页面的基本可视结构:AppBar + Body
        appBar: AppBar(
          title: const Text(title),
        ),
        // ListView 默认方向为垂直 (Axis.vertical)
        body: ListView(
          // children 接收一个 Widget 列表,这里由 _buildList() 生成
          children: _buildList(),
        ),
      ),
    );
  }

  /// 将城市名称映射(map)为一个个列表项组件
  List<Widget> _buildList() {
    return cityNames                // 遍历 cityNames
        .map((city) => _item(city)) // 每个城市生成一个 _item
        .toList();                  // 转成 List<Widget>
  }

  /// 单个列表项(80 px 高、底色为 Amber、文字白色)
  Widget _item(String city) {
    return Container(
      height: 80,
      margin: const EdgeInsets.only(bottom: 5), // 每个条目底部留 5 px 间距
      alignment: Alignment.center,
      decoration: const BoxDecoration(color: Colors.amber),
      child: Text(
        city,
        style: const TextStyle(color: Colors.white, fontSize: 20),
      ),
    );
  }
}

水平列表

核心要点 :在 ListView 中把 scrollDirection 改成 Axis.horizontal 就能"变横";其余都是布局细节(限定高度、设置子项宽度、留空隙等)来保证 UI 协调。

dart 复制代码
import 'package:flutter/material.dart';

/// 城市名称列表
const cityNames = [
  '北京', '上海', '广州', '深圳', '杭州', '苏州', '成都', '武汉', '郑州', '洛阳', '厦门', '青岛', '拉萨'
];

/// 水平滚动列表示例页
class HorizontalListPage extends StatelessWidget {
  const HorizontalListPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    const title = '水平';            // AppBar 标题
    return MaterialApp(
      title: title,
      home: Scaffold(
        appBar: AppBar(title: const Text(title)),
        // 仅为 ListView 限定高度,防止占满屏幕导致溢出
        body: SizedBox(
          height: 200,
          child: ListView(
            // ★ 关键:将主轴方向改成水平
            scrollDirection: Axis.horizontal,
            // children 接受一组子 Widget
            children: _buildList(),
          ),
        ),
      ),
    );
  }

  /// 将城市名称转换成 Widget 列表
  List<Widget> _buildList() {
    // map: 把每个 city 字符串映射成 _item(city) 小部件
    return cityNames.map((city) => _item(city)).toList();
  }

  /// 单个城市条目
  Widget _item(String city) {
    return Container(
      width: 160,                            // 定宽,让水平滚动更自然
      margin: const EdgeInsets.only(right: 5), // 条目间隔
      alignment: Alignment.center,
      decoration: const BoxDecoration(color: Colors.amber), // 背景色
      child: Text(
        city,
        style: const TextStyle(color: Colors.white, fontSize: 20),
      ),
    );
  }
}

可展开的列表

ExpansionTile 利用状态切换 + 尺寸动画 (SizeTransition) 来控制子列表显隐,从而在 ListView 中实现可展开/折叠的分组列表

ExpansionTile 实现可展开列表的原理

  1. 内部状态管理
    ExpansionTile 是一个 StatefulWidget

    • 它内部维护一个布尔型 _isExpanded 状态,用于记录当前是「展开」还是「折叠」。
    • 在标题(ListTile + InkWell 包裹)被点击时,切换 _isExpanded 并调用 setState() 触发重建。
  2. 动画效果

    • 展开/折叠动画通过 ExpandIcon + RotationTransition + SizeTransition 实现:

      • ExpandIcon(箭头)旋转角度与 _isExpanded 绑定。
      • 子节点(children)被包在 ClipRectAlignSizeTransition 中,SizeTransitionaxisAlignment-1.0,只在垂直方向拉伸,形成上下收起 / 展开动画。
    • 默认动画曲线使用 Curves.easeInOut,时长 200 ms,用户可通过 animationDuration 参数自定义。

  3. 惰性渲染

    • children 处于折叠状态,内部 SizeTransition 的尺寸为 0,高度为 0,不会占用可见空间;但是 Widget 树仍然存在,有轻微内存占用。
    • 若想在折叠时完全释放子树资源,可结合 ExpansionPanelList 或自行实现「懒加载」逻辑。
  4. ListView 的协同

    • ListView 本身负责垂直滚动。ExpansionTile 放在 ListView 的子节点列表中,只要总高度超出视口,Scrollable 机制会按需滚动。
    • 展开某个城市时,ExpansionTile 内部的高度变化会通知 ListView 重新布局,从而出现平滑的「列表撑开」效果。
  5. 自定义能力

    • 通过 trailingleading 等参数可定制标题区域外观;
    • 通过 backgroundColortilePadding 等参数可改变布局与配色;
    • 若需一次只能展开一项,可结合 ExpansionPanelList.radio 或在外层统一管理 _isExpanded 状态。
dart 复制代码
import 'package:flutter/material.dart';

/// 一个 Map,键为城市,值为该城市下辖的区县(子列表)
const cityNames = {
  '北京': ['东城区', '西城区', '朝阳区', '丰台区', '石景山区', '海淀区', '顺义区'],
  '上海': ['黄浦区', '徐汇区', '长宁区', '静安区', '普陀区', '闸北区', '虹口区'],
  '广州': ['越秀', '海珠', '荔湾', '天河', '白云', '黄埔', '南沙', '番禺'],
  '深圳': ['南山', '福田', '罗湖', '盐田', '龙岗', '宝安', '龙华'],
  '杭州': ['上城区', '下城区', '江干区', '拱墅区', '西湖区', '滨江区'],
  '苏州': ['姑苏区', '吴中区', '相城区', '高新区', '虎丘区', '工业园区', '吴江区']
};

/// 页面:使用 ExpansionTile 构建可展开/折叠的列表
class ExpansionTilePage extends StatelessWidget {
  const ExpansionTilePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    const title = '可展开的列表';
    return MaterialApp(
      title: title,
      home: Scaffold(
        appBar: AppBar(title: const Text(title)),
        // ListView 外层滚动容器
        body: ListView(
          // 将所有城市条目组合成 children
          children: _buildList(),
        ),
      ),
    );
  }

  /// 根据 cityNames 生成城市条目列表
  List<Widget> _buildList() {
    List<Widget> widgets = [];
    for (var key in cityNames.keys) {
      // 每个城市对应一个 ExpansionTile
      widgets.add(_item(key, cityNames[key]!));
    }
    return widgets;
  }

  /// 单个城市条目(可展开)
  Widget _item(String city, List<String> subCities) {
    return ExpansionTile(
      // 标题区域(始终可见)
      title: Text(
        city,
        style: const TextStyle(fontSize: 20, color: Colors.black54),
      ),
      // children:展开后展示的子 Widget 列表
      children: subCities
          .map((subCity) => _buildSub(subCity))
          .toList(), // 将子区县转换为 Widget
    );
  }

  /// 子列表条目(区县)
  Widget _buildSub(String subCity) {
    return FractionallySizedBox(
      widthFactor: 1, // 让子项宽度占满父级 ExpansionTile 的宽度
      child: Container(
        height: 50,
        margin: const EdgeInsets.only(bottom: 5),
        decoration: const BoxDecoration(color: Colors.amber),
        alignment: Alignment.centerLeft,
        child: Text(
          subCity,
          style: const TextStyle(fontSize: 16, color: Colors.white),
        ),
      ),
    );
  }
}

基于GridView实现网格布局

GridView.count 通过固定列数 + SliverGrid 代理,自动计算单元格尺寸并在滚动时按需加载,轻松实现性能友好的网格布局;我们只需决定列数、间距及子项 Widget 即可。

GridView.count 实现网格布局的原理

组件层级 作用
GridView 一种可 滚动 的二维布局容器,内部使用 Sliver 机制实现惰性渲染。
GridView.count GridView 的便捷构造函数,通过 crossAxisCount 指定横轴单元格数量(即列数)。
SliverGrid GridView 背后的核心,负责把列表数据排布成网格。
SliverChildBuilderDelegate / SliverChildListDelegate 根据滑动位置按需创建 / 销毁子项,避免一次生成全部子组件导致性能浪费。

工作流程

  1. 布局规则(SliverGridDelegate

    • GridView.count 内部创建 SliverGridDelegateWithFixedCrossAxisCount

      • crossAxisCount=2 → 横轴固定 2 列。
      • 计算每个格子的 宽度(可用宽度 - 总水平间距) / 2
      • 高度默认与宽度等比;若要自定义可通过 childAspectRatio 控制。
  2. 惰性加载

    • GridView 继承自 BoxScrollViewScrollable
    • 当页面滚动时,Viewport 只会要求 SliverGrid 创建可见范围内的子项,其余部分延迟构建或回收,节省内存与布局时间。
  3. 轴向解释

    • 主轴 (mainAxis) = 滚动方向(本例为垂直方向)。
    • 交叉轴 (crossAxis) = 与滚动方向垂直的轴(横向)。
    • crossAxisCount=2 表示交叉轴上固定 2 单元格;主轴方向无限延展,超过视口部分即可滚动。
  4. 间距与外边距

    • 子项自身 margin 负责条目间外间距。
    • 若需整体网格内边距可用 paddingGridView.count(padding: EdgeInsets.all(8), ...)
  5. 自定义方案

    • GridView.count 外,还可使用 GridView.builder(按需构建)或 SliverGrid + CustomScrollView 手动组合更复杂的瀑布流 / 不等宽高场景。
dart 复制代码
import 'package:flutter/material.dart';

/// 需要在网格中展示的城市名称
const cityNames = [
  '北京', '上海', '广州', '深圳', '杭州', '苏州',
  '成都', '武汉', '郑州', '洛阳', '厦门', '青岛', '拉萨'
];

/// 基于 GridView 构建的网格布局页面
class GridViewPage extends StatelessWidget {
  const GridViewPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    const title = 'GridView';             // AppBar 标题
    return MaterialApp(
      title: title,
      home: Scaffold(
        appBar: AppBar(title: const Text(title)),
        body: GridView.count(
          // crossAxisCount 指定一行放两个格子
          crossAxisCount: 2,
          // 通过 _buildList() 生成所有网格单元
          children: _buildList(),
        ),
      ),
    );
  }

  /// 将 cityNames 转换为 Widget 列表
  List<Widget> _buildList() {
    return cityNames.map((city) => _item(city)).toList();
  }

  /// 单个网格子项
  Widget _item(String city) {
    return Container(
      height: 80,
      margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
      alignment: Alignment.center,
      decoration: const BoxDecoration(color: Colors.amber),
      child: Text(
        city,
        style: const TextStyle(fontSize: 20, color: Colors.white),
      ),
    );
  }
}

高级功能列表下拉刷新与上拉加载更多功能实现

下拉刷新依赖 RefreshIndicator 内置手势检测与动画;上拉加载则通过监听滚动位置自主触发异步数据追加。两者结合即可实现常见移动端列表的「下拉翻新 + 滑到底部自动分页」。

功能实现原理

功能 关键组件 / API 工作机制
下拉刷新 RefreshIndicator 内部通过 ScrollNotification 监听垂直拖拽距离,当滑动到列表顶部再继续下拉且达到阈值 ≈ 80 px 时,触发回调 onRefresh;随后显示内置的「水滴→圆环」动画,等待返回的 Future 完成后收起。
上拉加载 ScrollControllerNotificationListener<ScrollNotification> 判断 position.pixels == position.maxScrollExtent ⇒ 已到底部,再调用自定义 _loadData();此方法通常发起网络请求,将新数据添加到数据源后 setState() 刷新 UI。

细节说明

  1. ScrollController 与 ListView

    • 通过 controller 属性将 ScrollController 绑定到 ListView
    • ScrollController.position 提供当前滚动相关信息,如 pixelsmaxScrollExtentminScrollExtent
    • 滑动过程中触发监听器 _scrollController.addListener()
  2. 避免重复加载

    • 增加 _isLoadingMore 标志,异步结束前屏蔽再次触发;
    • 若使用分页接口,还需维护当前页码 / 是否还有更多数据等状态。
  3. 资源释放

    • ScrollController 属于 ChangeNotifier,需要在 dispose()dispose() 防止内存泄漏。
  4. 性能优化

    • 列表项较多时,推荐改用 ListView.builder(惰性构建)。
    • 加载时可在列表尾部插入"加载中"占位 Widget,提升用户感知。
  5. 进阶做法

    • 使用第三方库如 pull_to_refreshflutter_easyrefresh,可获得更丰富的刷新指示器与分页控制;
    • 使用 ScrollNotification 统一处理顶部 / 底部各种状态,逻辑更集中。
dart 复制代码
import 'package:flutter/material.dart';

/// 高级功能:下拉刷新 & 上拉加载更多
class RefreshLoadMorePage extends StatefulWidget {
  const RefreshLoadMorePage({Key? key}) : super(key: key);

  @override
  State<RefreshLoadMorePage> createState() => _RefreshLoadMorePageState();
}

class _RefreshLoadMorePageState extends State<RefreshLoadMorePage> {
  /// 用于监听滚动位置,判断是否滑到底部
  final ScrollController _scrollController = ScrollController();

  /// 列表数据源
  List<String> cityNames = [
    '北京', '上海', '广州', '深圳', '杭州', '苏州',
    '成都', '武汉', '郑州', '洛阳', '厦门', '青岛', '拉萨',
  ];

  /// 是否正在加载更多,避免重复触发
  bool _isLoadingMore = false;

  @override
  void initState() {
    super.initState();
    // 监听滚动事件:当像素位置 == 最大滚动范围,说明滑到底部
    _scrollController.addListener(_onScroll);
  }

  /// 释放资源,避免内存泄漏
  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  /// 滚动监听回调
  void _onScroll() {
    // 已经到底部且当前不在加载状态
    if (_scrollController.position.pixels ==
            _scrollController.position.maxScrollExtent &&
        !_isLoadingMore) {
      _loadData(); // 触发上拉加载
    }
  }

  @override
  Widget build(BuildContext context) {
    const title = '下拉刷新 + 上拉加载';
    return MaterialApp(
      title: title,
      home: Scaffold(
        appBar: AppBar(title: const Text(title)),
        // RefreshIndicator 实现下拉刷新
        body: RefreshIndicator(
          onRefresh: _handleRefresh, // 下拉手势触发的方法
          child: ListView(
            controller: _scrollController, // 绑定滚动控制器
            children: _buildList(),        // 构建列表项
          ),
        ),
      ),
    );
  }

  /// 将 cityNames 转为 Widget 列表
  List<Widget> _buildList() {
    return cityNames.map(_item).toList();
  }

  /// 单个条目样式
  Widget _item(String city) {
    return Container(
      height: 80,
      margin: const EdgeInsets.only(bottom: 5),
      alignment: Alignment.centerLeft,
      decoration: const BoxDecoration(color: Colors.redAccent),
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 12.0),
        child: Text(city, style: const TextStyle(fontSize: 20, color: Colors.white)),
      ),
    );
  }

  /* ---------------- 下拉刷新 ---------------- */
  /// RefreshIndicator 要求返回 Future<void>
  Future<void> _handleRefresh() async {
    // 模拟网络请求 2 秒
    await Future.delayed(const Duration(seconds: 2));
    if (!mounted) return;
    setState(() {
      // 简单演示:把列表倒序,以便看出刷新效果
      cityNames = cityNames.reversed.toList();
    });
  }

  /* ---------------- 上拉加载更多 ---------------- */
  Future<void> _loadData() async {
    _isLoadingMore = true;
    // 给用户一点"加载中"的时间感
    await Future.delayed(const Duration(milliseconds: 800));
    if (!mounted) return;
    setState(() {
      // 演示:往末尾再追加一份数据
      cityNames = List<String>.from(cityNames)..addAll(cityNames);
    });
    _isLoadingMore = false;
  }
}
相关推荐
恋猫de小郭1 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端