Live View Kit+ArkTS:排队实况窗双通道更新水位收敛【鸿蒙心迹】
排队取餐这种场景,用户看到的状态看似很简单:前面还有多少单、预计等待多久、是不是轮到自己。但在工程上,真正麻烦的不是画一条进度条,而是同一个排队事件可能由本地应用和服务端推送两条通道先后更新。两边网络和生命周期不同,旧消息未必先到。若UI收到“前方还有12单”后,又被一条延迟抵达的“前方还有15单”覆盖,用户会认为队伍倒退了。
因此本文不从实况窗模板样式入手,而是从事件版本的所有权入手。Demo叫QueueLiveFence,测试任务LV-1010-14、业务排队号Q-274、演示性实况窗ID41027。我们设定业务版本从41递增到45,共收到7次更新:5条有效版本被接纳,1条重复版本43和1条迟到版本42被拒绝。当前水位45,前方数量12,应用状态HANDOFF_PENDING。平台本地创建、Push Kit更新与结束流程都标记NOT_RUN,因为当前没有申请到业务服务能力,也没有真实设备、Push Token或服务端接入证据。

一、实况窗不等于应用里的悬浮窗
先把一个容易混淆的术语讲清楚:Live View Kit的“实况窗”是一种在通知中心、状态栏、锁屏等系统位置展示持续服务动态的能力;它不是Window Kit创建的应用悬浮窗,也不是本系列先前写过的FloatView。这两类能力在生命周期、操作权限和界面承载位置上有本质差异,不能因为中文都带“窗”就套用相同的窗口API。
华为官方实况窗指南列出了取餐、排队、配送、出行等持续性服务场景,并提供liveViewManager.isLiveViewEnabled()、startLiveView、updateLiveView、stopLiveView等能力。官方同时指出本地更新依赖应用进程存活;若希望进程离开后仍持续更新和结束,推荐由Push Kit按业务事件更新。于是我们有两个状态体系:业务订单的权威状态在后端;设备端的实况窗只是其中一个可见的投影。应用不能把“调用显示接口返回”当成“业务订单已经完成”。
这个区分直接影响本例做什么、不做什么。我们确实可以在本地构造一组有序和乱序消息,并验证一个应用层的序号门禁会如何处理;但无法在这里证明设备已经获得实况窗权限、服务端已经关联pushToken、用户没有主动清除通知、系统最终已经停止实况窗。第一件事是模型可测的,后几件事都要有真实集成证据。本文不会把模型的ACCEPT写成系统级的UPDATED。
二、只认单调递增的业务版本,不认谁最后抵达
先固定数据协议。业务事件包含queueId、version、aheadCount、state、occurredAt、eventId。其中version由同一队列号对应的业务源分配,必须保证同一个队列里单调递增;设备端自己使用Date.now()作为版本是错误方案,因为两台设备时钟可能不同,网络传输时间也无法代表业务变更顺序。
在我们的事件序列里,10:20接纳41,10:21接纳42,10:22接纳43;随后又来一条43,判定为重复;10:23接纳44,接着迟到的42再来一次,只能丢弃;10:24接纳45。5条有效事件代表“创建后的五个演示业务版本”,并不等于真实系统SDK调用成功5次。最终水位45、eventsReceived=7、applied=5、duplicateIgnored=1、staleIgnored=1。此处aheadCount=12只是最后一条演示业务数据。
需要明确终态规则。排队事件最终可能进入SERVED或CANCELLED,一旦写入终态,任何没有更高权威版本的旧进度都不能把它重新改回排队中。更棘手的是同版本不同payload:这不是“最后写入胜出”,而应该记录协议冲突并发出服务端核查信号,不能静默覆盖。若业务允许在取消后重新排队,应该产生新的队列实例ID或新的业务epoch,不能复活旧会话。
为了让这个判断可以独立测试,先写一段完全不依赖平台接口的ArkTS模型。QueueRevisionGate是应用自己定义的类,ACCEPT、DUPLICATE、STALE也是本文私有状态;它们不是Live View Kit返回的枚举值。
interface QueueEvent {
queueId: string
eventId: string
version: number
aheadCount: number
state: 'WAITING' | 'SERVED' | 'CANCELLED'
}
type Verdict = 'ACCEPT' | 'DUPLICATE' | 'STALE' | 'CONFLICT'
class QueueRevisionGate {
private watermark: number = 40
private seen: Map<number, string> = new Map()
public applied: number = 0
public duplicateIgnored: number = 0
public staleIgnored: number = 0
accept(event: QueueEvent): Verdict {
if (event.version < this.watermark) {
this.staleIgnored += 1
return 'STALE'
}
const oldId = this.seen.get(event.version)
if (oldId !== undefined) {
if (oldId !== event.eventId) return 'CONFLICT'
this.duplicateIgnored += 1
return 'DUPLICATE'
}
if (event.version === this.watermark) {
return 'CONFLICT'
}
this.watermark = event.version
this.seen.set(event.version, event.eventId)
this.applied += 1
return 'ACCEPT'
}
version(): number { return this.watermark }
}
这份代码不是最终完整的分布式去重实现。为了避免所有相同版本事件被错误地解释成业务冲突,服务端应该提供稳定eventId,并约定何时重试保留相同ID。当前代码在进程重启后会丢失seen,因此如果系统真正接入,我们还必须把会话ID、水位和终态持久化,并与服务端恢复快照核对。示例优先展示“乱序不能倒灌”的条件判断,不虚构它解决了全部可靠投递问题。
1. 水位持久化才真正面对进程重启
纯内存模型里的watermark45足够展示判定,但用户把应用切后台、系统回收进程后,内存数据会消失。再次打开页面时如果无条件从40开始,就可能让缓存旧消息42重新进入ACCEPT,让队伍人数出现倒退。真实接入应保存队列实例ID、事件epoch、业务水位及终态,再在恢复阶段向可信服务核实最新权威版本。持久化是缩短恢复路径的线索,不是代替服务端业务真相的账本。
还要考虑排队编号被重复使用。两家门店在同一天都可能出现Q-274,即便是同一家门店不同日期也可能重号。本文只用queueId作为教学示例主键,正式业务还需加入商户、租户、业务日期或服务端签发的不可复用实例ID。不能把Push Token、设备身份或手机号写入公开HiLog来凑去重条件,那既有安全风险,也会把本不该进入系统通知的数据暴露出去。
三、平台调用先过准入门,不把模板构造当成投递结果
只有通过了业务版本门禁,才能考虑更新实况窗。但“考虑更新”仍有多重前置条件:实况窗开关是否开启,当前设备和应用是否符合场景接入要求,是否已有可更新的实例,实例是否被用户清除,以及是否有可靠的推送交接记录。官方文档明确提醒本地创建与本地更新需要进程和应用场景条件,Push Kit交接也要求服务端保存实况窗ID、pushToken、业务场景与状态属性,不能在本地凭空造一个token。
下面第二段代码只演示一层能力门禁,保留平台正式调用点,不用一套看起来能运行却缺少完整LiveView结构的伪代码冒充已编译项目。集成时应参考官方取餐/排队模板示例构造包含event、layoutData、clickAction等字段的liveViewManager.LiveView,经能力开通、真机检验后再执行。本文不会把纯数字41027视为系统已经确认有效的实况窗ID。
import { liveViewManager } from '@kit.LiveViewKit'
type DeliveryGate = 'READY_TO_CALL' | 'DISABLED' | 'NOT_PREPARED'
class LiveViewPort {
private prepared: boolean = false
markPrepared(yes: boolean): void { this.prepared = yes }
async canCall(): Promise<DeliveryGate> {
if (!this.prepared) return 'NOT_PREPARED'
const enabled: boolean = await liveViewManager.isLiveViewEnabled()
return enabled ? 'READY_TO_CALL' : 'DISABLED'
}
// 接入真实服务后,再用官方LiveView完整结构调用:
// await liveViewManager.startLiveView(view)
// await liveViewManager.updateLiveView(view)
// await liveViewManager.stopLiveView(view)
}
这里prepared=false是一种刻意选择。它让本轮截图中的localStart=NOT_RUN、pushUpdate=NOT_RUN保持真实含义:模型没有走任何平台系统调用。这不是SDK故障,更不是用户禁用服务的实测结论。开发联调时canCall()自身也可能抛出业务错误,必须捕获并记录错误码及版本环境。若用户关闭了实况窗开关,产品可以继续展示应用内订单页,但不应冒充系统卡片仍在更新。
四、把双通道交接写成业务状态机
本地应用与Push Kit之间不能靠“两个模块都在写一份UI”协同。理想路径是服务端成为唯一真相源:客户端创建实况窗成功并拿到确认后,将ID与场景信息上报,服务端关联pushToken;之后每个业务事件持有服务端版本,服务端判断要走Push更新、终止还是不应再发送。客户端可以在前台做本地展示和按官方规则尝试本地更新,但它和Push都必须尊重同一业务版本与终态。
我们把状态分为LOCAL_READY、START_REQUESTED、SERVER_BOUND、HANDOFF_PENDING、PUSH_ACTIVE、END_REQUESTED、ENDED,另外保留USER_REMOVED和FAILED。这组名称属于应用状态模型,并非系统API官方状态名。当前演示只有HANDOFF_PENDING:五条模型事件已经经过序号筛选,业务输出准备好,但没有证据表明本地系统实例创建成功,更谈不上服务端绑定完成。为了保持这个边界,图片中的两个系统调用指标都写NOT_RUN。
不要把服务端版本当成系统展示版本。推送接口返回成功可能仅表示服务接收请求,不保证用户设备立刻显示;用户也可能手动删除实况窗。用户删除后继续用相同ID反复尝试更新,不一定符合平台行为,必须参考Live View Kit的清除与恢复限制。应用的正确状态应同时包含businessRevision和deliveryAcknowledgement,后者可能一直停留在待核实,而前者已经到了45。

1. 推送受理、设备到达与业务提交要有三本账
把一次版本45更新拆成三个时刻会更清楚:业务服务先将排队状态写入权威账本;推送服务随后受理投递请求;用户设备最后才可能接收并显示新内容。这三步相隔数秒甚至可能某一步没有回执。业务日志只能证明“version45 committed”,服务端返回只能证明请求被受理,设备真正观察到新内容必须靠真实系统回调或测试证据。若只记一个success=true,下一次用户投诉手机仍显示旧人数时根本无法判定出错链路。
双通道还要规定失败优先级。前台本地更新超时,不能立即推断后台推送也失败;推送受理成功,也不能倒推通知必然显示。任何展示回执与业务状态冲突时,都应先保留业务权威版本,把投影异常写入独立审计队列,而非倒退业务人数迁就系统卡片。对取餐这种与到店时机相关的体验,宁愿明确告诉用户“系统通知同步中”,也不能给出没有来源的“已更新成功”。
五、用一张主屏幕解释模型结果,而不是伪造通知中心
我们的QueueHomePage展示“取餐排队Q-274,前方12单”,并画出41、42、43、44、45的有效演示事件进程。下面的统计卡片写收到7条、应用5条、重复忽略1条、过期忽略1条,当前业务版本45。HANDOFF_PENDING提醒观众这只是一组业务序列验证。旁边明确展示本地创建实况窗、推送更新都为NOT_RUN,避免把内部业务屏误认成系统通知中心或锁屏实况窗截图。
这一层产品文案非常重要。对用户而言“更新成功”往往意味着系统界面已经被刷新,对开发者而言它可能只是“事件通过了前置门禁”。二者不能混用。如果正式应用拿不到系统确认,就应显示“等待同步”或保留应用内状态,而不是告诉用户“系统通知已更新”。同理,业务端前方人数12只是排队服务返回的数据,不代表系统UI展示可以承诺剩余等待时间18分钟一定准确。

六、七条消息怎样验证四种相反结论
为了复核门禁,下面给出不需要后端的固定输入序列。Q-274的41、42、43、44、45五条有效消息分别拥有稳定eventId,重复43再次到达但eventId不变,迟到42携带已经被后续版本覆盖的旧版本。模型的输出应依次是ACCEPT, ACCEPT, ACCEPT, DUPLICATE, ACCEPT, STALE, ACCEPT。这里没有模拟业务终态,HANDOFF_PENDING也不是SERVED,不能让系统消息误关窗。
第三段代码是一个可以直接放进ArkTS纯函数测试的最小夹具。通过明确的数组与断言,既能避免图片中的日志凭空捏造,也能给后续服务端接入留出回归入口。
const gate = new QueueRevisionGate()
const rows: QueueEvent[] = [
{ queueId:'Q-274', eventId:'e41', version:41, aheadCount:19, state:'WAITING' },
{ queueId:'Q-274', eventId:'e42', version:42, aheadCount:17, state:'WAITING' },
{ queueId:'Q-274', eventId:'e43', version:43, aheadCount:16, state:'WAITING' },
{ queueId:'Q-274', eventId:'e43', version:43, aheadCount:16, state:'WAITING' },
{ queueId:'Q-274', eventId:'e44', version:44, aheadCount:14, state:'WAITING' },
{ queueId:'Q-274', eventId:'e42', version:42, aheadCount:17, state:'WAITING' },
{ queueId:'Q-274', eventId:'e45', version:45, aheadCount:12, state:'WAITING' }
]
const actual: Verdict[] = rows.map((row: QueueEvent) => gate.accept(row))
const expected: Verdict[] = [
'ACCEPT', 'ACCEPT', 'ACCEPT', 'DUPLICATE',
'ACCEPT', 'STALE', 'ACCEPT'
]
const passed = JSON.stringify(actual) === JSON.stringify(expected) &&
gate.version() === 45 && gate.applied === 5 &&
gate.duplicateIgnored === 1 && gate.staleIgnored === 1
为什么这里不顺手接上真实updateLiveView?因为测试尚未具有完整业务准入、WantAgent配置和服务端存储凭据。纯函数测试通过,只能证明“如果收到这些输入,应用层会做这些判定”。这也是工程报告里必须写清的适用边界。正式环境还要加入消息签名或可信来源鉴别,拒绝其他queueId混入,处理重连后的水位恢复,并为终态冲突定义人工复核和日志留存规则。
1. 终态允许重试,但不能重复产生业务副作用
如果后续收到SERVED事件,例如版本46宣告取餐完成,不能把“发出了结束请求”直接视作最终终态。网络超时可能导致多次相同终止请求,服务端应以队列实例与终态版本为幂等键。客户端收到同版本终态重放时,不应再创建新实况窗,更不能重置计时器。系统结束接口失败时必须保留待核实状态及有界重试窗口;用户已经清除了实况窗时,则应尊重平台行为,不可无限重建同一个ID。
还可以把“水位应该前进”和“是否值得通知用户”分开。版本46可能只是更新内部审计属性,前方人数仍是12,这仍然是有效业务版本,却不一定值得额外调用一次updateLiveView。业务门禁负责接纳权威版本,展示策略负责判断可见内容是否变化;这样既减少无意义系统更新,又保留完整审计链。不能为了节流而丢掉权威版本事件,更不应把系统限流简单理解成业务允许倒退。
七、实况窗时间窗口、用户清除与终态比UI更重要
官方对实况窗生命周期和显示行为有明确限制。实况窗存在最长生命周期,更新长期停滞会影响状态栏、锁屏或通知中心的展示;用户还可以主动清除。系统行为与业务队列事件不是一回事。应用要保留业务状态真相,同时正确停止已不符合展示条件的实况窗。排队号已经被服务端取消,不能只因为本地还显示12单就一直让系统窗口驻留。
尤其注意stopLiveView和业务“已服务”不是同一个动作。理想路径是先由权威业务终态产生更高版本,经应用或服务器确认需要结束窗口,再按官方接口更新/停止并记录结果。如果进程已经退出,本地方式不能保证按时结束,必须依赖满足条件的推送通道。当前模型并未接入Push Kit,因此终态收敛只能写成待实现的工程设计,不能把stopLiveView在注释里出现当作已真正执行。
另一方面也不能把Push Kit的频率限制理解成应用可以无限补发。官方文档对不同场景给出更新频率、持续时间及网络图片等限制,正式系统要优先合并短时间内频繁抖动的业务人数,再发送对用户有意义的更新。若排队人数在数秒内来回变化,系统通知每次都刷可能造成干扰。应用可按业务稳定窗口合并消息,但不允许用合并结果倒退业务序号。平台限流和业务降噪应分开记录,否则排查时很难知道到底是被服务丢弃,还是主动没有发送。
诊断图如果出现updateLiveViewContent之类文字,应理解为演示界面自行定义的动作说明,不是华为SDK方法名;官方公开的更新入口以liveViewManager.updateLiveView为准。诊断页中逐条保留版本和判定:43重复、42过期,两个被拒的事件有不同原因;它们加起来等于2,但不能合并成一个模糊的“错误2次”。业务水位45与应用待交接状态可以同时为真。上报日志时还应保留LiveView请求是否真正调用、result返回值、Push服务受理ID及终态通知,任何缺失都必须保持NOT_VERIFIED而非随意补零。

1. 系统级展示的隐私与可访问性也要验收
实况窗进入状态栏、锁屏后,内容可能被旁观者看到。排队号、店名及剩余人数通常更容易设计为低敏业务提示,但顾客姓名、电话号码、订单备注等不应因为原始接口里存在就自动塞进系统卡片。应建立明确的字段白名单,并分别核对胶囊态、卡片态和小折叠外屏可显示的信息,避免把移动应用详情页的个人信息不加筛选复制过去。模板能承载某字段,不代表业务必须展示它。
字体放大、语音读屏和多语言同样会影响体验。一位数人数变成两位数时,高亮数字不应挤占主要内容;颜色不能成为判断“已叫号”的唯一依据。真实验收要覆盖从系统通知点击后回到正确排队详情的路径,并检查用户在后台、锁屏及不同设备中收到更新的差异。当前演示没有真正的WantAgent、Push Token和系统展示截图,因此上述项目只能列入待验收清单,不能写“通过”或虚构测试耗时。
八、可上线前必须补上的真实验证
本轮能作为证据的,是已定义的输入向量、模型输出规则和可读UI示意:收到7、接纳5、拒绝重复1、拒绝过期1、业务版本45、前方12单。它们说明门禁的业务意图,不能证明任何一部HarmonyOS设备已创建系统实况窗,也不能证明后台推送已经成功。集成阶段至少需要在已开通Live View Kit的开发者应用中检查用户开关、模板字段、WantAgent跳转、Push Token上报、服务端绑定、冷后台更新和窗口结束回执。
还有几个不容易提前发现的边界:用户主动删除后继续更新、应用重装后旧业务队列ID复用、服务端连续发出重复终态、用户时区变化导致显示时间解释差异,以及同一账号在多设备上同时持有不同实况窗ID。每一项都要求独立场景记录。单设备里的JavaScript或ArkTS断言即使全部通过,也不能替代真实消息在网络和系统通道中的传输验证。
对课程或技术文章而言,最有价值的结论不是“几行代码就做出了实况窗”,而是把交付边界摆正:本地业务版本水位负责阻止旧状态倒灌,平台实况窗负责展示、推送负责跨进程更新,业务终态负责真正结束服务。三者各自拥有可验证的凭据和失败状态,工程才有可能稳定收尾。如果把它们都塞进一个布尔值isLiveViewSuccess,任何随机迟到消息都足以让原本正常的排队体验失去可信度。
文档与能力边界:QUEUE场景、Live View Kit相关API以及本地/Push交接方向来自华为官方指南。QueueRevisionGate、HANDOFF_PENDING和全部任务数字为应用自定义演示模型;本轮未执行平台调用。
参考链接:
- https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-v5/liveview-create-locally-V5
- https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/liveview-update-by-push
- https://developer.huawei.com/consumer/en/doc/harmonyos-guides-V14/liveview-introduction-V14
- https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/liveview-faq-3-V5
更多推荐




所有评论(0)