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

使用Riverpod StateNotifier初始化时异步加载SharedPreferences出现竞态条件问题

解决方案:确保统计数据加载完成后再执行修改操作

从你的日志能明显看出问题所在:incrementStarts()在_load()完成前就被调用了,导致你先把初始的starts:0递增到1,随后加载完成的保存数据(starts:4)覆盖了这个值,最终统计数据丢失了一次启动记录。

另外,我注意到你的GuessTheWordStats是可变类,直接修改它的属性再更新状态是不符合Riverpod中状态不可变的设计原则的——这可能导致StateNotifier无法正确检测到状态变化,进而引发UI不更新或者数据不一致的问题,所以我们先修复这个问题,再解决加载顺序的问题。

步骤1:将GuessTheWordStats改为不可变类

把所有字段改为final,并添加copyWith方法,这样每次修改都生成新的实例:

class GuessTheWordStats {
  final int wins;
  final int loses;
  final int starts;
  final int tried;
  final List<int> guesses;

  GuessTheWordStats({
    required this.wins,
    required this.loses,
    required this.starts,
    required this.tried,
    required this.guesses,
  });

  factory GuessTheWordStats.fromJson(Map<String, dynamic> json) {
    return GuessTheWordStats(
      wins: json['wins'],
      loses: json['loses'],
      starts: json['starts'],
      tried: json['tried'],
      guesses: List<int>.from(json['guesses']),
    );
  }

  Map<String, dynamic> toJson() => {
        'wins': wins,
        'loses': loses,
        'starts': starts,
        'tried': tried,
        'guesses': guesses,
      };

  static GuessTheWordStats init() {
    return GuessTheWordStats(
        wins: 0, loses: 0, starts: 0, tried: 0, guesses: []);
  }

  // 添加copyWith方法,用于生成新的实例
  GuessTheWordStats copyWith({
    int? wins,
    int? loses,
    int? starts,
    int? tried,
    List<int>? guesses,
  }) {
    return GuessTheWordStats(
      wins: wins ?? this.wins,
      loses: loses ?? this.loses,
      starts: starts ?? this.starts,
      tried: tried ?? this.tried,
      guesses: guesses ?? this.guesses,
    );
  }
}

步骤2:确保修改操作等待加载完成

这里提供两种优雅的方案,你可以根据自己的偏好选择:

方案一:用Completer跟踪加载状态

在StatsStateNotifier中添加一个Completer,用来标记_load()是否完成,所有修改方法都先等待这个Completer完成后再执行:

class StatsStateNotifier extends StateNotifier<StatsState> {
  final Completer<void> _loadCompleter = Completer<void>();

  StatsStateNotifier() : super(settingsInitialState()) {
    _init();
  }

  Future<void> _init() async {
    await _load();
    _loadCompleter.complete(); // 标记加载完成
  }

  Future<void> _load() async {
    print('StatsStateNotifier::load()');
    final prefs = await SharedPreferences.getInstance();
    final String? json = prefs.getString('GuessTheWordStats');
    print('json: $json');
    GuessTheWordStats guessTheWordStats = GuessTheWordStats.init();
    if (json != null) {
      guessTheWordStats = GuessTheWordStats.fromJson(jsonDecode(json));
    }
    state = state.copyWith(
      guessTheWordStats: guessTheWordStats,
    );
    print('StatsStateNotifier::load() -- done');
  }

  Future<void> _save() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(
        'GuessTheWordStats', jsonEncode(state.guessTheWordStats.toJson()));
  }

  // 修改incrementStarts为异步方法,等待加载完成
  Future<void> incrementStarts() async {
    await _loadCompleter.future; // 等待加载完成
    print('StatsStateNotifier::incrementStarts()');
    print('starts: ${state.guessTheWordStats.starts}');
    // 用copyWith生成新实例,更新状态
    final newStats = state.guessTheWordStats.copyWith(starts: state.guessTheWordStats.starts + 1);
    state = state.copyWith(guessTheWordStats: newStats);
    await _save();
    print('starts: ${state.guessTheWordStats.starts}');
  }

  // 同样修改其他increment方法
  Future<void> incrementWins() async {
    await _loadCompleter.future;
    final newStats = state.guessTheWordStats.copyWith(wins: state.guessTheWordStats.wins + 1);
    state = state.copyWith(guessTheWordStats: newStats);
    await _save();
  }

  Future<void> incrementLoses() async {
    await _loadCompleter.future;
    final newStats = state.guessTheWordStats.copyWith(loses: state.guessTheWordStats.loses + 1);
    state = state.copyWith(guessTheWordStats: newStats);
    await _save();
  }
}

然后在GuessTheWordPage的_initNewGame中,要await这个异步方法:

_initNewGame() async {
  await ref.read(guessTheWordProvider.notifier).newGame();
  // 等待加载完成后再递增
  await ref.read(statsProvider.notifier).incrementStarts();
}

方案二:用FutureProvider提前加载初始数据

把加载SharedPreferences的逻辑移到一个FutureProvider中,确保StateNotifier创建时已经拿到了保存的统计数据:

// 先定义一个FutureProvider加载初始统计数据
final initialStatsProvider = FutureProvider<GuessTheWordStats>((ref) async {
  final prefs = await SharedPreferences.getInstance();
  final String? json = prefs.getString('GuessTheWordStats');
  if (json != null) {
    return GuessTheWordStats.fromJson(jsonDecode(json));
  }
  return GuessTheWordStats.init();
});

// 修改StatsStateNotifier的构造函数,接收初始数据
class StatsStateNotifier extends StateNotifier<StatsState> {
  StatsStateNotifier(GuessTheWordStats initialStats) 
    : super(StatsState(guessTheWordStats: initialStats));

  Future<void> _save() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(
        'GuessTheWordStats', jsonEncode(state.guessTheWordStats.toJson()));
  }

  void incrementStarts() {
    print('StatsStateNotifier::incrementStarts()');
    print('starts: ${state.guessTheWordStats.starts}');
    final newStats = state.guessTheWordStats.copyWith(starts: state.guessTheWordStats.starts + 1);
    state = state.copyWith(guessTheWordStats: newStats);
    _save();
    print('starts: ${state.guessTheWordStats.starts}');
  }

  // 其他increment方法同样用copyWith修改
}

// 修改statsProvider,依赖initialStatsProvider
final statsProvider = StateNotifierProvider<StatsStateNotifier, StatsState>((ref) {
  // 等待FutureProvider加载完成
  final initialStats = ref.watch(initialStatsProvider).value!;
  return StatsStateNotifier(initialStats);
});

然后在页面的_initNewGame中,等待初始数据加载完成:

_initNewGame() async {
  await ref.read(guessTheWordProvider.notifier).newGame();
  // 等待初始统计数据加载完成
  await ref.read(initialStatsProvider.future);
  ref.read(statsProvider.notifier).incrementStarts();
}

方案对比

  • 方案一:保留了StateNotifier内部的加载逻辑,代码更内聚,适合简单场景。
  • 方案二:把加载逻辑抽离到单独的Provider,符合单一职责原则,适合复杂的初始化场景,也方便在其他地方复用初始数据。

两种方案都能解决你的问题,而且不需要在HomePageView中冗余监听统计数据,完全符合你的需求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.28 15:02:38