☰
Flutter跨平台开发鸿蒙笔记应用:从环境搭建到实战适配全记录
2026/10/22 3:30:17 网站建设 项目流程

前几天有读者来问:考研复试的资料越攒越多,常见笔记软件用着总觉得不对味,想自己做一个专门管理复试知识点的App,又不想学两套原生开发,该从哪儿下手?我的回答很直接:用Flutter,一套代码同时覆盖安卓和鸿蒙。这篇文章就是我实际完成一个考研复试笔记应用的全记录,重点放在跨平台方案选型、核心功能实现、以及在鸿蒙平台上从编译到上手的适配过程。整个项目做完,最大的体会就是:跨平台开发的真正难点不在UI怎么写,而在平台之间那层看不见的差异。适合正好有这个想法、或者准备把应用迁移到鸿蒙生态的开发者参考。

1. 项目背景:为什么用Flutter做鸿蒙笔记应用

1.1 考研复试笔记应用的功能需求

先捋清楚这个应用到底要解决什么问题。考研复试和初试最大的区别是信息高度分散,专业课笔试范围、英语口语问答、综合素质面试、导师研究方向,每一种资料的形式都不一样,而且复习场景经常是图书馆、自习室、通勤路上,网络条件并不稳定。

所以这个笔记应用的核心需求很明确:第一,录入要快,打开App到写完一条笔记不能超过十秒;第二,分类要灵活,既能按复试科目归档,又能跨科目打标签;第三,必须有离线能力,没有网络时所有功能照常可用;第四,界面要克制,复习的时候不需要花哨的动效,重点是信息密度和数据检索效率。

如果只做一个端,这个需求在安卓上用原生开发也能完成,但我的实际使用场景是两台设备,一台属于安卓生态,另一台属于鸿蒙生态。这就把问题从“怎么实现功能”变成了“怎么用一套代码同时服务于两个平台”。

1.2 跨平台方案选型与鸿蒙适配现状

跨平台框架在市面上有不少,我重点比较了Flutter和其他主流方案。当时考虑的几个维度:UI一致性能到什么程度、原生能力能不能覆盖、鸿蒙平台的适配进度、以及社区活跃度。

Flutter的优势在于UI是自绘渲染,说白了就是同一套像素绘制逻辑在安卓和鸿蒙上不会出现明显的控件风格差异。这一点在做笔记类工具时很关键,因为文本渲染、滚动惯性、输入框交互这些细节,如果依赖系统控件,两个平台很容易出现观感割裂。

再就是鸿蒙生态对Flutter的适配。从工具链层面,新版本的Flutter SDK已经内置了对ohos平台的支持,可以像创建安卓包一样直接构建出鸿蒙安装包。这意味着不需要把整个工程推倒重来,只需要在原有Flutter工程基础上增加一个鸿蒙平台目录,再处理一些平台相关的插件兼容问题。

我最终选择Flutter,还有一个原因是纯Dart实现的技术栈迁移成本低。笔记应用的存储层我用的是纯Dart写的本地持久化方案,这样在鸿蒙端几乎不需要碰原生代码,省掉了很大一部分适配工作量。

2. 环境搭建:Flutter鸿蒙开发工具链全流程

2.1 版本匹配关系

做鸿蒙开发最怕的就是版本不配套,环境问题能卡住半天进度。我这次踩完坑之后整理了一张版本匹配表,建议按这个组合来搭建,都是实测能稳定跑通的组合。

组件建议版本说明
Flutter SDK3.29.x 及以上内置ohos平台支持,无需额外补丁
DevEco Studio5.0.x 及以上用于打开鸿蒙工程、签名、查看真机日志
OpenHarmony SDKAPI 12 及以上编译时需要的系统接口库
Java 环境17DevEco和构建工具链依赖

安装顺序也很重要。先把DevEco Studio装好,它会顺带把OpenHarmony SDK下载到本地。然后再配置Flutter SDK,最后在环境变量里显式指定OpenHarmony SDK的位置。如果顺序反过来,Flutter命令可能找不到SDK路径,构建阶段会莫名其妙报错。

2.2 初始化一个带ohos平台的Flutter工程

在旧版本里要让Flutter支持鸿蒙,需要手动引入第三方适配工具链,现在简单多了。确认版本没问题之后,先执行一次配置命令启用ohos平台支持:

flutter config --enable-ohos

然后就可以用常规方式创建工程。我建议在创建时指定项目名和组织的名称,避免包名冲突:

flutter create --project-name review_notes --org com.example review_notes

这里的project-name会直接成为鸿蒙侧的模块名,org则对应应用包名,如果希望后续能顺利上架,这里要提前想好命名规则。

创建完成后,直接运行设备检测指令,确认鸿蒙设备能被识别:

flutter doctor

如果看到ohos一栏显示设备在线,说明环境已经通了。接着用下面这条命令,直接在一台鸿蒙真机或模拟器上启动应用:

flutter run -d ohos

第一次构建特别慢,因为要生成鸿蒙侧的中间产物,我实测在配置一般的电脑上可能要等五到十分钟,这不是卡死了,耐心等就行。后续增量构建会快很多。

2.3 工程结构里多了什么

创建完工程后,你会看到原来熟悉的lib、android、ios目录旁边多了一个ohos目录。这个就是鸿蒙端的原生工程壳子,作用类似于安卓的android目录。

打开ohos目录,里面的关键文件有这么几个。先是oh-package.json5,它声明了鸿蒙侧的依赖包,类似于pubspec.yaml的作用。然后是entry/src/main/module.json5,对应安卓里的AndroidManifest.xml,应用的模块配置、权限声明都在这里改。再往下一层是entry/src/main/ets/pages,里面是鸿蒙原生的页面入口文件。通常情况下不需要手动改这些文件,但权限声明和启动配置例外。

还有个容易忽略的地方:ohos目录下还有一个AppScope/app.json5,它定义的是整个应用级别的信息,比如应用名称、图标、版本号。而entry下面那份module.json5管的是模块级配置。如果上架后发现应用名称不对,多半就是改错了文件。

3. 笔记应用的数据设计与本地存储实现

3.1 笔记模型设计

先想清楚一条笔记要承载哪些信息,再动手写代码。我把复试笔记抽象成这样一个数据模型:

class ReviewNote { final String id; String title; String content; String folderId; List<String> tags; bool starred; int createdAt; int updatedAt; ReviewNote({ required this.id, required this.title, required this.content, required this.folderId, required this.tags, this.starred = false, required this.createdAt, required this.updatedAt, }); Map<String, dynamic> toJson() { return { 'id': id, 'title': title, 'content': content, 'folderId': folderId, 'tags': tags, 'starred': starred, 'createdAt': createdAt, 'updatedAt': updatedAt, }; } factory ReviewNote.fromJson(Map<String, dynamic> json) { return ReviewNote( id: json['id'], title: json['title'], content: json['content'], folderId: json['folderId'], tags: List<String>.from(json['tags']), starred: json['starred'], createdAt: json['createdAt'], updatedAt: json['updatedAt'], ); } }

字段设计看起来简单,但每个都是实际使用逼出来的。比如folderId,每条笔记只属于一个文件夹,避免分类混乱。tags则是列表,允许一条笔记同时挂上多个跨维度标签,比如一篇关于“操作系统”的笔记,既可以属于“专业课”,又带“高频考点”的标签。starred字段用来做重点标记,复试前几天就只刷星标笔记。

时间戳我用的是从1970年1月1日开始计算的毫秒级整数,而不是字符串。这样做的原因是排序非常方便,收藏列表和时间线能够直接按照数字大小倒序排列,不需要做字符串解析。

3.2 存储方案对比:Hive与数据库

做本地存储时,我最初考虑两种方案:一种是用SQLite数据库,另一种是用Hive,一种基于Dart的键值型本地存储。最终选了Hive。

为什么放弃数据库?因为复试笔记的数据量撑死几千条,还都是单机使用,完全没有复杂事务和关联查询的需求。引入SQLite意味着要在安卓端和鸿蒙端分别处理原生插件的适配,编译成本和出错概率都上去了。Hive是纯Dart实现,理论上只要Dart运行时能跑,它就能跑,所以跨平台兼容性非常好。

Hive需要为自定义类型写适配器,或者直接把数据序列化为JSON字符串再存储。我的做法是直接把ReviewNote转成JSON字符串,存到一个Hive的Box里,读取时再反序列化回对象。这样写起来最直白,也不容易踩类型适配器的坑。

初始化逻辑如下:

class NoteStorage { late Box _box; Future<void> init() async { final dir = await getApplicationDocumentsDirectory(); Hive.init(dir.path); _box = await Hive.openBox('review_notes'); } Future<void> saveNote(ReviewNote note) async { await _box.put(note.id, jsonEncode(note.toJson())); } Future<ReviewNote?> getNote(String id) async { final raw = _box.get(id) as String?; if (raw == null) return null; return ReviewNote.fromJson(jsonDecode(raw)); } }

这里有个细节要注意。getApplicationDocumentsDirectory()这个函数在不同平台上返回的路径完全不同,在鸿蒙上尤其特殊。我后面会单独讲这段踩坑经历,这里先记住一个原则:不要自己去拼文件路径,永远用path_provider方案生成。

3.3 文件夹、标签和星标的关系

数据结构设计完以后,怎么用一个界面把这三层关系呈现出来,也值得单独想想。我把主界面做成了三栏结构,由外到内依次是文件夹列表、笔记列表、笔记详情。

文件夹这一层是硬分类,一个文件夹就是一个复试科目。它的数据量很少,所以用一张单独的表按名称排序。标签是软分类,不单独建文件夹,而是作为一个筛选条件。星标则完全独立,跟文件夹和标签都不冲突。

实际复习时的典型检索路径是这样的:我打开App默认进入“专业课”文件夹,里面是最近编辑过的笔记;如果正在集中看某一章,就点开对应标签做二次过滤;冲刺阶段则直接切到星标列表,按更新时间倒序刷。

这样设计之后,用户永远只需要两步操作就能定位到想要的笔记,整个App的信息架构也比较清爽。

4. 核心功能模块的开发实录

4.1 笔记列表与多选操作

笔记列表是这个应用的门面,流畅度和交互效率直接影响使用心情。我用的基础组件是ListView.separated,每一行显示笔记的标题、所属文件夹名称、标签缩略和更新时间。其中标题用大号字体加粗,更新时间用浅灰色小字放在右下角,保证视觉重心始终在内容上。

为了照顾移动端的操作习惯,我加了两套列表交互。第一套是单条滑动删除,用Dismissible组件实现,向左滑动会出现删除按钮,确认后从存储和列表中同时移除。第二套是长按进入多选模式,选中多条之后可以统一打标签或批量移动到其他文件夹,这个功能在整理笔记时特别实用,比一条一条改效率高很多。

多选模式下有一个交互细节比较容易忽略:当列表处于编辑状态时,必须禁掉单击进入详情的操作,否则就会出现选中一条笔记的时候突然跳转到编辑页的情况。我用一个布尔值控制列表项的点击行为,进入多选模式后单击只切换选中状态,退出后才恢复原来的跳转逻辑。

4.2 编辑页与Markdown预览

笔记编辑页是整个应用使用频率最高的页面,所以我把性能优化放在首位,而不是功能堆砌。编辑区域直接用TextField,设置maxLines: null让它随内容自动增高,配合Expanded撑满剩余屏幕空间。为了内容足够安全,我又加了一个自动保存机制:每停顿两秒自动写入本地存储,不需要用户手动点击保存按钮。这是使用体验上最关键的一个设计,复试复习时注意力完全在内容上,根本不应该被“保存”这个动作打断思路。

笔记内容支持Markdown,因为复试笔记里经常需要写算法伪代码、英文面试稿、专业术语表这类结构化的内容。我引入了一个纯Dart的Markdown解析库,在编辑页顶部加了一个预览切换开关。预览页用MarkdownBody渲染,代码块自动高亮,表格也能正常展示。这样就能在同一套工程里,不用引入浏览器控件就完成轻量级Markdown预览。

这里有个性能相关的技术点:如果笔记内容特别长,比如超过一万字,一次完整的Markdown解析耗时会明显上升。我的处理方式是只在预览模式开启时执行解析,平时编辑模式完全不做渲染。用户从预览切回编辑时,解析结果直接丢弃,下次进入再重新计算,这样避免了一边打字一边做实时解析的性能浪费。

4.3 搜索:更像一个复习检索工具

考研复试笔记有一个特有场景,就是你在图书馆里突然想到一个概念,但只记得大概读音或某一个词,需要把相关笔记全部捞出来。这种场景下搜索功能必须做到标题和正文同时匹配,而不是只查标题。

我实现了一个轻量级的本地全文搜索。数据量不大,所以没有引入搜索引擎方案,直接在内存里遍历所有笔记,对标题和正文做字符串匹配:

List<ReviewNote> searchNotes(String query) { final lowerQuery = query.toLowerCase(); return allNotes.where((note) { return note.title.toLowerCase().contains(lowerQuery) || note.content.toLowerCase().contains(lowerQuery); }).toList(); }

这个写法简单到有点笨,但对于几千条笔记来说已经够用了,实测搜索响应在毫秒级。真正的体验提升在于搜索结果页的展示方式:关键词在标题和正文里用高亮颜色标出,用户不用打开笔记也能快速判断是否是自己要找的内容。

搜索页还加了一个筛选栏,可以限定在某个文件夹内搜索,或者只搜星标笔记。这个筛选栏不是装饰品,复试复习时“只搜星标”这个操作基本每天都要用好几遍,直接决定了搜索的效率。

4.4 数据导入导出与跨设备备份

笔记应用做到后期,一个最现实的需求冒出来了:数据安全和设备迁移。因为本地存储的数据都在应用沙箱里,一旦卸载应用或者换手机,所有笔记都会消失。考研复试的笔记价值比普通备忘要高得多,必须留一条备份后路。

我的方案是提供一个文件导出功能,把全部笔记序列化为JSON文件,通过系统分享面板发送到其他地方。导出格式如下:

{ "version": 1, "exportedAt": "2025-03-01T10:00:00Z", "notes": [ { "id": "note_001", "title": "操作系统进程调度算法", "folderId": "os", "tags": ["高频考点"], "starred": true } ] }

导出文件不包含笔记正文,目的是保护一些比较私密的复习内容,也可以改成完整导出,看个人需求。导入时对应增加一个解析入口,把同格式的JSON文件读回来写入本地存储。这种做法的实现成本不高,但给用户带来的安全感非常明显,属于性价比很高的功能。

5. 鸿蒙平台适配要点与踩坑记录

5.1 依赖插件需不需要专门换

Flutter工程跑在鸿蒙上,第一个需要面对的问题就是第三方插件能不能用。由于ohos平台起步比安卓晚,很多老牌插件的官方版本并不直接支持鸿蒙。我在做这个笔记应用时主要涉及三类插件:文件路径获取、目录访问、系统分享。

文件路径获取用的path_provider,这个插件在鸿蒙端有专门的适配版本,在pubspec.yaml里需要确认引入的版本包含ohos支持。如果版本太老,运行时会直接抛MissingPluginException错误,一眼就能看出是插件没适配。

分享文件到其他应用这个功能,我最初找了一圈没有找到理想的鸿蒙适配版本,于是换了一种更轻量的方案:不调用系统分享面板,而是把导出的JSON文件写入到应用沙箱之外指定的公共目录,让用户通过系统文件管理器自行查看并分享。当然了,这会受到鸿蒙沙箱权限限制,所以我退了一步,把导出功能变成“复制到剪贴板”加“保存到本地指定文件夹”,实测在鸿蒙真机上可以跑通。

这里给个经验准则:不要指望所有插件都能在鸿蒙上原样运行,优先选择纯Dart实现的库,原生桥接越少的插件适配风险越低。

5.2 文件路径与沙箱目录的差异

鸿蒙和安卓的系统架构不同,应用存储区域的路径逻辑也差别很大。安卓的getApplicationDocumentsDirectory()返回路径类似/data/user/0/包名/app_flutter,而鸿蒙返回的是基于应用沙箱的区域路径,实际目录形如/data/storage/el2/base/haps/entry/files。

这个差异带来的影响是:如果你在代码里硬编码了任何路径拼接逻辑,跨平台后一定会出事。比如直接用字符串拼接一个名为notes_backup.json的文件绝对路径,在安卓上能工作,在鸿蒙上可能落到一个没有写权限的目录里,写入时直接报权限错误。

解决办法很简单——永远通过path_provider获取基础目录,再用平台的路径拼接方法:

final baseDir = await getApplicationDocumentsDirectory(); final file = File(p.join(baseDir.path, 'review_notes_export.json'));

另外还要注意一个细节,鸿蒙的沙箱目录在应用更新时可能会变动,所以不要把绝对路径硬编码到持久化数据里。如果你需要存一条“上次导出的文件位置”,应该在每次启动时重新获取基础路径再拼接。

5.3 权限申请的配置差异

鸿蒙的权限模型和安卓不完全一样,权限声明需要同时在原生配置和Flutter侧代码里处理。

先讲原生配置。权限集中在entry/src/main/module.json5文件里声明,用requestPermissions字段添加。比如笔记应用如果需要读取图片来插入笔记封面,就要加上:

{ "name": "ohos.permission.READ_IMAGEVIDEO", "reason": "用于在笔记中插入本地图片", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }

安卓里声明权限是在AndroidManifest.xml里写<uses-permission>标签,如果项目同时跨两个平台,很容易忘记鸿蒙这边也要同步声明。我踩过的坑是:安卓端运行正常,鸿蒙端一点权限提示都没弹,直接静默失败,查了半天才发现是module.json5里根本没有声明权限。

权限弹窗的触发时机也有差异。鸿蒙系统会结合usedScene的配置智能决定是否在应用运行时弹出授权框,所以即便你调用了系统API,也不一定每次都能看到权限弹窗。调试时如果发现拿不到权限,先回module.json5里把when改成always再试一次。

5.4 打包构建hap与真机调试

鸿蒙应用的产物格式是hap包,完整流程是先在Flutter工程目录下执行构建命令:

flutter build hap --release

构建完成后,生成的hap包位于build/ohos/目录下,有些版本会继续细分到entry/build/default/outputs路径下。把这个hap包安装到鸿蒙手机,需要用到鸿蒙调试工具hdc:

hdc install -r build/ohos/entry/build/default/outputs/default/entry-default-signed.hap

真机调试前,一个关键前置动作是签名。如果设备没有配置开发者证书,要使用DevEco Studio打开ohos目录,在工程配置项里完成自动签名。自动签名会绑定你的开发者账号,生成对应的证书和配置文件。这一步不做,hap包即使构建成功也无法正常安装到真机上。

调试过程中遇到最频繁的问题是热重载在鸿蒙端的支持不够稳定。Flutter在安卓上的热重载体验已经很成熟了,但在鸿蒙上,修改Dart代码后按r键刷新,有时候UI没有变化,有时候直接白屏。这种情况下我一般先试热重载,不行就切换到整机重启(大写的R键),再不行就重新flutter run。虽说不那么优雅,但基本能解决九成问题。

6. 常见问题速查:构建、调试、性能

6.1 构建期报错怎么排查

运行flutter build hap时,我遇到过的报错大致可以分成三类。第一类是环境类报错,提示找不到OpenHarmony SDK,大概率是环境变量没配置,打开系统环境变量检查一下OHOS_SDK_HOME是否指向了DevEco Studio实际安装的SDK路径。

第二类是编译类报错,错误信息里会出现类似hvigor这样构建工具的关键词,通常是Java版本不匹配或者工程缓存的问题。先检查Java是否为17版本,然后执行一次彻底的清理:

rm -rf build ohos/.hvigor ohos/.idea flutter clean flutter pub get

第三类是代码兼容性报错,Dart代码里用了某个不支持鸿蒙的API。这类问题在报错信息里通常能直接看到文件名和行号,对照修改即可。如果报错信息很模糊,就把flutter build hap -v加上,用详细模式重新构建,定位问题会更准确。

6.2 真机调试与签名的坑

首次连接鸿蒙真机时,电脑上执行hdc list targets如果看不到设备,先检查开发者模式是否打开。鸿蒙的开发者模式开启路径和安卓不完全一样,需要在系统设置中打开开发者选项,并允许USB调试。

签名这块还有一个容易让人困惑的地方:DevEco Studio自动签名生成的证书是有有效期的,一般是三个月到一年不等。过期之后,构建出的hap包在真机上会报签名校验错误,出现“安装失败”,这种情况不要怀疑代码有问题,直接去DevEco Studio里重新生成一次签名配置即可。

调试时如果发现某些功能在真机上和模拟器上表现不一致,优先怀疑权限弹窗差异和文件路径差异。模拟器的沙箱目录结构可能和真机不同,导致读取存储文件时不兼容,这种问题在鸿蒙平台上尤其常见。

6.3 性能与发热优化

笔记应用本身不算重负载应用,但在真机测试初期确实出现过滚动卡顿和机身发热的情况。主要原因出在列表每个Item的构建逻辑过于复杂,每次刷新都重建了很多不必要的小组件。

优化思路无外乎几个常规手段。尽量让列表项的分组和数量稳定,不要频繁插入移除;复杂文本渲染改用TextPainter测量尺寸而不是直接放进列表构建;搜索结果的实时高亮,在输入过程中防抖延迟二百毫秒。

另外一个鸿蒙平台特有的性能细节:在应用启动阶段,Flutter引擎初始化的耗时比安卓更长,导致冷启动会出现白屏。给用户感知更舒服的做法,是在鸿蒙原生入口页加一个启动占位图,让用户第一时间看到内容框架,等Flutter引擎就绪后再切换到真正的笔记界面。这个占位图不是必须,但做了之后体验差距很明显。

对于追求极致性能的开发者,可以考虑把纯Dart逻辑和UI进一步分离。比如把所有笔记数据的加载放后台Isolate处理,UI侧只做渲染。复试笔记这种数据量不高的应用,我实际测试后发现单线程已经够用,Isolate方案更多是为了学习技术细节,实际收益不大。

个人经验收尾

做这个考研复试笔记应用的过程中,我最大的收获不是学会了多少Flutter API,而是理解了跨平台方案在鸿蒙时代真正的定位。Flutter能把90%的逻辑和UI带到鸿蒙端,但剩下10%的平台差异,比如权限模型、路径约定、签名流程,才是最考验开发者功底的部分。

如果你也打算做类似的小工具,我的建议是从数据结构和存储方案先想清楚,再动手写页面。因为界面布局可以随时重做,但数据层一旦设计抽象了,迁到鸿蒙或其他平台时会特别吃力。

最后再分享一个小技巧:开发这类需要长期记录的个人工具,一定要把导出功能放在很显眼的位置,不要在设置里藏太深。考研复试复习三个月积累的笔记,可能比一整年的社交软件记录更有价值。数据备份、可迁移,这些都是实战中才会真正意识到很重要的能力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询