使用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
相关产品推荐
相关产品推荐

