Flutter借助Firebase实现多设备/屏幕同步计时的最优方案
计分板实时同步优化方案
问题描述
我正在开发一款计分板应用,已完成本地秒表功能,但遇到同步问题:启动秒表后,其他设备或屏幕需同步显示计时状态,启动、暂停等操作要实时同步给所有用户。尝试了部分实现方案,但运行不流畅,求最优实现方式。
当前代码
time.dart(私有控制端)
import 'dart:async'; import 'package:flutter/material.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; final CollectionReference time = FirebaseFirestore.instance.collection('game'); class Time extends StatefulWidget { @override State<Time> createState() => TimeState(); } class TimeState extends State<Time> { Duration duration = Duration(); Timer? timer; final myController = TextEditingController(); @override void initState() { super.initState(); reset(); } void reset() { setState(() => duration = Duration()); } void addTime() { const addSeconds = 1; setState(() { final seconds = duration.inSeconds + addSeconds; duration = Duration(seconds: seconds); }); } void startTimer({bool resets = true}) { if (resets) { reset(); } timer = Timer.periodic(Duration(seconds: 1), (_) => addTime()); } void stopTimer({bool resets = true}) { if (resets) { reset(); } setState(() { timer?.cancel(); }); } void firstHalfEnd() { setState(() => duration = Duration(minutes: 45, seconds: 00)); stopTimer(resets: false); } void secondHalfEnd() { setState(() => duration = Duration(minutes: 90, seconds: 00)); stopTimer(resets: false); } void addMinute (){ setState(() { duration = duration + const Duration(minutes: 1); }); } void subtractMinute (){ setState(() { duration = duration - const Duration(minutes: 1); }); } @override Widget build(BuildContext context) => Scaffold( backgroundColor: Colors.green, body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ buiildTime(), SizedBox(height: 20), buildButtons() ], )), ); Widget buiildTime() { String twoDigits(int n) => n.toString().padLeft(2, '0'); final minutes = twoDigits(duration.inMinutes.remainder(100)); final seconds = twoDigits(duration.inSeconds.remainder(60)); setState(() { time.doc('Time').collection('minutes').doc('minutes').set({'minutes': '$minutes'}); time.doc('Time').update({'seconds': '$seconds'}); }); return Text( '$minutes:$seconds', style: TextStyle(fontSize: 20), ); } Widget buildButtons() { bool time_going = timer == null ? false : timer!.isActive; bool time_stops = duration.inSeconds == 0; return time_going || !time_stops ? Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Row( children: [ TextButton( child:Text(time_going ? 'PAUSE' : 'RESUME'), onPressed: () { if (time_going) { stopTimer(resets: false); } else { startTimer(resets: false); } }), SizedBox(width: 12), TextButton(child: Text("RESET"), onPressed: reset), ], ), SizedBox(height: 10), Row( crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ TextButton( onPressed: firstHalfEnd, child: Text('1. Half END'), style: ButtonStyle( backgroundColor: MaterialStateProperty.all<Color>(Colors.blue), foregroundColor: MaterialStateProperty.all<Color>(Colors.white), ), ), SizedBox(width: 10), TextButton( onPressed: secondHalfEnd, child: Text('2. Half END'), style: ButtonStyle( backgroundColor: MaterialStateProperty.all<Color>(Colors.blue), foregroundColor: MaterialStateProperty.all<Color>(Colors.white), ), ), ], ), Row( children: [ ElevatedButton(onPressed: addMinute, child: Text('+1 minute')), SizedBox(width: 10), ElevatedButton(onPressed: subtractMinute, child: Text('-1 minute')) ], ) ], ) : TextButton( onPressed: startTimer, child: Text('Start Timer!'), style: ButtonStyle( backgroundColor: MaterialStateProperty.all<Color>(Colors.black), foregroundColor: MaterialStateProperty.all<Color>(Colors.white), ), ); } }
scoreboard.dart(公共展示端)
Container( height: 100, width: 100, child: StreamBuilder( stream: clubData.doc('Time').snapshots(), builder: (context, snapshot) { if (!snapshot.hasData) { return Text("Loading"); } else { Map<String, dynamic> data = snapshot.data!.data() as Map<String, dynamic>; return Text( ":${data['seconds']}", style: TextStyle(color: Colors.white, fontSize: 20), ); } } ))
核心问题分析
- 频繁的Firestore读写:当前代码在
buiildTime方法中每秒执行Firestore更新,build方法会被频繁调用,导致每秒都向Firestore写入数据,不仅消耗资源,还会让展示端的StreamBuilder频繁重建,引发卡顿。 - 同步逻辑错误:直接同步显示的分钟/秒数,而不是同步计时的核心状态(运行状态、基准时间),导致同步依赖高频写入,无法保证多设备时间一致性。
优化实现方案
1. 调整Firestore存储结构
只存储计时的核心状态字段,避免每秒写入:
// Firestore文档结构示例 { "isRunning": bool, // 是否正在计时 "elapsedSeconds": int, // 暂停时已流逝的总秒数 "startTimestamp": FieldValue.serverTimestamp(), // 启动时的服务器时间 }
2. 控制端逻辑修改
将计时逻辑与Firestore同步分离,仅在状态变更时写入:
class TimeState extends State<Time> { Duration duration = Duration(); Timer? timer; final DocumentReference timeDoc = FirebaseFirestore.instance.collection('game').doc('Time'); @override void initState() { super.initState(); // 初始化时从Firestore拉取最新状态 timeDoc.get().then((doc) { if (doc.exists) { var data = doc.data() as Map<String, dynamic>; setState(() { duration = Duration(seconds: data['elapsedSeconds'] ?? 0); if (data['isRunning'] == true) { // 如果远程状态是运行中,本地继续计时 resumeTimerFromRemote(data['startTimestamp']); } }); } }); } // 从远程状态恢复计时 void resumeTimerFromRemote(Timestamp startTimestamp) { final elapsedSinceStart = DateTime.now().difference(startTimestamp.toDate()); duration = Duration(seconds: (duration.inSeconds + elapsedSinceStart.inSeconds)); timer = Timer.periodic(Duration(seconds: 1), (_) => addTime()); } void addTime() { setState(() { duration += Duration(seconds: 1); }); } void startTimer({bool resets = true}) { if (resets) { duration = Duration(); } timer = Timer.periodic(Duration(seconds: 1), (_) => addTime()); // 同步到Firestore timeDoc.set({ 'isRunning': true, 'elapsedSeconds': duration.inSeconds, 'startTimestamp': FieldValue.serverTimestamp(), }, SetOptions(merge: true)); } void stopTimer({bool resets = true}) { timer?.cancel(); if (resets) { duration = Duration(); } // 同步到Firestore timeDoc.set({ 'isRunning': false, 'elapsedSeconds': duration.inSeconds, }, SetOptions(merge: true)); } void firstHalfEnd() { timer?.cancel(); setState(() => duration = Duration(minutes: 45)); timeDoc.set({ 'isRunning': false, 'elapsedSeconds': 45 * 60, }, SetOptions(merge: true)); } void secondHalfEnd() { timer?.cancel(); setState(() => duration = Duration(minutes: 90)); timeDoc.set({ 'isRunning': false, 'elapsedSeconds': 90 * 60, }, SetOptions(merge: true)); } void addMinute() { setState(() => duration += Duration(minutes: 1)); timeDoc.set({ 'elapsedSeconds': duration.inSeconds, }, SetOptions(merge: true)); } void subtractMinute() { setState(() => duration -= Duration(minutes: 1)); timeDoc.set({ 'elapsedSeconds': duration.inSeconds, }, SetOptions(merge: true)); } // 移除build中的Firestore写入,仅显示本地计算的时间 Widget buiildTime() { String twoDigits(int n) => n.toString().padLeft(2, '0'); final minutes = twoDigits(duration.inMinutes.remainder(100)); final seconds = twoDigits(duration.inSeconds.remainder(60)); return Text( '$minutes:$seconds', style: TextStyle(fontSize: 20), ); } // ... 其余buildButtons代码保持不变 }
3. 展示端逻辑修改
通过远程状态实时计算当前时间,无需依赖高频更新:
Container( height: 100, width: 100, child: StreamBuilder<DocumentSnapshot>( stream: FirebaseFirestore.instance.collection('game').doc('Time').snapshots(), builder: (context, snapshot) { if (!snapshot.hasData) { return Text("Loading"); } final data = snapshot.data!.data() as Map<String, dynamic>?; if (data == null) return Text("00:00"); int totalSeconds = data['elapsedSeconds'] ?? 0; if (data['isRunning'] == true) { final startTimestamp = data['startTimestamp'] as Timestamp?; if (startTimestamp != null) { final elapsedSinceStart = DateTime.now().difference(startTimestamp.toDate()); totalSeconds += elapsedSinceStart.inSeconds; } } final minutes = totalSeconds ~/ 60; final seconds = totalSeconds % 60; String twoDigits(int n) => n.toString().padLeft(2, '0'); return Text( "${twoDigits(minutes)}:${twoDigits(seconds)}", style: TextStyle(color: Colors.white, fontSize: 20), ); }, ), )
优化效果
- 大幅减少Firestore读写次数:仅在启动、暂停、手动调整时间时写入,不再每秒更新。
- 多设备时间一致性更高:基于服务器时间计算,避免本地计时误差。
- 展示端流畅度提升:
StreamBuilder仅在状态变更时重建,不再每秒刷新。
内容的提问来源于stack exchange,提问作者Robert Tomic
相关产品推荐
相关产品推荐

