You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 20:48:14