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應用渲染原理、常見性能瓶頸識別方法、以及列表渲染優化、內存管理、動畫優化等實用技巧。通過性能優化,你的應用將實現更流暢的用户體驗,為後續的綜合實戰項目打下堅實基礎。