I'm building a stopwatch app in Flutter that uses a StreamBuilder to update elapsed and lap times. The app works fine when the stopwatch is running, but after pausing the stopwatch, leaving the page, and reloading it, the StreamBuilder for elapsed and lap times outputs this:
AsyncSnapshot<Duration>(ConnectionState.waiting, null, null, null)
Because of this, the stopwatch UI resets to 00:00.00 until I start the stopwatch again. I expect it to load and display the correct paused elapsed and lap times immediately after the page is reloaded.
Here's a simplified version of the code for the stopwatch service and UI:
class StopwatchService {
final StreamController<Duration> _elapsedTimeController = StreamController<Duration>.broadcast();
final StreamController<Duration> _lapTimeController = StreamController<Duration>.broadcast();
Duration _pausedDuration = Duration.zero;
int? _startTimeEpoch;
int? _lastStopTimeEpoch;
Stream<Duration> get elapsedTimeStream => _elapsedTimeController.stream;
Stream<Duration> get lapTimeStream => _lapTimeController.stream;
void start() {
_startTimeEpoch ??= DateTime.now().millisecondsSinceEpoch;
_tick();
}
void stop() {
_lastStopTimeEpoch = DateTime.now().millisecondsSinceEpoch;
_pausedDuration += Duration(milliseconds: _lastStopTimeEpoch! - _startTimeEpoch!);
_elapsedTimeController.add(_pausedDuration);
_lapTimeController.add(Duration.zero); // For simplicity
}
void reset() {
_elapsedTimeController.add(Duration.zero);
_lapTimeController.add(Duration.zero);
}
Future<void> loadState(SharedPreferences prefs) async {
final lastStopTimeEpoch = prefs.getInt('lastStopTimeEpoch') ?? DateTime.now().millisecondsSinceEpoch;
final startTimeEpoch = prefs.getInt('startTimeEpoch') ?? 0;
final pausedDuration = Duration(milliseconds: prefs.getInt('pausedDuration') ?? 0);
if (startTimeEpoch != 0) {
final elapsedTime = Duration(milliseconds: lastStopTimeEpoch - startTimeEpoch - pausedDuration.inMilliseconds);
_elapsedTimeController.add(elapsedTime);
_lapTimeController.add(Duration.zero);
}
}
void _tick() {
Timer.periodic(Duration(milliseconds: 10), (timer) {
if (_startTimeEpoch == null) return;
final now = DateTime.now().millisecondsSinceEpoch;
final elapsedTime = Duration(milliseconds: now - _startTimeEpoch! - _pausedDuration.inMilliseconds);
_elapsedTimeController.add(elapsedTime);
});
}
}
class StopwatchPage extends StatefulWidget {
const StopwatchPage({super.key});
@override
StopwatchPageState createState() => StopwatchPageState();
}
class StopwatchPageState extends State<StopwatchPage> {
final StopwatchService _stopwatchService = StopwatchService();
@override
void initState() {
super.initState();
_loadState();
}
Future<void> _loadState() async {
final prefs = await SharedPreferences.getInstance();
await _stopwatchService.loadState(prefs);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<Duration>(
stream: _stopwatchService.elapsedTimeStream,
builder: (context, snapshot) {
final elapsed = snapshot.data ?? Duration.zero;
return Text(elapsed.toString());
},
),
);
}
}
I have tried:
Adding Initial Values to Streams: I explicitly emit Duration.zero in _elapsedTimeController and _lapTimeController when loading the state or resetting the stopwatch.
Debugging the StreamBuilder: The StreamBuilder
still shows waiting and null data immediately after the page is reloaded.
Forcing UI Updates: Tried manually triggering state updates with setState
after emitting values to streams.
Expected Behavior:
When the stopwatch page reloads, the StreamBuilder
should display the correct paused elapsed and lap times without requiring user input.
Actual Behavior:
The StreamBuilder
shows AsyncSnapshot<Duration>(ConnectionState.waiting, null, null, null)
and resets the display to 00:00.00.
How can I ensure that the StreamBuilder displays the correct paused elapsed and lap times immediately after the page reloads? Is there a best practice for initializing StreamController
with previously saved data to avoid this issue?