鸿蒙 ArkTS 实况窗(Live View)实战:即时配送场景的全生命周期管理

文章目录
前言
实况窗(Live View)是 HarmonyOS 中极具辨识度的系统级交互能力。它将应用的关键任务状态——如外卖配送进度、打车行程、航班动态——直接外显到状态栏胶囊、通知中心和锁屏界面,让用户无需打开应用即可"一眼掌握进度"。这种"信息主动找人"的范式,彻底改变了传统通知的被动模式。
本文将结合你提供的即时配送 Demo,从实况窗的三大形态、完整生命周期、本地与远程双通道更新机制、后台保活策略到工程化架构,系统性地拆解实况窗的开发实践。
一、 实况窗的三大形态:胶囊、卡片与沉浸态
实况窗在系统中有三种展示形态,分别对应不同的用户交互场景。Demo 中的 ProgressSection 通过 DELIVERY_STAGES[stageIndex] 的 capsuleText 字段来驱动胶囊态的文案更新。
1.1 胶囊态(Capsule)
当应用退到后台后,实况窗以胶囊形态出现在状态栏。胶囊空间极其有限,只能展示最核心的信息——通常是一个图标 + 一行文字(如"配送中 2.5km"或"预计15分钟")。
在代码中,胶囊的配置通过 capsule 对象实现:
capsule: {
type: liveViewManager.CapsuleType.CAPSULE_TYPE_TEXT,
status: 1,
icon: 'capsule_store.png',
backgroundColor: '#FF308977',
title: '待支付',
content: '...'
}
胶囊态的设计原则是"一图一意"——一个图标只传达一个核心语义,避免信息过载。在 HarmonyOS 6.0 中,实况胶囊在挖孔设备上支持居中展示,左右文本信息量倍增。
1.2 卡片态(Card)
点击胶囊后展开为卡片,展示在通知中心或锁屏界面。卡片空间充裕,可以包含进度条、多行文本、操作按钮等丰富信息。
卡片的核心配置在 primary 对象中,包含标题、内容、布局类型和点击跳转:
primary: {
title: '餐品待支付',
content: [
{ text: '咖啡 ', textColor: '#FF000000' },
{ text: '等2件商品', textColor: '#FF000000' }
],
keepTime: 15,
clickAction: wantAgent, // 点击跳转配置
layoutData: {
layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PROGRESS,
progress: 30,
nodeIcons: ['icon_order.png', 'icon_finish.png']
}
}
Demo 中的 ProgressSection 用 Progress 组件模拟了卡片中的进度条效果,通过 DELIVERY_STAGES[stageIndex] 驱动阶段标题和详情的切换。
1.3 沉浸态(Immersive)
在锁屏状态下点击实况窗胶囊,可展开为沉浸态,展示更全面的任务信息(如音乐专辑封面、运动轨迹、导航详情)。沉浸态的交互深度最高,但开发复杂度也最高,Demo 中未涉及。
二、 完整生命周期:从创建到结束的四个阶段
Demo 中的 LifecycleSection 清晰地列出了实况窗的四个核心阶段。理解这个生命周期是正确使用实况窗的前提。
2.1 阶段一:创建(startLiveView)
创建实况窗是整个流程的起点。在调用 liveViewManager.startLiveView() 之前,必须先检查实况窗能力是否开启:
// LiveViewController 中的能力检测
public static async checkEnabled(): Promise<boolean> {
const enabled = await liveViewManager.isLiveViewEnabled()
AppStorage.setOrCreate('liveViewEnabled', enabled)
return enabled
}
isLiveViewEnabled() 返回 false 通常意味着:用户未登录华为账号、实况窗开关被关闭,或者应用未申请实况窗权益。
创建实况窗时,需要构造一个完整的 LiveView 对象,包含场景类型(event)、主视图数据(primary)和胶囊数据(capsule):
const liveView: liveViewManager.LiveView = {
id: 0, // 实况窗ID,开发者生成
event: 'DELIVERY', // 场景类型:即时配送
liveViewData: {
primary: { /* 卡片数据 */ },
capsule: { /* 胶囊数据 */ }
}
}
await liveViewManager.startLiveView(liveView)
Demo 中的 DeliverySession.start() 封装了这一流程,调用后自动启动计时器驱动阶段流转。
2.2 阶段二:更新(updateLiveView)
创建后,通过 updateLiveView() 推进节点。每次更新都需要传入完整的 LiveView 对象(而非增量),系统会用新数据替换旧数据:
// 更新进度到下一阶段
liveViewData.primary.title = '骑手已取餐'
liveViewData.primary.layoutData.progress = 50
liveViewData.capsule.title = '配送中'
await liveViewManager.updateLiveView(liveView)
Demo 中的 DeliverySession 通过计时器自动推进 stageIndex,每次推进时调用 updateLiveView() 更新实况窗内容。sequence++ 确保每次更新都有唯一的序列号,系统据此判断更新顺序。
2.3 阶段三:后台处理(onBackground)
当应用退到后台,本地更新能力受限。此时有两种策略:
- 本地更新:仅在应用进程存活时有效。如果进程被系统冻结或回收,更新将中断。
- Push Kit 远程更新:通过系统级推送通道更新实况窗,不依赖应用进程存活。这是生产环境的推荐方案。
Demo 中的 deliveryPaused 状态标记了应用切入后台的场景,并提示"真机可用 Push Kit 继续更新实况窗"。
2.4 阶段四:结束(stopLiveView)
结束实况窗时,需要先进行最后一次更新(如将进度设为100%、标题改为"已送达"),然后调用 stopLiveView():
// 结束前的最终更新
liveViewData.primary.title = '商品已送达'
liveViewData.primary.layoutData.progress = 100
liveViewData.capsule.title = '已送达'
// 结束实况窗
await liveViewManager.stopLiveView(liveView)
结束后,系统会保留实况窗约15秒的"留存期",让用户有机会看到最终状态,然后自动清理。
三、 双通道更新机制:本地 vs Push Kit
实况窗的更新有两条通道,各有适用场景。理解它们的差异是架构设计的关键。
3.1 本地更新通道
通过 liveViewManager.updateLiveView() 直接在端侧更新。优势是延迟极低、无需网络;劣势是依赖应用进程存活。
适用场景:
- 应用在前台运行时的高频更新(如计时器每秒刷新)
- 用户主动操作触发的状态变更(如点击"确认取餐")
3.2 Push Kit 远程更新通道
通过服务端向 Push Kit 发送更新消息,系统代为更新实况窗。优势是不依赖应用进程,即使应用被杀也能更新;劣势是有一定延迟,且受频率限制。
远程更新的完整流程:
- 端侧创建实况窗后,将
liveViewId、pushToken、场景event等信息上报到业务服务端 - 业务状态变更时,服务端通过 Push Kit API 推送更新消息
- 系统接收推送后,自动更新实况窗内容
应用端 → 创建实况窗 → 上报 liveViewId + pushToken → 业务服务端
业务服务端 → 状态变更 → Push Kit API → 系统 → 更新实况窗
3.3 频率限制
Push Kit 更新有严格的频率限制:
- 出行打车与赛事比分:每设备每5分钟最多30次,每小时最多180次
- 其余场景:每设备每5分钟最多10次,每小时最多60次
- 系统级流控:所有应用合计,创建每秒最多15次,更新每秒最多30次
- 应用级流控:单个应用,创建每秒最多10次,更新每秒最多20次
超过频率限制的部分将被丢弃不下发。因此,本地更新适合高频场景(如计时器),Push Kit 适合低频但关键的节点更新(如"骑手已取餐"、“已送达”)。
四、 后台保活与时效性约束
4.1 后台进程管控
在 HarmonyOS 7.0(API 26)中,系统对后台进程的管控极为严格。退到后台的应用可能被冻结甚至直接回收,导致实况窗数据停滞。
Demo 中的 deliveryPaused 状态正是对这一场景的模拟。在生产环境中,推荐的解决方案是:
- 首选 Push Kit:将更新逻辑从端侧迁移到服务端,通过推送通道更新实况窗
- 备选后台保活:申请原子化服务后台权限,但审核门槛较高
4.2 时效性自动调整
系统会根据实况窗的更新频率自动调整展示策略:
- 超过2小时未更新:状态栏胶囊和锁屏胶囊被隐藏,仅保留通知中心展示
- 超过4小时未更新:系统判定实况窗已结束,从所有展示入口清除
- 生命周期上限:单个实况窗最长不超过8小时
这意味着开发者必须确保更新频率足够高,否则用户将看不到实况窗。Demo 中的计时器默认每15秒更新一次,这个频率在大多数场景下是合理的。
五、 工程化架构:Demo 的分层设计
Demo 的代码结构体现了清晰的职责分离,分为四个层次:
5.1 UI 层(LiveViewDemo)
负责页面渲染和用户交互。通过 @StorageLink 与 AppStorage 双向绑定,实现数据驱动的 UI 刷新。所有状态变量(liveViewEnabled、liveViewActive、progress 等)都通过 AppStorage 管理,确保跨组件状态一致性。
5.2 会话层(DeliverySession)
封装业务逻辑。管理配送阶段的流转、计时器的启停、进度的计算。DeliverySession.start() 是业务入口,它协调实况窗的创建和阶段推进。
5.3 控制层(LiveViewController)
封装 Live View Kit 的 API 调用。将 startLiveView、updateLiveView、stopLiveView、isLiveViewEnabled 等系统 API 封装为业务友好的接口,屏蔽底层细节。
5.4 数据层(LiveViewTypes)
定义常量和类型。DELIVERY_STAGES 数组定义了配送的各个阶段(标题、详情、胶囊文案),是驱动整个流程的数据源。
六、 支持场景与权益申请
实况窗并非所有应用都能使用,它受权益管控,需要在 AppGallery Connect 中申请。
6.1 支持的场景类型
| 场景类型 | EVENT 取值 | 适用范围 |
|---|---|---|
| 出行打车 | TAXI | 网约车、出租车、拼车 |
| 即时配送 | DELIVERY | 外卖、生鲜、同城配送 |
| 航班 | FLIGHT | 航班动态、登机提醒 |
| 高铁/火车 | TRAIN | 车次信息、检票提醒 |
| 排队 | QUEUE | 医院、银行、餐饮排队 |
| 取餐 | PICK_UP | 线下取餐、取件 |
| 赛事比分 | SCORE | 游戏、体育赛事 |
| 共享租赁 | RENT | 共享单车、充电宝 |
| 计时 | TIMER | 番茄时钟、倒计时(仅端侧) |
| 运动锻炼 | WORKOUT | 跑步、骑行记录 |
| 导航 | NAVIGATION | 步行、骑行、车辆导航 |
6.2 场景准入原则
- 活动有明确的开始和结束时间,总时长不超过8小时
- 用户对该活动的实况窗通知有明确预期(通常是用户主动触发)
- 展示内容对用户有足够价值,不可用于营销、广告场景
- 内容需要动态更新,确保时效性
6.3 开发准备流程
- 在 AppGallery Connect 中申请推送服务权益
- 在"开放能力管理"中申请实况窗服务权益
- 设置数据处理位置为中国(实况窗仅支持中国境内)
- 通过白名单设备联调测试(上限100台,24小时内生效)
- 提交正式审核,审批通过后即可全量使用
七、 常见踩坑与排查指南
7.1 isLiveViewEnabled 返回 false
- 确认设备已登录华为账号
- 确认"设置 > 通知和状态栏 > 实况窗"中开关已开启
- 确认应用已在 AppGallery Connect 中申请实况窗权益
- 确认设备在调试白名单中(联调阶段)
7.2 实况窗创建成功但不显示
- 确认
event取值与申请的场景类型一致 - 确认胶囊和卡片的数据符合设计规范(颜色、文案、图标等)
- 确认应用未被用户在系统设置中关闭实况窗
7.3 后台更新中断
- 本地更新依赖进程存活,进程被杀后更新将中断
- 生产环境应使用 Push Kit 远程更新作为兜底方案
- 确保更新频率不低于每2小时一次,否则胶囊会被隐藏
7.4 Push Kit 更新返回错误码
- 错误码 155/265:用户已手动清除实况窗或关闭实况窗开关,应停止对该
liveViewId的后续推送 - 频率超限:检查是否超过每5分钟/每小时的更新次数限制
7.5 实况窗超时被系统清除
- 超过4小时未更新,系统自动清除
- 超过8小时生命周期上限,系统强制结束
- 解决方案:合理设计更新频率,确保关键节点及时推送
完整代码
import { DELIVERY_STAGES, LiveViewLogItem } from '../liveview/LiveViewTypes'
import { DeliverySession } from '../liveview/DeliverySession'
import { LiveViewController } from '../liveview/LiveViewController'
struct LiveViewDemo {
('liveViewEnabled') liveViewEnabled: boolean = false
('liveViewActive') liveViewActive: boolean = false
('liveViewStageIndex') stageIndex: number = 0
('liveViewProgress') progress: number = 0
('liveViewStatusText') statusText: string = ''
('liveViewLogs') logs: LiveViewLogItem[] = []
('deliveryRunning') deliveryRunning: boolean = false
('deliveryPaused') deliveryPaused: boolean = false
('abilityState') abilityState: string = ''
('lastTransition') lastTransition: string = ''
aboutToAppear(): void {
LiveViewController.getInstance().checkEnabled()
}
build() {
Scroll() {
Column({ space: 16 }) {
this.HeaderSection()
this.StatusSection()
this.ProgressSection()
this.ControlSection()
this.LifecycleSection()
this.ArchitectureSection()
this.LogSection()
}
.width('100%')
.padding({ top: 36, left: 16, right: 16, bottom: 24 })
}
.width('100%')
.height('100%')
.backgroundColor('#0C0A09')
.scrollBar(BarState.Off)
}
HeaderSection() {
Column({ space: 6 }) {
Text('实况窗 Live View')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#FAFAF9')
Text('具有生命周期的后台交互 · 即时配送场景')
.fontSize(14)
.fontColor('#A8A29E')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
}
StatusSection() {
Column({ space: 8 }) {
Row() {
Text('系统能力')
.fontSize(13)
.fontColor('#E7E5E4')
Blank()
Text(this.liveViewEnabled ? '已开启' : '未开启/无权益')
.fontSize(12)
.fontColor(this.liveViewEnabled ? '#34D399' : '#FBBF24')
}
.width('100%')
Row() {
Text('实况窗状态')
.fontSize(13)
.fontColor('#E7E5E4')
Blank()
Text(this.liveViewActive ? '运行中' : '未创建')
.fontSize(12)
.fontColor(this.liveViewActive ? '#60A5FA' : '#78716C')
}
.width('100%')
Text(this.statusText)
.fontSize(12)
.fontColor('#A8A29E')
.width('100%')
Text(`Ability: ${this.abilityState} · ${this.lastTransition}`)
.fontSize(11)
.fontColor('#78716C')
.width('100%')
}
.width('100%')
.padding(14)
.backgroundColor('rgba(99,102,241,0.12)')
.borderRadius(14)
}
ProgressSection() {
Column({ space: 10 }) {
Text('配送进度')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FAFAF9')
.width('100%')
if (this.deliveryRunning) {
Text(DELIVERY_STAGES[this.stageIndex]?.title ?? '准备中')
.fontSize(15)
.fontColor('#C4B5FD')
.fontWeight(FontWeight.Medium)
Text(DELIVERY_STAGES[this.stageIndex]?.detail ?? '')
.fontSize(12)
.fontColor('#A8A29E')
} else {
Text('点击「开始配送」创建实况窗并自动推进节点')
.fontSize(12)
.fontColor('#78716C')
}
Progress({ value: this.progress, total: 100, type: ProgressType.Linear })
.width('100%')
.color('#8B5CF6')
.backgroundColor('rgba(255,255,255,0.1)')
Text(`${this.progress}% · 胶囊: ${DELIVERY_STAGES[this.stageIndex]?.capsuleText ?? '-'}`)
.fontSize(12)
.fontColor('#A8A29E')
}
.width('100%')
.padding(14)
.backgroundColor('rgba(255,255,255,0.05)')
.borderRadius(14)
.alignItems(HorizontalAlign.Start)
}
ControlSection() {
Column({ space: 10 }) {
Button(this.deliveryRunning ? '配送进行中…' : '开始配送实况窗')
.width('100%')
.height(46)
.fontSize(14)
.backgroundColor('#7C3AED')
.enabled(!this.deliveryRunning)
.onClick(() => DeliverySession.start())
Row({ space: 8 }) {
Button('手动结束')
.layoutWeight(1)
.height(42)
.fontSize(12)
.backgroundColor('#DC2626')
.enabled(this.liveViewActive)
.onClick(() => DeliverySession.stop())
Button('检测能力')
.layoutWeight(1)
.height(42)
.fontSize(12)
.backgroundColor('#475569')
.onClick(() => LiveViewController.getInstance().checkEnabled())
}
.width('100%')
if (this.deliveryPaused) {
Text('已切入后台 · 计时暂停 · 真机可用 Push Kit 继续更新实况窗')
.fontSize(11)
.fontColor('#FBBF24')
.width('100%')
}
}
.width('100%')
}
LifecycleSection() {
Column({ space: 8 }) {
Text('生命周期')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FAFAF9')
.width('100%')
this.StepRow('①', 'startLiveView → 创建胶囊 + 进度卡片')
this.StepRow('②', 'updateLiveView → sequence++ 推进节点')
this.StepRow('③', 'onBackground → 本地更新受限,Push 远程更新')
this.StepRow('④', 'stopLiveView → 结束任务,15s 留存期')
}
.width('100%')
.padding(14)
.backgroundColor('rgba(139,92,246,0.1)')
.borderRadius(14)
.alignItems(HorizontalAlign.Start)
}
ArchitectureSection() {
Column({ space: 8 }) {
Text('后台交互架构')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FAFAF9')
.width('100%')
this.StepRow('UI', 'Index 页展示进度 · AppStorage 双向绑定')
this.StepRow('会话', 'DeliverySession 计时器驱动阶段流转')
this.StepRow('控制', 'LiveViewController 封装 Kit API')
this.StepRow('系统', '状态栏胶囊 + 通知中心卡片 + 锁屏')
this.StepRow('远程', 'Push Kit 推送 payload 更新(应用被杀仍可用)')
}
.width('100%')
.padding(14)
.backgroundColor('rgba(255,255,255,0.05)')
.borderRadius(14)
.alignItems(HorizontalAlign.Start)
}
LogSection() {
Column({ space: 8 }) {
Row() {
Text('事件日志')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FAFAF9')
Blank()
Text(`${this.logs.length} 条`)
.fontSize(12)
.fontColor('#A78BFA')
}
.width('100%')
if (this.logs.length === 0) {
Text('等待操作…')
.fontSize(12)
.fontColor('#78716C')
} else {
ForEach(this.logs, (item: LiveViewLogItem) => {
Column({ space: 2 }) {
Text(`[${item.time}] ${item.phase}`)
.fontSize(11)
.fontColor('#F472B6')
Text(item.message)
.fontSize(11)
.fontColor('#D6D3D1')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
}, (item: LiveViewLogItem) => item.id)
}
}
.width('100%')
.padding(14)
.backgroundColor('rgba(255,255,255,0.05)')
.borderRadius(14)
.alignItems(HorizontalAlign.Start)
}
StepRow(index: string, text: string) {
Row({ space: 10 }) {
Text(index)
.fontSize(12)
.fontColor('#A78BFA')
.width(24)
Text(text)
.fontSize(12)
.fontColor('#D6D3D1')
.layoutWeight(1)
}
.width('100%')
}
}

总结
实况窗是鸿蒙生态中最具"感知度"的系统级能力之一。通过本文的实战分析,我们可以总结出以下工程化要点:
- 三种形态各司其职:胶囊态传递核心信息,卡片态展示详细进度,沉浸态提供深度交互。开发时应根据场景特点合理设计每种形态的内容。
- 生命周期四步走:创建 → 更新 → 后台处理 → 结束,每一步都有对应的 API 和注意事项,尤其是结束前必须做一次最终更新。
- 双通道更新策略:前台高频更新用本地通道(延迟低),后台/关键节点更新用 Push Kit(不依赖进程)。两者配合使用,才能实现全生命周期的稳定更新。
- 时效性是生命线:超过2小时不更新胶囊隐藏,超过4小时实况窗被清除。必须确保更新频率满足系统要求。
- 权益申请是前置条件:实况窗受权益管控,必须在 AppGallery Connect 中完成申请和白名单配置后才能使用。
掌握这些核心概念和工程实践,你就能在鸿蒙应用中构建出流畅、可靠的实况窗体验,让用户在任何界面、任何时刻都能"一眼掌握进度"。
更多推荐



所有评论(0)