Search code examples
flutterdartproviderriverpod

UI not updating despite watching provider


I am using Riverpod and have set up a Provider and Notifier for my data, however when I modify the data in the provider, my UI is not reflecting that change. If I save and hot-reload my app, the new values are shown.

For context, my Board contains a property that is a Map of Players and their Scores. On my Board screen, I am iterating through a list conversion of the 'boardData' map for a GridView.builder, like so:

    ref.read(boardProvider.notifier).loadBoard(widget.board);
    final providedBoard = ref.watch(boardProvider);

    List<MapEntry<PlayerModel, int>> boardDataList =
        providedBoard.boardData.entries.toList();


      body: GridView.builder(
        padding: const EdgeInsets.all(16),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          childAspectRatio: 3 / 2, //1.5
          crossAxisSpacing: 10,
          mainAxisSpacing: 10,
        ),
        itemCount: providedBoard.boardData.length,
        itemBuilder: (context, index) => BoardPlayerGridItem(
          player: boardDataList[index].key,
          score: boardDataList[index].value,
        ),
      ),

Here is my BoardModel class:

import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';

import 'package:counter/models/player_model.dart';

const uuid = Uuid();

class BoardModel {
  BoardModel({
    required this.name,
    required this.icon,
    required this.initialValue,
    required this.increment,
    required this.boardData,
    required this.owner,
    required this.isManaged,
    required this.isOnline,
    required this.autosaveEnabled,
    required this.lastUpdated,
    String? id,
  }) : id = id ?? uuid.v4();

  final String id;
  final String name;
  final Icon icon;
  final int initialValue; // default to 0
  final int increment; // default to 1
  final Map<PlayerModel, int> boardData; // Players and scores for the board
  final PlayerModel owner;
  final bool isManaged; // Only owner can make changes
  final bool isOnline; // Saved to cloud
  final bool autosaveEnabled;
  final DateTime lastUpdated;
}

Here is my provider:

class BoardNotifier extends StateNotifier<BoardModel> {
  BoardNotifier() : super(sampleBoard);

  // Load the currently viewed board into notifier
  loadBoard(BoardModel board) {
    state = board;
  }

  // Update the player's score in the board by the configured increment
  incrementPlayerScore(BoardModel board, PlayerModel player) {
    board.boardData.update(player, (value) => value + board.increment);

    // Update our board in state
    state = board;
  }
}

final boardProvider = StateNotifierProvider<BoardNotifier, BoardModel>((ref) {
  return BoardNotifier();
});

Here is my BoardPlayerGridItem (the widget in the GridView) that is not updating.

class BoardPlayerGridItem extends ConsumerStatefulWidget {
  const BoardPlayerGridItem({
    super.key,
    required this.player,
    required this.score,
  });

  final PlayerModel player;
  final int? score;

  @override
  ConsumerState<ConsumerStatefulWidget> createState() =>
      _BoardPlayerGridItemState();
}

class _BoardPlayerGridItemState extends ConsumerState<BoardPlayerGridItem> {
  @override
  Widget build(BuildContext context) {
    final providedBoard = ref.watch(boardProvider);

    return GestureDetector(
      onTap: () {
        ref.watch(boardProvider.notifier).incrementPlayerScore(
              providedBoard,
              widget.player,
            );
      },
      onLongPress: () {}, // Open edit form
      child: Container(
        color: Theme.of(context).highlightColor,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              widget.player.name,
              style: Theme.of(context)
                  .textTheme
                  .titleMedium!
                  .copyWith(color: Theme.of(context).colorScheme.onBackground),
            ),
            const SizedBox(height: 10),
            Text(
              widget.score.toString(),
              style: Theme.of(context)
                  .textTheme
                  .displayLarge!
                  .copyWith(color: Theme.of(context).colorScheme.onBackground),
            ),
          ],
        ),
      ),
    );
  }
}

The UI is a grid of tiles and tapping on each increments the counter. I thought by adding watching my boardProvider and then changing it as per the onTap function, that would prompt the rebuild, but seems not. Any pointers appciated.


Solution

  • The problem is that inside incrementPlayerScore, you're already updating the board before reassigning the state. This is why it's important to use immutable state.

    As a workaround, you can make a local variable inside incrementPlayerScore that copy the board state first and make modifications on that copied variable. You haven't shown the BoardModel class, but I assume it should have some kind of copyWith method. If so, you can do this:

    incrementPlayerScore(BoardModel board, PlayerModel player) {
      final newBoard = board.copyWith();
      newBoard.boardData.update(player, (value) => value + board.increment);
    
      // Update our board in state
      state = newBoard;
    }
    

    (Note: there could be a better way to use the copyWith that is by assigning the new boardData with the boardData parameter of copyWith)


    Additionally, you should use ref.read instead of ref.watch inside the onTap of GestureDetector.