☰
Flutter-Notebook 可展开控件(Expansion)系列实战:ExpansionTile 与 ExpansionPanelList 用法与注意事项
2026/10/15 15:37:38 网站建设 项目流程
  • 示例工程
  • 移动开发

【免费下载链接】Flutter-Notebook

FlutterDemo合集,今天你fu了吗

项目地址:https://gitcode.com/gh_mirrors/fl/Flutter-Notebook
点击查看免费下载

本篇文章基于 Flutter-Notebook 仓库中 expansion_demo 示例 的源码与注释,系统讲解 Flutter 中两类可展开(expansion)控件的完整用法:单条目展开的ExpansionTile与多条目分组展开的ExpansionPanelList。读完本文,你将掌握这两类控件的核心参数、展开状态的管理方式、与ListView配合时的状态保留技巧,以及实际使用中的关键注意事项,可直接落地到自己的项目中。

一、示例工程概览

expansion_demo 是 Flutter-Notebook 仓库(mecury_project/example/目录)下众多 Flutter 控件演示工程之一,其文档定位非常明确:"flutter 中可展开控件(expansion)系列,使用方法及注意事项已在注释中给出"。也就是说,这个 Demo 的技术精华不在于 README 本身,而在于 lib 目录 下两个演示文件中的逐行中文注释:

  • expansion_tile.dart:演示单条可展开列表项ExpansionTile;
  • expansion_panel_list.dart:演示多条目展开面板ExpansionPanelList。

两个文件分别对应 README 中的两张运行效果截图。工程入口 main.dart 通过切换home属性来选择展示哪个页面:

void main() => runApp(new MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', theme: new ThemeData.dark(), // home: ExpansionPanelListDemo(), home: ExpansionTileDemo(), ); } }

从源码结构看,默认展示的是ExpansionTileDemo,想要查看面板列表效果时,取消home: ExpansionPanelListDemo()的注释并注释掉ExpansionTileDemo()即可,这是阅读本示例时最直接的切换方式。

该工程依赖非常精简(见 pubspec.yaml),仅使用flutterSDK 与cupertino_icons,无任何第三方可展开控件库,因此非常适合作为学习 Flutter 官方 Material 展开控件的入门样例。

二、ExpansionTile:单条可展开列表项

2.1 基础用法

ExpansionTile是"最基础的展开小部件"(源码注释原话)。它的用法非常简单:将需要被展开的部件放在children中即可,其余属性与ListTile十分相似。示例核心代码如下(见 expansion_tile.dart):

ExpansionTile( title: Text('Expansion Tile'), leading: Icon(Icons.ac_unit), backgroundColor: Colors.white12, children: <Widget>[ ListTile( title: Text('list tile'), subtitle: Text('subtitle'), ), ], // initiallyExpanded: true, ),

2.2 核心属性逐个解析

结合源码注释与 Flutter 框架标准 API,ExpansionTile的常用属性及其作用如下:

属性作用示例中的取值
title折叠时显示的标题,是列表项的必填内容Text('Expansion Tile')
leading标题左侧的前导图标,用法与ListTile一致Icon(Icons.ac_unit)
trailing右侧的小箭头,可自行替换为其他部件未设置,使用默认展开箭头
backgroundColor展开时的背景色,配合展开动画过渡Colors.white12
children被展开后显示的子部件列表一个ListTile
initiallyExpanded初始状态是否展开,默认为false(不展开)示例中以注释形式给出

2.3 展开动画与交互细节

源码注释指出一个容易被忽略的细节:当ExpansionTile被展开时,可以看到backgroundColor会进行一个 transition 动画进行过渡。这意味着背景色不是瞬间切换,而是跟随展开动画平滑渐变,在设计深色主题或强调展开状态的 UI 时可以利用这一点。

2.4 关键注意事项:ListView 中的 PageStorageKey

源码注释特别强调了一个实战中极易踩坑的问题:

当一个 ListView 中有多个 ExpansionTile 的时候,需要给每一个 ExpansionTile 指定唯一的PageStorageKey,以保证在滑动的过程中,能够记住 ExpansionTile 的开关状态。

原因在于:ListView 的懒加载机制会销毁/复用滑出屏幕的列表项,如果没有稳定的 key,滚动回来后ExpansionTile的展开状态就会丢失。通过PageStorageKey可以把每个条目的开关状态持久化保存,这正是官方文档与社区实践中推荐的做法。示例用法:

ListView( children: <Widget>[ ExpansionTile( key: PageStorageKey('tile-1'), title: Text('Item 1'), children: ..., ), ExpansionTile( key: PageStorageKey('tile-2'), title: Text('Item 2'), children: ..., ), ], )

三、ExpansionPanelList:多条目展开面板

3.1 与 ExpansionTile 的定位差异

如果说ExpansionTile是"单兵作战"的展开项,那么ExpansionPanelList就是"集团作战"的展开面板——它在一个容器内管理多个ExpansionPanel条目的展开/收起。示例工程中实现了 10 个面板的列表(见 expansion_panel_list.dart)。

3.2 使用前提(源码注释强调的三点)

源码注释对ExpansionPanelList给出了三条明确的注意事项:

  1. ExpansionPanelList必须放在可滑动组件中使用——示例中将其包在SingleChildScrollView内,因为面板展开后内容会增长,需要滚动容器来承载;
  2. ExpansionPanel只能在ExpansionPanelList中使用——它是专属于面板列表的子控件,不能脱离容器单独使用;
  3. 除了ExpansionPanel还有一种特殊的ExpansionPanelRadio,同样只能在ExpansionPanelList中使用,用于实现"同一时刻只展开一个面板"的互斥效果。

3.3 展开状态管理:核心难点

与ExpansionTile自带状态管理不同,ExpansionPanelList是无状态的——每个面板是否展开由外部数据驱动。示例通过一个ExpandStateBean数据模型来保存每个面板的展开状态:

class ExpandStateBean { var isOpen; var index; ExpandStateBean(this.index, this.isOpen); }

初始化时创建 10 个面板的状态对象,初始全部为收起状态:

_ExpansionPanelListDemoState() { mList = new List(); expandStateList = new List(); for (int i = 0; i < 10; i++) { mList.add(i); expandStateList.add(ExpandStateBean(i, false)); } }

用户点击面板时,ExpansionPanelList的expansionCallback会回调被点击面板的索引与当前展开状态,示例通过setState反转对应面板的isOpen,从而驱动 UI 刷新:

_setCurrentIndex(int index, isExpand) { setState(() { expandStateList.forEach((item) { if (item.index == index) { item.isOpen = !isExpand; } }); }); }

这里expansionCallback: (index, bol) => _setCurrentIndex(index, bol)的参数bol表示"该面板当前的展开状态",取反后即为目标状态,实现点击展开、再次点击收起。

3.4 完整示例代码

面板列表的完整构建逻辑如下(headerBuilder负责渲染折叠时的头部,body负责渲染展开后的内容,isExpanded从状态列表中读取):

body: SingleChildScrollView( child: ExpansionPanelList( expansionCallback: (index, bol) { _setCurrentIndex(index, bol); }, children: mList.map((index) { return new ExpansionPanel( headerBuilder: (context, isExpanded) { return new ListTile( title: new Text('This is NO. $index'), ); }, body: ListTile( title: Text('expansion no.$index'), ), isExpanded: expandStateList[index].isOpen, ); }).toList(), ), )

3.5 扩展:ExpansionPanelRadio 的互斥模式

若需要"手风琴"式交互(打开一个面板自动收起其他面板),可以使用ExpansionPanelRadio。从框架 API 看,它通过value标识每个面板,配合ExpansionPanelList的expandedHeaderPadding等参数使用,展开时自动互斥。需要说明的是,示例工程本身未提供该模式的运行代码,此处仅作概念性补充,具体实现可查阅 Flutter 官方 API 文档。

四、运行与验证

若想在本地运行本示例,需要先安装 Flutter SDK,然后在mecury_project/example/expansion_demo/目录下执行:

flutter run

工程入口 main.dart 已配置好MaterialApp(采用ThemeData.dark()深色主题),切换home即可分别体验ExpansionTileDemo与ExpansionPanelListDemo两个页面。

关于测试:工程自带的 widget_test.dart 是 Flutter 脚手架生成的默认计数器 smoke test,并未针对展开控件编写断言。从源码看,若要为展开控件补充测试,可以基于WidgetTester模拟点击ExpansionTile/面板头部,再通过find.text校验展开后的子部件是否出现——这与脚手架测试中tester.tap+tester.pump的交互模式一脉相承。

五、小结

回到本示例文档的主题——"可展开控件(expansion)系列",可以提炼出两条核心经验:

  1. 单条展开选ExpansionTile:开箱即用、自带状态与动画,放在ListView中时记得用唯一的PageStorageKey保住开关状态;
  2. 多条分组展开选ExpansionPanelList:必须配合可滑动容器使用,展开状态由外部数据驱动,需要自行维护状态列表并通过expansionCallback更新,互斥场景使用ExpansionPanelRadio。

这两类控件覆盖了绝大多数"折叠/展开"交互场景,本仓库的 expansion_demo 以其详尽的注释提供了教科书级别的参考实现,值得在开发中反复查阅对照。

  • 示例工程
  • 移动开发

【免费下载链接】Flutter-Notebook

FlutterDemo合集,今天你fu了吗

项目地址:https://gitcode.com/gh_mirrors/fl/Flutter-Notebook
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询