Flutter+OpenHarmony 智能家居开发 Day7|ObjectBox 本地持久化 + 离线设备管理全流程

欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

大家好~ 经过 Day6 的开发,我们实现了设备的搜索、筛选与分组管理,让用户能快速定位目标设备,完成了从 “能用” 到 “易用” 的升级。但新的核心体验问题随之而来:当网络断开时,用户无法查看设备列表、更无法进行搜索筛选—— 智能家居 APP 作为高频使用的工具,断网不可用的体验会让用户感受大打折扣,这也是从 “易用” 到 “实用” 必须攻克的关键问题。

这就是 Day7 要解决的核心问题:集成 ObjectBox 本地数据库实现设备数据持久化,打造全场景的离线设备管理能力。不同于前 6 天的网络请求、UI 交互开发,Day7 的开发重点在本地数据持久化、前后端数据同步、离线逻辑封装,既要保证断网时能无缝查看、搜索、筛选设备,又要确保联网后本地数据与后端实时同步,同时解决 “数据同步冲突、离线操作丢失、本地查询卡顿” 等高频问题。

ObjectBox 是一款高性能的移动端 NoSQL 数据库,相比 SharedPreferences(仅适合轻量键值对)、Hive(性能一般),它拥有更快的查询速度、更简洁的实体注解、更好的 Flutter 跨平台支持,且已完成 OpenHarmony 适配,是智能家居本地数据持久化的最优选择。

本文将延续前 6 天的「逐步骤拆解 + 逐行解析 + 全场景踩坑解决」风格,从 ObjectBox 集成、数据模型改造、本地持久化实现,到离线搜索筛选复用、离线操作缓存、鸿蒙多终端适配,每一步都讲清楚为什么做、怎么做、可能出什么问题、怎么解决,同时补充工程化实践细节(如数据同步策略、并发查询优化、鸿蒙数据库权限配置),让代码不仅能落地,还能经得起实际场景的考验。

本文适合 Flutter+OpenHarmony 跨平台开发学习者、智能家居 APP 开发者,无论你是新手还是有一定经验,都能从本文中获取可直接复用的代码、完整的离线解决方案,以及贴合实际开发的思路技巧。话不多说,我们正式开启 Day7 的详细开发之旅!

一、Day7 核心目标(明确方向,不走弯路)

在正式开发前,我们先明确 Day7 的核心目标,所有开发工作都围绕这些目标展开,确保不偏离 “断网可用、联网同步” 的核心诉求,同时覆盖用户体验和工程化要求:

  1. 集成 ObjectBox 本地数据库,完成基础配置与鸿蒙多终端适配,解决数据库路径、权限、编译兼容等问题;
  2. 改造设备数据模型,添加 ObjectBox 实体注解,实现基类 + 子类的多设备类型本地持久化;
  3. 扩展数据层,实现设备数据的本地增删改查(CRUD),封装 **“联网优先同步后端、断网优先读取本地”** 的双数据源逻辑;
  4. 复用 Day6 的搜索筛选工具类,实现离线状态下的本地设备过滤,保证断网时搜索、筛选、分组功能正常使用;
  5. 实现设备数据自动同步策略:联网时拉取后端最新数据更新本地、设备控制后实时同步本地数据、后台静默同步(鸿蒙适配);
  6. 封装离线操作缓存逻辑:断网时的设备控制操作缓存至本地,联网后自动执行并同步至后端,解决离线操作丢失问题;
  7. 处理数据同步冲突:制定后端数据优先、本地操作缓存兜底的冲突解决策略,避免前后端数据状态错乱;
  8. 优化本地数据查询性能,保证 100 + 台设备下本地搜索 / 筛选响应时间<50ms,无卡顿、无掉帧;
  9. 完成 OpenHarmony 专属适配:解决 DAYU200 开发板数据库路径权限、平板端本地查询性能、手机端后台同步权限等问题;
  10. 实现多终端测试验证:在模拟器、OpenHarmony 手机、DAYU200 开发板上验证断网 / 联网全场景功能,确保离线可用、同步正常;
  11. 规范代码结构,将本地数据库逻辑与原有网络逻辑解耦,为后续 Day8 的 MQTT 实时同步、Day9 的批量控制奠定基础。

二、前置准备(衔接前文,避免脱节)

Day7 的开发基于 Day3-Day6 已完成的项目架构,核心是在原有数据层、业务层中集成 ObjectBox 本地数据库,复用 Day6 的搜索筛选逻辑,因此在开始开发前,我们需要先确认前置环境和已有代码是否就绪,避免开发过程中出现衔接问题。

2.1 已有架构确认(重点衔接)

我们先回顾一下 Day3-Day6 已完成的核心代码和架构,确保所有依赖和基础组件都能支撑 Day7 的离线开发:

  • 数据层:已完成 BaseDevice 及子类数据模型(空调 / 灯光 / 窗帘)、封装 HttpClient 网络工具、实现 DeviceRepository 仓库(网络请求 + 内存缓存)、封装 Day6 的 DeviceFilterUtil 搜索筛选工具类;
  • 业务层:已封装 DeviceProvider 状态管理,管理设备列表、控制状态、搜索筛选状态,支持乐观更新、并发控制、状态回滚;
  • UI 层:已完成设备列表、搜索框、筛选栏、设备卡片等组件,支持多终端布局适配,实现了空结果、离线状态的基础提示;
  • 工具类:已封装防抖工具、全局异常处理器、日志工具、设备类型判断工具,可直接复用;
  • 鸿蒙适配:已配置网络权限、多终端触控适配、鸿蒙原生桥接基础,可在此基础上扩展本地数据库权限;
  • 状态管理:已实现 Provider 全局状态管理,支持 UI 层与业务层的状态联动,可直接扩展本地数据状态。

2.2 依赖检查与补充(核心:ObjectBox 适配)

Day7 的核心新增依赖是ObjectBox 本地数据库,同时需要确认已有依赖是否正确配置,避免出现依赖冲突导致的编译报错。

2.2.1 新增 ObjectBox 核心依赖

在 pubspec.yaml 中添加 ObjectBox 相关依赖,重点注意 OpenHarmony 适配版本,ObjectBox 官方已提供 Flutter for OpenHarmony 的兼容版本,直接配置即可:

yaml

dependencies:
  flutter:
    sdk: flutter
  # 原有依赖(Day5-Day6)
  dio: ^5.4.0 # 网络请求框架
  json_annotation: ^4.8.1 # JSON解析注解
  provider: ^6.1.1 # 状态管理
  get_it: ^7.2.0 # 依赖注入
  logger: ^1.1.0 # 日志工具
  device_info_plus: ^9.1.0 # 设备信息获取(判断鸿蒙系统)
  flutter_windowmanager: ^0.2.0 # 窗口管理(适配多终端)
  permission_handler: ^11.0.1 # 动态权限申请(兼容鸿蒙)
  shared_preferences: ^2.2.2 # 轻量存储(用于保存同步状态)
  # 新增:ObjectBox本地数据库(核心依赖,适配OpenHarmony)
  objectbox: ^2.0.0 # ObjectBox核心库
  objectbox_flutter_libs: ^2.0.0 # ObjectBox Flutter插件(含鸿蒙适配)

dev_dependencies:
  flutter_test:
    sdk: flutter
  # 原有开发依赖
  json_serializable: ^6.7.1 # JSON解析代码生成
  build_runner: ^2.4.6 # 代码生成工具
  flutter_lints: ^2.0.0 # 代码规范检查
  # 新增:ObjectBox代码生成工具
  objectbox_generator: ^2.0.0 # ObjectBox实体代码生成
2.2.2 执行依赖安装命令

在项目根目录执行以下命令,确保所有依赖(原有 + 新增)都能正常加载,重点注意 ObjectBox 的鸿蒙编译依赖是否下载成功

bash

运行

flutter pub get
2.2.3 可能出现的问题与解决方案

表格

问题场景 根因 解决方案
ObjectBox 依赖安装失败,提示 “OpenHarmony 平台不支持” objectbox_flutter_libs 未使用鸿蒙兼容版本 确认依赖版本为 ^2.0.0(官方适配鸿蒙的最低版本),执行flutter clean清除缓存后重新执行flutter pub get;若仍失败,从 OpenHarmony 社区下载 ObjectBox 鸿蒙适配源码手动集成
执行 pub get 后,提示 “找不到 objectbox_generator” 开发依赖未添加或版本不匹配 确认 dev_dependencies 中已添加 objectbox_generator: ^2.0.0,且与 objectbox 核心库版本一致
与 shared_preferences 冲突,提示 “存储路径冲突” 两个存储库的默认路径在鸿蒙系统中重叠 手动指定 ObjectBox 的数据库存储路径(后续模块 1 会详细讲解),避免与 shared_preferences 冲突

2.3 数据模型改造前置(ObjectBox 实体注解基础)

Day7 的核心改造点之一是为已有设备数据模型添加 ObjectBox 实体注解,ObjectBox 通过注解实现实体与数据库表的映射,核心注解需提前了解,避免改造时出现注解错误:

  1. @Entity:标记该类为 ObjectBox 实体,对应数据库中的一张表,必须添加;
  2. @Id:标记主键字段,类型为 int,ObjectBox 会自动自增(若为 0 则自动生成 ID),必须添加;
  3. @Unique:标记唯一字段(如设备 ID deviceId),避免重复数据,建议添加;
  4. @Index:为字段添加索引,提升查询速度(如设备名称、房间、设备类型),高频查询字段建议添加;
  5. @Transient:标记无需持久化的字段(如临时状态、计算属性),避免存入数据库。

核心注意:ObjectBox 的实体类必须满足无参构造函数(可通过 late 关键字实现),且字段类型需为 ObjectBox 支持的类型(String、int、bool、double、枚举等,枚举需手动实现序列化 / 反序列化)。

2.4 开发环境确认(多终端适配前提)

确认开发环境已就绪,确保后续 ObjectBox 集成和离线功能开发完成后,能快速进行多终端测试,重点确认鸿蒙开发环境的数据库权限配置

  • Flutter 环境:Flutter 3.16.0+,已配置 Flutter for OpenHarmony 插件,ObjectBox 代码生成工具正常;
  • 鸿蒙开发环境:DevEco Studio 4.0+,已配置鸿蒙 SDK(API 10+),已添加存储权限(后续模块 5 会详细讲解);
  • 测试设备:模拟器(Mate 80 Pro Max)、OpenHarmony 6.0 + 手机、DAYU200 开发板(已刷入鸿蒙系统);
  • 调试工具:开启 Flutter DevTools(监控本地数据状态)、开启 logger 日志打印(监控数据库操作)、ObjectBox Studio(可选,可视化查看本地数据库数据);
  • 数据库工具:(可选)下载 ObjectBox Studio(https://objectbox.io/studio/),用于可视化查看、调试本地数据库,提升开发效率。

2.5 代码规范确认(工程化要求)

延续 Day3-Day6 制定的代码规范,Day7 所有开发严格遵循,同时新增本地数据库代码规范

  1. 原有规范:文件夹命名(小写 + 下划线)、类命名(大驼峰)、方法 / 变量命名(小驼峰)、常量命名(全大写 + 下划线)、分层架构(数据层 / 业务层 / UI 层);
  2. 新增规范:
    • ObjectBox 实体类与原有数据模型复用,不单独创建,避免数据冗余;
    • 本地数据库操作逻辑封装在数据层的 ObjectBox 工具类中,业务层仅调用,不直接操作数据库;
    • 本地数据库与网络数据的同步逻辑封装在 DeviceRepository 中,实现 “数据层单一入口”;
    • 所有本地数据库操作(增删改查)都添加 try-catch 捕获异常,避免数据库操作失败导致 APP 闪退;
    • 本地查询结果与内存缓存保持一致,避免 UI 层展示数据不一致。

三、逐步骤详细开发(核心部分,每一步到位)

做好前置准备后,我们开始正式开发,Day7 的开发分为6 个核心模块,按「ObjectBox 基础集成→数据模型改造→数据层扩展(本地 CRUD + 双数据源)→业务层封装(离线逻辑 + 同步策略 + 操作缓存)→UI 层适配(离线状态 + 同步反馈)→OpenHarmony 专属适配」的顺序逐模块开发,每个模块分步骤拆解,每一步都讲清楚细节、问题和解决方案,确保大家能跟着步骤直接落地。

模块 1:ObjectBox 基础集成与全局配置(数据层基础)

本模块是 Day7 的开发基础,核心完成ObjectBox 的全局初始化、工具类封装、数据库路径配置,实现本地数据库的单例管理,避免多实例操作导致的数据库锁死,同时解决 OpenHarmony 系统中的数据库路径和权限问题。

步骤 1:ObjectBox 核心文件生成(初始化)

ObjectBox 需要通过代码生成工具生成数据库模型文件(objectbox-model.json)和实体映射代码,这是连接 Flutter 代码与本地数据库的核心,步骤如下:

  1. 在项目根目录创建objectbox文件夹,用于存放数据库模型文件和配置文件;
  2. 在 lib 目录下创建core/objectbox文件夹,用于封装 ObjectBox 工具类、实体模型、数据库配置;
  3. 执行以下代码生成命令,ObjectBox 会自动扫描项目中的实体类(后续步骤 2 改造后),生成核心文件:

    bash

    运行

    flutter pub run build_runner build --delete-conflicting-outputs
    
  4. 生成成功后,项目根目录会出现objectbox-model.json文件(数据库模型配置,记录实体、字段、索引等信息),lib 目录下的实体类会生成对应的.g.dart 文件(实体映射代码)。

核心注意:后续每次修改实体类(如添加字段、修改注解),都需要重新执行上述命令生成代码,否则数据库无法识别修改。

步骤 2:封装 ObjectBox 全局工具类(单例模式)

ObjectBox 的数据库实例必须采用单例模式,避免多实例同时操作数据库导致的锁死、数据错乱问题,我们在 core/objectbox 中封装全局的 ObjectBox 工具类,实现数据库的初始化、单例获取、关闭等功能,重点适配 OpenHarmony 的数据库存储路径

文件路径:lib/core/objectbox/objectbox_instance.dart

dart

import 'dart:io';
import 'package:objectbox/objectbox.dart';
import 'package:path_provider/path_provider.dart';
import 'package:logger/logger.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:smart_home_flutter/core/constants/network_constants.dart';

// 日志工具
final Logger _logger = Logger();

// ObjectBox全局单例类
class ObjectBoxInstance {
  // 数据库存储实例
  late final Store store;
  // 单例对象
  static ObjectBoxInstance? _instance;

  // 私有构造函数,避免外部实例化
  ObjectBoxInstance._internal(this.store);

  // 单例获取方法
  static Future<ObjectBoxInstance> getInstance() async {
    if (_instance == null) {
      _instance = await _init();
    }
    return _instance!;
  }

  // 核心:初始化ObjectBox,适配OpenHarmony多终端路径
  static Future<ObjectBoxInstance> _init() async {
    try {
      // 1. 根据设备类型(手机/平板/DAYU200)指定数据库存储路径,避免鸿蒙路径冲突
      final String dbPath = await _getDbPath();
      _logger.d('ObjectBox数据库初始化,存储路径:$dbPath');

      // 2. 创建数据库存储实例,指定路径和数据库名称
      final Store store = await openStore(
        directory: dbPath,
        // 数据库名称,区分开发/测试/生产环境
        name: NetworkConstants.currentEnv == 'development' ? 'smart_home_dev' : 'smart_home_prod',
      );

      _logger.d('ObjectBox数据库初始化成功,版本:${store.version},实体数量:${store.model.entities.length}');
      return ObjectBoxInstance._internal(store);
    } catch (e) {
      _logger.e('ObjectBox数据库初始化失败,错误信息:$e');
      throw Exception('本地数据库初始化失败,请检查存储权限:$e');
    }
  }

  // 辅助方法:获取适配OpenHarmony的数据库存储路径
  static Future<String> _getDbPath() async {
    final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
    // 判断是否为OpenHarmony系统
    final bool isOpenHarmony = await deviceInfo.isOpenHarmony;
    Directory appDocDir;

    if (isOpenHarmony) {
      // OpenHarmony系统:使用应用专属存储路径,避免权限问题
      appDocDir = await getApplicationSupportDirectory();
    } else {
      // 其他系统(Android/iOS):使用默认文档路径
      appDocDir = await getApplicationDocumentsDirectory();
    }

    // 拼接数据库子路径,与其他存储区分开
    final String dbSubPath = 'objectbox/smart_home';
    final String fullPath = '${appDocDir.path}/$dbSubPath';

    // 创建路径(若不存在)
    final Directory dir = Directory(fullPath);
    if (!dir.existsSync()) {
      dir.createSync(recursive: true);
      _logger.d('ObjectBox数据库路径不存在,已创建:$fullPath');
    }

    return fullPath;
  }

  // 关闭数据库连接(应用退出时调用)
  void close() {
    store.close();
    _instance = null;
    _logger.d('ObjectBox数据库连接已关闭');
  }
}

// 全局获取ObjectBox Store的快捷方法
Future<Store> getObjectBoxStore() async {
  final ObjectBoxInstance instance = await ObjectBoxInstance.getInstance();
  return instance.store;
}

代码详解

  1. 单例模式:通过私有构造函数_internal和静态方法getInstance实现单例,确保整个应用只有一个数据库实例;
  2. 鸿蒙路径适配:通过device_info_plus判断是否为 OpenHarmony 系统,使用getApplicationSupportDirectory()获取应用专属存储路径,避免鸿蒙系统的存储权限问题;
  3. 环境区分:根据开发 / 测试 / 生产环境指定不同的数据库名称,避免环境切换时的数据混淆;
  4. 异常处理:所有初始化操作都添加 try-catch,捕获路径创建失败、权限不足等异常,抛出明确的错误信息;
  5. 快捷方法:封装getObjectBoxStore()全局快捷方法,方便后续数据层、业务层快速获取数据库实例。
步骤 3:配置 objectbox-model.json(核心模型文件)

执行代码生成命令后,项目根目录会生成objectbox-model.json文件,这是 ObjectBox 的核心模型配置文件,记录了实体类、字段、索引、主键等信息,需要手动配置并添加到版本控制,避免团队开发时模型不一致。

核心配置要点

  1. 确保entities数组中包含后续改造的设备实体类(BaseDevice/AirConditionerDevice 等);
  2. 检查fields配置,确保主键、索引、唯一字段配置正确;
  3. 保留modelVersionlastEntityId,ObjectBox 通过这些标识实现数据库版本升级;
  4. 在.gitignore 中不要忽略该文件,确保团队开发时模型同步。

示例配置(后续改造后)

json

{
  "formatVersion": 1,
  "modelVersion": 1,
  "lastEntityId": "1:123456",
  "entities": [
    {
      "id": "1:123456",
      "name": "BaseDevice",
      "flags": 0,
      "fields": [
        {
          "id": "1:0",
          "name": "id",
          "type": "String",
          "flags": 8192 // 唯一字段
        },
        {
          "id": "2:0",
          "name": "name",
          "type": "String",
          "flags": 4096 // 索引字段
        },
        {
          "id": "3:0",
          "name": "type",
          "type": "Int",
          "flags": 4096 // 索引字段
        }
      ],
      "relations": []
    }
  ],
  "lastIndexId": "0:0",
  "lastRelationId": "0:0",
  "lastSequenceId": "0:0"
}
步骤 4:依赖注入配置(结合 getIt)

为了实现数据库实例的全局注入和解耦,我们将 ObjectBoxInstance 的初始化添加到原有依赖注入配置中,确保数据层、业务层可以通过 getIt 快速获取数据库实例,无需手动初始化。

文件路径:lib/core/di/injection.dart

dart

import 'package:get_it/get_it.dart';
import 'package:objectbox/objectbox.dart';
import 'package:smart_home_flutter/core/objectbox/objectbox_instance.dart';
import 'package:smart_home_flutter/domain/repositories/device_repository_impl.dart';
import 'package:smart_home_flutter/domain/providers/device_provider.dart';

// 全局依赖注入实例
final getIt = GetIt.instance;

// 初始化依赖注入
Future<void> initInjection() async {
  // 1. 初始化ObjectBox并注入Store实例(核心:添加本地数据库)
  final Store store = await getObjectBoxStore();
  getIt.registerLazySingleton<Store>(() => store);

  // 2. 原有依赖注入(Day3-Day6)
  getIt.registerLazySingleton<DeviceRepository>(() => DeviceRepositoryImpl(
        httpClient: getIt(),
        store: getIt(), // 传递ObjectBox Store到仓库
      ));
  getIt.registerFactory<DeviceProvider>(() => DeviceProvider(
        deviceRepository: getIt(),
      ));

  _logger.d('依赖注入初始化完成,已注入ObjectBox Store');
}

代码详解

  1. 先初始化 ObjectBox 获取 Store 实例,再通过registerLazySingleton注册为全局单例,确保整个应用中只有一个 Store 实例;
  2. 将 Store 实例传递给 DeviceRepositoryImpl,让数据层可以直接操作本地数据库;
  3. 原有依赖注入逻辑保持不变,实现无侵入式集成,不修改原有业务逻辑。
步骤 5:可能出现的问题与解决方案

表格

问题场景 根因 解决方案
执行代码生成命令后,未生成 objectbox-model.json 文件 未标记任何 @Entity 实体类,代码生成工具无扫描目标 先为至少一个类添加 @Entity 注解(后续步骤 2 会改造 BaseDevice),再重新执行代码生成命令
鸿蒙系统中初始化失败,提示 “权限不足,无法创建路径” 未申请鸿蒙的存储权限,或路径为系统目录 1. 在 module.json5 中添加鸿蒙存储权限(后续模块 5 详细讲解);2. 确保使用getApplicationSupportDirectory()获取应用专属路径,而非根目录
团队开发时,objectbox-model.json 文件冲突 多人修改实体类后,模型版本和 ID 不一致 1. 将该文件添加到版本控制;2. 冲突时手动合并,确保lastEntityIdmodelVersion为最新值;3. 避免多人同时修改同一实体类
数据库初始化成功,但后续操作提示 “Store 已关闭” 单例模式实现错误,多次调用 close () 检查 ObjectBoxInstance 的单例实现,确保_instance为静态变量,且 close () 方法仅在应用退出时调用(如 Flutter 的 dispose ())
与其他存储库(如 Hive)冲突,提示 “文件被占用” 存储路径重叠,多个库操作同一文件 为 ObjectBox 指定独立的子路径(如 objectbox/smart_home),与其他存储库区分开

模块 2:设备数据模型改造(添加 ObjectBox 注解)

本模块是 Day7 的核心改造点,为 Day3 已实现的 BaseDevice 及子类(AirConditionerDevice/LightDevice/CurtainDevice)添加 ObjectBox 实体注解,实现设备数据的本地持久化,同时保证原有 JSON 解析逻辑(json_serializable)正常工作,实现 **“一次改造,同时支持网络 JSON 解析和本地数据库持久化”**。

核心改造原则
  1. 复用原有模型:不单独创建 ObjectBox 实体类,直接在原有 BaseDevice 及子类上添加注解,避免数据冗余和同步问题;
  2. 注解与 JSON 解析兼容:保证 @JsonKey 和 ObjectBox 注解同时生效,不冲突;
  3. 枚举序列化:ObjectBox 原生支持枚举的 int 类型映射,需为设备类型、状态枚举实现 int 序列化 / 反序列化;
  4. 添加索引:为高频查询字段(设备名称、设备类型、房间、在线状态)添加 @Index 注解,提升本地搜索 / 筛选速度;
  5. 唯一字段:将设备 ID(deviceId)标记为 @Unique,避免本地数据库中出现重复设备;
  6. 无参构造函数:通过 late 关键字实现 ObjectBox 要求的无参构造函数,同时保留原有带参构造函数。
步骤 1:改造基础设备模型(BaseDevice)

BaseDevice 是所有设备子类的父类,核心改造:添加 @Entity 注解、主键 @Id、唯一字段 @Unique(deviceId)、索引字段 @Index(name/type/room/online)、实现枚举的 int 序列化 / 反序列化,同时兼容原有 JSON 解析注解。

文件路径:lib/data/models/device_model.dart

dart

import 'package:json_annotation/json_annotation.dart';
import 'package:objectbox/objectbox.dart'; // 新增:ObjectBox注解
import 'package:smart_home_flutter/core/constants/device_constants.dart';

part 'device_model.g.dart';

// 设备类型枚举(与API返回type字段对应,新增int序列化)
enum DeviceTypeEnum {
  airConditioner(0), // 空调
  light(1), // 灯光
  curtain(2), // 窗帘
  waterHeater(3), // 热水器
  tv(4), // 电视
  other(99); // 其他设备

  // 新增:枚举int值
  final int value;
  const DeviceTypeEnum(this.value);

  // 从int值解析枚举
  static DeviceTypeEnum fromInt(int value) {
    return DeviceTypeEnum.values.firstWhere(
      (e) => e.value == value,
      orElse: () => DeviceTypeEnum.other,
    );
  }
}

// 设备状态枚举(新增int序列化)
enum DeviceStatusEnum {
  on(0), // 开启
  off(1); // 关闭

  // 新增:枚举int值
  final int value;
  const DeviceStatusEnum(this.value);

  // 从int值解析枚举
  static DeviceStatusEnum fromInt(int value) {
    return DeviceStatusEnum.values.firstWhere(
      (e) => e.value == value,
      orElse: () => DeviceStatusEnum.off,
    );
  }
}

// 新增:ObjectBox实体注解,对应本地数据库表
@Entity()
// 原有JSON解析注解
@JsonSerializable()
class BaseDevice {
  // 新增:ObjectBox主键,int类型,自动自增
  @Id()
  int obxId = 0;

  // 设备ID(后端返回):新增@Unique(唯一)+@Index(索引),原有JSON注解
  @Unique()
  @Index()
  @JsonKey(name: 'id')
  final String deviceId;

  // 设备名称:新增@Index(索引),原有JSON注解
  @Index()
  @JsonKey(name: 'name')
  final String name;

  // 设备类型:原有JSON解析,新增ObjectBox映射(int)
  @Index()
  @JsonKey(fromJson: _deviceTypeFromJson, toJson: _deviceTypeToJson)
  final DeviceTypeEnum type;

  // 设备状态:原有JSON解析,新增ObjectBox映射(int)
  @JsonKey(fromJson: _deviceStatusFromJson, toJson: _deviceStatusToJson)
  final DeviceStatusEnum status;

  // 在线状态:新增@Index(索引),原有JSON注解
  @Index()
  @JsonKey(name: 'online')
  final bool online;

  // 房间:Day6新增,新增@Index(索引),原有JSON注解
  @Index()
  @JsonKey(name: 'room', defaultValue: '未分组')
  final String room;

  // 最后活动时间:原有JSON注解
  @JsonKey(name: 'lastActiveTime')
  final String lastActiveTime;

  // 设备图标URL:原有JSON注解
  @JsonKey(name: 'iconUrl')
  final String iconUrl;

  // 原有带参构造函数(保持不变,支持网络JSON解析)
  BaseDevice({
    required this.deviceId,
    required this.name,
    required this.type,
    required this.status,
    required this.online,
    required this.room,
    required this.lastActiveTime,
    required this.iconUrl,
  });

  // 新增:ObjectBox要求的无参构造函数(late关键字)
  BaseDevice.empty()
      : deviceId = '',
        name = '',
        type = DeviceTypeEnum.other,
        status = DeviceStatusEnum.off,
        online = false,
        room = '未分组',
        lastActiveTime = '',
        iconUrl = '';

  // 原有JSON解析方法(保持不变)
  factory BaseDevice.fromJson(Map<String, dynamic> json) => _$BaseDeviceFromJson(json);
  Map<String, dynamic> toJson() => _$BaseDeviceToJson(this);

  // 原有设备类型JSON解析(保持不变)
  static DeviceTypeEnum _deviceTypeFromJson(String value) {
    switch (value) {
      case 'airConditioner':
        return DeviceTypeEnum.airConditioner;
      case 'light':
        return DeviceTypeEnum.light;
      case 'curtain':
        return DeviceTypeEnum.curtain;
      default:
        return DeviceTypeEnum.other;
    }
  }
  static String _deviceTypeToJson(DeviceTypeEnum value) => value.name;

  // 原有设备状态JSON解析(保持不变)
  static DeviceStatusEnum _deviceStatusFromJson(String value) =>
      value == 'on' ? DeviceStatusEnum.on : DeviceStatusEnum.off;
  static String _deviceStatusToJson(DeviceStatusEnum value) => value.name;

  // 新增:ObjectBox枚举映射辅助方法(将枚举转为int,用于本地存储)
  int get typeInt => type.value;
  int get statusInt => status.value;

  // 新增:从本地存储的int值更新枚举(用于本地查询后解析)
  void updateTypeFromInt(int value) => type = DeviceTypeEnum.fromInt(value);
  void updateStatusFromInt(int value) => status = DeviceStatusEnum.fromInt(value);
}

核心改造详解

  1. 枚举序列化:为 DeviceTypeEnum 和 DeviceStatusEnum 添加 int 类型的 value 值,实现枚举与 int 的映射,因为 ObjectBox 原生支持 int 类型的枚举存储,比字符串更节省空间、查询更快;
  2. ObjectBox 注解
    • @Entity():标记 BaseDevice 为 ObjectBox 实体,对应本地数据库中的一张表;
    • @Id():添加主键 obxId,类型为 int,默认值 0,ObjectBox 会自动为新数据生成自增 ID;
    • @Unique():标记 deviceId 为唯一字段,避免本地数据库中出现重复的设备数据;
    • @Index():为 deviceId、name、type、online、room 添加索引,这些是 Day6 搜索筛选的高频字段,索引能将查询速度提升 10 倍以上;
  3. 无参构造函数:添加BaseDevice.empty()无参构造函数,满足 ObjectBox 的实体要求,通过 late 关键字初始化字段;
  4. 兼容原有逻辑:所有原有 JSON 解析注解(@JsonKey)、fromJson/toJson 方法保持不变,确保网络请求的数据解析正常工作;
  5. 枚举映射辅助方法:添加typeInt/statusInt获取枚举的 int 值,updateTypeFromInt/updateStatusFromInt从 int 值解析枚举,实现本地存储与枚举的双向映射。
步骤 2:改造设备子类(AirConditionerDevice/LightDevice/CurtainDevice)

设备子类(空调 / 灯光 / 窗帘)继承自 BaseDevice,改造原则:添加 @Entity 注解、复用父类的主键和注解、为子类特有字段添加索引(如空调的 temperature、灯光的 brightness),确保子类特有数据也能本地持久化。

以空调设备为例,文件路径:lib/data/models/air_conditioner_device.dart

dart

import 'package:json_annotation/json_annotation.dart';
import 'package:objectbox/objectbox.dart';
import 'package:smart_home_flutter/data/models/device_model.dart';

part 'air_conditioner_device.g.dart';

// 新增:ObjectBox实体注解
@Entity()
// 原有JSON解析注解
@JsonSerializable()
class AirConditionerDevice extends BaseDevice {
  // 新增:ObjectBox主键,复用父类obxId(必须与父类主键名称一致)
  @Id()
  int obxId = 0;

  // 空调温度:新增@Index(索引),原有JSON注解
  @Index()
  @JsonKey(name: 'temperature', defaultValue: 25)
  final int temperature;

  // 空调模式:新增@Index(索引),原有JSON注解
  @Index()
  @JsonKey(name: 'mode', defaultValue: 'cool')
  final String mode;

  // 空调风速:新增@Index(索引),原有JSON注解
  @Index()
  @JsonKey(name: 'fanSpeed', defaultValue: 'medium')
  final String fanSpeed;

  // 原有带参构造函数(保持不变)
  AirConditionerDevice({
    required super.deviceId,
    required super.name,
    required super.type,
    required super.status,
    required super.online,
    required super.room,
    required super.lastActiveTime,
    required super.iconUrl,
    required this.temperature,
    required this.mode,
    required this.fanSpeed,
  });

  // 新增:ObjectBox要求的无参构造函数
  AirConditionerDevice.empty()
      : temperature = 25,
        mode = 'cool',
        fanSpeed = 'medium',
        super.empty();

  // 原有JSON解析方法(保持不变)
  factory AirConditionerDevice.fromJson(Map<String, dynamic> json) =>
      _$AirConditionerDeviceFromJson(json);
  @override
  Map<String, dynamic> toJson() => _$AirConditionerDeviceToJson(this);
}

子类改造核心要点

  1. 必须添加@Entity()注解,标记为独立的 ObjectBox 实体,对应本地数据库中的独立表;
  2. 必须添加@Id()主键,名称与父类一致(obxId),确保父子类的主键关联;
  3. 为子类特有高频字段(如空调的 temperature/mode/fanSpeed、灯光的 brightness/color)添加@Index()注解,提升本地查询速度;
  4. 保留原有 JSON 解析逻辑,添加无参构造函数,与父类改造原则一致;
  5. 灯光、窗帘等其他子类的改造与空调完全一致,按此模板即可。
步骤 3:重新生成 ObjectBox 代码(关键)

完成数据模型改造后,必须重新执行代码生成命令,让 ObjectBox 扫描注解并更新模型文件和映射代码,否则数据库无法识别改造后的实体类:

bash

运行

flutter pub run build_runner build --delete-conflicting-outputs

生成成功后,会在实体类的.g.dart 文件中生成 ObjectBox 的映射代码,同时更新 objectbox-model.json 文件,添加实体和字段配置。

步骤 4:可能出现的问题与解决方案

表格

问题场景 根因 解决方案
代码生成失败,提示 “父类未标记 @Entity” 子类继承的父类未添加 @Entity 注解 确保 BaseDevice 及所有子类都添加了 @Entity 注解,ObjectBox 支持实体继承
枚举存储失败,提示 “不支持的类型” ObjectBox 不支持直接存储枚举类型 将枚举转为 int 类型存储(如本步骤的 value 值),避免直接存储枚举对象
与 JSON 注解冲突,提示 “字段重复标记” 同一字段同时添加 @JsonKey 和 ObjectBox 注解,代码生成工具识别错误 确认注解标记的是不同的逻辑(@JsonKey 用于网络 JSON,@ObjectBox 用于本地存储),两个注解可同时生效,无需删除;若仍失败,升级 json_serializable 和 objectbox_generator 版本
本地存储后,子类特有字段为 null 子类未添加无参构造函数,或无参构造函数未初始化特有字段 为子类添加无参构造函数,并初始化所有特有字段的默认值(如空调温度默认 25)
主键冲突,提示 “obxId 已存在” 手动设置了 obxId 的值,或重复插入同一设备 确保 obxId 的默认值为 0,ObjectBox 会自动自增;通过 deviceId 的 @Unique 注解避免重复插入

模块 3:数据层扩展 —— 设备数据本地 CRUD + 双数据源逻辑

本模块是 Day7 的核心数据层开发,在原有 DeviceRepository 仓库中集成 ObjectBox 本地数据库操作,实现设备数据的本地增删改查(CRUD),并封装 **“联网优先同步后端、断网优先读取本地”** 的双数据源逻辑,让业务层无需关心数据来源(网络 / 本地),实现 “数据层单一入口”。

核心设计思路
  1. 双数据源策略
    • 联网状态:优先从后端拉取最新设备数据,拉取成功后同步更新本地数据库和内存缓存;
    • 断网状态:直接从本地数据库读取设备数据,加载到内存缓存,保证断网可用;
  2. 本地 CRUD 封装:将设备的本地增、删、改、查方法封装在 DeviceRepository 中,业务层仅调用,不直接操作数据库;
  3. 数据同步原则
    • 拉取设备列表后,全量覆盖本地数据(后端数据优先);
    • 控制设备后,实时更新本地数据(保证本地数据与设备实际状态一致);
    • 新增设备后,同时添加到本地数据库(联网 / 断网都能看到);
  4. 解耦设计:本地数据库操作与网络请求操作完全解耦,可单独维护,不影响原有业务逻辑。
步骤 1:扩展 DeviceRepository 抽象类(添加本地操作接口)

在原有 DeviceRepository 抽象类中添加本地数据库操作的抽象方法,定义数据层的本地操作规范,确保实现类的一致性,同时保留原有网络请求接口。

文件路径:lib/domain/repositories/device_repository.dart

dart

import 'package:dio/dio.dart';
import 'package:logger/logger.dart';
import 'package:objectbox/objectbox.dart';
import 'package:smart_home_flutter/data/models/device_model.dart';
import 'package:smart_home_flutter/core/constants/cache_constants.dart';
import 'package:smart_home_flutter/core/exception/global_exception_handler.dart';

final Logger _logger = Logger();

// 仓库抽象类(新增本地数据库操作接口)
abstract class DeviceRepository {
  // 原有网络请求接口(Day3-Day6)
  Future<List<BaseDevice>> getDeviceList({bool refresh = false, int page = 1, int size = 15});
  void clearCache();
  Future<BaseDevice> controlDevice({required String deviceId, required Map<String, dynamic> params});

  // 新增:本地数据库操作接口(核心)
  /// 本地添加设备(单条)
  Future<void> addDeviceToLocal(BaseDevice device);
  /// 本地批量添加设备
  Future<void> addDevicesToLocal(List<BaseDevice> devices);
  /// 本地更新设备
  Future<void> updateDeviceInLocal(BaseDevice device);
  /// 本地删除设备
  Future<void> deleteDeviceFromLocal(String deviceId);
  /// 本地获取所有设备
  Future<List<BaseDevice>> getDevicesFromLocal();
  /// 本地根据条件查询设备(复用Day6的过滤参数)
  Future<List<BaseDevice>> queryDevicesFromLocal({
    String? keyword,
    DeviceTypeEnum? type,
    bool? online,
    String? room,
  });
  /// 清空本地设备数据
  Future<void> clearDevicesFromLocal();
}

接口设计详解

  1. 基础 CRUD:添加 addDeviceToLocal(单条)、addDevicesToLocal(批量)、updateDeviceInLocal、deleteDeviceFromLocal、getDevicesFromLocal 方法,实现本地设备数据的基础增删改查;
  2. 条件查询:添加 queryDevicesFromLocal 方法,复用 Day6 的搜索筛选参数(keyword/type/online/room),实现本地的条件过滤,为后续离线搜索筛选提供数据支持;
  3. 清空本地:添加 clearDevicesFromLocal 方法,用于数据同步时的全量覆盖,避免本地数据冗余。
步骤 2:实现 DeviceRepositoryImpl(集成 ObjectBox 本地操作)

在 DeviceRepositoryImpl 实现类中,注入 ObjectBox 的 Store 实例,实现抽象类中的所有本地数据库操作接口,同时改造原有网络请求接口,添加双数据源逻辑数据同步逻辑

文件路径:lib/domain/repositories/device_repository_impl.dart

dart

import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:logger/logger.dart';
import 'package:objectbox/objectbox.dart';
import 'package:connectivity_plus/connectivity_plus.dart'; // 新增:网络状态判断
import 'package:smart_home_flutter/data/api/http_client.dart';
import 'package:smart_home_flutter/data/models/device_model.dart';
import 'package:smart_home_flutter/data/models/air_conditioner_device.dart';
import 'package:smart_home_flutter/data/models/light_device.dart';
import 'package:smart_home_flutter/data/models/curtain_device.dart';
import 'package:smart_home_flutter/core/constants/cache_constants.dart';
import 'package:smart_home_flutter/core/constants/network_constants.dart';
import 'package:smart_home_flutter/core/exception/global_exception_handler.dart';
import 'package:smart_home_flutter/domain/repositories/device_repository.dart';
import 'package:smart_home_flutter/core/utils/device_filter_util.dart'; // 复用Day6过滤工具

final Logger _logger = Logger();

// 设备仓库实现类(新增ObjectBox本地数据库操作,改造原有网络逻辑)
class DeviceRepositoryImpl implements DeviceRepository {
  final HttpClient _httpClient;
  final Store _store; // 新增:注入ObjectBox Store实例
  final Connectivity _connectivity = Connectivity(); // 新增:网络状态判断
  List<BaseDevice>? _cacheDeviceList; // 原有内存缓存
  DateTime? _cacheTime; // 原有缓存时间

  // 构造函数注入(新增Store)
  DeviceRepositoryImpl({
    required HttpClient httpClient,
    required Store store, // 新增:接收ObjectBox Store
  }) : _httpClient = httpClient, _store = store;

  // 原有:获取设备列表(改造核心:双数据源逻辑)
  @override
  Future<List<BaseDevice>> getDeviceList({bool refresh = false, int page = 1, int size = 15}) async {
    try {
      // 步骤1:判断网络状态
      final ConnectivityResult connectivityResult = await _connectivity.checkConnectivity();
      final bool isOnline = connectivityResult != ConnectivityResult.none;
      _logger.d('获取设备列表,网络状态:${isOnline ? "联网" : "断网"},refresh:$refresh');

      // 步骤2:联网状态——优先从后端拉取最新数据
      if (isOnline) {
        _logger.d('联网状态,从后端拉取设备列表');
        final response = await _httpClient.get(
          NetworkConstants.deviceListPath,
          queryParameters: {'page': page, 'size': size},
        );

        // 解析后端数据
        final List<BaseDevice> networkDevices = _parseDeviceList(response);
        _logger.d('后端拉取设备数量:${networkDevices.length}');

        // 同步更新本地数据库(批量添加,全量覆盖)
        await clearDevicesFromLocal(); // 先清空本地,避免冗余
        await addDevicesToLocal(networkDevices);
        _logger.d('设备数据已同步至本地数据库,数量:${networkDevices.length}');

        // 更新内存缓存
        _cacheDeviceList = networkDevices;
        _cacheTime = DateTime.now();
        return networkDevices;
      } else {
        // 步骤3:断网状态——从本地数据库读取数据
        _logger.d('断网状态,从本地数据库读取设备列表');
        final List<BaseDevice> localDevices = await getDevicesFromLocal();
        _logger.d('本地数据库读取设备数量:${localDevices.length}');

        // 更新内存缓存
        _cacheDeviceList = localDevices;
        return localDevices;
      }
    } catch (e) {
      // 步骤4:网络请求失败(如超时、服务器错误)——降级到本地数据
      _logger.e('网络拉取设备列表失败,降级到本地数据,错误:$e');
      final List<BaseDevice> localDevices = await getDevicesFromLocal();
      _cacheDeviceList = localDevices;
      return localDevices;
    }
  }

  // 原有:控制设备(改造:控制成功后更新本地数据库)
  @override
  Future<BaseDevice> controlDevice({required String deviceId, required Map<String, dynamic> params}) async {
    try {
      // 原有网络请求逻辑(保持不变)
      _validateControlParams(deviceId, params);
      final response = await _httpClient.post(
        '${NetworkConstants.baseUrl}/device/control/$deviceId',
        data: params,
      );
      final BaseDevice updatedDevice = _parseSingleDevice(response);
      _updateCache(updatedDevice);
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐