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

Flutter中Riverpod与CustomPainter结合的Provider更新报错求助

问题分析与解决方案

代码中的关键错误

  1. 在paint方法内修改Provider状态
    paint属于Flutter的绘制生命周期,和build方法一样,在此阶段修改Provider状态会触发UI重新构建,导致状态不一致或无限循环,这也是Riverpod抛出错误的直接原因。

  2. shouldRepaint返回固定false
    你的shouldRepaint直接返回false,意味着即使依赖的Provider数据(节点列表、起止键、卡片位置)发生变化,CustomPainter也不会重新绘制,会导致视图无法更新。

  3. 直接修改ref.watch返回的状态对象
    代码中尝试直接调用node.add((start!, end!))修改ref.watch拿到的列表,这违反了Riverpod的状态管理规范——状态必须通过对应的Notifier/Controller来更新,不能直接修改ref.watch返回的对象。

  4. 直接传递WidgetRef给CustomPainter
    将WidgetRef传入CustomPainter会让绘制组件耦合状态逻辑,不符合单一职责原则,也增加了出错风险。


解决方案

核心思路是将状态更新逻辑与绘制逻辑分离:CustomPainter只负责接收数据并绘制,状态更新放到合适的生命周期(如帧绘制完成后)处理。

步骤1:重构CustomPainter,使其纯绘制

class EdgePainter extends CustomPainter {
  final List<(String, String)> nodeList;
  final String? startKey;
  final String? endKey;
  final Map<String, Offset> cardPositions;

  EdgePainter({
    required this.nodeList,
    required this.startKey,
    required this.endKey,
    required this.cardPositions,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()..color = Colors.blue;

    // 绘制已连接的节点线条
    for (var node in nodeList) {
      final startPos = cardPositions[node.$1];
      final endPos = cardPositions[node.$2];
      if (startPos != null && startPos != Offset.infinite && endPos != null && endPos != Offset.infinite) {
        canvas.drawLine(startPos, endPos, paint);
      }
    }

    // 绘制临时线条(起止键存在时)
    if (startKey != null && endKey != null) {
      final startPos = cardPositions[startKey!];
      final endPos = cardPositions[endKey!];
      if (startPos != null && startPos != Offset.infinite && endPos != null && endPos != Offset.infinite) {
        canvas.drawLine(startPos, endPos, paint);
      }
    }
  }

  @override
  bool shouldRepaint(covariant EdgePainter oldDelegate) {
    // 当依赖数据变化时,触发重绘
    return oldDelegate.nodeList != nodeList ||
        oldDelegate.startKey != startKey ||
        oldDelegate.endKey != endKey ||
        oldDelegate.cardPositions != cardPositions;
  }
}

步骤2:在Consumer中处理状态更新

使用WidgetsBinding.instance.addPostFrameCallback在当前帧绘制完成后执行状态更新,避免在build/paint生命周期内修改状态:

Consumer(
  builder: (BuildContext context, WidgetRef ref, Widget? child) {
    final nodeList = ref.watch(connectedNodeListProvider);
    final startKey = ref.watch(startKeyProvider);
    final endKey = ref.watch(endKeyProvider);
    final cardPositions = ref.watch(cardPositionMapProvider);

    // 帧绘制完成后处理状态更新
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (startKey != null && endKey != null) {
        final startPos = cardPositions[startKey];
        final endPos = cardPositions[endKey];
        if (startPos != null && startPos != Offset.infinite && endPos != null && endPos != Offset.infinite) {
          // 通过Notifier更新节点列表
          ref.read(connectedNodeListProvider.notifier).update((state) {
            return List.from(state)..add((startKey, endKey));
          });
          // 重置起止键状态
          ref.read(startKeyProvider.notifier).state = null;
          ref.read(endKeyProvider.notifier).state = null;
        }
      }
    });

    return CustomPaint(
      painter: EdgePainter(
        nodeList: nodeList,
        startKey: startKey,
        endKey: endKey,
        cardPositions: cardPositions,
      ),
    );
  },
)

额外说明

  • 如果你的connectedNodeListProvider是StateProvider,上述更新逻辑完全适用;如果是自定义的NotifierProvider,只需调整对应的更新方法(比如调用notifier.addNode((startKey, endKey)))。
  • 这种分离方式让代码更易维护:CustomPainter专注绘制,状态逻辑统一在Consumer或专门的业务逻辑类中处理。

内容的提问来源于stack exchange,提问作者user23295538

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 22:06:03