茶器艺科智造HarmonyOS应用实战-65-订单key把status和progress也拼进去,进度一变为何整张卡片像新节点:稳定身份与展示状态要分开

商城列表把 id:status:progress 拼成 ForEach 的 key。订单每推进一次,key 也跟着变化,框架看到的就不再是“同一订单更新了内容”,而更像“旧身份消失、新身份出现”。与此同时,订单 ID 只靠 Date.now 生成,还留下同一毫秒创建时的碰撞窗口。本篇分别处理渲染身份和业务身份,避免用展示字段弥补 ID 设计。

第65篇封面

本文以 Gitee 仓库 aiding521/chaqi-app-giteemaster 分支、提交 f671fcd8de277973bd7518a3c462f68384575f1e 的静态源码为事实基线。本文没有修改项目源码,也没有执行 HAP 构建、模拟器或真机验证;建议代码属于演进方案,不能描述成已经落地的功能。

一、一条订单为什么会拥有多个渲染身份

静态源码里,商城 ForEach 的 key 同时包含订单 ID、状态和进度,当前写法可以概括为:

ForEach(this.filteredOrders(), (o: ShopOrder) => {},
  (o: ShopOrder) => `${o.id}:${o.status}:${o.progress}`)

假设同一订单从 pending/0 变成 printing/20,两次 key 字符串不同。业务对象仍是那笔订单,但列表协调过程失去了跨状态的稳定锚点;卡片内部若有展开状态、焦点、动效进度或局部资源,也存在随节点替换而重建的风险。这里能确认的是 key 随可变字段变化,不能仅凭静态阅读断言某台设备已经出现闪烁或状态丢失。

另一个问题位于更上游:如果 id 本身只取 Date.now,同一毫秒内创建的两笔订单可能拿到相同候选值。稳定 key 的前提不是“永远返回 id”这一句,而是业务层先保证 id 唯一;否则两笔不同订单反而会争用同一渲染身份。

二、业务身份与展示状态必须正交

修复可以拆成两条独立约束。第一条:ForEach 的 key 只返回不可变的 order.id,状态、进度、价格等字段只驱动卡片内容。第二条:ID 生成器单独保证新订单的唯一性,恢复持久化数据时也检查重复 ID。不能把 statusprogress 拼回 key 来“区分”重复订单,那只会把数据完整性问题藏到渲染层。

第65篇流程图

流程图应从 ID 创建开始:新订单先获得唯一业务身份,进入集合后始终沿用这个身份;状态变化只更新对象字段;列表协调始终读取同一个 key。恢复旧数据时,则先发现重复或缺失身份,再决定拒绝、迁移或生成兼容标识。这样每一层只回答一个问题:数据层回答“它是谁”,状态层回答“它现在怎样”,界面层回答“如何展示”。

三、ForEach key 只保留不可变 ID

渲染侧的最小修改很短,重点是删掉所有会随订单推进而改变的部分:

ForEach(this.filteredOrders(), (order: ShopOrder) => {
  this.OrderCard(order)
}, (order: ShopOrder) => order.id)

此后,pending -> printing -> completed 都对应同一个 order.id,只有卡片接收的数据变化。这里不需要另建“renderId”,因为独立渲染 ID 会带来业务对象与节点身份的第二套映射;除非现有模型明确允许一个订单在同一列表中出现多次,否则直接复用唯一订单 ID 更容易审计。

稳定 key 不等于禁止排序、筛选或进度刷新。订单可以在过滤结果中进入或离开,列表也可以改变排序;只要同一订单再次出现时仍返回相同 key,就没有必要把当前展示位置和当前进度编码进身份。

四、订单 ID 生成要补上同毫秒序列

渲染侧依赖 order.id,因此创建侧必须承担唯一性。现有建议在毫秒值后增加进程内序列:

private nextSliceOrderId(nowMs: number): string {
  this.orderSequence++;
  return `ORD-SL-${nowMs}-${this.orderSequence.toString().padStart(4, '0')}`;
}

同一 nowMs 下,递增序列能让连续创建的候选 ID 不同;格式中的 ORD-SL 还保留订单用途的可读前缀。接入前要核对 orderSequence 的初始化和重置位置:如果页面反复创建就从零开始,而历史订单仍在集合中,仅有进程内递增还不足以覆盖恢复后的重复风险。因此加载订单时应做一次重复检查,创建新 ID 时也应避开已有集合。

本篇只建议收口订单创建与商城 ForEach 的身份路径,不延伸到杯型、切片算法或档案页面。若仓库中还有其它订单列表,应搜索它们是否使用同一 ID,并逐个确认 key 规则,而不是把这次改动机械复制给所有 ForEach

五、用变化矩阵判断字段能否进入 key

订单变化order.idstatus/progresskey 应否变化
pending 进入 printing不变变化不变
printing 更新进度不变progress 变化不变
completed 后刷新文案不变status 或展示值变化不变
用户改变筛选条件不变订单数据可不变订单离开/进入结果集,但身份不重算
新建另一笔订单必须不同初始值可以相同必须不同
恢复数据发现重复 ID冲突任意先处理数据冲突,不能拼状态掩盖

判断一个字段是否适合进入 key,可以问一句:这个字段变化后,业务上还是不是同一个对象?状态、进度和展示文案的答案显然是“还是”;新订单 ID 的答案才是“不是”。把这个判断写进评审规则,比记住某一段 key 写法更可靠。

六、测试要观察 key 序列而不只看页面结果

interface BoundaryCase<T> {
  name: string;
  input: T;
  expected: string;
}

const cases: BoundaryCase<string>[] = [
  { name: 'empty', input: '', expected: 'reject' },
  { name: 'normal', input: 'valid', expected: 'accept' },
  { name: 'repeat', input: 'valid', expected: 'idempotent' }
];

上面的边界用例骨架可以继续使用,但本篇的输入应是订单快照,输出应是 key。对同一 id 构造多组 status/progress,断言输出始终相等;对不同订单,即使状态与进度完全相同,也要断言输出不同。ID 生成器则单独输入相同 nowMs,连续调用后检查候选值不重复。

还需要覆盖恢复数据:空 ID、重复 ID、历史 ID 与新生成候选冲突,都必须走明确的兼容或拒绝路径。静态单元用例能证明字符串规则,却不能证明卡片在真实 ArkUI 列表里的状态保留与动效表现;后者仍需要模拟器或真机观察,不能用“函数返回一样”替代。

七、稳定 key 能减少重建风险,但不替代资源清理

private disposeOwnedResources(): void {
  // timer/listener只由创建它的组件清理
  // 异步结果写状态前核对taskId或pageGeneration
  // 可恢复任务先保存业务事实,再释放进程句柄
}

这段清理约束不是本篇的主修点,却揭示了可变 key 的额外代价:如果卡片节点因 key 变化被替换,组件内部持有的 timer、listener 或控制器就会更频繁地经历创建与销毁。稳定 key 能让正常的数据刷新继续对应同一身份,但组件仍必须按自身生命周期成对清理资源,不能把稳定 key 当作不会销毁的保证。

页面级的 aboutToAppear/aboutToDisappear 与卡片级的 onAppear/onDisAppear 也不能混为一谈。订单离开筛选结果不代表业务订单被删除;卡片销毁时释放视图资源即可,不应顺手改变订单终态。反过来,业务订单状态更新也不应通过改变 key 来强制刷新页面。

八、验证时同时盯住 ID 和节点身份

  1. 搜索订单列表,确认 key 生成函数不再包含 statusprogress 或其它展示字段。
  2. 固定一笔订单,依次改变状态和进度,记录每次计算出的 key,确认全程等于 order.id
  3. 在同一毫秒参数下连续生成多笔订单,确认序列部分不同;再与已有订单集合比对。
  4. 构造两笔状态完全相同的订单,确认它们仍有不同 ID 和不同 key。
  5. 加载包含重复或缺失 ID 的恢复数据,确认走显式兼容路径,而不是靠状态后缀区分。
  6. 在列表中展开、筛选、排序并刷新进度,观察卡片局部状态和动效;这些结果需用模拟器或真机记录。
  7. 静态检查、构建、模拟器和真机分别记录,未执行项明确写“未执行”。

九、身份问题最容易被“更复杂的 key”掩盖

误修问题正确方向
在 key 后继续拼时间戳每次渲染都可能产生新身份key 只取已分配的业务 ID
id:status:progress 区分重复 ID重复数据仍存在,且正常更新会换 key数据层识别并处理 ID 冲突
只把 key 改成 id若 id 生成仍可碰撞,两笔订单会争用身份同时修正创建与恢复校验
为列表单独创建 renderId增加第二套映射,恢复和调试更复杂优先复用不可变的唯一 order.id
看到页面不闪就结束局部状态、焦点和资源重建未被记录用状态序列和设备证据验收

排查顺序应从业务 ID 开始,再看 key 函数,最后看卡片内部状态。视觉上暂时没有明显闪烁,并不能反证 key 合理;同样,key 字符串看起来“足够唯一”,也不代表它在对象整个生命周期内稳定。

十、ID 规则属于模型,key 选择属于视图

第65篇结构图

按本文静态基线,entry 负责产品入口,libraryhsp 负责页面和交互,libraryhar 放置模型、算法、路由与通用服务。订单 ID 格式、唯一性检查和恢复冲突策略可归入模型或纯服务;ForEach 如何取 key 则留在具体页面组件。这样即使以后出现第二种订单视图,也能共享同一业务身份,而不必共享页面实现。

entry / libraryhsp -> 页面、组件、生命周期
libraryhar        -> 纯类型、纯函数、可持久化模型
tests             -> 边界、幂等、恢复序列
runtime evidence  -> 日志、设备行为、服务读回

图中的 tests 也应分两层:模型测试验证相同毫秒的 ID 分配与重复数据处理,视图测试验证同一 ID 在进度刷新前后 key 不变。运行证据再补充节点行为、焦点、动效和局部状态是否符合预期。

十一、本文能确认什么

baseline: f671fcd8de277973bd7518a3c462f68384575f1e
scope: source-level proposal
build: not run
simulator/device: not run
network/account: not run
result: static boundary identified; implementation pending

本文能确认当前 key 包含可变的 status/progress,也能确认建议代码将 key 收回到 order.id 并为创建侧增加序列。它不能证明改法已经落地,也没有用运行日志证明节点重建次数、焦点保持或动效连续性。构建、模拟器、真机、账号、网络和性能结果均未执行,结论仍停留在源码级方案。

十二、把一次进度刷新拆成可复现场景

最小复现不是反复刷新整个页面,而是固定订单 A 的 ID,只改变 A 的 status/progress,逐次记录 key、卡片局部状态和节点行为。随后加入订单 B,确保 B 即使拥有相同进度,也不会与 A 共用身份。最后再用重复 ID 的恢复数据验证防线位于数据层。

场景操作序列必须观察的证据不足以作为结论的现象
单笔更新A: pending/0→printing/20每个快照的 key 都等于 A.id卡片文字正确
高频进度A 连续更新多个 progresskey 序列不变,局部状态可跟踪最终进度正确
两笔同态A、B 都为 pending/0A.id 与 B.id 不同,key 也不同两张卡片数量正确
同毫秒创建固定 nowMs 连续生成 A、B序列后缀不同,并避开已有集合肉眼看到 ID 很长
恢复冲突读入两个相同 id明确的拒绝或迁移记录临时拼 status 后可显示

日志无需记录完整订单内容,至少包含 orderId、key、旧状态、新状态和进度摘要。ID 冲突日志还应说明来源是新建还是恢复,但不要记录账号令牌、用户完整输入或敏感材料。

interface OperationTrace {
  taskId: string;
  generation: number;
  startedAtMs: number;
  finishedAtMs?: number;
  terminalSource?: 'ui' | 'service' | 'restore' | 'cancel';
}

function canApplyResult(trace: OperationTrace, currentGeneration: number): boolean {
  return trace.generation === currentGeneration &&
    trace.finishedAtMs === undefined;
}

上面的通用轨迹结构可以继续记录操作,但本篇判定 canApplyResult 之后,仍要保证写回只修改原订单的可变字段,不改 order.id。评审时应问:同一订单换状态时 key 是否稳定;两笔订单在同毫秒创建时 ID 是否不同;恢复数据重复时是否被发现;卡片退出筛选结果时资源是否正常释放。四个问题分别对应身份稳定、身份唯一、数据兼容和视图生命周期。

真实验收建议保留状态变化前后的截图、key 日志和可重放步骤。编辑器预览不足以证明 ArkUI 节点复用行为,本地构造的 ID 也不足以证明恢复数据没有冲突。本文没有执行这些运行步骤,因此结果栏仍保持“待验证”。

补充审查要求:示例中的订单前缀、序列宽度、状态名和持久化字段要与变更时仓库核对;若源码、SDK 版本或 ID 规则变化,应重新建立基线,不沿用本文快照。没有执行的构建、设备、网络和账号步骤继续标记为未执行。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐