Race condition when using Riverpod StateNotifier to Async load SharedPreferences on instantiation

Viewed 151

Widgets:

HomePageView
  -- GuessTheWordPage 

I have a provider that keeps track of stats. When the StateNotifier instantiates it loads the saved stats from SharedPrefferences. The issue is that when it is first used it calls incrementStarts() but the _load() has not completed by then so it increments before it loads the saved data.

I can solve this by doing a watch on the provider in HomePageView instead of GameView. But it feels dirty as it is never used there.

How can I make sure the data is loaded from SharedPrefferences before it is used? I have tried to change it so _load() is called from another function _init() async { await _load(); }, but it does not solve the issue.

As I have said I can change HomeView to a ConsumerWidget and add: StatsState statsState = ref.watch(statsProvider); but it feels dirty.

logs
I/flutter (11187): StatsStateNotifier::load()
I/flutter (11187): StatsStateNotifier::incrementStarts()
I/flutter (11187): starts: 0
I/flutter (11187): starts: 1
I/flutter (11187): json: {"wins":0,"loses":0,"starts":4,"tried":0,"guesses":[]}
I/flutter (11187): StatsStateNotifier::load() -- done
class GuessTheWordStats {
  int wins; // won a game
  int loses; // lost a game, did not find the solution
  int starts; // started a new game
  int tried; // tried at least one solution
  List<int> guesses; // number of guesses to get solution

  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: []);
  }
}

@immutable
class StatsState {
  const StatsState({
    required this.guessTheWordStats,
  });
  final GuessTheWordStats guessTheWordStats;

  StatsState copyWith({
    GuessTheWordStats? guessTheWordStats,
  }) {
    return StatsState(
      guessTheWordStats: guessTheWordStats ?? this.guessTheWordStats,
    );
  }
}

StatsState settingsInitialState() {
  return StatsState(
    guessTheWordStats: GuessTheWordStats.init(),
  );
}

class StatsStateNotifier extends StateNotifier<StatsState> {
  StatsStateNotifier() : super(settingsInitialState()) {
    _load();
  }

  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('GuessTheWordStats.wins: ${guessTheWordStats.wins}');
    // print('GuessTheWordStats.loses: ${guessTheWordStats.loses}');
    // print('GuessTheWordStats.starts: ${guessTheWordStats.starts}');
    // print('GuessTheWordStats.tried: ${guessTheWordStats.tried}');
    // print('GuessTheWordStats.guesses: ${guessTheWordStats.guesses}');
    print('StatsStateNotifier::load() -- done');
  }

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

  incrementWins() {
    GuessTheWordStats guessTheWordStats = state.guessTheWordStats;
    guessTheWordStats.wins++;
    state = state.copyWith(
      guessTheWordStats: guessTheWordStats,
    );
    _save();
  }

  incrementStarts() {
    print('StatsStateNotifier::incrementStarts()');
    print('starts: ${state.guessTheWordStats.starts}');
    GuessTheWordStats guessTheWordStats = state.guessTheWordStats;
    guessTheWordStats.starts++;
    state = state.copyWith(
      guessTheWordStats: guessTheWordStats,
    );
    _save();
    print('starts: ${state.guessTheWordStats.starts}');
  }

  incrementLoses() {
    GuessTheWordStats guessTheWordStats = state.guessTheWordStats;
    guessTheWordStats.loses++;
    state = state.copyWith(
      guessTheWordStats: guessTheWordStats,
    );
    _save();
  }
}

final StateNotifierProvider<StatsStateNotifier, StatsState> statsProvider =
    StateNotifierProvider<StatsStateNotifier, StatsState>((ref) {
  return StatsStateNotifier();
});

class GuessTheWordPage extends ConsumerStatefulWidget {
  const GuessTheWordPage({Key? key}) : super(key: key);

  @override
  GuessTheWordPageState createState() => GuessTheWordPageState();
}

class GuessTheWordPageState extends ConsumerState<GuessTheWordPage> {
  final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();

  MainAppSettings mainAppSettings = MainAppSettings();

  _initSettings() async {
    await mainAppSettings.loadFromPrefs();
  }

  _initNewGame() async {
    await ref.read(guessTheWordProvider.notifier).newGame();
// first use of statsProvider //
    ref.read(statsProvider.notifier).incrementStarts();
// -- //
  }

  @override
  void initState() {
    if (kDebugMode) print('GuessTheWordPage::initState()');
    super.initState();
    _initSettings();
    _initNewGame();
  }

  @override
  Widget build(BuildContext context) {
0 Answers
Related