Home > Software engineering >  Riverpod state update does not rebuild the widget
Riverpod state update does not rebuild the widget

Time:07-23

I'm using StateProvider<List<String>> to keep track of user taps on the Tic Tac Toe board. Actual board is a widget that extends ConsumerWidget and consists of tap-able GridView.

Within the onTap event of GridViews child - following is invoked to update the state:

ref.read(gameBoardStateProvider.notifier).state[index] = 'X';

For some reason this does not invoke widget rebuild event. Due to this I cannot see the 'X' in the GridView item which was tapped.

However, if I add additional "simple" StateProvider<int> and invoke it as well within the same onTap event then the widget gets rebuilt and I can see the 'X' in the GridView. I am not even using or displaying this additional state provider but for some reason it invokes rebuild while my intended provided doesn't.

final gameBoardStateProvider = StateProvider<List<String>>((ref) => List.filled(9, '', growable: false));

final testStateProvider = StateProvider<int>((ref) => 0); //dummy state provider

class Board extends ConsumerWidget {
  const Board({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final gameBoard = ref.watch(gameBoardStateProvider);
    final testState = ref.watch(testStateProvider);

    return Expanded(
      child: Center(
        child: GridView.builder(
          itemCount: 9,
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
          shrinkWrap: true,
          itemBuilder: ((BuildContext context, int index) {
            return InkWell(
              onTap: () {
                //With this line only the widget does not get refreshed - and I do not see board refreshed with added 'X'
                ref.read(gameBoardStateProvider.notifier).state[index] = 'X';
                //??? If I add this line as well - for some reason the widget get refreshed - and I see board refreshed with added 'X'
                ref.read(testStateProvider.notifier).state  ;
              },
              child: Container(
                decoration: BoxDecoration(border: Border.all(color: Colors.white)),
                child: Center(
                  child: Text(gameBoard[index]),
                ),
              ),
            );
          }),
        ),
      ),
    );
  }
}

CodePudding user response:

Once you do

ref.read(gameBoardStateProvider.notifier).state[index] = 'X'

Means changing a single variable. In order to update the UI, you need to provide a new list as State.

You can do it like

List<String> oldState = ref.read(gameBoardStateProvider);
oldState[index] = "X";
ref
    .read(gameBoardStateProvider.notifier)
    .update((state) => oldState.toList());

Or

List<String> oldState = ref.read(gameBoardStateProvider);
oldState[index] = "X";
ref.read(gameBoardStateProvider.notifier).state =
    oldState.toList();

Why testStateProvider works:

Because it contains single int as State where gameBoardStateProvider contains List<String> as State.

Just updating single variable doesn't refresh ui on state_provider, You need to update the state to force a refresh

More about state_provider

You can also check change_notifier_provider

CodePudding user response:

The reason the ""simple"" StateProvider triggers a rebuild and your actual doesn't is because you aren't reassigning its value.

StateProviders works like this:

  1. StateProvider is just a StateNotifierProvider that uses a simple implementation of a StateNotifier that exposes its get state and set state methods;
  2. StateNotifier works like this: an actual state update consists of a reassignment. Whenever state gets reassigned it triggers its listeners (just like ChangeNotifier would do)

This means that, since you're exposing a List<String>, doing something like state[0] = "my new string will NOT trigger rebuilds. Only actions such as state = [... anotherList]; will do.

This is desirable, since StateNotifier pushes you to use immutable data, which is a good pattern in general. Instead, your int StateProvider will basically always trigger an update, since chances are that when you need to alter its state, you need to reassign its value.

For your use case you can do something like:

state[i] = 'x';
state = [...state];

This forces a re-assignment, and as such it will trigger the update.

Note: since you're implementing a tic-tac-toe, it is maybe desirable to handle mutable data for that grid. Try using a ChangeNotifier, implement some update logic (with notifiyListeners()) and exposing it with a ChangeNotifierProvider.

CodePudding user response:

Big thanks to both Yeasin and venir for clarifying what it means to reassign the state and the difference between simple and complex types.

While keeping the initial StateProvider I've replaced

ref.read(gameBoardStateProvider.notifier).state[index] = 'X';

with

var dataList = ref.read(gameBoardStateProvider);
dataList[index] = 'X';
ref.read(gameBoardStateProvider.notifier).state = [...dataList];

And it works now - the state gets updated and the widget rebuilt.

I've also tried the StateNotifier approach and it also works - although it seems more convoluted to me :) I'm pasting the code bellow as it can maybe benefit someone else to see the example.

class GameBoardNotifier extends StateNotifier<List<String>> {
  GameBoardNotifier() : super(List.filled(9, '', growable: false));

  void updateBoardItem(String player, int index) {
    state[index] = player;
    state = [...state];
  }
}

final gameBoardProvider = StateNotifierProvider<GameBoardNotifier, List<String>>((ref) => GameBoardNotifier());

class Board extends ConsumerWidget {
  const Board({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final gameBoard = ref.watch(gameBoardProvider);

    return Expanded(
      child: Center(
        child: GridView.builder(
          itemCount: 9,
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
          shrinkWrap: true,
          itemBuilder: ((BuildContext context, int index) {
            return InkWell(
              onTap: () {
                ref.read(gameBoardProvider.notifier).updateBoardItem('X', index);
              },
              child: Container(
                decoration: BoxDecoration(border: Border.all(color: Colors.white)),
                child: Center(
                  child: Text(gameBoard[index]),
                ),
              ),
            );
          }),
        ),
      ),
    );
  }
}
  • Related