HarmonyOS 新手入门:ArkData 分布式数据对象,像改变量一样同步页面状态
HarmonyOS 新手入门:ArkData 分布式数据对象,像改变量一样同步页面状态
前面我们写过 DeviceKVStore,也写过应用接续。分布式数据对象看起来也在做“跨设备同步”,但它的定位不一样。
它不是持久化数据库,更像把一个 JS 对象封装成可同步的共享状态。你在一个设备上改对象属性,同一个 sessionId 下的另一台设备可以收到变化。
官方文档可以先放在手边:
先说结论
KVStore 更像一个键值仓库,适合保存草稿、设置、轻量业务状态。
分布式数据对象更像“正在协同的页面状态”。官方文档里强调,它目前主要用于跨端迁移和跨设备 Call 多端协同场景。
适合这些:
- 多端协同时同步标题、光标、进度、开关状态。
- 跨端迁移时把当前页面状态交给接收端恢复。
- 小体量对象的字段变化监听。
不适合这些:
- 当成本地数据库长期保存大量数据。
- 同步大列表或大文件内容。
- 在没有迁移或跨设备 Call 场景时强行做复杂协同。
一句话:要长期保存,用 KVStore / RDB;要同步当前协同状态,再看分布式数据对象。
它怎么用
最小链路是这样:
create(context, source) -> genSessionId() -> setSessionId(sessionId) -> on('change') / on('status')
如果是跨端迁移,还会用到:
save(deviceId) -> 接收端用同一个 sessionId 恢复 -> revokeSave()
这里最重要的是 sessionId。不同设备上的分布式数据对象,只有加入同一个 sessionId,才会进入同一份共享对象数据。
这个 Demo 做什么
页面会做几件事:
- 用当前标题、内容、进度创建一个分布式数据对象。
- 生成
sessionId。 - 调用
setSessionId(sessionId)加入同步关系。 - 注册
change和status监听。 - 填写远端设备 ID 后调用
save(deviceId)。 - 用半模态展示对象快照、sessionId、监听反馈和保存状态。
单机可以看清楚对象创建和 session 流程。真正同步需要跨端迁移或跨设备 Call 场景,以及可信组网环境。
容易踩坑的地方
第一,它不是 Preferences,也不是 RDB。数据对象本身不是为了长期持久化设计的。
第二,同步最小单位是属性。对象里某个根属性变化时,同步的是这个属性。
第三,复杂类型不要想得太自由。官方文档说明,复杂类型目前主要支持根属性修改;如果是资产类型,还有单独的资产同步机制。
欢迎评论区一起分享~
如果你做多端协同,你会先同步哪些页面状态?标题、进度、选中项、临时筛选条件,还是正在编辑的表单字段?这些状态适合做成一个分布式数据对象,还是更适合落到 KVStore / RDB?
完整示例代码
文件位置:ArkDataDistributedObjectDemo.ets
import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit';
import { distributedDataObject } from '@kit.ArkData';
import { promptAction, router } from '@kit.ArkUI';
const DISTRIBUTED_PERMISSION: Permissions = 'ohos.permission.DISTRIBUTED_DATASYNC';
class DistributedDraftSource {
title: string = '';
content: string = '';
progress: number = 0;
updatedAt: string = '';
constructor(title: string, content: string, progress: number, updatedAt: string) {
this.title = title;
this.content = content;
this.progress = progress;
this.updatedAt = updatedAt;
}
}
class DataObjectInfoItem {
label: string = '';
value: string = '';
constructor(label: string, value: string) {
this.label = label;
this.value = value;
}
}
@Entry
@Component
struct ArkDataDistributedObjectDemo {
@State title: string = '跨端草稿';
@State content: string = '分布式数据对象适合同步轻量页面状态。';
@State progress: number = 40;
@State sessionId: string = '';
@State remoteDeviceId: string = '';
@State tips: string = '创建分布式数据对象后,多个设备使用同一个 sessionId 才能建立同步关系。';
@State changeTips: string = '尚未收到对象字段变化通知。';
@State statusTips: string = '尚未收到对象上下线通知。';
@State infoItems: DataObjectInfoItem[] = [];
@State showResultSheet: boolean = false;
private dataObject: distributedDataObject.DataObject | undefined = undefined;
private changeObserver: ((sessionId: string, fields: string[]) => void) | undefined = undefined;
private statusObserver: ((sessionId: string, networkId: string, status: 'online' | 'offline') => void) | undefined =
undefined;
aboutToAppear(): void {
this.updateInfoItems('初始化页面');
}
aboutToDisappear(): void {
const dataObject = this.dataObject;
if (dataObject !== undefined) {
const changeObserver = this.changeObserver;
const statusObserver = this.statusObserver;
if (changeObserver !== undefined) {
dataObject.off('change', changeObserver);
}
if (statusObserver !== undefined) {
dataObject.off('status', statusObserver);
}
}
}
private buildSource(): DistributedDraftSource {
return new DistributedDraftSource(
this.title.length > 0 ? this.title : '未命名草稿',
this.content,
this.progress,
new Date().toString()
);
}
private updateInfoItems(action: string): void {
this.infoItems = [
new DataObjectInfoItem('最近动作', action),
new DataObjectInfoItem('对象标题', this.title.length > 0 ? this.title : '未填写'),
new DataObjectInfoItem('对象内容', this.content.length > 0 ? this.content : '未填写'),
new DataObjectInfoItem('同步进度', `${this.progress}%`),
new DataObjectInfoItem('sessionId', this.sessionId.length > 0 ? this.sessionId : '未加入 session'),
new DataObjectInfoItem('远端设备 ID', this.remoteDeviceId.length > 0 ? this.remoteDeviceId : '未填写'),
new DataObjectInfoItem('数据变化监听', this.changeTips),
new DataObjectInfoItem('状态监听', this.statusTips),
new DataObjectInfoItem('适用场景', '跨端迁移、跨设备 Call 多端协同'),
new DataObjectInfoItem('限制提醒', '对象同步不是持久化数据库,大小和设备数量都有约束')
];
}
private async requestDistributedPermission(): Promise<boolean> {
try {
const context = getContext(this) as common.UIAbilityContext;
const atManager = abilityAccessCtrl.createAtManager();
const result = await atManager.requestPermissionsFromUser(context, [DISTRIBUTED_PERMISSION]);
const granted = result.authResults.length > 0 &&
result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
if (!granted) {
this.tips = '未获得分布式数据同步权限,页面本地预览仍可继续。';
this.updateInfoItems('权限未授权');
}
return granted;
} catch (err) {
this.tips = '申请分布式数据同步权限失败,页面本地预览仍可继续。';
this.updateInfoItems('权限申请失败');
console.error('Distributed object permission request failed');
return false;
}
}
private async createObject(): Promise<boolean> {
try {
const granted = await this.requestDistributedPermission();
if (!granted) {
promptAction.showToast({ message: '未获得分布式权限' });
return false;
}
const oldObject = this.dataObject;
if (oldObject !== undefined) {
const oldChangeObserver = this.changeObserver;
const oldStatusObserver = this.statusObserver;
if (oldChangeObserver !== undefined) {
oldObject.off('change', oldChangeObserver);
}
if (oldStatusObserver !== undefined) {
oldObject.off('status', oldStatusObserver);
}
this.changeObserver = undefined;
this.statusObserver = undefined;
}
const context = getContext(this) as common.UIAbilityContext;
this.dataObject = distributedDataObject.create(context, this.buildSource());
this.registerObservers();
this.tips = '分布式数据对象已创建,下一步生成 sessionId 并加入 session。';
this.updateInfoItems('创建 DataObject');
return true;
} catch (err) {
this.tips = '创建分布式数据对象失败,请查看日志。';
promptAction.showToast({ message: '创建失败' });
console.error('Distributed data object create failed');
return false;
}
}
private registerObservers(): void {
const dataObject = this.dataObject;
if (dataObject === undefined || this.changeObserver !== undefined || this.statusObserver !== undefined) {
return;
}
const changeObserver = (sessionId: string, fields: string[]) => {
this.changeTips = `session=${sessionId},变化字段数:${fields.length}`;
this.updateInfoItems('收到对象变化');
};
const statusObserver = (sessionId: string, networkId: string, status: 'online' | 'offline') => {
this.statusTips = `session=${sessionId},networkId=${networkId},status=${status}`;
this.updateInfoItems('收到对象状态变化');
};
dataObject.on('change', changeObserver);
dataObject.on('status', statusObserver);
this.changeObserver = changeObserver;
this.statusObserver = statusObserver;
}
private createSession(): void {
try {
this.sessionId = distributedDataObject.genSessionId();
this.tips = 'sessionId 已生成。另一台设备使用同一个 sessionId 才会进入同一份共享对象数据。';
this.updateInfoItems('生成 sessionId');
promptAction.showToast({ message: 'sessionId 已生成' });
} catch (err) {
this.tips = '生成 sessionId 失败。';
promptAction.showToast({ message: '生成失败' });
console.error('Generate sessionId failed');
}
}
private async joinSession(): Promise<void> {
if (this.sessionId.length === 0) {
this.createSession();
}
const dataObject = this.dataObject;
if (dataObject === undefined) {
const created = await this.createObject();
if (!created || this.dataObject === undefined) {
return;
}
}
try {
const currentObject = this.dataObject;
if (currentObject === undefined) {
return;
}
await currentObject.setSessionId(this.sessionId);
this.tips = '已加入 session。可信组网内同 sessionId 的对象可以自动同步属性变化。';
this.updateInfoItems('加入 session');
promptAction.showToast({ message: '已加入 session' });
} catch (err) {
this.tips = '加入 session 失败,请确认权限和分布式环境。';
promptAction.showToast({ message: '加入失败' });
console.error('Join distributed object session failed');
}
}
private async leaveSession(): Promise<void> {
const dataObject = this.dataObject;
if (dataObject === undefined) {
return;
}
try {
await dataObject.setSessionId();
this.tips = '已退出当前 session,对象退回本地数据对象状态。';
this.updateInfoItems('退出 session');
promptAction.showToast({ message: '已退出 session' });
} catch (err) {
this.tips = '退出 session 失败。';
promptAction.showToast({ message: '退出失败' });
console.error('Leave distributed object session failed');
}
}
private async refreshLocalSnapshot(): Promise<void> {
const created = await this.createObject();
if (!created) {
return;
}
const dataObject = this.dataObject;
if (dataObject !== undefined && this.sessionId.length > 0) {
await dataObject.setSessionId(this.sessionId);
}
this.tips = '已用当前页面输入重新创建对象快照。真机协同时,字段变化会通过同 sessionId 的对象同步。';
this.updateInfoItems('更新对象快照');
promptAction.showToast({ message: '对象快照已更新' });
}
private async saveToRemote(): Promise<void> {
const dataObject = this.dataObject;
const deviceId = this.remoteDeviceId.trim();
if (deviceId.length === 0) {
this.tips = '请先填写远端设备 ID。';
this.updateInfoItems('保存参数缺失');
promptAction.showToast({ message: '请填写设备 ID' });
return;
}
if (dataObject === undefined) {
const created = await this.createObject();
if (!created || this.dataObject === undefined) {
return;
}
}
try {
const currentObject = this.dataObject;
if (currentObject === undefined) {
return;
}
await currentObject.save(deviceId);
this.tips = '已调用 save 保存对象。跨端迁移场景下,接收端可按 sessionId 恢复数据。';
this.updateInfoItems('save 到远端设备');
promptAction.showToast({ message: '已调用 save' });
} catch (err) {
this.tips = 'save 调用失败,请确认设备能力和迁移场景。';
this.updateInfoItems('save 失败');
promptAction.showToast({ message: 'save 失败' });
console.error('Distributed data object save failed');
}
}
private async revokeRemoteSave(): Promise<void> {
const dataObject = this.dataObject;
if (dataObject === undefined) {
return;
}
try {
await dataObject.revokeSave();
this.tips = '已撤回已保存的分布式数据对象。';
this.updateInfoItems('revokeSave');
promptAction.showToast({ message: '已撤回保存' });
} catch (err) {
this.tips = '撤回保存失败,请查看日志。';
this.updateInfoItems('revokeSave 失败');
promptAction.showToast({ message: '撤回失败' });
console.error('Distributed data object revokeSave failed');
}
}
private showObjectInfo(): void {
this.updateInfoItems('查看对象信息');
this.showResultSheet = true;
}
@Builder
private objectInfoSheet() {
Column({ space: 14 }) {
Text('分布式数据对象信息')
.width('100%')
.fontSize(22)
.fontWeight(700)
.fontColor('#0F172A')
Text('下面展示对象快照、sessionId、监听反馈和保存状态。列表只展示当前 Demo 信息,不做 LazyForEach 懒加载渲染处理。')
.width('100%')
.fontSize(13)
.fontColor('#64748B')
Column({ space: 8 }) {
ForEach(this.infoItems, (item: DataObjectInfoItem) => {
Column({ space: 4 }) {
Text(item.label)
.fontSize(12)
.fontColor('#64748B')
Text(item.value)
.fontSize(16)
.fontColor('#0F172A')
}
.width('100%')
.padding(12)
.backgroundColor('#F8FAFC')
.borderRadius(8)
}, (item: DataObjectInfoItem) => item.label)
}
.width('100%')
Button('关闭')
.width('100%')
.height(44)
.fontSize(16)
.backgroundColor('#7C3AED')
.onClick(() => {
this.showResultSheet = false;
})
}
.width('100%')
.padding({ left: 20, right: 20, top: 12, bottom: 20 })
}
build() {
Scroll() {
Column({ space: 18 }) {
Row() {
Text('返回')
.fontSize(14)
.fontColor('#7C3AED')
.onClick(() => {
router.back();
})
Blank()
}
.width('100%')
Text('分布式数据对象')
.width('100%')
.fontSize(28)
.fontWeight(700)
.fontColor('#182431')
Text('把页面状态封装成对象,用同一个 sessionId 在可信设备之间同步轻量数据。')
.width('100%')
.fontSize(14)
.fontColor('#56616F')
TextInput({ placeholder: '输入标题', text: this.title })
.height(44)
.fontSize(16)
.backgroundColor('#F5F3FF')
.fontColor('#0F172A')
.onChange((value: string) => {
this.title = value;
})
TextArea({ placeholder: '输入内容', text: this.content })
.height(110)
.fontSize(16)
.backgroundColor('#F5F3FF')
.fontColor('#0F172A')
.onChange((value: string) => {
this.content = value;
})
Row() {
Text('同步进度')
.fontSize(16)
.fontColor('#0F172A')
.layoutWeight(1)
Text(`${this.progress}%`)
.fontSize(16)
.fontWeight(700)
.fontColor('#7C3AED')
}
.width('100%')
Slider({ value: this.progress, min: 0, max: 100, step: 5 })
.onChange((value: number) => {
this.progress = value;
})
TextInput({ placeholder: '真机组网后填写远端 deviceId', text: this.remoteDeviceId })
.height(44)
.fontSize(16)
.backgroundColor('#EDE9FE')
.fontColor('#0F172A')
.onChange((value: string) => {
this.remoteDeviceId = value;
this.updateInfoItems('更新设备 ID');
})
Row({ space: 10 }) {
Button('生成 session')
.layoutWeight(1)
.height(44)
.fontSize(15)
.backgroundColor('#7C3AED')
.onClick(() => {
this.createSession();
})
Button('加入')
.layoutWeight(1)
.height(44)
.fontSize(15)
.fontColor('#FFFFFF')
.backgroundColor('#0F766E')
.onClick(() => {
this.joinSession();
})
Button('退出')
.layoutWeight(1)
.height(44)
.fontSize(15)
.fontColor('#0F172A')
.backgroundColor('#DDD6FE')
.onClick(() => {
this.leaveSession();
})
}
.width('100%')
Row({ space: 10 }) {
Button('更新对象')
.layoutWeight(1)
.height(44)
.fontSize(15)
.fontColor('#0F172A')
.backgroundColor('#EDE9FE')
.onClick(() => {
this.refreshLocalSnapshot();
})
Button('save')
.layoutWeight(1)
.height(44)
.fontSize(15)
.fontColor('#FFFFFF')
.backgroundColor('#6D28D9')
.onClick(() => {
this.saveToRemote();
})
Button('撤回')
.layoutWeight(1)
.height(44)
.fontSize(15)
.fontColor('#0F172A')
.backgroundColor('#CBD5E1')
.onClick(() => {
this.revokeRemoteSave();
})
}
.width('100%')
Button('查看对象信息')
.width('100%')
.height(44)
.fontSize(15)
.fontColor('#0F172A')
.backgroundColor('#F3E8FF')
.onClick(() => {
this.showObjectInfo();
})
Column({ space: 6 }) {
Text(this.title.length > 0 ? this.title : '未命名对象')
.width('100%')
.fontSize(18)
.fontWeight(700)
.fontColor('#0F172A')
Text(this.content.length > 0 ? this.content : '这里预览对象内容')
.width('100%')
.fontSize(15)
.fontColor('#334155')
Text(this.sessionId.length > 0 ? `sessionId: ${this.sessionId}` : '尚未生成 sessionId')
.width('100%')
.fontSize(12)
.fontColor('#64748B')
}
.width('100%')
.padding(16)
.backgroundColor('#F8FAFC')
.borderRadius(8)
Text(this.tips)
.width('100%')
.fontSize(13)
.fontColor('#64748B')
Text(this.changeTips)
.width('100%')
.fontSize(13)
.fontColor('#7C3AED')
Text(this.statusTips)
.width('100%')
.fontSize(13)
.fontColor('#0F766E')
}
.width('100%')
.padding(24)
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
.bindSheet(this.showResultSheet, this.objectInfoSheet(), {
height: SheetSize.MEDIUM,
dragBar: true,
showClose: true,
onDisappear: () => {
this.showResultSheet = false;
}
})
}
}
- 点赞
- 收藏
- 关注作者
评论(0)