写备忘录这个功能,我是从一次真实需求出发的:团队想给OpenHarmony设备做一款生活助手App,第一刀就切在备忘录上。理由很简单——备忘录功能边界清晰、增删改查齐全,既能完整验证Flutter跨端能力在OpenHarmony上的落地效果,又不会一上来就被复杂业务拖垮节奏。如果你正打算在OpenHarmony上跑Flutter,或者只是好奇这套新生态到底能不能干活,这篇文章会直接给你一条能走通的路。
本文围绕Flutter for OpenHarmony生活助手App中备忘录功能的完整实现展开,覆盖环境搭建、数据层设计、UI交互、状态管理,以及我在RK3568开发板和真机上踩过的编译、运行、调试坑。无论你是Flutter老手想迁移到OpenHarmony,还是第一次接触Flutter的新手,都能从中拿到可直接复用的方案。
1. 项目背景与整体思路拆解
1.1 为什么选择Flutter而非ArkUI原生开发
OpenHarmony官方主推的是ArkTS + ArkUI声明式开发,既然做OpenHarmony适配,为什么还要绕一圈用Flutter?这是我被问得最多的问题,也是整个项目立项时争论最激烈的点。
核心原因是复用与生态。我们团队已经有成熟的Flutter业务代码库,如果全部用ArkUI重写,等于把UI层、状态管理、数据层全部推翻重来一遍,成本直接翻倍。Flutter for OpenHarmony的出现,让Flutter引擎能够跑在OpenHarmony设备上,这意味着大多数纯Dart层的业务代码可以原样保留,只需要处理平台相关的插件适配。
还有一个现实考量:团队招人。会Flutter的开发者远比会ArkTS的多,用Flutter能显著降低后续维护和扩展的人力门槛。
1.2 备忘录功能定位:为什么是它
生活助手App的功能候选有很多:日历、待办、记账、备忘录。我坚持先把备忘录做透,因为它麻雀虽小五脏俱全。
备忘录天然包含数据建模(标题、内容、时间戳)、增删改查操作、列表展示、编辑跳转、置顶排序、搜索过滤这些几乎所有App都会遇到的核心场景。把这些都跑通了,后续扩展任何新功能都只是往框架里填业务的问题。
更关键的是,备忘录对数据持久化的依赖非常强,正好用来验证OpenHarmony上SQLite数据库的适配程度。这是其他UI演示类Demo根本覆盖不到的深度。
1.3 整体技术架构选型
整个App采用分层架构,这是我在项目开工前就定死的规矩:
| 层级 | 职责 | 关键技术 |
|---|---|---|
| UI层 | 列表展示、编辑交互、搜索视图 | Widget、Provider |
| 状态管理层 | 备忘录数据变更与视图同步 | Provider + ChangeNotifier |
| 仓储层 | 屏蔽数据来源,提供统一接口 | Repository模式 |
| 数据层 | SQLite本地持久化 | sqflite_ohos |
选择Provider而不是Riverpod或Bloc,是因为备忘录功能的状态流相对简单,Provider的入门门槛低、样板代码少,团队成员都能快速上手。等后续功能复杂了再迁移到Riverpod也不迟,架构上已经通过仓储层把数据细节隔离掉了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开发环境搭建与工程初始化
2.1 Flutter for OpenHarmony环境搭建全流程
OpenHarmony的Flutter支持目前主要来自社区和官方分支的协作,环境搭建比标准Flutter要繁琐不少,我踩了两遍才算理清完整的链路。
第一步,准备基础工具链。除了常规的Flutter SDK,还需要从OpenHarmony官方仓库拉取flutter_flutter、flutter_engine和flutter_plugins三个核心仓库。这里要注意版本对齐,建议直接选择官方release分支,我一开始混用了不同分支导致引擎编译时各种接口对不上。
第二步,配置环境变量。需要单独设置OpenHarmony SDK路径,同时建议配置国内镜像源加速依赖下载。实际配置时我加了这样一段到环境变量配置文件里:
bash复制export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
export OPENHARMONY_HASDK=/path/to/ohos-sdk
export OHOS_SDK_HOME=/path/to/ohos-sdk
export PATH=$PATH:/path/to/flutter_flutter/bin
注意:国内镜像源这步千万别省。我第一次没配,下载engine产物等了一个多小时,配置后速度能快一个数量级。
2.2 创建工程并添加OHOS平台支持
环境就绪后,用flutter create创建工程是常规操作:
bash复制flutter create memo_app --org com.example --project-name memo_app
但这样创建出来的工程默认只有Android和iOS平台目录。OpenHarmony支持依赖于flutter_flutter分支对ohos平台的原生支持,需要在工程里手动添加ohos平台目录:
bash复制flutter create --platforms ohos .
这里有个容易忽略的细节:工程里的pubspec.yaml必须引用OpenHarmony适配过的插件版本。普通sqflite、path_provider在这些设备上是拿不到原生通道的,必须换成带ohos标识的适配版本。这个我在数据层章节会详细展开。
2.3 真机与开发板运行调试
运行到OpenHarmony设备有两种常见方式:使用DevEco Studio打开ohos目录后构建HAP运行,或者直接通过hdc命令行工具推送调试。我在RK3568开发板上主要用hdc方式,调试效率高不少。
第一步,用hdc确认设备识别:
bash复制hdc list targets
如果设备没被识别,检查开发板的USB调试开关和驱动。确认设备在线后,查看系统版本信息:
bash复制hdc shell param get const.product.name
hdc shell param get const.product.version
第二步,编译并运行:
bash复制flutter run -d <device-id>
OpenHarmony上flutter run的热重载支持还不稳定,我实际测试下来,修改原生代码后基本必须restart才能生效,纯Dart代码的热重载也偶发失效。所以调试时我的习惯是:小改动直接热重载,涉及插件或原生层的改动果断全量重启,别在无效热重载上浪费时间。
3. 备忘录数据层:模型设计、本地存储与仓库封装
3.1 数据模型设计与建表语句
备忘录的数据字段不算多,但每一列都得想清楚用途。我的Memo模型长这样:
dart复制class Memo {
int? id;
String title;
String content;
String tags;
bool isPinned;
bool isDone;
int? remindAt;
int createdAt;
int updatedAt;
Memo({
this.id,
required this.title,
this.content = '',
this.tags = '',
this.isPinned = false,
this.isDone = false,
this.remindAt,
required this.createdAt,
required this.updatedAt,
});
Map<String, dynamic> toMap() {
return {
'id': id,
'title': title,
'content': content,
'tags': tags,
'is_pinned': isPinned ? 1 : 0,
'is_done': isDone ? 1 : 0,
'remind_at': remindAt,
'created_at': createdAt,
'updated_at': updatedAt,
};
}
factory Memo.fromMap(Map<String, dynamic> map) {
return Memo(
id: map['id'],
title: map['title'],
content: map['content'] ?? '',
tags: map['tags'] ?? '',
isPinned: (map['is_pinned'] as int) == 1,
isDone: (map['is_done'] as int) == 1,
remindAt: map['remind_at'],
createdAt: map['created_at'],
updatedAt: map['updated_at'],
);
}
}
对应SQLite建表语句:
sql复制CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL DEFAULT '',
tags TEXT NOT NULL DEFAULT '',
is_pinned INTEGER NOT NULL DEFAULT 0,
is_done INTEGER NOT NULL DEFAULT 0,
remind_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX idx_memo_updated_at ON memo(updated_at DESC);
CREATE INDEX idx_memo_is_pinned ON memo(is_pinned);
几个设计点我专门解释下。
第一,tags用字符串而不是单独建关联表。备忘录的标签场景是轻量级的,用户不会对标签做复杂的管理操作,用逗号分隔存储足够,查询时用LIKE模糊匹配就行。等哪天需要标签统计再拆表也不晚。
第二,时间戳全部用int类型存毫秒值。一开始想过用TEXT存ISO格式,但排序、范围查询、时间计算都要先转换,效率低且代码丑。int毫秒值和Dart的DateTime配合无缝,是SQLite上最务实的方案。
第三,is_pinned和is_done用INTEGER 0/1而不是Boolean。SQLite原生没有布尔类型,sqflite虽然能自动映射,但显式用int更保险,也能避免个别适配版本的类型转换bug。
3.2 sqflite_ohos接入与数据库封装
OpenHarmony上不能直接用标准sqflite,必须使用官方或社区适配的sqflite_ohos。在pubspec.yaml里装配好之后,我用一个DatabaseHelper单例封装所有数据库操作:
dart复制import 'package:sqflite_ohos/sqflite_ohos.dart';
import 'package:path_provider_ohos/path_provider_ohos.dart';
class DatabaseHelper {
static final DatabaseHelper _instance = DatabaseHelper._internal();
factory DatabaseHelper() => _instance;
DatabaseHelper._internal();
static const _dbName = 'memo_app.db';
static const _dbVersion = 1;
Database? _db;
Future<Database> get database async {
_db ??= await _initDb();
return _db!;
}
Future<Database> _initDb() async {
final dir = await getApplicationDocumentsDirectory();
final path = '${dir.path}/$_dbName';
return openDatabase(
path,
version: _dbVersion,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
}
Future<void> _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL DEFAULT '',
tags TEXT NOT NULL DEFAULT '',
is_pinned INTEGER NOT NULL DEFAULT 0,
is_done INTEGER NOT NULL DEFAULT 0,
remind_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
''');
}
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
// 后续加字段或加表在这里迁移
}
}
关键点在获取数据库路径时必须用path_provider_ohos的getApplicationDocumentsDirectory,而不是硬编码路径。OpenHarmony的文件系统隔离和Android类似,写死绝对路径轻则拿不到正确目录,重则在某些设备上直接没权限。这也是整个适配过程中最常见的插件替换点之一。
提示:所有plugin级别的依赖,都要去pub.dev确认是否存在ohos适配版本,比如path_provider_ohos、shared_preferences_ohos、sqflite_ohos。找不到适配版的话,要么等功能补全,要么自己用Platform Channel写一套原生实现,后者工作量不小。
3.3 仓储层封装:把数据库细节关进笼子里
数据库操作直接散落在UI层会非常难维护,我在UI和数据库之间加了一层MemoRepository:
dart复制class MemoRepository {
final DatabaseHelper _dbHelper = DatabaseHelper();
Future<List<Memo>> getAllMemos() async {
final db = await _dbHelper.database;
final result = await db.query(
'memo',
orderBy: 'is_pinned DESC, updated_at DESC',
);
return result.map((e) => Memo.fromMap(e)).toList();
}
Future<List<Memo>> searchMemos(String keyword) async {
final db = await _dbHelper.database;
final result = await db.query(
'memo',
where: 'title LIKE ? OR content LIKE ? OR tags LIKE ?',
whereArgs: ['%$keyword%', '%$keyword%', '%$keyword%'],
orderBy: 'is_pinned DESC, updated_at DESC',
);
return result.map((e) => Memo.fromMap(e)).toList();
}
Future<int> insertMemo(Memo memo) async {
final db = await _dbHelper.database;
return db.insert('memo', memo.toMap());
}
Future<int> updateMemo(Memo memo) async {
final db = await _dbHelper.database;
return db.update(
'memo',
memo.toMap(),
where: 'id = ?',
whereArgs: [memo.id],
);
}
Future<int> deleteMemo(int id) async {
final db = await _dbHelper.database;
return db.delete('memo', where: 'id = ?', whereArgs: [id]);
}
}
仓储层带来的最大好处,是UI层和状态管理层只管调用方法,完全不需要知道SQL语句长什么样。后面我增加置顶排序逻辑、分页加载、软删除时,只需要在repository里改查询逻辑,UI层一行不动。这就是分层架构在实战中最直接的回报。
4. 备忘录UI层:列表、编辑、搜索与状态管理
4.1 首页列表设计与状态管理
首页是整个备忘录的入口,我采用了Material 3风格的Card列表,每条备忘录显示标题、内容摘要、置顶图标和操作菜单。
状态管理用Provider + ChangeNotifier,MemoListModel持有备忘录列表并暴露操作方法:
dart复制class MemoListModel extends ChangeNotifier {
final MemoRepository _repository = MemoRepository();
List<Memo> _memos = [];
String _keyword = '';
List<Memo> get memos => _memos;
Future<void> loadMemos() async {
_memos = _keyword.isEmpty
? await _repository.getAllMemos()
: await _repository.searchMemos(_keyword);
notifyListeners();
}
Future<void> addMemo(Memo memo) async {
await _repository.insertMemo(memo);
await loadMemos();
}
Future<void> updateMemo(Memo memo) async {
await _repository.updateMemo(memo);
await loadMemos();
}
Future<void> deleteMemo(int id) async {
await _repository.deleteMemo(id);
await loadMemos();
}
Future<void> togglePin(Memo memo) async {
memo.isPinned = !memo.isPinned;
memo.updatedAt = DateTime.now().millisecondsSinceEpoch;
await _repository.updateMemo(memo);
await loadMemos();
}
void setKeyword(String keyword) {
_keyword = keyword.trim();
loadMemos();
}
}
为什么用Provider而不用Bloc?备忘录的状态流是典型的"用户操作触发→数据变更→UI刷新",没有复杂的异步事件交互。Provider的ChangeNotifier天然契合这种模式,代码量比Bloc少一半以上,团队上手成本也低。
列表部分用Consumer监听MemoListModel,build方法里根据状态展示加载中、空态或列表。空态很重要,很多新手会忽略,但一个精心设计的空态页面能大幅提升产品完成度。
4.2 新建与编辑页实现,以及草稿保护
编辑页复用了同一个MemoEditPage,通过构造函数传入可选的Memo对象,传null就是新建,传现有对象就是编辑。这个模式在Flutter里很常规,但在OpenHarmony上的表现却给了我惊喜——页面切换的流畅度和Android原生基本没有差别,说明Flutter引擎在OpenHarmony上的渲染管线已经跑得相当顺畅。
草稿保护是一个隐藏很深的痛点。用户在编辑页输入了一半内容,突然切后台或不小心退出,内容就丢了,非常影响体验。我的处理方案是监听WidgetsBindingObserver的生命周期回调:
dart复制class MemoEditPage extends StatefulWidget {
const MemoEditPage({super.key, this.memo});
final Memo? memo;
@override
State<MemoEditPage> createState() => _MemoEditPageState();
}
class _MemoEditPageState extends State<MemoEditPage>
with WidgetsBindingObserver {
late TextEditingController _titleController;
late TextEditingController _contentController;
bool _hasPendingChanges = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_titleController = TextEditingController(text: widget.memo?.title ?? '');
_contentController = TextEditingController(text: widget.memo?.content ?? '');
_titleController.addListener(_markDirty);
_contentController.addListener(_markDirty);
}
void _markDirty() {
_hasPendingChanges = true;
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.inactive ||
state == AppLifecycleState.paused) {
_autoSaveDraft();
}
}
void _autoSaveDraft() {
if (!_hasPendingChanges) return;
// 将当前内容写入隔离的draft表或临时文件
}
}
这个功能的实现思路其实很朴素:用户一旦切后台就自动保存一份草稿,下次进入编辑页时检测到草稿存在就弹提示恢复。实现不复杂,但能把用户从数据丢失的踩坑体验里拉回来,投入产出比极高。
4.3 搜索、置顶与标签功能
搜索功能借助repository里的searchMemos,配合TextField的onChanged在每个输入变更时更新关键词并重新加载列表。但如果不做防抖,每敲一个字就查一次数据库,性能会出现肉眼可见的卡顿。
我用了简单的Timer防抖方案:
dart复制Timer? _debounce;
void _onSearchChanged(String value) {
_debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 300), () {
model.setKeyword(value);
});
}
300毫秒的延迟既保证搜索流畅,又不会让界面有迟滞感。实际测试下来,在几千条数据规模下搜索基本是秒开。
置顶功能我选择做成交互手势和长按菜单两种入口。列表项长按弹出BottomSheet,包含"置顶/取消置顶""编辑""删除"操作。置顶的实现逻辑简单粗暴:修改isPinned字段后重新排序。排序规则在model里已经通过SQL的ORDER BY is_pinned DESC, updated_at DESC实现了,所以UI层只需要调用togglePin,不需要自己处理数组重排。
标签功能当前版本做得比较克制:在编辑页提供一个可选的标签输入框,多个标签用逗号分隔存储,列表页展示第一个标签作为小徽章。这样的设计足够覆盖大多数轻量使用场景,又不至于引入标签管理模块拖慢进度。
有一个FontSize相关的细节值得单独提:OpenHarmony系统设置里的字体大小调整会直接影响Flutter应用的字体缩放,某些设备上把字体调到最大后,备忘录卡片会出现文字溢出。我的处理方法是给关键文本组件设置maxLines + overflow属性,并在极端情况下用FittedBox兜底。测试时记得把系统字体调到最大跑一遍,这个坑在正式环境一定会有人踩到。
5. OpenHarmony适配踩坑实录:编译、运行与调试
5.1 编译期报错排查记录
OpenHarmony上编译Flutter工程,最容易挂在插件解析阶段。典型报错类似这样:
bash复制Error resolving plugin [id: 'dev.flutter.flutter-plugin-loader', version: ...]
这个问题的根源是工程构建时使用了原始Flutter的Gradle插件加载方式,而OpenHarmony工程用的是hvigor构建体系,两者对插件的解析逻辑不兼容。解决办法是确保工程已经用支持OHOS的平台目录创建,并且settings.gradle里引用的是适配过的flutter-plugin-loader版本。我在排查时还发现过依赖版本冲突,普通Flutter插件和ohos适配插件混用会导致原生工程编译失败。
另外常见的是NDK或OHOS SDK版本不匹配,报错信息五花八门,但核心思路是检查flutter_flutter、flutter_engine、flutter_plugins三个仓库的版本是否对齐到同一个release标签。版本不对齐是所有OpenHarmony编译问题的头号原因,没有之一。
我把踩过的编译问题整理成了一个速查表:
| 报错特征 | 根因 | 解决办法 |
|---|---|---|
| plugin-loader解析失败 | 构建系统不匹配 | 使用ohos平台目录和适配版插件 |
| engine头文件找不到 | flutter_engine版本不对 | 对齐三个官方仓库的release标签 |
| SDK location not found | OHOS_SDK_HOME未配置 | 显式导出SDK路径环境变量 |
| 符号链接链接失败 | 依赖库缺兼容产物 | 检查pubspec中是否使用ohos适配包 |
| JAVA内存溢出 | 构建产物过大 | 调整hvigor/构建工具的堆内存参数 |
5.2 运行期问题与生命周期陷阱
运行期的问题集中在两类:插件通道失败和生命周期异常。
插件通道失败最典型的表现是调用某个功能直接抛MissingPluginException。原因是该插件只有Android/iOS实现,没有OHOS端原生实现。排查方法很简单——看插件名有没有ohos适配版。这里的经验之谈是:在pubspec.yaml里依赖任何插件前,先去它的GitHub仓库看release列表里有没有ohos相关分支或版本标记。
生命周期陷阱主要体现在App切换到后台再恢复时,Flutter引擎的状态可能与预期不一致。我在测试时发现备忘录编辑页在切后台一段时间后恢复,偶发出现输入框失焦或内容没有同步的问题。后来定位到是didChangeAppLifecycleState的时机问题,最终通过监听inactive状态及时保存草稿解决了。
热重载失效也是OpenHarmony上Flutter的已知痛点。纯Dart修改偶尔生效,但只要动了原生层代码,热重载基本不工作。我的习惯是保持"小步快跑"的开发节奏,代码逻辑尽量收敛在Dart层,把原生依赖封装成独立插件,这样绝大多数改动都能通过普通热重载验证。
5.3 HDC调试与日志分析技巧
OpenHarmony的高效调试离不开hdc命令行工具。除了常规的设备列表和设备信息查询,我还常用它拉日志:
bash复制hdc shell hilog | grep flutter
Flutter自身的日志通过hilog输出后,配合grep过滤能快速定位Dart层的异常。如果能拿到崩溃现场的堆栈,hilog输出的内容往往比控制台信息更详细。
数据持久化调试也有讲究。SQLite数据库文件默认藏在应用沙箱目录,想看表结构或验证数据是否正确写入,先要找到db文件路径。在repository的初始化代码里打日志,或者用hdc的find命令定位:
bash复制hdc shell find /data -name "memo_app.db"
我实际开发中被这招救过好几次,比如数据库版本升级后字段没对上,直接拉出db文件用sqlite3命令手动查询,比反复加日志快得多。
注意:设备上的db文件权限限制很严,直接pull可能需要root权限。开发板一般可以切换root用户,但正式设备要提前规划好调试策略。
6. 打包发布与后续扩展建议
6.1 打包HAP与签名配置
当功能开发进入尾声,就涉及打包发布了。OpenHarmony应用的产物是HAP包,类似Android的APK。两种打包路径我都跑通过:一种是用DevEco Studio打开ohos目录,通过IDE的构建菜单完成打包签名;另一种是命令行方式,适合CI/CD场景。
无论用哪种方式,签名配置都是绕不开的一环。OpenHarmony的签名机制和Android类似,但证书体系和配置文件不同。我在第一次打包时忽略了签名配置,导致安装到设备上直接报错,排查了半天发现只是证书没配对。建议提前在DevEco Studio里生成好调试证书和发布证书,并确认profile文件与应用的bundleName一致。
打包完之后,可以用hdc直接安装验证:
bash复制hdc install entry-default-signed.hap
安装成功后,配合hilog日志做冒烟测试,重点检查数据库读写、页面跳转、搜索排序这几个核心路径是否正常。
6.2 后续扩展:提醒推送、云同步与桌面卡片
备忘录功能跑通后,我给自己列了一份扩展清单,按优先级排序接下来可以做三件事。
第一是本地提醒通知。备忘录加时间提醒是很自然的需求,但OpenHarmony上的通知渠道通知能力还不算完善。我的计划是先调研系统AlarmManager能力的适配插件,不行就落在应用前台时的本地提醒,后台提醒功能谨慎排期。
第二是云同步。当前数据完全存在本地,换设备或重装App数据就丢了。同步方案里,SQLite数据同步优先选择增量同步策略,需要设计同步协议和冲突解决逻辑。这个功能工作量不小,但用户价值极高,算是一个中期的核心课题。
第三是桌面卡片。OpenHarmony的卡片服务在原生应用上体验很好,如果Flutter应用也能支持卡片展示备忘录内容,会让整个App的竞争力上一个台阶。不过这涉及Flutter层和OpenHarmony卡片服务的桥接,技术路径还需要继续验证。
最后再分享一个小技巧:在OpenHarmony这种快速演进的生态里做开发,一定要盯紧官方仓库的release说明和issue列表。很多适配问题不是你的代码写错了,而是生态本身在变。与其死磕一个方案,不如保持灵活,及时跟上版本迭代的节奏。我在做这个备忘录功能的过程中就经历过两轮插件版本升级,每一次升级都带来新的构建方式调整,但核心的Dart业务代码几乎没有改动,这恰恰说明Flutter for OpenHarmony的架构方向是对的。
