从安卓到鸿蒙开发:那些差点把我劝退的坑
这段时间把一个偏生活工具的小应用从 Android/Flutter 的思路搬到 HarmonyOS 上,做了任务、备忘录、记账、地图足迹、长图导出、提醒、OCR 识图收纳、底部沉浸导航这些功能。
整体感受是:鸿蒙不是 Android 换皮,也不是 Flutter 的另一套 UI 皮肤。它的 ArkTS、权限模型、图库写入、通知提醒、深色模式、组件截图、上架审核都更“规矩”,很多在 Android 上顺手的事情,到了 HarmonyOS 上需要换一种写法。
这篇不是教程大全,只记录我实际踩过的坑,以及最后比较稳的处理方式。
本文基于 HarmonyOS / DevEco Studio / ArkTS 的实战经验整理。SDK、API 和审核规则会变化,真正上架前一定要以当前 DevEco、AppGallery Connect 和华为开发者文档为准。
一、SDK 路径和构建环境别靠猜
最开始我遇到过 DevEco 预览器正常,但命令行编译报:
1 | SDK component missing |
后来发现本机 SDK 并不在默认位置,而是在类似:
1 | $env:DEVECO_SDK_HOME='D:\harmony\harmonyos\DevEco Studio\sdk' |
命令行构建要显式指定 SDK:
1 | $OutputEncoding = [System.Text.UTF8Encoding]::new() |
避坑建议:
- DevEco 预览器能跑,不代表命令行构建环境一定正确。
- 换电脑、换 SDK 路径、换 DevEco 版本后,先跑一次
hvigorw.bat assembleApp。 - Windows 终端里最好设置 UTF-8 输出,不然中文日志和报错容易看着像乱码。
- 如果审核要求 API 6.1.0(23),要确认工程配置、签名证书、SDK 能力和实际构建目标一致。
二、鸿蒙的权限比 Android 更像“场景申请”
Android 开发里很多权限像是“我声明了,运行时弹一下,用户同意就能用”。鸿蒙这边更强调:你为什么需要、在哪个场景用、用多久、用户是否明确触发。
我踩过的典型问题:
1 | failed to install bundle. code:9568289 |
还有:
1 | ohos.permission.SHORT_TERM_WRITE_IMAGEVIDEO |
以及审核时提示:
1 | 软件包 user_grant 类型权限与隐私政策中声明的权限不一致 |
这类问题不能只靠“把权限加进 module.json5”解决。尤其是保存图片到图库,最好优先使用系统给的受控入口,比如 SaveButton,让用户明确点击“保存图片”后获得短时授权。
我的处理方式:
- 保存图片按钮使用系统
SaveButton。 - 点击成功后立刻执行图库保存,不再中间弹自定义头像/用户名窗口。
- 用户名和头像改放到“我的”页提前设置,避免短时授权过期。
- 不需要的图片/视频权限不要乱写进配置。
- 隐私政策里声明的权限,要和包里实际申请的权限一致。
和 Android 的差异:
Android 常见做法是申请 READ_MEDIA_IMAGES、WRITE_EXTERNAL_STORAGE 或走系统选择器。HarmonyOS 更推荐“用户主动触发 + 系统授权控件 + 受控写入”。如果你在保存前弹了一个自己的设置窗口,短时授权可能已经不是原来的那次用户动作了。
三、深色模式不是“能看见”就算适配
喵球第一次被审核打回,有一条就是深色模式输入框文字看不清:
- 任务 - 新建任务:输入框文字看不清
- 推录 - 备忘录/记账 - 新建:输入框文字看不清
- 地图 - 编辑足迹/省份列表 - 点击标记:输入框文字看不清
这类问题的本质是:系统进入深色模式后,TextInput、TextArea、弹窗背景、placeholder、caret、文字颜色如果没有明确设置,就可能被系统默认颜色影响。
我的修法:
1 | TextInput({ placeholder: '标题', text: this.taskDraftTitle }) |
类似地,弹窗和卡片也要明确设置背景色、文字色、边框色。不要以为浅色模式好看,深色模式就会自动好看。
避坑建议:
- 所有输入控件都显式设置
fontColor、placeholderColor、caretColor、backgroundColor。 - 弹窗、底部 Sheet、列表卡片不要依赖默认背景。
- 上架前在真机切深色模式跑一遍核心流程。
- 浅色模式也要检查对比度,审核会抓控件坐标和对比度。
四、状态刷新要主动设计,不能指望“数组改了就全刷新”
从 Flutter 过来,很容易带着 setState 的思维:我改了数组,UI 应该马上刷新。
ArkUI 当然也会响应状态变化,但我实际遇到过这些问题:
- 地图标记“想去/去过”后,上方统计数字不刷新。
- 记账保存后,下面列表更新了,但上面的收入支出卡片没更新。
- 备忘录总数卡片不刷新。
- 首页今日任务图表要重开 App 才刷新。
- 任务状态从待开始变进行中,需要切页才刷新。
最后我的做法是给重要业务区加“刷新 tick”:
1 | taskStatsTick: number = 0; |
每次保存任务、完成任务、删除任务、保存备忘录、保存地图标记后,主动:
1 | this.taskStatsTick += 1; |
有些统计函数里也会读一下 tick,让它成为 UI 依赖:
1 | const reactiveTick = this.recordStatsTick; |
这看起来有点“土”,但对复杂页面很管用。
和 Flutter 的差异:
Flutter 里你可能更习惯 setState()、Provider、Riverpod、Bloc。HarmonyOS ArkTS 里如果页面由很多 @Builder、数组派生统计、多个 Tab、多个弹窗组成,最好一开始就把“哪些动作会刷新哪些区域”设计清楚。
五、持久化别忘了,预览器里的数据不是魔法
我一开始也犯过很普通的错:页面看着能添加任务和备忘录,但重新打开 App 数据没了。
原因很简单:只存在 @State 里,没有落盘。
后来改成统一的快照结构:
1 | interface AppStateSnapshot { |
关键动作后统一 persistAppData(),启动时读取并 applyAppStateSnapshot()。
避坑建议:
- 不要等功能快做完才补持久化。
- 数据结构要考虑后续升级,字段读取时给默认值。
- 导入/导出 JSON 很适合做工具 App 的兜底能力。
- 每次修改数据模型后,用旧数据启动一次,确认不会崩。
六、图库保存和系统分享要分开处理
备忘录长图导出是我踩坑最多的功能之一。
遇到过的问题:
- 导出图片失败。
- 保存后提示成功,但图库里找不到。
- 系统分享后一直显示“分享中”。
- 生成的图片底部大片空白。
- 九张图时导出图片正文消失。
- 预览页图片删了但 UI 还留着旧图。
最后拆成几件事处理:
1. 保存图库走 SaveButton
用户点击系统保存按钮后,马上执行保存,不要中间再弹自定义设置弹窗。
2. 分享状态一定要 finally 复位
1 | try { |
否则分享失败或取消后,页面会一直卡在“分享中”。
3. 导出图片的 UI 不一定适合直接复用页面 UI
我一开始把阅读页九宫格直接拿去截图。页面上看着没问题,但离屏截图时 Grid 高度不稳定,九张图会把正文、关联地图、水印挤出画面。
最后改成:
- 阅读页可以用漂亮的九宫格。
- 导出图使用固定尺寸的
Flex九宫格。 - 导出高度单独估算。
- 图片区域、正文、标签、关联信息、水印都预留高度。
类似这样:
1 | private recordShareSnapshotHeight(record: RecordItem, includeImage: boolean): number { |
核心教训:页面组件和导出组件最好分开。
导出图片是“排版生成”,不是“把当前页面截一下就完事”。
七、九宫格图片别只看预览器
备忘录图片最多九张,我遇到过:
- 编辑页删除图片,图片没有消失。
- 继续添加图片后,仍显示旧图。
- 预览页从九宫格变成两列。
- 其中一张图片突然空白。
最后的处理方式:
- 编辑页图片数组统一走
setRecordDraftImages()。 - 删除图片按下标删,不按 URI 删。
- 图片组件 key 里加刷新 tick。
- 普通卡片九宫格和导出九宫格使用不同尺寸。
- 卡片内宽不足时,不要固定太大的单元格。
比如普通预览用更稳的尺寸:
1 | this.MemoImageGrid(this.recordImageUris(record), record.id.toString(), 86) |
导出图再用更大的:
1 | this.MemoImageGrid(this.recordImageUris(record), record.id.toString(), 102, false) |
和 Flutter 的差异:
Flutter 里 GridView.count(crossAxisCount: 3) 对尺寸约束比较直观。ArkUI 里如果你在不同容器、不同 Sheet、离屏截图里复用同一个 Grid,最好确认组件的实际宽高,否则就会出现“页面上还行,导出炸了”的情况。
八、提醒功能不是普通通知那么简单
任务提醒是另一个大坑。
普通通知权限给了,不代表定时提醒一定能成功。尤其是后台、息屏、系统限制、代理提醒能力、审核场景说明,都要考虑。
我后来申请提醒能力时,需要准备类似这些信息:
- 使用场景:任务到时间提醒用户。
- 申请理由:工具 App 不联网,需要本地按计划提醒。
- 通知标题:任务标题。
- 通知文本:任务备注或开始时间。
- 通知场景:用户主动创建任务并设置提醒时间。
- 通知频率:由用户创建任务数量决定,不做营销推送。
避坑建议:
- 不要把“通知权限已授权”等同于“定时任务一定会弹”。
- 提醒类能力要看当前 HarmonyOS 推荐的代理提醒方案。
- 删除任务时同步删除提醒。
- 保存任务时同步注册提醒。
- 给用户显示明确状态:已开启、注册失败、系统限制等。
和 Android 的差异:
Android 里常见是 AlarmManager、WorkManager、前台服务、通知渠道。HarmonyOS 更强调系统代理提醒和使用场景合规,审核也会关心是不是必要、是不是用户主动触发、频率是否合理。
九、底部导航和系统滚动条也会影响观感
我一开始自己做底部导航,后来换成更接近官方的 Tabs / BottomTabBarStyle 思路,再叠加沉浸光感玻璃效果。
但又遇到一个细节:五个页面右侧都会出现系统滚动条,工具 App 的卡片式页面看起来很突兀。
处理方式是在主页面 Scroll 上关闭滚动条:
1 | Scroll() { |
避坑建议:
- 底部栏尽量用系统推荐组件或相近结构。
- 沉浸/玻璃效果要真机看,预览器不一定准。
- ScrollBar、状态栏、导航栏区域都要单独检查。
- 不要只看单页,要切五个 Tab 看整体一致性。
十、上架审核和 Android 最大的不同
Android 应用上架当然也有审核,但 HarmonyOS 审核给我的感觉是:UX 规范、权限声明、深色模式、对比度、功能即时刷新这些细节会被抓得很具体。
我遇到过的审核反馈包括:
- 深色模式输入框文字看不清。
- 浅色模式文字和背景对比度不足。
- 地图“想去”却计入“去过次数”,逻辑不符合预期。
- 修改标记后需要切换页面才刷新。
- 保存图片失败。
- 图片预览只显示第一次添加的图片。
- 系统分享后一直显示分享中。
- user_grant 权限与隐私政策不一致。
- 使用 beta API 构建。
这些问题在开发时看起来都不算大,但审核时就是会被打回。
我的上架前检查清单:
- 浅色模式完整跑一遍。
- 深色模式完整跑一遍。
- 真机跑,不只看预览器。
- 所有新增、编辑、删除后统计卡片立即刷新。
- 所有弹窗输入框文字清晰。
- 权限声明和隐私政策一致。
- 保存图片、分享、导入导出 JSON 都实际操作一遍。
- 关闭后台、重启 App 后数据还在。
- 删除任务后提醒也删除。
- 地图“想去”和“去过”统计不能混。
- 预览页和导出图都要测长文字、多图片、关联地图。
十一、从 Android/Flutter 迁移到鸿蒙的思维变化
如果一句话总结:
Android/Flutter 更像“我来掌控应用”,HarmonyOS 更像“在系统规范里完成体验”。
几个明显差异:
| 方面 | Android / Flutter 常见思路 | HarmonyOS / ArkTS 更稳的思路 |
|---|---|---|
| UI 状态 | setState 或状态管理刷新 |
@State + 明确刷新 tick + 派生数据依赖 |
| 图片保存 | 权限或 MediaStore | 系统 SaveButton / PhotoAccessHelper / 用户触发 |
| 通知提醒 | AlarmManager / WorkManager | 通知权限 + 代理提醒能力 + 场景申请 |
| 页面截图 | 截 Widget / RepaintBoundary | 组件快照要注意图片加载和离屏布局高度 |
| 深色模式 | 主题适配为主 | 输入框、弹窗、卡片最好显式颜色 |
| 上架审核 | 权限和内容为重点 | UX、对比度、功能即时性、权限一致性都很细 |
| 本地文件 | 路径和 SAF | Picker、短时授权、沙箱和用户选择更重要 |
| 底部导航 | 自定义很常见 | 尽量贴近系统组件和 UX 规范 |
十二、我现在会怎么从零开始做
如果重新做一次,我会这样安排:
- 先确定目标 API、SDK、签名证书和命令行构建。
- 先搭浅色/深色颜色体系,不等审核打回再补。
- 数据模型和持久化第一天就做。
- 每个业务模块设计刷新 tick。
- 所有保存、删除、编辑都马上刷新统计卡片。
- 图片选择、九宫格、导出图从一开始就分页面组件和导出组件。
- 保存图库只走用户明确触发的系统入口。
- 提醒功能先查当前官方方案,再写代码。
- 真机测试优先级高于预览器。
- 上架前按审核视角做一次“找茬测试”。
最后
鸿蒙开发并不难到不能做,但它确实不是把 Android/Flutter 代码翻译一遍就结束。
它最容易让人崩溃的地方,不是某个 API 不会用,而是很多小细节叠在一起:权限、深色模式、状态刷新、截图、图库、提醒、审核规范。每个单看都不大,凑在一起就很磨人。
但反过来,如果一开始就按鸿蒙的规则设计:用户明确触发、系统受控授权、UI 状态即时刷新、深色模式显式适配、导出组件独立排版,那么这个平台其实可以做出挺细腻的工具应用。
喵球这次一路修下来,我最大的感受是:别和系统拧着来,顺着它的规范做,反而更稳。
