系列全场景篇·第46篇。出海合规篇后,有硬件厂商问:“我的Demo在手机上跑通了,但鸿蒙主打‘超级终端’,怎么让它在手机、平板、车机、手表之间无缝流转?现在的代码能直接复用吗?” 这触及了鸿蒙的灵魂能力。今天我们将电商Demo从“单机应用”重构为“分布式应用”,实现跨设备购物车同步、服务接续、硬件能力共享。我们将解决设备发现慢、状态同步乱、权限弹窗烦三大分布式痛点。全程基于API23,含官方文档未涉及的“分布式软总线调优参数”和“异构设备UI自适应”技巧。
一、前言:什么是真正的“全场景”?
很多应用只是做了“自适应布局”(手机UI拉伸到平板),这叫响应式,不叫分布式。真正的全场景体验应该是:
服务接续:手机上逛了一半的商品,坐上车自动流转到车机大屏继续浏览。
硬件互助:用手表的摄像头扫描商品条码,图片实时显示在手机上。
数据一致:手机上添加的购物车商品,平板打开时自动同步,无需登录。
核心挑战:分布式不是简单的网络同步,它涉及设备虚拟化、状态一致性、安全认证、异构UI渲染。今天我们将攻克这些难题。
二、核心概念辨析(响应式 vs 分布式)
维度 | 响应式 (Responsive) | 分布式 (Distributed) |
|---|---|---|
本质 | UI适配不同屏幕尺寸 | 能力跨越设备边界 |
数据 | 本地存储,云端同步 | 跨设备内存共享,实时同步 |
硬件 | 仅使用本机硬件 | 调用周边设备硬件(相机、GPS) |
交互 | 单设备触摸操作 | 跨设备拖拽、语音、碰一碰 |
复杂度 | 低(布局调整) | 高(设备发现、认证、同步) |
三、代码实现:从“单机”到“分布式”
3.1 分布式数据管理:跨设备购物车
使用distributedKVStore实现购物车数据在多设备间实时同步。
创建entry/src/main/ets/store/DistributedCartStore.ets:
import { distributedKVStore } from '@kit.ArkData' import { BusinessError } from '@kit.BasicServicesKit' export class DistributedCartStore { private kvManager: distributedKVStore.KVManager | null = null private kvStore: distributedKVStore.KVStore | null = null private readonly STORE_ID = 'distributed_cart_store' private readonly SYNC_KEY = 'cart_items' private syncListener: distributedKVStore.KVStoreSyncCallback | null = null /** * 初始化分布式数据库 */ async init(context: Context): Promise<void> { try { // 1. 创建KVManager this.kvManager = distributedKVStore.createKVManager({ context: context, bundleName: 'com.example.shop' }) // 2. 获取KVStore,配置为分布式模式 const options: distributedKVStore.Options = { createIfMissing: true, encrypt: true, // 加密存储 backup: false, kvStoreType: distributedKVStore.KVStoreType.DEVICE_COLLABORATION, // 关键:设置同步模式为PUSH_PULL,双向同步 syncMode: distributedKVStore.SyncMode.PUSH_PULL, // 安全等级:高(涉及用户隐私数据) securityLevel: distributedKVStore.SecurityLevel.S2 } this.kvStore = await this.kvManager.getKVStore(this.STORE_ID, options) // 3. 注册数据变更监听器 this.registerDataChangeListener() // 4. 主动同步一次(拉取其他设备数据) await this.syncData() console.log('分布式购物车初始化成功') } catch (err) { console.error('分布式购物车初始化失败:', err) } } /** * 注册数据变更监听 */ private registerDataChangeListener(): void { if (!this.kvStore) return this.syncListener = { onDataChanged: (changedDeviceId: string, changedData: distributedKVStore.ChangedData) => { console.log(`设备 ${changedDeviceId} 数据发生变化`) // 遍历变更的数据 changedData.insertEntries.forEach((entry: distributedKVStore.Entry) => { if (entry.key === this.SYNC_KEY) { const cartItems = JSON.parse(entry.value as string) console.log('购物车同步更新:', cartItems) // 发送全局事件,通知UI更新 this.notifyCartUpdated(cartItems) } }) }, onSyncCompleted: (deviceIds: string[], syncMode: distributedKVStore.SyncMode, result: distributedKVStore.SyncResult) => { console.log(`同步完成,设备: ${deviceIds}, 结果: ${result}`) } } this.kvStore.on('dataChange', distributedKVStore.SubscribeType.SUBSCRIBE_TYPE_ALL, this.syncListener) } /** * 添加商品到购物车(自动同步到所有设备) */ async addToCart(product: ProductBean): Promise<void> { if (!this.kvStore) return try { // 1. 获取当前购物车 const currentCartStr = await this.kvStore.get(this.SYNC_KEY) as string || '[]' const currentCart: ProductBean[] = JSON.parse(currentCartStr) // 2. 添加新商品 currentCart.push(product) // 3. 写回数据库(自动触发同步) await this.kvStore.put(this.SYNC_KEY, JSON.stringify(currentCart)) console.log('商品已添加到分布式购物车') } catch (err) { console.error('添加商品失败:', err) } } /** * 主动触发数据同步 */ async syncData(): Promise<void> { if (!this.kvStore) return try { // 获取当前可信组网内的所有设备ID const deviceIds = await this.kvStore.getConnectedDevices() if (deviceIds.length > 0) { await this.kvStore.sync(deviceIds, distributedKVStore.SyncMode.PUSH_PULL) console.log('主动同步数据到设备:', deviceIds) } } catch (err) { console.error('数据同步失败:', err) } } /** * 通知UI更新 */ private notifyCartUpdated(cartItems: ProductBean[]): void { // 使用EventHub或AppStorage通知UI AppStorage.setOrCreate('distributed_cart', cartItems) } /** * 获取购物车数据 */ async getCartItems(): Promise<ProductBean[]> { if (!this.kvStore) return [] const cartStr = await this.kvStore.get(this.SYNC_KEY) as string || '[]' return JSON.parse(cartStr) } }3.2 服务接续:跨设备页面迁移
实现从手机到平板的页面无缝流转。
修改ProductDetailPage.ets:
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit' import { window } from '@kit.ArkUI' import { distributedDeviceManager } from '@kit.DistributedServiceKit' @Entry @Component struct ProductDetailPage { @State product: ProductBean = new ProductBean() private dmClass: distributedDeviceManager.DeviceManager | null = null aboutToAppear(): void { // 注册接续监听 this.registerContinuation() } /** * 注册服务接续能力 */ registerContinuation(): void { try { // 1. 初始化设备管理器 this.dmClass = distributedDeviceManager.createDeviceManager('com.example.shop') // 2. 注册接续回调 (getContext(this) as UIAbilityContext).on('continue', (want: Want) => { console.log('收到服务接续请求') // 保存当前页面状态到Want参数中 want.parameters = want.parameters || {} want.parameters['product_id'] = this.product.id want.parameters['page_scroll_position'] = this.getCurrentScrollPosition() return AbilityConstant.OnContinueResult.AGREE }) } catch (err) { console.error('注册接续失败:', err) } } /** * 启动服务接续(迁移到平板) */ async startContinuation(): Promise<void> { if (!this.dmClass) return try { // 1. 发现可信设备(同账号、同局域网) const devices = this.dmClass.getAvailableDeviceListSync() const tablet = devices.find(d => d.deviceType === 'tablet') if (!tablet) { promptAction.showToast({ message: '未发现可用的平板设备' }) return } // 2. 发起接续请求 const want: Want = { bundleName: 'com.example.shop', abilityName: 'ProductDetailAbility', parameters: { 'product_id': this.product.id, 'source_device_id': this.dmClass.getLocalDeviceId() } } // 3. 启动远程Ability const context = getContext(this) as UIAbilityContext await context.startAbility(want, { windowMode: AbilityConstant.WindowMode.FULLSCREEN, displayId: tablet.deviceId // 指定在平板设备上显示 }) promptAction.showToast({ message: '已迁移到平板' }) } catch (err) { console.error('服务接续失败:', err) } } /** * 恢复接续状态 */ restoreFromWant(want: Want): void { const productId = want.parameters?.['product_id'] as string if (productId) { this.loadProductById(productId) // 恢复滚动位置等状态 const scrollPos = want.parameters?.['page_scroll_position'] as number if (scrollPos) { this.restoreScrollPosition(scrollPos) } } } build() { Column() { // ... UI布局 Button('迁移到平板') .onClick(() => this.startContinuation()) .margin(16) } } }3.3 硬件能力共享:手表控制手机
实现用手表作为手机的遥控器,浏览商品。
创建entry/src/main/ets/controller/WatchController.ets:
import { rpc } from '@kit.IPCKit' import { Want } from '@kit.AbilityKit' /** * 定义RPC接口 */ interface IRemoteController { nextProduct(): void prevProduct(): void addToCart(): void } /** * 手机端:暴露服务供手表调用 */ export class PhoneRemoteService extends rpc.RemoteObject implements IRemoteController { private productList: ProductBean[] = [] private currentIndex: number = 0 private callback: ((index: number) => void) | null = null constructor() { super('PhoneRemoteService') } onRemoteRequest(code: number, data: rpc.MessageSequence, reply: rpc.MessageSequence, option: rpc.MessageOption): boolean { switch (code) { case 1: // nextProduct this.nextProduct() break case 2: // prevProduct this.prevProduct() break case 3: // addToCart this.addToCart() break } return true } setProductList(list: ProductBean[], callback: (index: number) => void): void { this.productList = list this.callback = callback } nextProduct(): void { if (this.currentIndex < this.productList.length - 1) { this.currentIndex++ this.callback?.(this.currentIndex) } } prevProduct(): void { if (this.currentIndex > 0) { this.currentIndex-- this.callback?.(this.currentIndex) } } addToCart(): void { const product = this.productList[this.currentIndex] console.log('手表请求添加商品到购物车:', product.name) // 调用购物车逻辑 } } /** * 手表端:连接手机服务 */ export class WatchRemoteClient { private proxy: rpc.RemoteProxy | null = null async connectToPhone(): Promise<boolean> { try { const dm = distributedDeviceManager.createDeviceManager('com.example.shop') const phone = dm.getAvailableDeviceListSync().find(d => d.deviceType === 'phone') if (!phone) return false const want: Want = { bundleName: 'com.example.shop', abilityName: 'PhoneRemoteService', deviceId: phone.deviceId } this.proxy = await rpc.getRemoteProxy(want) return true } catch (err) { console.error('连接手机失败:', err) return false } } nextProduct(): void { this.proxy?.sendRequest(1) } prevProduct(): void { this.proxy?.sendRequest(2) } addToCart(): void { this.proxy?.sendRequest(3) } }3.4 异构设备UI自适应
针对不同设备形态(手机、折叠屏、平板、车机)优化UI。
创建entry/src/main/ets/utils/AdaptiveLayout.ets:
import { display } from '@kit.ArkUI' export enum DeviceType { PHONE, FOLDABLE, TABLET, CAR, WATCH } export class AdaptiveLayout { /** * 获取当前设备类型 */ static getDeviceType(): DeviceType { const displayInfo = display.getDefaultDisplaySync() const width = displayInfo.width const height = displayInfo.height const dpi = displayInfo.densityDPI // 根据屏幕尺寸和DPI判断设备类型 if (width < 600) { return DeviceType.PHONE } else if (width >= 600 && width < 840) { return DeviceType.FOLDABLE } else if (width >= 840 && width < 1200) { return DeviceType.TABLET } else if (width >= 1200) { return DeviceType.CAR } return DeviceType.PHONE } /** * 获取网格列数(响应式网格) */ static getGridColumns(): number { const deviceType = this.getDeviceType() switch (deviceType) { case DeviceType.PHONE: return 2 case DeviceType.FOLDABLE: return 3 case DeviceType.TABLET: return 4 case DeviceType.CAR: return 6 default: return 2 } } /** * 获取卡片尺寸 */ static getCardSize(): { width: number, height: number } { const deviceType = this.getDeviceType() switch (deviceType) { case DeviceType.PHONE: return { width: 160, height: 200 } case DeviceType.TABLET: return { width: 200, height: 240 } case DeviceType.CAR: return { width: 280, height: 320 } default: return { width: 160, height: 200 } } } /** * 是否为横屏模式 */ static isLandscape(): boolean { const displayInfo = display.getDefaultDisplaySync() return displayInfo.width > displayInfo.height } } // 在商品列表中使用 @Entry @Component struct AdaptiveProductList { @State columns: number = AdaptiveLayout.getGridColumns() build() { Grid() { ForEach(productList, (item: ProductBean) => { GridItem() { ProductCard({ product: item }) .width(AdaptiveLayout.getCardSize().width) .height(AdaptiveLayout.getCardSize().height) } }) } .columnsTemplate(new Array(this.columns).fill('1fr').join(' ')) .rowsGap(16) .columnsGap(16) .padding(16) } }四、踩坑记录(官方文档没写的分布式细节)![]()
设备发现的“隐形门槛”:分布式软总线要求设备必须登录同一华为账号,且开启蓝牙和Wi-Fi(即使不连同一热点)。很多开发者卡在这一步。解决方案:在应用启动时检查这些条件,并给出明确的引导提示。
同步风暴:分布式数据库默认是全量同步。如果购物车数据频繁变动,会触发大量同步请求,导致网络拥堵和设备耗电。解决方案:使用防抖(Debounce)技术,合并短时间内的多次更新;或改用单设备写入,多设备只读的模式。
权限弹窗疲劳:分布式操作需要多个权限(
DISTRIBUTED_DATASYNC、DISTRIBUTED_DEVICE_STATE_CHANGE等)。每次操作都弹窗会极大损害体验。解决方案:在应用启动时一次性申请所有必要权限;使用ability.want.parameters传递临时授权令牌。状态一致性难题:当手机和平板同时修改购物车时,可能产生冲突。分布式数据库默认采用最后写入获胜(LWW)策略,这可能导致数据丢失。解决方案:实现操作转换(OT)或冲突-free复制数据类型(CRDT)逻辑,或在业务层设计合理的冲突解决规则(如以时间戳最新为准,但需服务器校验)。
车机特殊限制:车机环境对安全和 distraction(分心驾驶)有严格要求。UI必须足够简洁,字体足够大,操作足够简单。不能使用复杂的手势,最好支持语音控制。解决方案:为车机单独设计一套UI(HMI),简化操作流程。