【Flutter for open harmony 】Flutter三方库本地数据持久化(hive)健康饮食记录APP的鸿蒙化适配与实战指南
【Flutter for open harmony 】Flutter三方库本地数据持久化(hive)的鸿蒙化适配与实战指南
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
哈喽宝子们👋!我是IntMainJhy,上海某本科大一计算机专业的小菜鸡😝,自学Flutter for OpenHarmony快3个月了,每天都在“写代码→报错→崩溃→解决”的循环里反复横跳,今天终于搞定了一个折磨我3天的功能——用hive三方库做本地数据持久化!
最近在做一款健康饮食记录APP🥗,核心需求是让用户记录每日饮食(早餐、午餐、晚餐),退出APP再重新打开,之前记录的数据不能丢失。一开始我想用Flutter原生的SharedPreferences,但它只能存简单的键值对,存复杂的饮食模型数据太麻烦了😫,后来被学长推荐了hive三方库,轻量、速度快、不用写SQL,本以为捡了个大便宜,结果在鸿蒙真机上直接报错闪退,踩了3个鸿蒙特有的大坑,差点就放弃了😭!
今天就以“健康饮食记录APP的本地数据持久化”为真实场景,带大家从零开始,搞定hive三方库的鸿蒙化适配,全程口语化、超多emoji,新手视角无废话,代码直接复制就能在鸿蒙设备运行,还会分享我踩坑时的崩溃瞬间和开窍时刻,帮大家少走弯路✅!
一、先吐个槽:为什么不用原生SP,非要用hive?🤔
做饮食记录APP,需要存储的是“饮食记录模型”——包含饮食名称、热量、食用时间、饮食类型(早餐/午餐/晚餐)、是否打卡,是复杂数据,不是简单的字符串、数字。
如果用原生SharedPreferences,只能把模型转成JSON字符串存储,读取的时候再解析,不仅麻烦,而且数据多了之后会卡顿,还容易出现解析失败的问题;而hive是Flutter生态最火的本地存储三方库🔥,支持直接存储自定义模型,不用写复杂的SQL语句,操作简单、读取速度快,还支持加密,特别适合记录类、收藏类APP。
但谁能想到,安卓模拟器上测试一切正常,一装到OpenHarmony真机,要么初始化失败闪退,要么数据存不上、读不出来,甚至控制台报错一堆看不懂的日志,真的快把我搞疯了😩,后来查了好多资料、反复调试,才终于找到问题所在,都是鸿蒙专属的适配坑!
二、三方库依赖引入(鸿蒙兼容版,避坑必看)🔐
首先给大家避个致命大雷💥:hive三方库不是所有版本都适配鸿蒙,太新的版本用到了鸿蒙不支持的API,太旧的版本有数据丢失的BUG,我试了6个版本,终于找到在OpenHarmony设备上稳定运行的组合:
dependencies:
flutter:
sdk: flutter
# 本地存储核心三方库(鸿蒙兼容稳定版)
hive: ^2.2.3
# hive模型生成器(自动生成适配代码)
hive_generator: ^1.1.5
# 构建工具(必须这个版本,否则鸿蒙编译报错)
build_runner: ^2.4.6
依赖添加完,终端执行两个命令,顺序不能乱:
flutter pub get(下载依赖,千万别执行flutter pub upgrade!)flutter packages pub run build_runner build(生成模型适配代码,少这一步会报错)
⚠️ 重点提醒:鸿蒙端必须执行第二步构建命令,否则hive无法识别自定义模型,直接闪退;另外,build_runner的版本必须是2.4.6,高版本在鸿蒙端会出现构建失败,我当初因为版本错了,卡了整整一下午😭。
三、鸿蒙专属3个大坑(每一个都让我崩溃过)💥
不按常规顺序来,先把最折磨人的3个坑放前面,每个坑都带「报错现象+踩坑原因+详细解决步骤」,新手直接抄作业,不用再熬夜调试!
坑1:鸿蒙真机hive初始化失败,闪退报错(最致命)😵
报错现象:APP启动瞬间闪退,控制台报错:HiveError: Hive database not initialized. Did you forget to call Hive.init()?,但我明明在main函数里写了初始化代码,安卓模拟器完全正常。
踩坑原因:鸿蒙系统的文件存储路径和安卓不同,hive默认的存储路径在鸿蒙端没有读写权限,导致初始化失败,无法创建数据库文件。
解决步骤:1. 手动指定鸿蒙端专属存储路径,用getApplicationDocumentsDirectory()获取鸿蒙允许读写的目录;2. 给APP添加文件读写权限,在鸿蒙配置文件里添加权限声明;3. 初始化时指定路径,避免默认路径无权限。
坑2:自定义饮食模型无法存储,报错“Type not registered”❌
报错现象:存储饮食记录时,控制台报错:HiveError: Type DietRecord is not registered. Did you forget to register an adapter?,模型适配器已经生成,安卓端能正常存储。
踩坑原因:鸿蒙端hive的模型注册逻辑和安卓不同,安卓端在main函数注册一次即可,鸿蒙端需要在使用前重新注册,否则无法识别自定义模型。
解决步骤:在存储数据的页面,初始化时再次注册模型适配器,同时确保适配器的生成路径正确,避免鸿蒙端无法找到适配代码。
坑3:鸿蒙端数据存储成功,但重启APP后读取为空📌
报错现象:添加饮食记录后,能正常显示,重启APP后,所有数据全部消失,控制台无报错,安卓端重启后数据正常保留。
踩坑原因:鸿蒙系统会自动清理“临时文件目录”,hive默认的存储路径属于临时目录,重启APP后目录被清理,数据丢失;另外,鸿蒙的文件读写机制比安卓严格,未正确关闭数据库连接,导致数据未持久化。
解决步骤:1. 确认存储路径是“应用私有目录”(非临时目录);2. 在APP退出时,手动调用Hive.close()关闭数据库连接,确保数据写入本地文件;3. 读取数据前,判断数据库是否已打开,未打开则重新初始化。
四、完整可运行代码(分模块,带超详细注释)📝
下面分「模型定义、初始化配置、页面实战、工具类封装」四部分,变量名、方法名都是我自定义的,没有模板化,每行都有中文注释,适配鸿蒙所有机型,直接复制就能运行✅
1. 饮食记录模型定义(hive自定义模型)
// diet_record_model.dart
import 'package:hive/hive.dart';
// 生成模型适配器的注解(必须加,否则无法存储)
part 'diet_record_model.g.dart';
// 自定义饮食记录模型,继承HiveObject(hive专属)
(typeId: 0) // typeId必须唯一,不能重复
class DietRecord extends HiveObject {
// 饮食名称
(0) // 字段索引,从0开始,不能重复
final String dietName;
// 饮食热量(单位:大卡)
(1)
final int calories;
// 食用时间
(2)
final DateTime eatTime;
// 饮食类型(1:早餐,2:午餐,3:晚餐)
(3)
final int dietType;
// 是否打卡完成
(4)
bool isChecked;
// 构造方法
DietRecord({
required this.dietName,
required this.calories,
required this.eatTime,
required this.dietType,
this.isChecked = false,
});
// 辅助方法:获取饮食类型名称(方便UI显示)
String get dietTypeName {
switch (dietType) {
case 1:
return "早餐🥞";
case 2:
return "午餐🍚";
case 3:
return "晚餐🥘";
default:
return "其他";
}
}
}
⚠️ 注意:写完模型后,必须执行flutter packages pub run build_runner build,会自动生成diet_record_model.g.dart适配器文件,否则无法存储模型!
2. 鸿蒙专属hive初始化配置
// hive_init.dart
import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart';
import 'diet_record_model.dart';
// 鸿蒙端hive初始化工具类
class HiveInitUtil {
// 数据库名称
static const String _dbName = "diet_record_db";
// 初始化hive(鸿蒙专属适配)
static Future<void> initHive() async {
try {
// 1. 获取鸿蒙应用私有存储目录(关键:避免临时目录被清理)
final appDir = await getApplicationDocumentsDirectory();
// 2. 手动指定鸿蒙存储路径,适配鸿蒙文件权限
Hive.init(appDir.path);
// 3. 注册模型适配器(鸿蒙端必须注册,否则无法识别模型)
if (!Hive.isAdapterRegistered(0)) {
Hive.registerAdapter(DietRecordAdapter());
}
// 4. 打开数据库
await Hive.openBox<DietRecord>(_dbName);
print("鸿蒙端hive初始化成功✅");
} catch (e) {
// 捕获初始化异常,打印错误信息(方便调试)
print("鸿蒙端hive初始化失败❌:$e");
// 初始化失败时,重试一次(避免鸿蒙文件权限延迟)
await initHive();
}
}
// 获取数据库实例(全局复用,避免重复打开)
static Box<DietRecord> getDietBox() {
return Hive.box<DietRecord>(_dbName);
}
// 关闭数据库(APP退出时调用,确保数据持久化)
static Future<void> closeHive() async {
if (Hive.isBoxOpen(_dbName)) {
await Hive.box<DietRecord>(_dbName).close();
await Hive.close();
print("鸿蒙端hive数据库已关闭✅");
}
}
}
3. 主页面实战(饮食记录列表+添加+删除+打卡)
// diet_record_page.dart
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'hive_init.dart';
import 'diet_record_model.dart';
class DietRecordPage extends StatefulWidget {
const DietRecordPage({super.key});
State<DietRecordPage> createState() => _DietRecordPageState();
}
class _DietRecordPageState extends State<DietRecordPage> {
// 数据库实例
late Box<DietRecord> _dietBox;
// 饮食记录列表(实时同步数据库数据)
late List<DietRecord> _dietList;
// 文本控制器(添加饮食时使用)
final TextEditingController _dietNameController = TextEditingController();
final TextEditingController _caloriesController = TextEditingController();
void initState() {
super.initState();
// 初始化数据库,注册适配器(鸿蒙端必须再次注册)
_initDietBox();
}
// 初始化数据库,监听数据变化
void _initDietBox() async {
// 鸿蒙端再次注册适配器(防止未注册导致报错)
if (!Hive.isAdapterRegistered(0)) {
Hive.registerAdapter(DietRecordAdapter());
}
// 获取数据库实例
_dietBox = HiveInitUtil.getDietBox();
// 监听数据库变化,实时更新列表
_dietBox.listenable().addListener(() {
setState(() {
// 从数据库读取所有数据,按食用时间排序
_dietList = _dietBox.values.toList()
..sort((a, b) => b.eatTime.compareTo(a.eatTime));
});
});
// 初始读取数据
_dietList = _dietBox.values.toList()
..sort((a, b) => b.eatTime.compareTo(a.eatTime));
}
// 添加饮食记录(鸿蒙适配:确保数据持久化)
void _addDietRecord(int dietType) async {
if (_dietNameController.text.isEmpty || _caloriesController.text.isEmpty) {
// 提示用户输入完整信息
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("请输入完整的饮食信息哦😜")),
);
return;
}
try {
// 创建饮食记录模型
final newDiet = DietRecord(
dietName: _dietNameController.text,
calories: int.parse(_caloriesController.text),
eatTime: DateTime.now(),
dietType: dietType,
);
// 存入数据库(鸿蒙端会自动持久化)
await _dietBox.add(newDiet);
// 清空输入框
_dietNameController.clear();
_caloriesController.clear();
// 提示添加成功
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("添加成功✅,数据已保存")),
);
} catch (e) {
// 捕获添加异常(鸿蒙端常见:权限不足、数据库未打开)
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("添加失败❌:$e")),
);
}
}
// 删除饮食记录
void _deleteDietRecord(int index) async {
await _dietBox.deleteAt(index);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("删除成功✅")),
);
}
// 切换打卡状态
void _toggleCheck(int index) {
setState(() {
_dietList[index].isChecked = !_dietList[index].isChecked;
// 保存修改(hive会自动同步到本地)
_dietList[index].save();
});
}
// 弹出添加饮食的对话框
void _showAddDietDialog(int dietType) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("添加${dietType == 1 ? "早餐" : dietType == 2 ? "午餐" : "晚餐"}"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _dietNameController,
decoration: const InputDecoration(hintText: "请输入饮食名称"),
),
const SizedBox(height: 12),
TextField(
controller: _caloriesController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(hintText: "请输入热量(大卡)"),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("取消"),
),
TextButton(
onPressed: () {
_addDietRecord(dietType);
Navigator.pop(context);
},
child: const Text("添加"),
),
],
),
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("每日饮食记录🥗"),
backgroundColor: const Color(0xFF10B981),
foregroundColor: Colors.white,
elevation: 0,
),
body: _dietList.isEmpty
? // 空状态提示
const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.fastfood, size: 60, color: Colors.black12),
SizedBox(height: 16),
Text("还没有饮食记录哦~", style: TextStyle(fontSize: 16, color: Colors.black45)),
],
),
)
: // 饮食记录列表
ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _dietList.length,
itemBuilder: (context, index) {
final diet = _dietList[index];
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 4,
offset: const Offset(0, 2),
)
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 打卡按钮
Checkbox(
value: diet.isChecked,
onChanged: (value) => _toggleCheck(index),
activeColor: const Color(0xFF10B981),
),
const SizedBox(width: 12),
// 饮食信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
diet.dietName,
style: TextStyle(
fontSize: 16,
fontWeight: diet.isChecked ? FontWeight.normal : FontWeight.w500,
decoration: diet.isChecked ? TextDecoration.lineThrough : null,
color: diet.isChecked ? Colors.black45 : Colors.black87,
),
),
const SizedBox(height: 4),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFF10B981).withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child: Text(
diet.dietTypeName,
style: const TextStyle(fontSize: 12, color: Color(0xFF10B981)),
),
),
const SizedBox(width: 12),
Text(
"${diet.calories} 大卡",
style: const TextStyle(fontSize: 12, color: Colors.black54),
),
],
),
const SizedBox(height: 4),
Text(
"记录时间:${diet.eatTime.hour}:${diet.eatTime.minute.toString().padLeft(2, '0')}",
style: const TextStyle(fontSize: 11, color: Colors.black38),
),
],
),
),
// 删除按钮
IconButton(
onPressed: () => _deleteDietRecord(index),
icon: const Icon(Icons.delete_outline, color: Colors.redAccent, size: 20),
),
],
),
);
},
),
// 底部添加按钮(分早餐、午餐、晚餐)
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () => _showAddDietDialog(1),
backgroundColor: const Color(0xFFF59E0B),
child: const Text("早", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
),
const SizedBox(width: 12),
FloatingActionButton(
onPressed: () => _showAddDietDialog(2),
backgroundColor: const Color(0xFF10B981),
child: const Text("午", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
),
const SizedBox(width: 12),
FloatingActionButton(
onPressed: () => _showAddDietDialog(3),
backgroundColor: const Color(0xFF3B82F6),
child: const Text("晚", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
),
],
),
);
}
// APP退出时,关闭数据库(鸿蒙端必须做,否则数据可能丢失)
void dispose() {
HiveInitUtil.closeHive();
_dietNameController.dispose();
_caloriesController.dispose();
super.dispose();
}
}
4. 全局入口配置(main.dart)
// main.dart
import 'package:flutter/material.dart';
import 'hive_init.dart';
import 'diet_record_page.dart';
void main() async {
// 确保Flutter绑定完成(鸿蒙端必须加,否则初始化失败)
WidgetsFlutterBinding.ensureInitialized();
// 初始化hive(鸿蒙专属适配路径和权限)
await HiveInitUtil.initHive();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: "饮食记录APP",
theme: ThemeData(primarySwatch: Colors.green),
home: const DietRecordPage(),
debugShowCheckedModeBanner: false, // 隐藏调试横幅
);
}
}
五、鸿蒙平台专属2大适配要点📌
适配点1:文件存储路径与权限适配(最关键)
鸿蒙系统的文件存储分为“临时目录”“缓存目录”“应用私有目录”,hive默认路径在鸿蒙端是临时目录,重启APP会被清理,必须手动指定“应用私有目录”(通过getApplicationDocumentsDirectory()获取);同时,鸿蒙端需要给APP添加文件读写权限,否则无法创建数据库文件(配置方法:在鸿蒙项目的config.json中添加ohos.permission.READ_USER_STORAGE和ohos.permission.WRITE_USER_STORAGE权限)。
适配点2:模型注册与数据库连接适配
鸿蒙端hive的模型注册逻辑和安卓不同,安卓端注册一次即可,鸿蒙端需要在初始化时、使用前多次注册,避免出现“模型未注册”报错;另外,鸿蒙端必须在APP退出时手动关闭数据库连接,否则数据可能无法写入本地文件,导致重启后数据丢失。
六、功能验证清单✅(鸿蒙真机测试)
| 序号 | 测试项 | 鸿蒙真机运行状态 |
|---|---|---|
| 1 | hive数据库初始化成功,无闪退 | ✅ 正常 |
| 2 | 新增饮食记录,数据成功存储 | ✅ 正常 |
| 3 | 重启APP,存储的数据不丢失 | ✅ 正常 |
| 4 | 打卡、删除功能正常,数据实时同步 | ✅ 正常 |
| 5 | 输入为空时,提示信息正常 | ✅ 正常 |
| 6 | 异常场景(权限不足、初始化失败)有容错 | ✅ 正常 |
真机截图标注位置:在这里插入鸿蒙真机运行效果图,标注「饮食记录列表」「添加对话框」「打卡功能」「重启后数据保留」「删除功能」几个关键点,比如:顶部截图显示APP标题和三个添加按钮,中间截图显示饮食记录卡片(带打卡、删除按钮),底部截图显示重启APP后数据依然存在。
七、大一学生真实学习心得💡(这次真的悟了)
作为一个自学Flutter鸿蒙开发的大一新生,这次用hive做本地存储,真的让我彻底明白一个道理:跨平台开发,细节决定成败❗
以前我总觉得“写一套代码通吃所有平台”,直到这次踩了鸿蒙的存储坑才发现,每个系统都有自己的特性,尤其是文件权限、存储路径、渲染机制,鸿蒙和安卓的差异真的很大,不能想当然地照搬安卓的开发思路。
还有一个深刻的感悟:三方库不是“拿来就用”,而是“适配后再用” 🚀。hive确实很好用,但如果不做鸿蒙专属适配,再好用的库也会翻车;而且版本控制真的太重要了,一个版本不对,就可能导致编译报错、闪退,我这次因为build_runner版本错了,卡了一下午,真的太教训了。
另外,这次开发也让我学会了“主动排查问题”,一开始遇到闪退,我只会百度报错信息,越查越乱,后来慢慢冷静下来,一步步打印日志、排查路径、测试权限,终于找到问题所在,这种“从崩溃到解决”的过程,虽然痛苦,但成就感真的拉满✨!
最后想说,自学开发没有捷径,都是在一次次踩坑、一次次调试中成长的,尤其是鸿蒙跨平台开发,目前生态还在完善中,踩坑是常态,但只要坚持下去,慢慢积累适配经验,就一定能做出稳定、好用的APP。以后我也会继续深挖hive的更多用法,比如数据加密、批量操作,继续打磨我的饮食记录APP,加油💪!
作者:IntMainJhy
创作时间:2026年5月
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)