HarmonyOS开发之多端协同案例——分布式购物车

第一部分:引入

在日常购物场景中,我们经常遇到这样的困扰:手机上浏览商品添加到购物车,走到电脑前想要结算时,却发现购物车空空如也;或者与家人一起购物时,想要合并结算却需要反复分享商品链接。这种设备孤岛协作壁垒严重影响了购物体验的连贯性。

HarmonyOS的分布式购物车技术正是为解决这一痛点而生。它通过分布式数据管理能力,将多个设备的购物车状态实时同步,让手机、平板、手表等设备成为购物流程中的不同触点。无论你切换设备还是与他人协作,购物车数据始终无缝流转,真正实现"一处添加,处处可见"的超级终端体验。

第二部分:讲解

一、分布式购物车的架构设计

1.1 核心架构原理

分布式购物车基于HarmonyOS的分布式数据管理框架,采用"发布-订阅"模式实现数据同步。其架构分为三层:

数据层:使用分布式键值数据库(KV Store)存储购物车商品信息。每个商品被封装为可观察对象,当数据变更时自动触发同步机制。

同步层:负责设备发现、连接管理和数据传输。通过分布式软总线自动建立设备间安全通道,采用增量同步策略减少网络开销。

UI层:各设备根据自身屏幕特性和交互方式,展示统一的购物车数据。平板采用双列网格布局,手机使用单列列表,手表则显示精简信息。

1.2 数据同步流程
// 文件:src/main/ets/service/DistributedCartService.ts
import distributedData from '@ohos.data.distributedData';
import distributedDevice from '@ohos.distributedDevice';

@Component
export class DistributedCartService {
    private kvManager: distributedData.KVManager | null = null;
    private kvStore: distributedData.SingleKVStore | null = null;
    private deviceList: distributedDevice.DeviceInfo[] = [];
    
    // 初始化KV Store
    async initKVStore(context: Context): Promise<void> {
        try {
            // 创建KV管理器配置
            const kvManagerConfig: distributedData.KVManagerConfig = {
                context: context,
                bundleName: 'com.example.distributedcart'
            };
            
            this.kvManager = distributedData.createKVManager(kvManagerConfig);
            
            // 配置KV Store选项
            const options: distributedData.KVStoreOptions = {
                createIfMissing: true,
                encrypt: false,
                backup: false,
                autoSync: true, // 开启自动同步
                kvStoreType: distributedData.KVStoreType.SINGLE_VERSION,
                securityLevel: distributedData.SecurityLevel.S1
            };
            
            this.kvStore = await this.kvManager.getKVStore('shopping_cart_store', options) as distributedData.SingleKVStore;
            
            // 订阅数据变更事件
            this.kvStore.on('dataChange', distributedData.SubscribeType.SUBSCRIBE_TYPE_ALL, (data) => {
                this.handleDataChange(data); // 处理数据变更
            });
            
        } catch (error) {
            console.error('KV Store初始化失败:', error);
        }
    }
}

二、完整代码实现

2.1 权限配置

首先在配置文件中声明必要的分布式权限:

// 文件:src/main/module.json5
{
    "module": {
        "requestPermissions": [
            {
                "name": "ohos.permission.DISTRIBUTED_DATASYNC",
                "reason": "$string:permission_reason_distributed_datasync",
                "usedScene": {
                    "abilities": ["EntryAbility"],
                    "when": "inuse"
                }
            },
            {
                "name": "ohos.permission.ACCESS_SERVICE_DM",
                "reason": "$string:permission_reason_access_service_dm",
                "usedScene": {
                    "abilities": ["EntryAbility"],
                    "when": "inuse"
                }
            }
        ]
    }
}

在字符串资源中定义权限说明:

// 文件:src/main/resources/base/element/string.json
{
    "string": [
        {
            "name": "permission_reason_distributed_datasync",
            "value": "用于在不同设备间同步您的购物车数据,提供无缝体验"
        },
        {
            "name": "permission_reason_access_service_dm",
            "value": "用于发现和连接附近的信任设备,以实现分布式功能"
        }
    ]
}
2.2 数据模型定义

定义商品数据模型,使用@Observed装饰器实现深度观察:

// 文件:src/main/ets/model/Product.ts
import { Observed } from '@arkts.observer';

@Observed
export class Product {
    id: string;
    name: string;
    price: number;
    count: number;
    imageUrl: string;
    deviceId: string; // 添加设备的ID,用于冲突解决
    
    constructor(id: string, name: string, price: number, imageUrl: string = '') {
        this.id = id;
        this.name = name;
        this.price = price;
        this.count = 1;
        this.imageUrl = imageUrl;
        this.deviceId = '';
    }
    
    // 商品总价计算
    get totalPrice(): number {
        return this.price * this.count;
    }
}
2.3 购物车服务实现

实现核心的分布式购物车服务:

// 文件:src/main/ets/service/DistributedCartService.ts
import { BusinessError } from '@ohos.base';
import distributedData from '@ohos.data.distributedData';
import distributedDevice from '@ohos.distributedDevice';
import { Product } from '../model/Product';

export class DistributedCartService {
    private static instance: DistributedCartService = new DistributedCartService();
    private kvStore: distributedData.SingleKVStore | null = null;
    public cartItems: Map<string, Product> = new Map();
    private changeCallbacks: Array<(items: Map<string, Product>) => void> = [];
    
    public static getInstance(): DistributedCartService {
        return this.instance;
    }
    
    // 添加商品到购物车
    async addProduct(product: Product): Promise<void> {
        if (!this.kvStore) {
            throw new Error('KV Store未初始化');
        }
        
        try {
            // 设置设备ID,用于冲突解决
            product.deviceId = await this.getLocalDeviceId();
            
            // 将商品数据序列化并存储
            const productKey = `product_${product.id}`;
            const productData = JSON.stringify(product);
            
            await this.kvStore.put(productKey, productData);
            console.info('商品添加成功:', product.name);
            
        } catch (error) {
            console.error('添加商品失败:', error);
            throw error;
        }
    }
    
    // 从购物车移除商品
    async removeProduct(productId: string): Promise<void> {
        if (!this.kvStore) {
            throw new Error('KV Store未初始化');
        }
        
        try {
            const productKey = `product_${productId}`;
            await this.kvStore.delete(productKey);
            console.info('商品移除成功:', productId);
            
        } catch (error) {
            console.error('移除商品失败:', error);
            throw error;
        }
    }
    
    // 更新商品数量
    async updateProductCount(productId: string, newCount: number): Promise<void> {
        if (!this.kvStore) {
            throw new Error('KV Store未初始化');
        }
        
        try {
            const productKey = `product_${productId}`;
            const existingProduct = this.cartItems.get(productId);
            
            if (existingProduct) {
                existingProduct.count = newCount;
                const productData = JSON.stringify(existingProduct);
                await this.kvStore.put(productKey, productData);
            }
            
        } catch (error) {
            console.error('更新商品数量失败:', error);
            throw error;
        }
    }
    
    // 加载购物车数据
    private async loadCartData(): Promise<void> {
        if (!this.kvStore) return;
        
        try {
            const entries = await this.kvStore.getEntries('product_');
            this.cartItems.clear();
            
            entries.forEach(entry => {
                try {
                    const product = JSON.parse(entry.value.toString()) as Product;
                    this.cartItems.set(product.id, product);
                } catch (parseError) {
                    console.error('解析商品数据失败:', parseError);
                }
            });
            
            // 通知所有订阅者数据已更新
            this.notifyChange();
            
        } catch (error) {
            console.error('加载购物车数据失败:', error);
        }
    }
    
    // 处理数据变更
    private handleDataChange(data: distributedData.ChangeData): Promise<void> {
        console.info('接收到数据变更:', JSON.stringify(data));
        return this.loadCartData();
    }
    
    // 获取本地设备ID
    private async getLocalDeviceId(): Promise<string> {
        // 实际实现中应调用分布式设备管理API
        return 'local_device_id';
    }
    
    // 注册数据变更回调
    registerChangeCallback(callback: (items: Map<string, Product>) => void): void {
        this.changeCallbacks.push(callback);
    }
    
    // 通知数据变更
    private notifyChange(): void {
        this.changeCallbacks.forEach(callback => {
            callback(new Map(this.cartItems));
        });
    }
}
2.4 购物车UI实现

实现跨设备适配的购物车界面:

// 文件:src/main/ets/pages/ShoppingCartPage.ts
import { DistributedCartService } from '../service/DistributedCartService';
import { Product } from '../model/Product';

@Entry
@Component
struct ShoppingCartPage {
    @State cartItems: Map<string, Product> = new Map();
    @State totalPrice: number = 0;
    @State isConnected: boolean = false;
    
    private cartService: DistributedCartService = DistributedCartService.getInstance();
    
    aboutToAppear(): void {
        // 注册数据变更回调
        this.cartService.registerChangeCallback((items: Map<string, Product>) => {
            this.cartItems = new Map(items);
            this.calculateTotalPrice();
        });
        
        // 初始化购物车服务
        this.initCartService();
    }
    
    // 初始化购物车服务
    async initCartService(): Promise<void> {
        try {
            // 在实际实现中应传递正确的Context
            await this.cartService.initKVStore(getContext(this));
            this.isConnected = true;
        } catch (error) {
            console.error('购物车服务初始化失败:', error);
        }
    }
    
    // 计算总价
    calculateTotalPrice(): void {
        this.totalPrice = Array.from(this.cartItems.values()).reduce(
            (total, product) => total + product.totalPrice, 0
        );
    }
    
    // 增加商品数量
    increaseQuantity(productId: string): void {
        const product = this.cartItems.get(productId);
        if (product) {
            this.cartService.updateProductCount(productId, product.count + 1);
        }
    }
    
    // 减少商品数量
    decreaseQuantity(productId: string): void {
        const product = this.cartItems.get(productId);
        if (product && product.count > 1) {
            this.cartService.updateProductCount(productId, product.count - 1);
        }
    }
    
    // 结算操作
    checkout(): void {
        if (this.cartItems.size === 0) {
            alert('购物车为空,请先添加商品');
            return;
        }
        
        // 在实际实现中应跳转到结算页面
        console.info('开始结算,总金额:', this.totalPrice);
    }
    
    build() {
        Column({ space: 20 }) {
            // 标题栏
            Text('分布式购物车')
                .fontSize(24)
                .fontWeight(FontWeight.Bold)
                .width('100%')
                .textAlign(TextAlign.Center)
                .margin({ top: 20, bottom: 10 })
            
            // 连接状态指示器
            Row() {
                Text(this.isConnected ? '设备已连接' : '设备未连接')
                    .fontSize(14)
                    .fontColor(this.isConnected ? '#00FF00' : '#FF0000')
                
                Image(this.isConnected ? 'connected.png' : 'disconnected.png')
                    .width(20)
                    .height(20)
                    .margin({ left: 10 })
            }
            .width('100%')
            .justifyContent(FlexAlign.Start)
            .padding({ left: 20, right: 20 })
            
            // 商品列表
            List({ space: 15 }) {
                ForEach(Array.from(this.cartItems.entries()), ([id, product]) => {
                    ListItem() {
                        this.buildProductItem(product);
                    }
                })
            }
            .layoutWeight(1)
            .width('100%')
            .padding(10)
            
            // 底部结算栏
            this.buildCheckoutBar()
        }
        .width('100%')
        .height('100%')
        .backgroundColor('#F5F5F5')
    }
    
    // 构建商品项组件
    @Builder
    buildProductItem(product: Product) {
        Row({ space: 15 }) {
            // 商品图片
            Image(product.imageUrl || 'default_product.png')
                .width(80)
                .height(80)
                .objectFit(ImageFit.Cover)
                .borderRadius(8)
            
            // 商品信息
            Column({ space: 5 }) {
                Text(product.name)
                    .fontSize(16)
                    .fontWeight(FontWeight.Medium)
                    .width('100%')
                    .textAlign(TextAlign.Start)
                
                Text(`¥${product.price.toFixed(2)}`)
                    .fontSize(14)
                    .fontColor('#FF6B00')
                    .width('100%')
                    .textAlign(TextAlign.Start)
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            
            // 数量控制器
            Row({ space: 10 }) {
                Button('-')
                    .width(30)
                    .height(30)
                    .fontSize(14)
                    .onClick(() => this.decreaseQuantity(product.id))
                
                Text(product.count.toString())
                    .fontSize(16)
                    .width(40)
                    .textAlign(TextAlign.Center)
                
                Button('+')
                    .width(30)
                    .height(30)
                    .fontSize(14)
                    .onClick(() => this.increaseQuantity(product.id))
            }
            
            // 移除按钮
            Button('删除')
                .width(60)
                .height(30)
                .fontSize(12)
                .backgroundColor('#FF4757')
                .fontColor('#FFFFFF')
                .onClick(() => this.cartService.removeProduct(product.id))
        }
        .width('100%')
        .padding(15)
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .shadow({ radius: 2, color: '#000000', offsetX: 0, offsetY: 1 })
    }
    
    // 构建结算栏组件
    @Builder
    buildCheckoutBar() {
        Column({ space: 10 }) {
            // 总价信息
            Row() {
                Text('合计:')
                    .fontSize(16)
                
                Text(`¥${this.totalPrice.toFixed(2)}`)
                    .fontSize(20)
                    .fontColor('#FF6B00')
                    .fontWeight(FontWeight.Bold)
            }
            .width('100%')
            .justifyContent(FlexAlign.SpaceBetween)
            .padding({ left: 20, right: 20 })
            
            // 结算按钮
            Button('去结算')
                .width('90%')
                .height(45)
                .fontSize(18)
                .backgroundColor('#07C160')
                .fontColor('#FFFFFF')
                .onClick(() => this.checkout())
        }
        .width('100%')
        .padding(15)
        .backgroundColor('#FFFFFF')
    }
}

三、关键API参数说明

API接口 参数说明 返回值 使用场景
distributedData.createKVManager() config: KV管理器配置 KVManager实例 创建分布式数据管理器
KVManager.getKVStore() storeId: 存储ID options: 存储选项 KVStore实例 获取分布式数据库实例
KVStore.put() key: 键名 value: 键值 Promise<void> 存储或更新数据
KVStore.delete() key: 要删除的键名 Promise<void> 删除指定数据
KVStore.on() type: 订阅类型 callback: 回调函数 void 订阅数据变更事件
KVStore.getEntries() prefix: 键前缀 Promise<Entry[]> 获取匹配前缀的所有数据项

四、多设备UI适配策略

4.1 设备类型检测与适配
// 文件:src/main/ets/utils/DeviceAdapter.ts
import display from '@ohos.display';

export class DeviceAdapter {
    // 检测设备类型
    static getDeviceType(): DeviceType {
        const displayInfo = display.getDefaultDisplaySync();
        const width = displayInfo.width;
        const height = displayInfo.height;
        const minSize = Math.min(width, height);
        
        if (minSize < 600) {
            return DeviceType.WEARABLE; // 手表
        } else if (minSize >= 600 && minSize < 1200) {
            return DeviceType.PHONE; // 手机
        } else {
            return DeviceType.TABLET; // 平板
        }
    }
    
    // 获取布局配置
    static getLayoutConfig(): LayoutConfig {
        const deviceType = this.getDeviceType();
        
        switch (deviceType) {
            case DeviceType.WEARABLE:
                return {
                    columns: 1,
                    itemHeight: 80,
                    fontSize: 14,
                    imageSize: 60
                };
            case DeviceType.PHONE:
                return {
                    columns: 1,
                    itemHeight: 100,
                    fontSize: 16,
                    imageSize: 80
                };
            case DeviceType.TABLET:
                return {
                    columns: 2,
                    itemHeight: 120,
                    fontSize: 18,
                    imageSize: 100
                };
            default:
                return {
                    columns: 1,
                    itemHeight: 100,
                    fontSize: 16,
                    imageSize: 80
                };
        }
    }
}

export enum DeviceType {
    WEARABLE = 'wearable',
    PHONE = 'phone',
    TABLET = 'tablet'
}

export interface LayoutConfig {
    columns: number;
    itemHeight: number;
    fontSize: number;
    imageSize: number;
}

五、注意事项与最佳实践

5.1 数据同步冲突解决

最后写入优先策略:当多个设备同时修改同一商品时,采用时间戳机制,最后修改的操作覆盖之前的操作。

设备优先级:手机作为主要操作设备,其修改优先级高于手表等辅助设备。

// 冲突解决示例
async resolveConflict(productId: string, localProduct: Product, remoteProduct: Product): Promise<Product> {
    const localTimestamp = localProduct.timestamp || 0;
    const remoteTimestamp = remoteProduct.timestamp || 0;
    
    // 时间戳更新的优先
    if (remoteTimestamp > localTimestamp) {
        return remoteProduct;
    } else {
        return localProduct;
    }
}
5.2 性能优化建议
  1. 数据分页加载:购物车商品过多时,采用分页加载策略。
  2. 图片懒加载:商品图片在进入可视区域时再加载。
  3. 防抖处理:商品数量修改操作添加防抖,避免频繁同步。
  4. 本地缓存:在分布式存储基础上添加本地缓存,提升读取速度。
5.3 常见问题及解决方案

问题1:设备连接不稳定

  • 解决方案:实现重连机制,在网络恢复时自动重新同步。

问题2:同步延迟

  • 解决方案:添加本地状态提示,明确告知用户数据同步状态。

问题3:权限申请失败

  • 解决方案:提供友好的权限引导界面,指导用户手动开启权限。

第三部分:总结

核心要点回顾

  1. 分布式架构是基础:通过KV Store实现多设备数据实时同步,采用发布-订阅模式确保数据一致性。
  2. 权限配置是关键:必须正确声明DISTRIBUTED_DATASYNCACCESS_SERVICE_DM权限,并在运行时动态申请。
  3. 设备适配是体验保障:根据设备类型采用不同的UI布局和交互方式,确保在各设备上都有良好体验。
  4. 冲突解决是稳定性核心:实现合理的数据冲突解决策略,确保在多设备同时操作时的数据正确性。

行动建议

  1. 开发阶段:使用DevEco Studio的分布式模拟器进行多设备联调,重点测试网络切换、数据冲突等边界场景。
  2. 测试阶段:覆盖单设备异常、网络波动、权限拒绝等异常情况,确保应用健壮性。
  3. 上线前:在多款真机设备上进行完整流程测试,验证不同设备型号的兼容性。

下篇预告

下一篇我们将深入探讨渲染性能优化——让应用如丝般顺滑。你将学习到HarmonyOS应用渲染原理、常见性能瓶颈识别方法、以及列表渲染优化、内存管理、动画优化等实用技巧。通过性能优化,你的应用将实现更流畅的用户体验,为后续的综合实战项目打下坚实基础。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐