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

使用Riverpod实现撤销/重做按钮时遭遇void类型错误求助

问题根源

  1. 错误使用StateProvider<void>:StateProvider用于存储可观察的有效状态值,但void类型不代表任何实际状态,这本身就是设计错误。即便返回null也无意义,因为void类型无法承载任何值。
  2. 无意义的按钮逻辑:undo/redo按钮的onPressed中仅读取ref.read(undoProvider).state,这行代码既没有实现撤销/重做功能,还因读取void类型值触发编译错误。
  3. 核心功能缺失:当前DrawingAreaNotifier未维护绘制历史记录,根本不具备撤销/重做的基础能力。

解决方案

1. 重构DrawingAreaNotifier,添加历史管理逻辑

在状态管理类中维护三个列表:当前绘制点列表、撤销历史栈、重做历史栈,同时实现undo()、redo()方法,确保每次操作都更新历史记录。

2. 移除无用的undoProvider和redoProvider

撤销/重做属于绘制状态管理的一部分,无需单独的StateProvider,直接通过drawingAreaProvider.notifier调用对应方法即可。

3. 修改按钮的onPressed逻辑

直接调用DrawingAreaNotifier中的undo()和redo()方法,实现实际功能。

修改后的完整代码

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

final drawingAreaProvider =
    StateNotifierProvider.autoDispose<DrawingAreaNotifier, List<Offset>>((ref) {
  return DrawingAreaNotifier();
});

final toggleGridProvider = StateProvider.autoDispose<bool>((ref) => false);

class DrawingAreaNotifier extends StateNotifier<List<Offset>> {
  // 撤销历史栈:存储每次添加新点前的状态
  final List<List<Offset>> _undoStack = [];
  // 重做历史栈:存储撤销操作后被移除的状态
  final List<List<Offset>> _redoStack = [];

  DrawingAreaNotifier() : super([]);

  void addPoint(Offset point) {
    // 添加新点前,将当前状态存入撤销栈
    _undoStack.add(List.from(state));
    // 清空重做栈(新操作会使之前的重做状态失效)
    _redoStack.clear();
    state = [...state, point];
  }

  void clearPoints() {
    _undoStack.add(List.from(state));
    _redoStack.clear();
    state = [];
  }

  void undo() {
    if (_undoStack.isNotEmpty) {
      // 将当前状态存入重做栈
      _redoStack.add(List.from(state));
      // 恢复到上一个状态
      state = _undoStack.removeLast();
    }
  }

  void redo() {
    if (_redoStack.isNotEmpty) {
      // 将当前状态存入撤销栈
      _undoStack.add(List.from(state));
      // 恢复到之前撤销的状态
      state = _redoStack.removeLast();
    }
  }
}

class DrawingArea extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final points = ref.watch(drawingAreaProvider);

    return GestureDetector(
      onPanStart: (details) {
        final RenderBox renderBox = context.findRenderObject() as RenderBox;
        final localPosition = renderBox.globalToLocal(details.globalPosition);
        ref.read(drawingAreaProvider.notifier).addPoint(localPosition);
      },
      onPanUpdate: (details) {
        final RenderBox renderBox = context.findRenderObject() as RenderBox;
        final localPosition = renderBox.globalToLocal(details.globalPosition);
        ref.read(drawingAreaProvider.notifier).addPoint(localPosition);
      },
      onPanEnd: (_) {
        // Do nothing on pan end
      },
      child: Stack(
        children: [
          CustomPaint(
            painter: DrawingPainter(points),
            size: Size.infinite,
          ),
          Positioned(
            top: 20,
            left: 20,
            child: Row(
              children: [
                IconButton(
                  icon: const Icon(Icons.undo),
                  onPressed: () {
                    ref.read(drawingAreaProvider.notifier).undo();
                  },
                ),
                IconButton(
                  icon: const Icon(Icons.redo),
                  onPressed: () {
                    ref.read(drawingAreaProvider.notifier).redo();
                  },
                ),
                IconButton(
                  icon: const Icon(Icons.grid_on),
                  onPressed: () {
                    ref.read(toggleGridProvider).state =
                        !ref.read(toggleGridProvider).state;
                  },
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class DrawingPainter extends CustomPainter {
  final List<Offset> points;

  DrawingPainter(this.points);

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.black
      ..strokeWidth = 2
      ..style = PaintingStyle.stroke;

    if (points.isEmpty) return;

    final path = Path();
    path.moveTo(points.first.dx, points.first.dy);
    for (int i = 1; i < points.length; i++) {
      path.lineTo(points[i].dx, points[i].dy);
    }

    canvas.drawPath(path, paint);

    final crossPaint = Paint()
      ..color = Colors.red
      ..strokeWidth = 2;

    for (final point in points) {
      canvas.drawLine(Offset(point.dx - 5, point.dy),
          Offset(point.dx + 5, point.dy), crossPaint);
      canvas.drawLine(Offset(point.dx, point.dy - 5),
          Offset(point.dx, point.dy + 5), crossPaint);
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}

关键说明

  • 历史栈管理:每次添加新点或清空画布时,会将当前状态存入_undoStack,并清空_redoStack(新操作会使之前的重做状态失效)。
  • 撤销逻辑:点击撤销按钮时,若_undoStack不为空,将当前状态存入_redoStack,再恢复到_undoStack的最后一个状态。
  • 重做逻辑:点击重做按钮时,若_redoStack不为空,将当前状态存入_undoStack,再恢复到_redoStack的最后一个状态。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 03:17:33