Flutter中如何使用Firebase Firestore数据填充ExpansionTile组件
Flutter 动态填充 ExpansionTile 实现方案
核心逻辑:将Firestore拉取的模块数据按进度状态分组,再把分组后的数据动态传入对应ExpansionTile的子项,替换原有写死的占位组件即可。
前置修正
你原有代码中initState里的WidgetsFlutterBinding.ensureInitialized()属于多余调用,该方法需要在main()函数的runApp()执行前调用,不需要放在页面初始化逻辑中。
实现步骤
- 使用
StreamBuilder监听Firestore的modules集合数据,自动处理加载中、加载失败、数据就绪三种界面状态,数据更新时会自动刷新界面 - 拿到全量模块数据后,遍历所有文档,按照模块的status字段拆分为「进行中」「未开始(Discover New)」「已完成」三个独立列表
- 给三个ExpansionTile分别传入对应分组的列表数据,动态生成子组件,分组为空时显示空状态提示
- 外层包裹
SingleChildScrollView避免内容溢出,设置Column的mainAxisSize为min适配滚动布局
参考实现代码
import 'package:flutter/material.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; class ChallengeScreen extends StatefulWidget { const ChallengeScreen({Key? key}) : super(key: key); @override _ChallengeScreenState createState() => _ChallengeScreenState(); } class _ChallengeScreenState extends State<ChallengeScreen> { // 此处状态值请和你Firestore中存储的字段保持一致,如果用数字标识状态修改对应判断逻辑即可 final String statusInProgress = 'inProgress'; final String statusNotStarted = 'notStarted'; final String statusCompleted = 'completed'; final Query _collectionRef = FirebaseFirestore.instance.collection('modules'); @override Widget build(BuildContext context) { return StreamBuilder<QuerySnapshot>( stream: _collectionRef.snapshots(), builder: (context, snapshot) { // 加载中显示进度条 if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } // 加载失败显示错误提示 if (snapshot.hasError) { return const Center(child: Text('数据加载失败,请重试')); } // 按状态拆分数据分组 List<QueryDocumentSnapshot> inProgressList = []; List<QueryDocumentSnapshot> notStartedList = []; List<QueryDocumentSnapshot> completedList = []; for (var doc in snapshot.data!.docs) { Map<String, dynamic> moduleData = doc.data() as Map<String, dynamic>; switch (moduleData['status']) { case 'inProgress': inProgressList.add(doc); break; case 'notStarted': notStartedList.add(doc); break; case 'completed': completedList.add(doc); break; } } return SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ ExpansionTile( initiallyExpanded: true, title: const Text('In Progress'), children: [ if (inProgressList.isEmpty) const Padding( padding: EdgeInsets.all(16), child: Text('暂无进行中的挑战模块'), ) else ...inProgressList.map((doc) { Map<String, dynamic> data = doc.data() as Map<String, dynamic>; // 此处替换为设计图对应的自定义卡片组件,示例用ListTile占位 return ListTile( leading: const Icon(Icons.directions_bike), title: Text(data['name'] ?? '未命名模块'), subtitle: LinearProgressIndicator(value: (data['progress'] ?? 0) / 100), trailing: Text('${data['progress'] ?? 0}%'), ); }).toList() ], ), ExpansionTile( title: const Text('Discover New'), children: [ if (notStartedList.isEmpty) const Padding( padding: EdgeInsets.all(16), child: Text('暂无新的挑战模块'), ) else ...notStartedList.map((doc) { Map<String, dynamic> data = doc.data() as Map<String, dynamic>; return ListTile( leading: const Icon(Icons.eco), title: Text(data['name'] ?? '未命名模块'), ); }).toList() ], ), ExpansionTile( title: const Text('Completed'), children: [ if (completedList.isEmpty) const Padding( padding: EdgeInsets.all(16), child: Text('暂无已完成的挑战模块'), ) else ...completedList.map((doc) { Map<String, dynamic> data = doc.data() as Map<String, dynamic>; return ListTile( leading: const Icon(Icons.check_circle, color: Colors.green), title: Text(data['name'] ?? '未命名模块'), ); }).toList() ], ), ], ), ); }, ); } }
样式调整说明
- 示例中用ListTile加简单进度条做占位,你可以自行替换为Container实现自定义卡片:添加圆角、背景色、阴影、模块封面图等元素匹配设计稿
- 不需要实时同步数据的话,可以把
StreamBuilder替换为FutureBuilder,在initState中发起一次数据请求即可 - ExpansionTile提供了
collapsedIconColor、iconColor、tilePadding、shape等属性,可以直接调整折叠/展开状态的样式,不需要额外嵌套组件
内容的提问来源于stack exchange,提问作者nabih bassil
相关产品推荐
相关产品推荐

