Flutter 组件集录 | 后悔药 UndoHistory

在现实世界中,没有后悔药可以吃。但在对于计算机世界来说,撤销、恢复是非常常见的功能。小屁孩在键盘上啪啪一顿输出,把你正在写的重要文档搅得面目全非,Ctrl + Z 轻松救场。编程开发的过程中,我们在不断输入和试错,这颗后悔药是我们敢于前行的底气,大不了重新来过。


1. 简单认识 UndoHistory

UndoHistory 是一个 StatefulWidget 组件,在源码中它主要为输入组件服务,只在 EditeableText 源码中打工。可编辑的文字确实是 Undo 使用的最佳场所。

首先来通过一个小案例体验一下 UndoHistory 的价值。现在有个小需求:

在输入面板上添加两个按钮,分别用于 回退一步 和 撤销回退一步

传统的方法处理这个需求,要自己维护列表,在输入变化时进行收集字符串的工作。另外,并非每个字符变化都需要记录,需要进行节流 throttle 的处理,否则历史列表中将会记录大量字符信息,而绝大多数是没有必要的。这些逻辑交给开发者自己处理,就会比较麻烦。为了简化对输入框回退和撤销的操作,Flutter 通过了 UndoHistory 组件。


2. 案例代码实现

界面布局非常简单,上下结构通过 Column 竖向排列:

  • 上方是两个操作按钮,需要根据是否可回退、可撤销展示是否激活的状态。
  • 下方是普通的 TextFiled 组件,延展高度区域并填充白色。

TextField 组件中有一个 undoController 的参数,可以传入 UndoHistoryController 对象,用于控制 UndoHistory 的内容。它是一个 ValueNotifier 可监听对象,也就是说是否标题栏可以监听它,来感知是否可回退、可撤销的状态数据。

dart 复制代码
final UndoHistoryController _undoController = UndoHistoryController();

@override
void dispose() {
  _undoController.dispose();
  super.dispose();
}
  
Widget _buildInputArea() {
  return TextField(
    undoController: _undoController,
    expands: true,
    maxLines: null,
    minLines: null,
    decoration: InputDecoration(
      filled: true,
      fillColor: Colors.white,
      hoverColor: Colors.transparent,
      border: InputBorder.none,
    ),
  );
}

如下所示,这里封了 _IconAction 组件处理图标按钮的展示效果,包括悬浮时的背景圆角,已经激活状态的 处理。封装完后标题栏的两个按钮就可以轻松复用 _IconAction 实现展示功能。当onTap 事件为null时,表示非激活状态,无法触发交互。

dart 复制代码
class _IconAction extends StatefulWidget {
  final IconData icon;
  final VoidCallback? onTap;

  const _IconAction({super.key, required this.icon, this.onTap});

  @override
  State<_IconAction> createState() => _IconActionState();
}

class _IconActionState extends State<_IconAction> {
  bool _hover = false;

  bool get enable => widget.onTap != null;
  Color? get color => (_hover && enable) ? Colors.grey.withOpacity(0.2) : null;

  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      cursor: (_hover && enable) ? SystemMouseCursors.click : SystemMouseCursors.basic,
      onExit: (_) => setState(() => _hover = false),
      onEnter: (_) => setState(() => _hover = true),
      child: GestureDetector(
        onTap: widget.onTap,
        child: Container(
            decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(4)),
            padding: const EdgeInsets.all(4.0),
            child: Icon(
              widget.icon,
              size: 20,
              color: enable ? null : Colors.grey,
            )),
      ),
    );
  }
}

UndoHistoryController 中维护了两个历史记录,一个是输入的历史列表,用于处理回退;另一个是回退的历史列表,用于处理撤销上一次回退,分别对应左右按钮。 UndoHistoryController#undo 和UndoHistoryController#redo 方法实现回退和撤销回退的功能。

此时,构建顶部栏,可以通过 ValueListenableBuilder 来监听 _undoController 可监听对象。是否可以回退和撤销回退的状态,已经记录在了控制器中。回调构建时取用即可。按钮的事件触发,执行控制器的 undo 和 redo 方法即可。

dart 复制代码
Widget _buildToolBar() {
  return Padding(
    padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
    child: ValueListenableBuilder<UndoHistoryValue>(
      valueListenable: _undoController,
      builder: (BuildContext context, UndoHistoryValue value, Widget? child) {
        return Wrap(
          spacing: 4,
          children: <Widget>[
            _IconAction(icon: Icons.undo, onTap: value.canUndo ? _undoController.undo : null),
            _IconAction(icon: Icons.redo, onTap: value.canRedo ? _undoController.redo : null),
          ],
        );
      },
    ),
  );
}

3. UndoHistoryController 承担的角色

仔细思考一下,UndoHistoryController 在功能需求实现过程中。它连接着和 TextFiled 顶部的按钮事件,其既持有状态数据,又具有修改数据的能力,还能触发通知更新。这样的对象在状态管理中,一般称之为视图模型 ViewModel 或业务逻辑层 BLoc 。


可以从回退按钮的点击事件来体会一下,在交互之后,数据的流向。如下绿色箭头所示:

触发 undo 之后,列表数据变化,触发通知更新。此时顶栏和输入框都监听了 UndoHistoryController ,所以两者的视图都会发生变化。顶栏会根据是否可撤销展示激活与否;输入框中展示的文字会发生变化。

同理,输入框的底层在输入过程中,也一定修改了 UndoHistoryController 的内部数据,并触发通知更新。大家可以自己想想此时的数据流向。


4. UndoHistory 源码简看

下面是 EditableTextState 构建逻辑内 UndoHistory 组件的使用场景,其中我们传入的 undoController 将会为作为构造参数传入。其中 onTriggered 回调时触发 undo 和 redo 的时机,会触发 userUpdateTextEditingValue 方法更新输入的信息:

UndoHistoryState 中维护了一个 _UndoStack 的栈,

这个栈是通过列表 List 实现的,输入框中 UndoHistory 组件使用的泛型是 TextEditingValue。所以本质来看 UndoHistoryState 状态类中,维护了一个 TextEditingValue 列表来容纳输入框的编辑内容。

dart 复制代码
class _UndoStack<T> { 
  _UndoStack(); 
  final List<T> _list = <T>[];

在 initState 中可以看到,UndoHistoryState 会监听输入控制器触发 _push 方法; 监听 UndoHistoryContorller 的变化,触发 onTriggered 来更新输入框内容。


另外,其中定义了节流相关的计时器,时长为 500 ms , 输入变化时的 _push 方法中,会先校验更新的条件。然后将新值放入节流器 _throttledPush 中。

dart 复制代码
late final _Throttled<T> _throttledPush;
Timer? _throttleTimer;
bool _duringTrigger = false;

static const Duration _kThrottleDuration = Duration(milliseconds: 500);

_throttledPush在 initState 中被初始化,触发的函数是为 _stack 添加元素,并更新状态。

在 _updateState 中会更新 UndoHistoryController 控制器的值,触发通知更新。外界就可以因此感知是否可以回退或取消回退。


到这里,UndoHistory 的基本运转方式就简单了解了一下。虽然 UndoHistory 只在源码中的输入框里发光发热,但是它的价值远不止此。所有需要回退或取消回退的场景,都可以使用它。比如绘制、图片编辑等。后面会结合具体的其他场景,来介绍 UndoHistory 组件自身的使用方式。那本文就到这里,谢谢观看~

相关推荐
wangruofeng4 小时前
AI Models 1.1.0 发布,查大模型价格、对比模型和找 API 地址
前端·aigc·ai编程
wangruofeng5 小时前
给前端开发者的 100 个设计参考网站
前端·设计
LinXunFeng8 小时前
拿竞品 App 一扫,技术栈全暴露了
android·flutter·开源
星栈8 小时前
视频即代码,Remotion 能做什么、不能做什么
前端·音视频开发
广州华水科技8 小时前
单北斗GNSS位移监测系统的应用与技术解析
前端
用户47949283569158 小时前
从一句话或代码仓到可交互 HTML:开源 Agent 画图技能 Archify v3.2 实测与选型边界
前端·后端·面试
风骏时光牛马10 小时前
生成一个AI多模态标题,仅输出一条标题,无额外文字、问句、备选、配图、文案等任何附加内容,不需要其他的任务服务 只要标题。
前端
Gracker10 小时前
AIW:从系统机制到性能排查,一本可以对照源码的 Android 电子书
android
三少爷的鞋11 小时前
代码越来越便宜,判断力越来越贵:AI 时代的 Android 架构与协程避坑指南
android
wangruofeng11 小时前
从 GIF 到 AVIF:网页图片格式演进与 2026 年选型指南
前端·架构