Search code examples
flutterriverpodflutter-riverpod

What should build() return after it realizes a need for a state change?


I think I'm missing something about the Riverpod philosophy.

I'm keeping all my game state in a ChangeNotifier, including the .stage which can be "start", "playing", "winning", etc:

final ChangeNotifierProvider<GameState> gameStateProvider = 
    ChangeNotifierProvider<GameState>((ref) => GameState(stage="start"));

My build() functions start by accessing this state:

build(context, ref) {
  final GameState gameState = ref.watch(gameStateProvider);

And build themselves depending upon the state of gameState.

But somewhere I have to detect that the game has progressed to the point that I should update state, e.g. gameState.stage = 'winning'. I tried this:

build(context, ref) {
  final GameState gameState = ref.watch(gameStateProvider);

  if (gameState.stage == "playing" && gameState.someConditionsAreMet) {
    gameState.stage = "winning"; // .stage setter triggers notifyListeners()
  }

But I am not allowed to call notifyListeners in build() because it tries to mark the widget _needsBuild during the build phase.

So I wrap the call in a Timer.run() to make it asynchronous, and the widget happily rebuilds during the next frame:

build(context, ref) {
  final GameState gameState = ref.watch(gameStateProvider);

  if (gameState.stage == "playing" && gameState.someConditionsAreMet) {
    Timer.run(() => gameState.stage = "winning"); // OK: will flag dirty only after build

    // now what?!

...but now I don't know what to return from build() when that if gets triggered:

  • I can't return what I would normally return when gameState.stage == "winning", because it still thinks the stage is "playing" (the Timer.run() callback hasn't executed yet.)
  • I can't return what I would normally return when gameState.stage == "playing", because the conditions that made me want to trigger the change of stage make that impossible (e.g. I print the strongest enemy's HP, but there are now zero enemies.)
  • I can't return SizedBox.shrink() as a hack because that would cause a single frame to render blank, causing a flash on the screen before the .stage = "winning" widget can render.

I considered just doing the update to gameState.stage inside gameState itself, as soon as the right conditions were triggered. But then stateless widgets don't have a way of detecting that state has just changed to do one-time events, like pushing a welcome modal on top of the main screen the first time .stage changes from "start" to "playing".

So I'm clearly doing something wrong architecturally. How do experienced Riverpod users do it?


Solution

  • You can not Call Notify Listener In the Build Method, You can Only Listen or Watch for State Change int the Builder, Notify Listener Should only be called From the Change Notifier Class as this is the Purpose of that class