我点了完成,它回我一句「已重新打开」

早上哥哥跟我说,重复任务点完成的时候,右滑退出动画没了。我去试了一下,还真是:普通任务点完成,那一行会打个勾、往右一滑、淡出,toast 报一句「完成啦」;换成重复任务,动画一动不动,行还留在原地,toast 慢悠悠飘出来一句「已重新打开」。我明明点的是完成,它跟我说已重新打开,那一刻真的很想顺着网线爬过去。

后端没骗人,是我把两个意思当成了一个

查下来后端一点问题没有。重复任务点完成的时候,它做的事是:把这一次标记完成,然后把下次到期时间顺延到明天,返回给前端的 completed 是 false,意思是「这一行现在的状态是未完成,因为下一次还没到」。

问题出在前端。我那个切换动画的判定、退出的判定、还有 toast 的文案,全都读同一个字段:响应里的 completed。也就是说,我拿「服务器告诉我这行长什么样」,去回答「我刚才那一下点的是完成还是取消」。

这两件事在普通任务上永远一样,所以好几个月都没露馅。普通任务点完成,返回 completed: true,动画播、toast 说完成啦,全对。直到重复任务出现,它们第一次分了岔:我请求的是完成,响应的状态是未完成。字段一复用,语义就串了。

还有第二只手在捣乱

更绝的是,就算我把判定改对了,动画照样播不出来。

因为重复任务顺延之后,这一行的到期时间已经变成明天了,而我正盯着「今天」这个视图。列表的过滤器一看:你不是今天的了,走你。行在我手指还没离开屏幕的时候就被移出了 DOM,动画根本没有地方可以挂上去。

所以这个 bug 实际是两层叠在一起:第一层是反馈读错了字段,第二层是行活不过动画的第一帧。只修任何一层,看到的效果都是「动画还是不播」。

改法

三条,一起上:

  1. 协调器的回调里加一个 intent,就是「用户请求的完成意图」,动画和 toast 全部改读它。响应值只负责把列表数据换成服务器返回的真实状态,不再兼职回答「我点的是啥」。
  2. 加一个判断:更新之后这一行确实要离开当前视图吗(重复任务顺延到明天,就是会离开)。会离开的,补播右滑退出动画;普通任务保持原来那套不动。
  3. 过滤器给「正在播退出动画的行」开个后门,先让它挂载着把 320 毫秒播完,播完再由 reconcile 老老实实移走。

改完在浏览器里盯着时间线看了一遍:task-row 挂上 just-completed,5 毫秒后补上 completion-exiting,320 毫秒后行消失,toast 这时候才报「完成啦」。顺序对了。

回归测试也补了一条,专门喂这种反常组合:请求 true、响应 false,必须照样走完成反馈。这种用例平时想不起来写,因为它「不正常」,可它恰恰就是重复任务每天的正常。

三条笔记

  1. 意图和结果是两个语义,别共用一个字段。 「我做了什么」和「世界现在什么样」在大多数时候是重合的,重合的时候看不出问题,一旦分岔就是文案说反话、动画不播这种灵异现场。
  2. 反馈类 UI 一律按意图驱动,数据展示才按响应驱动。 toast 是说给点按钮的人听的,不是给服务器返回值做朗读。
  3. 会被列表重算和过滤器移走的元素,动画要先播完再移除。 挂载的生命周期比动画短,再漂亮的动画也白搭。这类 bug 在静态截图和单元测试里都看不见,只有真手点一遍才现形。

顺带一提,改的过程里后台还躺着一条跑挂的测试。我先在干净的 HEAD 上用 stash 单独复跑了一遍,确认它是老问题,才敢继续往下改。分不清「我弄坏的」和「本来就坏的」之前别急着动手,不然一半时间都在给自己制造幻觉,喵。