You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter Riverpod:嵌套Provider变更时ConsumerWidget不重建的解决方法

Flutter Riverpod:嵌套Provider变更时ConsumerWidget不重建的问题与解决

问题场景

在Flutter应用中使用Riverpod时,遇到以下问题:当嵌套的bankrollTotalProvider数据变更时,监听bankrollListControllerProvider的BankrollListScreen(ConsumerWidget)不会触发重建。

BankrollListScreen的核心逻辑是监听银行账户列表,遍历每个账户并通过bankrollTotalProvider(bankroll.id)计算展示该账户的总额,简化代码如下:

class BankrollListScreen extends ConsumerWidget {
  const BankrollListScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    AsyncValue<List<Bankroll>> bankrollListValue =
        ref.watch(bankrollListControllerProvider);

    return bankrollListValue.when(
      data: (bankrollList) => ListView(
        children: [
          for (var bankroll in bankrollList)
            GameSessionListTile(
              bankrollTotal: Text(
                ref.watch(
                  bankrollTotalProvider(bankroll.id),
                ).toString(),
              ),
              onTap: () {
                context.pushNamed(
                  AppRoute.gameSessionList.name,
                  pathParameters: {
                    'id': bankroll.id,
                    'bankrollName': bankroll.name!
                  },
                );
              },
            ),
        ],
      ),
      // 处理loading、error状态
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (error, stack) => Center(child: Text('加载失败: $error')),
    );
  }
}

bankrollTotalProvider的计算逻辑为:

@riverpod
Future<double> bankrollTotal(
  BankrollTotalRef ref,
  String bankrollId,
) async {
  final gameSessionList = await ref
      .watch(gameSessionRepositoryProvider)
      .getGameSessionListForBankroll(bankrollId);

  final depositList = await ref
      .watch(depositRepositoryProvider)
      .getDepositListForBankroll(bankrollId);

  final gameSessionTotal = gameSessionList.fold(
      0.0, (sum, session) => sum + (session.cashout ?? 0.0));

  final depositTotal =
      depositList.fold(0.0, (sum, deposit) => sum + (deposit.amount));

  return gameSessionTotal + depositTotal;
}

问题原因

bankrollTotalProvider是带参数的FutureProvider,它依赖repository的getXXXListForBankroll方法获取数据。当新增游戏会话或存款后,repository的底层数据发生变化,但FutureProvider本身不会自动感知数据更新——因为它的缓存是基于传入的参数和依赖的Provider状态,而如果repositoryProvider没有对外暴露数据变化的通知,bankrollTotalProvider会认为数据未变更,不会重新计算,自然也不会触发UI重建。

解决方案

临时生效方案(已验证)

在添加新数据后,手动调用ref.invalidate()失效相关的repositoryProvider,强制让依赖这些repository的Provider(包括bankrollTotalProvider)重新获取数据:

onPressed: () async {
  final bankroll = await ref.read(bankrollRepositoryProvider).getBankroll(id);
  ref.watch(addGameSessionControllerProvider.notifier).addGameSession(bankroll);

  // 失效相关Provider缓存,触发数据重新获取
  ref.invalidate(bankrollRepositoryProvider);
  ref.invalidate(gameSessionRepositoryProvider);
  ref.invalidate(depositRepositoryProvider);

  if (context.mounted) {
    context.goNamed(AppRoute.home.name);
  }
},

该方案直接清空了repository层的缓存,确保所有依赖这些repository的Provider都会重新计算,从而更新UI。

更优方案(精准更新)

上述方案会失效所有相关repository的缓存,可能影响其他不相关数据的展示。可以改为只失效目标账户对应的bankrollTotalProvider实例,减少不必要的重建:

onPressed: () async {
  final bankroll = await ref.read(bankrollRepositoryProvider).getBankroll(id);
  await ref.watch(addGameSessionControllerProvider.notifier).addGameSession(bankroll);

  // 仅失效当前账户对应的总额Provider缓存
  ref.invalidate(bankrollTotalProvider(bankroll.id));

  if (context.mounted) {
    context.goNamed(AppRoute.home.name);
  }
},

长期优化方案(自动感知数据变化)

如果使用的是支持流(Stream)的数据源(如sqflite、Firebase、Hive的监听功能),可以将bankrollTotalProvider改为StreamProvider,让它自动监听数据变化:

@riverpod
Stream<double> bankrollTotal(
  BankrollTotalRef ref,
  String bankrollId,
) async* {
  // 假设repository返回Stream<List<GameSession>>和Stream<List<Deposit>>
  final gameSessionStream = ref
      .watch(gameSessionRepositoryProvider)
      .watchGameSessionListForBankroll(bankrollId);
  final depositStream = ref
      .watch(depositRepositoryProvider)
      .watchDepositListForBankroll(bankrollId);

  // 合并两个流,任一数据变化时重新计算总额
  await for (final (gameSessions, deposits) in StreamZip([gameSessionStream, depositStream])) {
    final gameSessionTotal = gameSessions.fold(0.0, (sum, s) => sum + (s.cashout ?? 0.0));
    final depositTotal = deposits.fold(0.0, (sum, d) => sum + d.amount);
    yield gameSessionTotal + depositTotal;
  }
}

此时UI层只需将ref.watch的结果转为AsyncValue,当底层数据变化时,StreamProvider会自动推送新值,触发对应的UI重建,无需手动调用invalidate。

内容的提问来源于stack exchange,提问作者Daniel HyunHo Shin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 05:40:17