Riverpod状态更新不会重新构建小部件。

4
我将尽力完成您的翻译请求。

我正在使用StateProvider<List<String>>来跟踪用户在井字游戏板上的点击。

实际的游戏板是扩展了ConsumerWidget并包含可点击的GridView的小部件。

在GridView子元素的onTap事件中,以下内容被调用以更新状态:

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

由于某些原因,这个操作没有触发小部件重建事件。因此,我无法在被点击的GridView项中看到“X”。
然而,如果我在同一个onTap事件中添加额外的“简单”StateProvider<int>并且也调用它,那么小部件会得到重建,我就可以在GridView中看到“X”。
我甚至没有使用或显示这个额外的state provider,但由于某种原因,它会调用重建,而我的预期provider却不会。
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]),
                ),
              ),
            );
          }),
        ),
      ),
    );
  }
}
3个回答

6

一旦你这样做了

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

意味着改变一个单一的变量。为了更新界面,您需要提供一个新的列表作为状态。

您可以这样做:

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

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

testStateProvider为什么有效:

因为它包含单个整数作为状态,而gameBoardStateProvider包含List<String>作为状态。

只更新单个变量不会在state_provider上刷新UI,您需要更新状态以强制刷新

有关state_provider的更多信息

您还可以查看change_notifier_provider


1
谢谢!我学到了新的东西 :) 我原本以为通过更新列表的值,我实际上是在改变列表,这意味着状态改变 - 但事实并非如此。 - Sasa

4
“StateProvider”会触发重新构建,而您的实际代码不会触发,原因是您没有重新分配其值。
“StateProvider”的工作原理如下:
1. “StateProvider”只是使用一个简单的“StateNotifier”实现的“StateNotifierProvider”,该实现公开了其“获取状态”和“设置状态”方法(以及另一个“更新”方法);
2. “StateNotifier”的工作方式如下:实际状态更新包括重新分配。每当“状态”被重新分配时,它都会触发其侦听器(就像“ChangeNotifier”一样)。请参见不可变模式。
这意味着,由于您正在公开一个“List ”,因此执行类似于“state [0] =“ my new string”的操作不会触发重建。只有像“state = [... anotherList]”这样的操作才能实现。
这是令人满意的,因为“StateNotifier”推动您使用不可变数据,在前端世界中可以成为一种良好的模式。
相反,您的“int StateProvider”基本上总是会触发更新,因为您需要重新分配其值才能更改其状态。
对于您的用例,您可以执行以下操作:
state[i] = 'x';
state = [...state];

这将强制重新分配,因此它将触发更新。
注意:由于您正在实现井字游戏,因此可能希望处理该网格的可变数据。尝试使用ChangeNotifier,实现一些更新逻辑(使用notifiyListeners()),并使用ChangeNotifierProvider公开它。
Riverpod实际上建议避免可变模式(即避免使用ChangeNotifier),但是,在计算机科学中没有银弹,您的用例可能不同。

谢谢。我已经查看了StateNotifierProvider文档,并且发现它是用于用户交互相关状态更新和更复杂状态数据的推荐方法。我将在下面的答案中添加实现方式。虽然与仅使用StateProvider相比似乎有些过度,但一旦我开始引入更复杂的业务逻辑,这可能会更有意义... - Sasa
非常欢迎!根据您的使用情况,我建议使用ChangeNotifier并操作可变列表。Riverpod的作者通常建议使用StateNotifier,但是在计算机科学中没有万能药,您的情况更适合使用ChangeNotifier - venir

1
感谢Yeasin和venir对重新分配状态以及简单类型和复杂类型之间差异的澄清。
在保留初始StateProvider的同时,我已经替换了
ref.read(gameBoardStateProvider.notifier).state[index] = 'X';

使用

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

现在它可以正常工作了 - 状态已更新并重建了小部件。
我也尝试了StateNotifier方法,它也可以工作 - 尽管对我来说似乎更加复杂:) 我将下面的代码粘贴在这里,因为它可能有助于其他人看到这个例子。
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]),
                ),
              ),
            );
          }),
        ),
      ),
    );
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接