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

Flutter秒表重载后StreamBuilder显示waiting状态空数据问题

问题分析

你的核心问题在于:

  1. 普通BroadcastStreamController没有缓存历史值,页面重建后StreamBuilder订阅流时会先进入waiting状态,直到流有新事件触发。
  2. loadState是异步操作,页面初始化时StreamBuilder已完成构建,此时本地状态还未加载完成,导致UI显示默认的Duration.zero。
解决方案

方案1:使用BehaviorSubject(推荐,基于rxdart)

BehaviorSubject会自动缓存最后一次发送的值,新订阅者一订阅就能收到最新值,完美匹配秒表恢复状态的需求。

步骤1:添加依赖

在pubspec.yaml中加入rxdart:

dependencies:
  flutter:
    sdk: flutter
  rxdart: ^0.27.7 # 或最新版本

步骤2:修改StopwatchService

import 'package:rxdart/rxdart.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:async';
import 'dart:core';

class StopwatchService {
  // 用BehaviorSubject替代普通StreamController,初始化时传入默认值
  final BehaviorSubject<Duration> _elapsedTimeController = BehaviorSubject.seeded(Duration.zero);
  final BehaviorSubject<Duration> _lapTimeController = BehaviorSubject.seeded(Duration.zero);

  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() async {
    _lastStopTimeEpoch = DateTime.now().millisecondsSinceEpoch;
    _pausedDuration += Duration(milliseconds: _lastStopTimeEpoch! - _startTimeEpoch!);
    _elapsedTimeController.add(_pausedDuration);
    _lapTimeController.add(Duration.zero);
    // 新增:保存状态到本地
    final prefs = await SharedPreferences.getInstance();
    _saveState(prefs);
  }

  void reset() async {
    _elapsedTimeController.add(Duration.zero);
    _lapTimeController.add(Duration.zero);
    // 重置内部状态
    _pausedDuration = Duration.zero;
    _startTimeEpoch = null;
    _lastStopTimeEpoch = null;
    // 清空本地保存
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove('lastStopTimeEpoch');
    await prefs.remove('startTimeEpoch');
    await prefs.remove('pausedDuration');
  }

  Future<void> loadState(SharedPreferences prefs) async {
    final lastStopTimeEpoch = prefs.getInt('lastStopTimeEpoch') ?? DateTime.now().millisecondsSinceEpoch;
    final startTimeEpoch = prefs.getInt('startTimeEpoch') ?? 0;
    final pausedDurationMs = prefs.getInt('pausedDuration') ?? 0;
    _pausedDuration = Duration(milliseconds: pausedDurationMs);

    if (startTimeEpoch != 0) {
      final elapsedTime = Duration(milliseconds: lastStopTimeEpoch - startTimeEpoch - pausedDurationMs);
      _elapsedTimeController.add(elapsedTime);
      _lapTimeController.add(Duration.zero);
    }
  }

  void _tick() {
    Timer.periodic(const Duration(milliseconds: 10), (timer) {
      if (_startTimeEpoch == null) {
        timer.cancel();
        return;
      }
      final now = DateTime.now().millisecondsSinceEpoch;
      final elapsedTime = Duration(milliseconds: now - _startTimeEpoch! - _pausedDuration.inMilliseconds);
      _elapsedTimeController.add(elapsedTime);
    });
  }

  // 新增:保存状态的私有方法
  void _saveState(SharedPreferences prefs) async {
    await prefs.setInt('lastStopTimeEpoch', _lastStopTimeEpoch ?? DateTime.now().millisecondsSinceEpoch);
    await prefs.setInt('startTimeEpoch', _startTimeEpoch ?? 0);
    await prefs.setInt('pausedDuration', _pausedDuration.inMilliseconds);
  }

  // 新增:释放资源,防止内存泄漏
  void dispose() {
    _elapsedTimeController.close();
    _lapTimeController.close();
  }
}

步骤3:修改页面State

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

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
  void dispose() {
    _stopwatchService.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: StreamBuilder<Duration>(
          stream: _stopwatchService.elapsedTimeStream,
          builder: (context, snapshot) {
            final elapsed = snapshot.data ?? Duration.zero;
            return Text(
              _formatDuration(elapsed),
              style: const TextStyle(fontSize: 40),
            );
          },
        ),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () => _stopwatchService.stop(),
            child: const Icon(Icons.pause),
          ),
          const SizedBox(width: 10),
          FloatingActionButton(
            onPressed: () => _stopwatchService.start(),
            child: const Icon(Icons.play_arrow),
          ),
          const SizedBox(width: 10),
          FloatingActionButton(
            onPressed: () => _stopwatchService.reset(),
            child: const Icon(Icons.refresh),
          ),
        ],
      ),
    );
  }

  // 可选:格式化时长为00:00.00的友好格式
  String _formatDuration(Duration duration) {
    String twoDigits(int n) => n.toString().padLeft(2, '0');
    final minutes = twoDigits(duration.inMinutes.remainder(60));
    final seconds = twoDigits(duration.inSeconds.remainder(60));
    final milliseconds = twoDigits((duration.inMilliseconds.remainder(1000)) ~/ 10);
    return "$minutes:$seconds.$milliseconds";
  }
}

方案2:原生Flutter实现(不依赖第三方库)

如果不想引入rxdart,可以通过缓存最后值+合并初始流的方式实现类似效果:

class StopwatchService {
  final StreamController<Duration> _elapsedTimeController = StreamController.broadcast();
  Duration? _lastElapsedTime;
  final StreamController<Duration> _lapTimeController = StreamController.broadcast();

  // 合并初始值和原流,确保新订阅者立即获取最新值
  Stream<Duration> get elapsedTimeStream {
    final initialValue = _lastElapsedTime ?? Duration.zero;
    return Stream.value(initialValue).concatWith([_elapsedTimeController.stream]);
  }

  // 封装add方法,同时缓存最后值
  void _addElapsedTime(Duration value) {
    _lastElapsedTime = value;
    _elapsedTimeController.add(value);
  }

  // 其余方法和方案1一致,仅把所有_elapsedTimeController.add替换为_addElapsedTime
}
初始化StreamController的最佳实践
  1. 优先使用BehaviorSubject:如果需要新订阅者能立即获取最新状态,这是最简洁可靠的方案。
  2. 缓存最后值:原生实现时,手动缓存流的最后一次发送值,在订阅时先发送该值。
  3. 必须关闭控制器:在组件销毁时调用close()方法,避免内存泄漏。
  4. 同步初始值:确保流在初始化时就有默认值,避免StreamBuilder进入waiting状态。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 07:32:33