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;
  }
}
相关推荐
前端工作日常3 分钟前
我理解的`npm pack` 和 `npm install <local-path>`
前端
李剑一17 分钟前
说个多年老前端都不知道的标签正确玩法——q标签
前端
嘉小华23 分钟前
大白话讲解 Android屏幕适配相关概念(dp、px 和 dpi)
前端
姑苏洛言23 分钟前
在开发跑腿小程序集成地图时,遇到的坑,MapContext.includePoints(Object object)接口无效在组件中使用无效?
前端
奇舞精选27 分钟前
Prompt 工程实用技巧:掌握高效 AI 交互核心
前端·openai
Danny_FD40 分钟前
React中可有可无的优化-对象类型的使用
前端·javascript
用户7575823185542 分钟前
混合应用开发:企业降本增效之道——面向2025年移动应用开发趋势的实践路径
前端
P1erce1 小时前
记一次微信小程序分包经历
前端
LeeAt1 小时前
从Promise到async/await的逻辑演进
前端·javascript
等一个晴天丶1 小时前
不一样的 TypeScript 入门手册
前端