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

Flutter多Widget共享单个StreamProvider 避免Firestore重复查询

解决方案

核心问题原因

你现在的代码每次调用streamBetDataBetweenDates都会生成独立的Stream实例,就算查询参数完全一致,也会触发两次独立的Firestore查询。之前把StreamProvider包裹在ListView外层报错,大概率是没有统一管理Stream的生命周期,或者没有处理用户为空的边界情况。

具体修改方案

1. 统一管理Stream实例

在_DailyPageState中存储复用的Bet数据流,只有当选中日期变化时才重新生成流,避免每次build重复创建流:

class _DailyPageState extends State<DailyPage> {
  final FirebaseAuth auth = FirebaseAuth.instance;
  final DateTime now = DateTime.now();
  final days = getWeekDayList;
  int _currentWeekDayNameAndNum = getCurrentWeekDayInt();
  final db = FirestoreService();
  // 新增:存储当前用户和共享的投注数据流
  User? _user;
  Stream<List<BetData>>? _betStream;

  @override
  void initState() {
    super.initState();
    _user = auth.currentUser;
    // 初始化首次查询流
    _refreshBetStream();
  }

  // 新增:根据选中日期更新查询流
  void _refreshBetStream() {
    if (_user == null) return;
    int selectedDay = int.parse(days[_currentWeekDayNameAndNum]['day']);
    DateTime startDate = DateTime(now.year, now.month, selectedDay, 0, 0);
    DateTime endDate = DateTime(now.year, now.month, selectedDay, 23, 59);
    _betStream = db.streamBetDataBetweenDates(_user!, startDate, endDate);
  }

  @override
  Widget build(BuildContext context) {
    // 处理用户未登录的边界情况
    if (_user == null || _betStream == null) {
      return const Scaffold(body: Center(child: CircularProgressIndicator()));
    }

    return Scaffold(
      backgroundColor: white.withOpacity(1),
      // 把StreamProvider移到ListView外层,所有子组件都可以共享这个流的数据
      body: StreamProvider<List<BetData>>.value(
        value: _betStream!,
        initialData: const [],
        child: ListView(
          children: [
            // 原来的日期选择组件代码完全保留,不需要修改
            Column(
              children: [
                Container(
                  margin: const EdgeInsets.all(10),
                  decoration: BoxDecoration(color: white, boxShadow: [
                    BoxShadow(
                      color: grey.withOpacity(0.5),
                      spreadRadius: 4,
                      blurRadius: 5,
                    ),
                  ]),
                  child: Padding(
                    padding: const EdgeInsets.only(
                        top: 10, right: 10, left: 10, bottom: 20),
                    child: Column(
                      children: [
                        Row(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          children: const [
                            Text(
                              "Daily Transactions",
                              style: TextStyle(
                                  fontSize: 20,
                                  fontWeight: FontWeight.bold,
                                  color: black),
                            ),
                          ],
                        ),
                        const SizedBox(
                          height: 25,
                        ),
                        Row(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: List.generate(days.length, (index) {
                              return GestureDetector(
                                onTap: () {
                                  setState(() {
                                    _currentWeekDayNameAndNum = index;
                                  });
                                  // 切换日期后刷新流
                                  _refreshBetStream();
                                },
                                child: SizedBox(
                                  width:
                                      (MediaQuery.of(context).size.width - 40) /
                                          7,
                                  child: Column(
                                    children: [
                                      Text(
                                        days[index]['label'],
                                        style: const TextStyle(fontSize: 10),
                                      ),
                                      const SizedBox(
                                        height: 10,
                                      ),
                                      Container(
                                        width: 30,
                                        height: 30,
                                        decoration: BoxDecoration(
                                            color: _currentWeekDayNameAndNum ==
                                                    index
                                                ? primary
                                                : Colors.transparent,
                                            shape: BoxShape.circle,
                                            border: Border.all(
                                                color:
                                                    _currentWeekDayNameAndNum ==
                                                            index
                                                        ? primary
                                                        : black
                                                            .withOpacity(0.1))),
                                        child: Center(
                                          child: Text(
                                            days[index]['day'],
                                            style: TextStyle(
                                                fontSize: 10,
                                                fontWeight: FontWeight.w600,
                                                color:
                                                    _currentWeekDayNameAndNum ==
                                                            index
                                                        ? white
                                                        : black),
                                          ),
                                        ),
                                      )
                                    ],
                                  ),
                                ),
                              );
                            }))
                      ],
                    ),
                  ),
                ),
              ],
            ),
            // 直接使用组件,不需要再单独套StreamProvider
            const ProfitDisplay(),
            const SizedBox(
              height: 10,
            ),
            const BetListDisplay()
          ],
        ),
      ),
    );
  }
}

2. 后续ProfitDisplay接入额外流的处理

你可以直接在ProfitDisplay内部用StreamBuilder监听额外的流,也可以在父层用MultiProvider添加其他的StreamProvider,不会影响当前共享的BetData流的使用,原有读取BetData的逻辑不需要修改。

验证效果

你可以观察streamBetDataBetweenDates中的print日志,切换日期时只会打印一次,说明只会触发一次Firestore查询,两个组件共享同一份数据。

内容的提问来源于stack exchange,提问作者Trax

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.27 18:54:02