HarmonyOS实战项目:构建一个跨设备的Todo清单应用(上)

1. 项目概述与架构设计

1.1 项目背景与核心价值

在HarmonyOS生态中,跨设备Todo应用是展示分布式能力的最佳实践场景。用户可以在手机端创建任务,在平板上查看编辑,在手表上接收提醒,实现真正的无缝体验。这种多设备协同模式解决了传统单设备应用的数据孤岛问题,让用户任务管理更加高效便捷。

与传统Todo应用的对比优势:

  • 数据实时同步:任一设备的操作秒级同步到所有设备
  • 设备能力互补:手机便捷输入、平板大屏浏览、手表即时提醒
  • 体验连续性:设备间任务状态无缝流转,不中断用户工作流

1.2 技术架构设计

本项目采用分层分布式架构,确保系统的高可用性和可扩展性:

应用层:UI界面(手机、平板、手表多端适配)
    ↓
业务层:任务管理、分布式同步、冲突解决
    ↓
服务层:数据持久化、设备管理、消息推送
    ↓
通信层:分布式软总线、数据同步引擎

核心模块职责划分:

  • 任务管理模块:负责Todo任务的CRUD操作和本地存储
  • 设备管理模块:发现可用设备、管理设备状态和能力
  • 同步引擎模块:处理跨设备数据同步和冲突解决
  • UI适配模块:针对不同设备尺寸提供最佳交互体验

2. 开发环境配置与项目初始化

2.1 环境准备与依赖配置

首先在package.json中配置项目依赖和HarmonyOS SDK版本要求:

{
  "dependencies": {
    "@ohos/distributedHardware": "^1.0.0",
    "@ohos/data.distributedDataObject": "^1.0.0",
    "@ohos.data.distributedKVStore": "^1.0.0",
    "@ohos.ability.featureAbility": "^1.0.0"
  },
  "harmonyos": {
    "compileSdkVersion": 12,
    "compatibleSdkVersion": 9
  }
}

2.2 权限声明配置

在module.json5中声明应用所需的分布式权限:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.DISTRIBUTED_DATASYNC",
        "reason": "用于跨设备同步Todo数据",
        "usedScene": {
          "abilities": ["MainAbility"],
          "when": "always"
        }
      },
      {
        "name": "ohos.permission.GET_DISTRIBUTED_DEVICE_INFO",
        "reason": "发现和连接其他设备",
        "usedScene": {
          "abilities": ["MainAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

3. 数据模型设计与分布式存储

3.1 核心数据模型定义

设计良好的数据模型是分布式应用的基础。以下是Todo应用的核心数据结构:

// Todo任务数据模型
interface TodoItem {
  id: string;                    // 全局唯一标识
  title: string;                 // 任务标题
  description?: string;          // 任务描述(可选)
  completed: boolean;            // 完成状态
  priority: 'low' | 'medium' | 'high';  // 优先级
  dueDate?: number;              // 截止时间戳
  createdAt: number;             // 创建时间
  updatedAt: number;             // 最后更新时间
  deviceId: string;             // 创建设备ID
  tags: string[];               // 标签分类
}

// 同步元数据
interface SyncMetadata {
  version: number;               // 数据版本号(用于冲突检测)
  lastSyncTime: number;         // 最后同步时间
  deviceId: string;             // 最后修改设备
}

// 设备信息模型
interface DeviceInfo {
  deviceId: string;             // 设备唯一标识
  deviceName: string;           // 设备显示名称
  deviceType: 'phone' | 'tablet' | 'watch' | 'tv'; // 设备类型
  capabilities: DeviceCapabilities; // 设备能力
  lastSeen: number;             // 最后在线时间
  isTrusted: boolean;           // 是否可信设备
}

3.2 分布式数据存储实现

采用KV Store作为主要存储方案,平衡性能与复杂度:

import distributedKVStore from '@ohos.data.distributedKVStore';
import { BusinessError } from '@ohos.base';

class TodoDataManager {
  private kvManager: distributedKVStore.KVManager | null = null;
  private kvStore: distributedKVStore.KVStore | null = null;
  
  // 初始化分布式KV Store
  async initializeDataStore(): Promise<void> {
    try {
      const config: distributedKVStore.KVManagerConfig = {
        bundleName: 'com.example.todoapp',
        userInfo: { userId: 'defaultUser' }
      };
      
      this.kvManager = distributedKVStore.createKVManager(config);
      
      const options: distributedKVStore.Options = {
        createIfMissing: true,
        encrypt: false,
        backup: false,
        autoSync: true,
        kvStoreType: distributedKVStore.KVStoreType.DEVICE_COLLABORATION,
        securityLevel: distributedKVStore.SecurityLevel.S2
      };
      
      this.kvStore = await this.kvManager.getKVStore('todo_store', options);
      
      // 设置数据变更监听
      this.setupDataChangeListener();
    } catch (error) {
      console.error('初始化数据存储失败:', error);
    }
  }
  
  // 保存Todo任务
  async saveTodoItem(todo: TodoItem): Promise<boolean> {
    if (!this.kvStore) return false;
    
    try {
      // 更新元数据
      todo.updatedAt = Date.now();
      const syncKey = `todo_${todo.id}`;
      
      await this.kvStore.put(syncKey, JSON.stringify(todo));
      return true;
    } catch (error) {
      console.error('保存Todo失败:', error);
      return false;
    }
  }
  
  // 获取所有Todo任务
  async getAllTodos(): Promise<TodoItem[]> {
    if (!this.kvStore) return [];
    
    try {
      const entries = await this.kvStore.getEntries('todo_');
      const todos: TodoItem[] = [];
      
      for (const entry of entries) {
        try {
          const todo = JSON.parse(entry.value as string) as TodoItem;
          todos.push(todo);
        } catch (parseError) {
          console.warn('解析Todo数据失败:', parseError);
        }
      }
      
      return todos.sort((a, b) => b.updatedAt - a.updatedAt);
    } catch (error) {
      console.error('获取Todo列表失败:', error);
      return [];
    }
  }
}

4. 设备管理与发现机制

4.1 设备发现与连接

实现设备的自动发现和可信连接管理:

import deviceManager from '@ohos.distributedHardware.deviceManager';

class DeviceDiscoveryManager {
  private availableDevices: Map<string, DeviceInfo> = new Map();
  private deviceCallbacks: Array<(devices: DeviceInfo[]) => void> = [];
  
  // 初始化设备发现
  async initializeDeviceDiscovery(): Promise<void> {
    try {
      const dm = await deviceManager.createDeviceManager('com.example.todoapp');
      
      // 监听设备状态变化
      dm.on('deviceStateChange', (data) => {
        this.handleDeviceStateChange(data);
      });
      
      // 开始设备发现
      const subscribeInfo = {
        subscribeId: Math.random().toString(36).substring(2),
        mode: 0xAA, // 主动发现模式
        medium: 2,   // Wi-Fi
        freq: 2,     // 正常频率
        isSameAccount: false,
        isWakeRemote: true,
        capability: 0
      };
      
      dm.startDeviceDiscovery(subscribeInfo);
    } catch (error) {
      console.error('设备发现初始化失败:', error);
    }
  }
  
  // 处理设备状态变化
  private handleDeviceStateChange(data: any): void {
    const { deviceId, deviceName, deviceType, state } = data;
    
    if (state === 'online') {
      // 设备上线
      const deviceInfo: DeviceInfo = {
        deviceId,
        deviceName,
        deviceType: this.mapDeviceType(deviceType),
        capabilities: await this.getDeviceCapabilities(deviceId),
        lastSeen: Date.now(),
        isTrusted: true
      };
      
      this.availableDevices.set(deviceId, deviceInfo);
      this.notifyDeviceCallbacks();
      
    } else if (state === 'offline') {
      // 设备离线
      this.availableDevices.delete(deviceId);
      this.notifyDeviceCallbacks();
    }
  }
  
  // 获取可信设备列表
  getTrustedDevices(): DeviceInfo[] {
    return Array.from(this.availableDevices.values())
      .filter(device => device.isTrusted)
      .sort((a, b) => b.lastSeen - a.lastSeen);
  }
}

4.2 设备能力协商

不同设备具有不同的能力,需要智能分配任务:

interface DeviceCapabilities {
  canEdit: boolean;        // 是否支持编辑
  canNotify: boolean;      // 是否支持通知
  screenSize: 'small' | 'medium' | 'large'; // 屏幕尺寸
  inputMethods: ('touch' | 'voice' | 'keyboard')[]; // 输入方式
}

class DeviceCapabilityManager {
  // 根据设备类型推断能力
  inferCapabilities(deviceType: string): DeviceCapabilities {
    const capabilities: { [key: string]: DeviceCapabilities } = {
      'phone': {
        canEdit: true,
        canNotify: true,
        screenSize: 'small',
        inputMethods: ['touch', 'voice']
      },
      'tablet': {
        canEdit: true,
        canNotify: true,
        screenSize: 'medium',
        inputMethods: ['touch', 'keyboard']
      },
      'watch': {
        canEdit: false,
        canNotify: true,
        screenSize: 'small',
        inputMethods: ['touch']
      },
      'tv': {
        canEdit: false,
        canNotify: false,
        screenSize: 'large',
        inputMethods: []
      }
    };
    
    return capabilities[deviceType] || capabilities.phone;
  }
  
  // 为任务分配合适的设备
  findSuitableDeviceForTask(task: TodoItem, devices: DeviceInfo[]): string | null {
    if (task.completed) {
      // 已完成任务适合在任何设备查看
      return devices[0]?.deviceId || null;
    }
    
    // 未完成任务优先选择支持编辑的设备
    const editableDevices = devices.filter(device => 
      device.capabilities.canEdit
    );
    
    if (editableDevices.length > 0) {
      // 优先选择屏幕较大的设备进行编辑
      return editableDevices.sort((a, b) => 
        this.getScreenSizeScore(b.capabilities.screenSize) - 
        this.getScreenSizeScore(a.capabilities.screenSize)
      )[0].deviceId;
    }
    
    return null;
  }
  
  private getScreenSizeScore(size: string): number {
    const scores = { 'small': 1, 'medium': 2, 'large': 3 };
    return scores[size] || 1;
  }
}

5. 分布式任务同步引擎

5.1 同步策略设计

实现高效可靠的数据同步机制:

class SyncEngine {
  private pendingChanges: Map<string, TodoItem> = new Map();
  private isSyncing: boolean = false;
  private syncQueue: Array<() => Promise<void>> = [];
  
  // 队列化同步操作
  async queueSyncOperation(operation: () => Promise<void>): Promise<void> {
    this.syncQueue.push(operation);
    
    if (!this.isSyncing) {
      await this.processSyncQueue();
    }
  }
  
  // 处理同步队列
  private async processSyncQueue(): Promise<void> {
    this.isSyncing = true;
    
    while (this.syncQueue.length > 0) {
      const operation = this.syncQueue.shift();
      if (operation) {
        try {
          await operation();
        } catch (error) {
          console.error('同步操作失败:', error);
          // 重试逻辑
          await this.retryOperation(operation);
        }
      }
    }
    
    this.isSyncing = false;
  }
  
  // 增量同步机制
  async performIncrementalSync(localTodos: TodoItem[], remoteTodos: TodoItem[]): Promise<TodoItem[]> {
    const mergedTodos: TodoItem[] = [];
    const localMap = new Map(localTodos.map(todo => [todo.id, todo]));
    const remoteMap = new Map(remoteTodos.map(todo => [todo.id, todo]));
    
    // 合并所有唯一的ID
    const allIds = new Set([...localMap.keys(), ...remoteMap.keys()]);
    
    for (const id of allIds) {
      const local = localMap.get(id);
      const remote = remoteMap.get(id);
      
      if (local && remote) {
        // 冲突解决
        mergedTodos.push(this.resolveConflict(local, remote));
      } else if (local) {
        mergedTodos.push(local);
      } else if (remote) {
        mergedTodos.push(remote);
      }
    }
    
    return mergedTodos;
  }
}

5.2 冲突解决策略

处理多设备同时修改的数据冲突:

class ConflictResolver {
  // 基于时间戳的冲突解决
  resolveConflict(local: TodoItem, remote: TodoItem): TodoItem {
    // 优先选择较新的版本
    if (remote.updatedAt > local.updatedAt) {
      return remote;
    } else if (local.updatedAt > remote.updatedAt) {
      return local;
    }
    
    // 时间戳相同,基于设备优先级
    return this.resolveByDevicePriority(local, remote);
  }
  
  // 基于设备类型的优先级解决
  private resolveByDevicePriority(local: TodoItem, remote: TodoItem): TodoItem {
    const devicePriority = { 'phone': 3, 'tablet': 2, 'watch': 1, 'tv': 0 };
    
    const localPriority = devicePriority[local.deviceType] || 0;
    const remotePriority = devicePriority[remote.deviceType] || 0;
    
    if (remotePriority > localPriority) {
      return remote;
    } else {
      return local;
    }
  }
  
  // 智能合并策略(用于复杂冲突)
  smartMerge(local: TodoItem, remote: TodoItem): TodoItem {
    // 检查哪些字段有冲突
    const conflictedFields = this.getConflictedFields(local, remote);
    
    if (conflictedFields.length === 0) {
      return local.updatedAt >= remote.updatedAt ? local : remote;
    }
    
    // 对每个冲突字段应用特定合并规则
    const merged = { ...local };
    
    for (const field of conflictedFields) {
      merged[field] = this.mergeField(field, local[field], remote[field]);
    }
    
    merged.updatedAt = Date.now();
    merged.deviceId = this.getCurrentDeviceId();
    
    return merged;
  }
}

6. 基础UI组件与交互设计

6.1 响应式布局组件

创建适配不同屏幕尺寸的UI组件:

@Component
struct TodoListItem {
  @Prop todo: TodoItem;
  @Prop onToggle: (id: string) => void;
  @Prop onEdit: (todo: TodoItem) => void;
  
  @StorageProp('currentDeviceType') deviceType: string;
  
  build() {
    Row() {
      // 完成状态复选框
      Checkbox({ selected: this.todo.completed })
        .onChange((checked: boolean) => {
          this.onToggle(this.todo.id);
        })
        .margin(12)
      
      // 任务内容区域
      Column() {
        Text(this.todo.title)
          .fontSize(this.getTitleFontSize())
          .decoration({ type: this.todo.completed ? 
            TextDecorationType.LineThrough : TextDecorationType.None })
          
        if (this.todo.description) {
          Text(this.todo.description)
            .fontSize(14)
            .fontColor(Color.Gray)
            .maxLines(2)
            .margin({ top: 4 })
        }
        
        // 标签和日期信息
        if (this.deviceType !== 'watch') {
          this.buildMetaInfo();
        }
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
      
      // 操作按钮(根据设备能力显示)
      if (this.deviceType !== 'watch') {
        Button('编辑')
          .onClick(() => this.onEdit(this.todo))
          .padding(8)
      }
    }
    .padding(16)
    .backgroundColor(Color.White)
    .borderRadius(8)
    .shadow(2)
  }
  
  // 根据设备类型调整字体大小
  private getTitleFontSize(): number {
    const sizes = { 'phone': 16, 'tablet': 18, 'watch': 14, 'tv': 20 };
    return sizes[this.deviceType] || 16;
  }
  
  @Builder
  buildMetaInfo() {
    Row() {
      if (this.todo.priority !== 'medium') {
        Text(this.getPriorityText())
          .fontSize(12)
          .fontColor(this.getPriorityColor())
          .padding({ left: 8, right: 8, top: 2, bottom: 2 })
          .backgroundColor(this.getPriorityBackground())
          .borderRadius(4)
      }
      
      if (this.todo.dueDate) {
        Text(this.formatDate(this.todo.dueDate))
          .fontSize(12)
          .fontColor(Color.Gray)
          .margin({ left: 8 })
      }
    }
    .margin({ top: 8 })
  }
}

6.2 设备适配的页面布局

主页面根据设备能力提供不同的布局:

@Entry
@Component
struct TodoMainPage {
  @State todos: TodoItem[] = [];
  @State currentDevice: DeviceInfo;
  @Provide('currentDeviceType') deviceType: string = 'phone';
  
  // 设备特定的布局配置
  private getLayoutConfig() {
    const configs = {
      'phone': { columns: 1, itemHeight: 80 },
      'tablet': { columns: 2, itemHeight: 100 },
      'tv': { columns: 3, itemHeight: 120 },
      'watch': { columns: 1, itemHeight: 60 }
    };
    
    return configs[this.deviceType] || configs.phone;
  }
  
  build() {
    Column() {
      // 标题栏
      this.buildHeader()
      
      // 内容区域
      if (this.deviceType === 'phone' || this.deviceType === 'watch') {
        this.buildMobileLayout();
      } else {
        this.buildTabletLayout();
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
  
  @Builder
  buildHeader() {
    Row() {
      Text('Todo清单')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
      
      Blank()
      
      // 设备连接状态指示器
      DeviceStatusIndicator({ device: this.currentDevice })
      
      // 同步状态指示器
      SyncStatusIndicator()
    }
    .padding(16)
    .backgroundColor(Color.White)
  }
  
  @Builder
  buildMobileLayout() {
    Column() {
      // 统计信息
      TodoStatistics({ todos: this.todos })
      
      // 列表区域
      List({ space: 12 }) {
        ForEach(this.todos, (todo: TodoItem) => {
          ListItem() {
            TodoListItem({ 
              todo: todo,
              onToggle: this.toggleTodo.bind(this),
              onEdit: this.editTodo.bind(this)
            })
          }
        })
      }
      .layoutWeight(1)
    }
  }
  
  @Builder
  buildTabletLayout() {
    Row() {
      // 侧边栏(统计和筛选)
      Column() {
        TodoStatistics({ todos: this.todos })
        TodoFilter()
      }
      .width('30%')
      .margin({ right: 12 })
      
      // 主内容区
      Column() {
        List({ space: 12 }) {
          ForEach(this.todos, (todo: TodoItem) => {
            ListItem() {
              TodoListItem({ 
                todo: todo,
                onToggle: this.toggleTodo.bind(this),
                onEdit: this.editTodo.bind(this)
              })
            }
          })
        }
      }
      .layoutWeight(1)
    }
  }
}

7. 总结与下篇预告

本篇我们完成了跨设备Todo应用的基础架构搭建,重点实现了:

7.1 本篇完成的核心功能

  1. 项目架构设计:确立了分层分布式架构和模块职责划分
  2. 数据模型设计:定义了完整的Todo数据结构和同步元数据
  3. 分布式存储:基于KV Store实现了数据的跨设备持久化
  4. 设备管理:实现了设备发现、能力协商和连接管理
  5. 同步引擎:设计了增量同步和冲突解决机制
  6. UI组件:创建了响应式布局和设备适配的界面组件

7.2 关键技术亮点

  • 智能设备发现:自动发现周边设备并建立可信连接
  • 冲突解决策略:基于时间戳和设备优先级的智能合并
  • 响应式设计:一套代码适配多种设备尺寸和能力
  • 队列化同步:确保数据同步的可靠性和顺序性

7.3 下篇内容预告

在下一篇中,我们将深入实现以下高级功能:

  1. 高级同步特性:实时同步、离线队列、批量操作优化
  2. 任务智能分配:基于设备能力和用户习惯的任务路由
  3. 高级UI交互:手势操作、语音输入、动效优化
  4. 性能优化:内存管理、渲染优化、电量控制
  5. 测试与调试:分布式场景下的调试技巧和性能分析

需要参加鸿蒙认证的请点击 鸿蒙认证链接

posted @ 2025-10-30 11:01  ifeng918  阅读(241)  评论(0)    收藏  举报