Flutter+OpenHarmony 智能家居开发 Day7|ObjectBox 本地持久化 + 离线设备管理全流程
欢迎加入开源鸿蒙跨平台社区:大家好~ 经过 Day6 的开发,我们实现了设备的搜索、筛选与分组管理,让用户能快速定位目标设备,完成了从 “能用” 到 “易用” 的升级。—— 智能家居 APP 作为高频使用的工具,断网不可用的体验会让用户感受大打折扣,这也是从 “易用” 到 “实用” 必须攻克的关键问题。。不同于前 6 天的网络请求、UI 交互开发,Day7 的开发重点在。
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 的核心目标,所有开发工作都围绕这些目标展开,确保不偏离 “断网可用、联网同步” 的核心诉求,同时覆盖用户体验和工程化要求:
- 集成 ObjectBox 本地数据库,完成基础配置与鸿蒙多终端适配,解决数据库路径、权限、编译兼容等问题;
- 改造设备数据模型,添加 ObjectBox 实体注解,实现基类 + 子类的多设备类型本地持久化;
- 扩展数据层,实现设备数据的本地增删改查(CRUD),封装 **“联网优先同步后端、断网优先读取本地”** 的双数据源逻辑;
- 复用 Day6 的搜索筛选工具类,实现离线状态下的本地设备过滤,保证断网时搜索、筛选、分组功能正常使用;
- 实现设备数据自动同步策略:联网时拉取后端最新数据更新本地、设备控制后实时同步本地数据、后台静默同步(鸿蒙适配);
- 封装离线操作缓存逻辑:断网时的设备控制操作缓存至本地,联网后自动执行并同步至后端,解决离线操作丢失问题;
- 处理数据同步冲突:制定后端数据优先、本地操作缓存兜底的冲突解决策略,避免前后端数据状态错乱;
- 优化本地数据查询性能,保证 100 + 台设备下本地搜索 / 筛选响应时间<50ms,无卡顿、无掉帧;
- 完成 OpenHarmony 专属适配:解决 DAYU200 开发板数据库路径权限、平板端本地查询性能、手机端后台同步权限等问题;
- 实现多终端测试验证:在模拟器、OpenHarmony 手机、DAYU200 开发板上验证断网 / 联网全场景功能,确保离线可用、同步正常;
- 规范代码结构,将本地数据库逻辑与原有网络逻辑解耦,为后续 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 通过注解实现实体与数据库表的映射,核心注解需提前了解,避免改造时出现注解错误:
- @Entity:标记该类为 ObjectBox 实体,对应数据库中的一张表,必须添加;
- @Id:标记主键字段,类型为 int,ObjectBox 会自动自增(若为 0 则自动生成 ID),必须添加;
- @Unique:标记唯一字段(如设备 ID deviceId),避免重复数据,建议添加;
- @Index:为字段添加索引,提升查询速度(如设备名称、房间、设备类型),高频查询字段建议添加;
- @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 所有开发严格遵循,同时新增本地数据库代码规范:
- 原有规范:文件夹命名(小写 + 下划线)、类命名(大驼峰)、方法 / 变量命名(小驼峰)、常量命名(全大写 + 下划线)、分层架构(数据层 / 业务层 / UI 层);
- 新增规范:
- 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 代码与本地数据库的核心,步骤如下:
- 在项目根目录创建objectbox文件夹,用于存放数据库模型文件和配置文件;
- 在 lib 目录下创建core/objectbox文件夹,用于封装 ObjectBox 工具类、实体模型、数据库配置;
- 执行以下代码生成命令,ObjectBox 会自动扫描项目中的实体类(后续步骤 2 改造后),生成核心文件:
bash
运行
flutter pub run build_runner build --delete-conflicting-outputs - 生成成功后,项目根目录会出现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;
}
代码详解:
- 单例模式:通过私有构造函数
_internal和静态方法getInstance实现单例,确保整个应用只有一个数据库实例; - 鸿蒙路径适配:通过
device_info_plus判断是否为 OpenHarmony 系统,使用getApplicationSupportDirectory()获取应用专属存储路径,避免鸿蒙系统的存储权限问题; - 环境区分:根据开发 / 测试 / 生产环境指定不同的数据库名称,避免环境切换时的数据混淆;
- 异常处理:所有初始化操作都添加 try-catch,捕获路径创建失败、权限不足等异常,抛出明确的错误信息;
- 快捷方法:封装
getObjectBoxStore()全局快捷方法,方便后续数据层、业务层快速获取数据库实例。
步骤 3:配置 objectbox-model.json(核心模型文件)
执行代码生成命令后,项目根目录会生成objectbox-model.json文件,这是 ObjectBox 的核心模型配置文件,记录了实体类、字段、索引、主键等信息,需要手动配置并添加到版本控制,避免团队开发时模型不一致。
核心配置要点:
- 确保
entities数组中包含后续改造的设备实体类(BaseDevice/AirConditionerDevice 等); - 检查
fields配置,确保主键、索引、唯一字段配置正确; - 保留
modelVersion和lastEntityId,ObjectBox 通过这些标识实现数据库版本升级; - 在.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');
}
代码详解:
- 先初始化 ObjectBox 获取 Store 实例,再通过
registerLazySingleton注册为全局单例,确保整个应用中只有一个 Store 实例; - 将 Store 实例传递给 DeviceRepositoryImpl,让数据层可以直接操作本地数据库;
- 原有依赖注入逻辑保持不变,实现无侵入式集成,不修改原有业务逻辑。
步骤 5:可能出现的问题与解决方案
表格
| 问题场景 | 根因 | 解决方案 |
|---|---|---|
| 执行代码生成命令后,未生成 objectbox-model.json 文件 | 未标记任何 @Entity 实体类,代码生成工具无扫描目标 | 先为至少一个类添加 @Entity 注解(后续步骤 2 会改造 BaseDevice),再重新执行代码生成命令 |
| 鸿蒙系统中初始化失败,提示 “权限不足,无法创建路径” | 未申请鸿蒙的存储权限,或路径为系统目录 | 1. 在 module.json5 中添加鸿蒙存储权限(后续模块 5 详细讲解);2. 确保使用getApplicationSupportDirectory()获取应用专属路径,而非根目录 |
| 团队开发时,objectbox-model.json 文件冲突 | 多人修改实体类后,模型版本和 ID 不一致 | 1. 将该文件添加到版本控制;2. 冲突时手动合并,确保lastEntityId、modelVersion为最新值;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 解析和本地数据库持久化”**。
核心改造原则
- 复用原有模型:不单独创建 ObjectBox 实体类,直接在原有 BaseDevice 及子类上添加注解,避免数据冗余和同步问题;
- 注解与 JSON 解析兼容:保证 @JsonKey 和 ObjectBox 注解同时生效,不冲突;
- 枚举序列化:ObjectBox 原生支持枚举的 int 类型映射,需为设备类型、状态枚举实现 int 序列化 / 反序列化;
- 添加索引:为高频查询字段(设备名称、设备类型、房间、在线状态)添加 @Index 注解,提升本地搜索 / 筛选速度;
- 唯一字段:将设备 ID(deviceId)标记为 @Unique,避免本地数据库中出现重复设备;
- 无参构造函数:通过 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);
}
核心改造详解:
- 枚举序列化:为 DeviceTypeEnum 和 DeviceStatusEnum 添加 int 类型的 value 值,实现枚举与 int 的映射,因为 ObjectBox 原生支持 int 类型的枚举存储,比字符串更节省空间、查询更快;
- ObjectBox 注解:
@Entity():标记 BaseDevice 为 ObjectBox 实体,对应本地数据库中的一张表;@Id():添加主键 obxId,类型为 int,默认值 0,ObjectBox 会自动为新数据生成自增 ID;@Unique():标记 deviceId 为唯一字段,避免本地数据库中出现重复的设备数据;@Index():为 deviceId、name、type、online、room 添加索引,这些是 Day6 搜索筛选的高频字段,索引能将查询速度提升 10 倍以上;
- 无参构造函数:添加
BaseDevice.empty()无参构造函数,满足 ObjectBox 的实体要求,通过 late 关键字初始化字段; - 兼容原有逻辑:所有原有 JSON 解析注解(@JsonKey)、fromJson/toJson 方法保持不变,确保网络请求的数据解析正常工作;
- 枚举映射辅助方法:添加
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);
}
子类改造核心要点:
- 必须添加
@Entity()注解,标记为独立的 ObjectBox 实体,对应本地数据库中的独立表; - 必须添加
@Id()主键,名称与父类一致(obxId),确保父子类的主键关联; - 为子类特有高频字段(如空调的 temperature/mode/fanSpeed、灯光的 brightness/color)添加
@Index()注解,提升本地查询速度; - 保留原有 JSON 解析逻辑,添加无参构造函数,与父类改造原则一致;
- 灯光、窗帘等其他子类的改造与空调完全一致,按此模板即可。
步骤 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),并封装 **“联网优先同步后端、断网优先读取本地”** 的双数据源逻辑,让业务层无需关心数据来源(网络 / 本地),实现 “数据层单一入口”。
核心设计思路
- 双数据源策略:
- 联网状态:优先从后端拉取最新设备数据,拉取成功后同步更新本地数据库和内存缓存;
- 断网状态:直接从本地数据库读取设备数据,加载到内存缓存,保证断网可用;
- 本地 CRUD 封装:将设备的本地增、删、改、查方法封装在 DeviceRepository 中,业务层仅调用,不直接操作数据库;
- 数据同步原则:
- 拉取设备列表后,全量覆盖本地数据(后端数据优先);
- 控制设备后,实时更新本地数据(保证本地数据与设备实际状态一致);
- 新增设备后,同时添加到本地数据库(联网 / 断网都能看到);
- 解耦设计:本地数据库操作与网络请求操作完全解耦,可单独维护,不影响原有业务逻辑。
步骤 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();
}
接口设计详解:
- 基础 CRUD:添加 addDeviceToLocal(单条)、addDevicesToLocal(批量)、updateDeviceInLocal、deleteDeviceFromLocal、getDevicesFromLocal 方法,实现本地设备数据的基础增删改查;
- 条件查询:添加 queryDevicesFromLocal 方法,复用 Day6 的搜索筛选参数(keyword/type/online/room),实现本地的条件过滤,为后续离线搜索筛选提供数据支持;
- 清空本地:添加 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);更多推荐



所有评论(0)