十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Flutter应用适配鸿蒙系统全流程实践与避坑指南

Flutter应用适配鸿蒙系统全流程实践与避坑指南 最近在把一个内部代号叫 Books 的电子书阅读应用从 Android 往鸿蒙系统上迁移过程比我预想中要“热闹”得多。Flutter 跨平台开发在 Android 和 iOS 上已经非常顺滑但鸿蒙并不是 Flutter 官方支持的目标平台这意味着“跨平台”三个字到了鸿蒙上先得打个问号。我最初以为只要把 Dart 代码原封不动编译一遍就能出包结果光环境适配就折腾了两天。这篇文章把 Books 在鸿蒙上的完整适配过程、踩过的坑、以及现在跑通的方案都整理出来给同样想用 Flutter 去碰鸿蒙的团队一个参考。如果你是那种想“一套代码全端跑”的 Flutter 开发者或者正在评估公司现有 Flutter 项目要不要接入鸿蒙这篇东西应该能帮你省下不少时间。文中涉及的知识点包括 Flutter 的环境安装与配置、鸿蒙工程改造、内嵌数据库选型、网络抓包调试、登录支付对接、打包签名以及 Flutter 产物反编译防护。我会尽量说清楚“为什么这么做”因为鸿蒙生态和 Android/iOS 的思路差别比很多人想象中大得多。1. 鸿蒙上的 Flutter 到底怎么跑起来环境与工程模板调整1.1 先把认知对齐Flutter 官方并不直接支持鸿蒙先说一个很多人容易忽略的事实你在 pub.dev 上看到的绝大多数 Flutter 插件官方并没有为鸿蒙做过适配。Flutter 官方支持的桌面端、移动端平台里没有鸿蒙这一项目前能跑在鸿蒙上的 Flutter主要靠 OpenHarmony 社区维护的 flutter_flutter 分支以及一票热心开发者移植的插件。Books 这个应用本身是一个典型的阅读器核心功能包括书库管理、EPUB/PDF 阅读、阅读进度同步、会员付费。从技术栈看它不算复杂Dart 代码里大部分是界面渲染和数据调度。可一旦进入鸿蒙你首先面对的不是“写代码”的问题而是“这套东西能不能编译成鸿蒙认识的文件”的问题。鸿蒙 NEXT 上市以后应用市场里上架的包基本是 HAP 格式不再兼容 APK所以简单拿安卓包硬塞进去这条路已经堵死。理解了这一点你就明白为什么要把“鸿蒙适配”当成一个独立工程来做而不是在现有 Android 工程上打几个补丁。Books 的迁移路径是Flutter 代码保持不动或尽量少动但构建目标里多出一个 ohos最终产出 HAP 包涉及原生能力的插件则逐个排查能换纯 Dart 实现就换换不了就走鸿蒙原生 MethodChannel。1.2 环境准备版本配对比想象中更挑Books 的迁移是从一套干净环境开始的。我强烈建议不要在自己日常开发机里直接改配置因为 Flutter 鸿蒙分支和官方 Flutter 版本的目录结构有差异共用一个 SDK 容易把两边都搞坏。我的做法是单独拉取 openharmony-sig/flutter_flutter 仓库到独立目录把它当作鸿蒙专用的 Flutter SDK。版本上我选了比较稳定的 3.x 系列配合 DevEco Studio 5.x 和对应的 HarmonyOS SDK整体配合还算顺滑。下面是我实际使用的环境清单可以照抄组件推荐版本/来源用途DevEco Studio5.x带 HarmonyOS SDK鸿蒙原生工程开发与签名HarmonyOS SDKAPI 12 或更高编译 HAP 所需系统库ohpm随 DevEco 安装管理鸿蒙原生依赖类似 pubhvigor随 DevEco 安装鸿蒙工程构建工具Flutter 鸿蒙分支openharmony-sig/flutter_flutter提供 ohos 构建目标Flutter 官方 SDK原版本另目录继续维护 Android/iOS环境变量方面先把鸿蒙 Flutter SDK 的 bin 目录加到 PATH 里然后运行 flutter doctor 看看识别情况。注意修改 PATH 之后一定要开一个新的终端窗口我在“flutter sdk 安装”“path 需要新终端生效”这两个问题上浪费了不少时间旧终端里敲 flutter 永远指到老版本。如果环境里还有 Android 构建需求ANDROID_HOME 也别删。鸿蒙工程虽然不依赖 Gradle但 Flutter 工具链的某些公共逻辑仍然会读取 Android SDK 路径。Books 团队里同时存在 Android 和鸿蒙两条产物流水线我就是让它们共用一个 HOME 目录按命令区分构建目标。1.3 Books 项目迁移的第一个大坑Gradle 插件报错迁移 Books 时遇到的第一个报错标题是“you are applying flutters main gradle plugin imperatively using the apply method”。这条报错在所有 Flutter 高版本项目中很常见原因是 Flutter 官方把 Gradle 插件推荐用法从“在 build.gradle 里直接 apply”改成了“在 settings.gradle 里用 pluginManagement 和 plugins 块声明式加载”。如果你还沿用老式写法只要环境里 Gradle 版本够新构建就会直接拒绝执行。鸿蒙 Flutter 分支在生成工程模板时有时候会把 Android 和鸿蒙两套构建脚本一起生成CI 里如果先触发 Gradle 检查这条报错会立刻跳出来。解决办法是把项目迁移到新的声明式写法// settings.gradle pluginManagement { def flutterSdkPath { def properties new Properties() file(local.properties).withInputStream { properties.load(it) } def flutterSdkPath properties.getProperty(flutter.sdk) assert flutterSdkPath ! null, flutter.sdk not set in local.properties return flutterSdkPath }() includeBuild($flutterSdkPath/packages/flutter_tools/gradle) repositories { google() mavenCentral() gradlePluginPortal() } } plugins { id dev.flutter.flutter-plugin-loader version 1.0.0 id com.android.application version 8.1.0 apply false id org.jetbrains.kotlin.android version 1.8.22 apply false }然后把原来 app/build.gradle 里的apply plugin:那几行删掉替换成 plugins 块plugins { id com.android.application id kotlin-android id dev.flutter.flutter-gradle-plugin }改完同步一下 Gradle原本的报错就消失了。Books 真正的鸿蒙工程目录是在 DevEco Studio 里单独创建的。鸿蒙侧使用的是 hvigorfile.ts不是 Gradle 文件负责把 Flutter 编译出来的 so 库、assets 资源包组装成 HAP。你可以把它理解成“Flutter 负责生成零件hvigor 负责组装整车”。如果没有特殊原因直接用社区脚手架生成鸿蒙壳工程再把 Books 的 Dart 源码路径指过去是最快的方式。2. Books 书库的鸿蒙改造数据库、本地文件与图书渲染2.1 本地书库选型从 sqflite 到纯 Dart 方案Books 这类阅读应用数据层核心是书籍元信息、阅读进度、书签、书架排序。Android 版本我用了 sqflite表格结构简单清晰但在鸿蒙分支上 sqflite 不可用。原因不复杂sqflite 底层依赖 Android 系统提供的 SQLite鸿蒙上根本没有这套 Java API插件一调用原生方法就崩。数据库选型时我比较了三条路线继续用 SQLite 系比如 drift。drift 本身是纯 Dart 的但底层仍然依赖 sqlite3_flutter_libs 来提供原生 so 库。鸿蒙上这个库没有官方预编译包要么自己用 OpenHarmony 交叉编译环境编一份要么找社区产物。你能编译成功性能确实不错但对普通团队来说维护成本偏高。使用 HarmonyOS 原生提供的 RelationalStore关系型数据库再通过 MethodChannel 暴露给 Flutter 调用。可靠但数据库操作全要自己封装一层 bridge工作量大。换纯 Dart 实现的数据库。我最终选了 sembast。它把数据存成文件没有原生依赖天然跨平台在鸿蒙上能直接跑。Books 的书库量级是几百本到几千本完全够用不用上重量级 SQLite。Books 的书库模型我简化成下面这种结构class Book { final String id; final String title; final String author; final String coverPath; final String filePath; final double progress; final int lastReadAt; final MapString, dynamic metadata; }打开时用 sembast 的 intMapStoreFactory.store(books) 存业务字段阅读进度和书签单独建 store。启动流程就三步打开数据库、读取书架列表、恢复上次阅读位置。整个过程不涉及任何原生调用鸿蒙和 Android 上逻辑完全一致这是纯 Dart 数据库最大的好处。2.2 导入图书与封面图库调用鸿蒙原生选择器Books 的图书来源主要是本地导入用户需要从文件管理器挑 EPUB/PDF或者从相册选封面。Flutter 常用的 file_picker、image_picker 插件在鸿蒙上并没有官方实现社区虽然有一些 fork但功能和使用体验差异很大。Books 的选择是自己封装原生 Channel这样最可控。核心做法是在鸿蒙工程里注册一个 MethodChannel名字叫books/fileFlutter 端发起pickFile、pickImage两个调用const channel MethodChannel(books/file); final MapString, dynamic? result await channel.invokeMethod(pickFile, {type: epub,application/pdf}); if (result ! null) { final String path result[path]; final String displayName result[displayName]; // 复制到应用沙箱目录 }鸿蒙原生侧用系统文件选择器拉起 DocumentPicker/PhotoViewPicker拿到文件 URI 后拷贝到应用沙箱再把后续可访问的路径返回给 Flutter。这里有一个容易踩的坑直接拿返回的临时 URI 去读文件可能只有当次启动有效杀掉进程后权限会失效。安全做法是第一时间把文件复制进应用目录之后只使用沙箱路径。权限方面鸿蒙的权限模型类似 Android 的运行时权限。读取图库需要ohos.permission.READ_MEDIA文件管理器方式通常不需要额外声明存储权限但首次跳转会弹窗体验上要比 Android 简单。Books 的导入流程在鸿蒙上比 Android 还顺一些系统级选择器返回的文件信息比较完整。2.3 EPUB/PDF 渲染与其适配插件不如把渲染丢给原生 WebView阅读器的核心是渲染 EPUB 和 PDF。这两类文件在 Flutter 社区本来就没有特别统一的跨端方案Android 上我会用 pdfx 渲染 PDFEPUB 则解析出来转 HTML 后用 webview_flutter 展示。鸿蒙适配时我发现这些插件都存在兼容性风险与其花大力气去调插件不如直接用鸿蒙原生 WebView 承载内容Flutter 只负责导航和 UI 壳。Books 的做法是EPUB 用 Dart 端解析解压出文本和章节结构生成干净的 HTML 内容PDF 直接给原生 WebView 一个文件路径让它自己渲染。Flutter 与原生之间仍然走 MethodChannel控制加载、翻页、缩放以及返回阅读进度。为什么这么做webview_flutter 在鸿蒙上虽然有社区适配但能力边界不明确很多 PDF 在线预览、下载回调之类的功能需要额外验证。而鸿蒙的 Web 组件本身就是系统能力对 EPUB 拆分出来的 HTML 和原生 PDF 渲染支持都很好稳定性高得多。Books 阅读器切换成“Flutter 壳 原生 WebView 核心”之后页面卡顿和字体错乱的问题基本消失代价是阅读器这一块不能完全跨端必须为鸿蒙维护一套独立通道。3. 联调期最容易翻车的三件事Dio 网络、微信登录与支付3.1 Dio 能跑但证书和抓包要提前铺好Books 的账号系统和阅读进度同步依赖后端接口Flutter 端用的是 Dio。好消息是 Dio 的 Dart 层实现并不依赖特定原生平台底层 Http 能力由 dart:io 提供鸿蒙的 Flutter 分支把这一层也适配了所以网络请求基本开箱即用。容易忽略的是权限。鸿蒙应用必须在 module.json5 里声明ohos.permission.INTERNET否则所有请求都会秒失败。这个错误在日志里往往不明显HTTP 层直接超时我第一次排查花了半个多小时。联调阶段绕不开抓包。这里把“flutter dio 如何抓包”的实操步骤统一写一遍手机和电脑连同一个局域网手机 Wi-Fi 设置里把代理指向电脑 IP 和抓包工具监听端口比如 Charles 的 8888。在电脑端安装 Charles 或 mitmproxy 的根证书然后让手机下载并安装该证书。鸿蒙新版系统对用户 CA 证书限制较严Debug 包需要配置网络安全信任允许调试场景信任用户证书Release 包保持默认不要开信任所有证书。用 Dio 时如果发现 HTTPS 请求报证书错误可以先确认真机时间和证书时间是否一致再考虑在 Debug 模式下临时加 badCertificateCallback。if (kDebugMode) { httpClientAdapter IOHttpClientAdapter() ..createHttpClient () { final client HttpClient(); client.badCertificateCallback (cert, host, port) true; return client; }; }上面这段代码被很多人直接复制到生产环境我觉得需要强调它只应该出现在 Debug 分支。Books 在 Release 包里严格保留系统默认证书校验否则用户数据会暴露在中间人攻击风险下。3.2 微信登录绕开纯 Flutter 插件直接走原生 ChannelBooks 之前用的是软件包市场上比较常见的微信登录 Flutter 插件这些插件底层对接的是 Android/iOS 微信 SDK。鸿蒙上微信提供的是 HarmonyOS 版本 SDK接口完全不一样继续用老插件不仅登录拉不起来在部分设备上还会引起崩溃。最终方案是封装自己的登录 Channel原生侧在鸿蒙工程中调用微信 HarmonyOS SDK注册 AppID发起登录请求接收回调再把 code 或 token 返回给 Flutterconst channel MethodChannel(books/auth); final MapString, dynamic? result await channel .invokeMethod(wechatLogin, {appId: wxYourAppId});鸿蒙原生侧需要在对应 UIAbility 的 onAcceptWant 回调里处理微信返回的授权结果然后把 success/error 信息传回 Flutter。整个过程并不复杂但它要求团队里有懂鸿蒙原生开发的人。如果你想完全靠 Flutter 插件解决社交登录目前还没有完美的选择。这里顺带提一个热搜里出现的现象“鸿蒙系统钉钉浏览器 SSO 登录白屏”。这种现象在 WebView 内嵌第三方登录页时很容易遇到多半不是鸿蒙系统的问题而是切换 WebView 内核后 UA 标识变了对方服务端无法识别或者页面里的 JavaScript 桥没有注入。Books 在某次对接 H5 登录页时也出现白屏排查下来是鸿蒙 WebView 默认关闭了 DOM StorageH5 脚本一执行就报错界面自然空白。在原生 WebView 配置里把 javaScriptEnabled 和 domStorageEnabled 都打开同时设置一个包含 HarmonyOS 标识的自定义 UA问题就消失了。3.3 会员与购买接 HMS IAP 而不是原来的支付渠道Books 的收入来源是会员订阅和单本购买。Android 端原本接的是 Google Play Billing 或者其他第三方聚合支付鸿蒙上架华为应用市场后这些渠道全部失效必须接入 AppGallery Connect 的应用内支付IAP。华为官方提供了 huawei_iap 插件虽然叫“Huawei IAP”但可以理解成 Flutter 侧的封装底层走的是鸿蒙原生支付能力。接入时需要注意几件事在 AGC 后台创建应用配置签名证书指纹开通“应用内支付”服务。创建商品 ID区分消耗型商品单本购买和订阅型商品会员包月/包年。客户端初始化 IAP拉起商品查询再发起购买。购买结果的票据校验必须放到服务端不能只信任客户端返回的 local data。一个比较隐蔽的坑是测试账房配置如果你没有在 AGC 后台添加测试账号购买流程会在支付前弹出一个“环境异常或账号未验证”之类的错误。Books 在联调时就卡在这里加上测试账号后沙箱支付环境立刻通了。另外IAP 商品状态修改后需要重新发版才生效商品信息有缓存经常出现“后台改了名字客户端还是旧名称”的现象不要太惊讶。支付回调的可靠性也需要关注。鸿蒙侧支付结果回调可能延迟Flutter 端不要一收到失败就立刻关闭订单页面最好加一个轮询或等待机制等服务端最终确认。Books 的做法是购买成功后先显示“处理中”等服务端回调验票成功再刷新会员状态。4. 从 HAP 到上架构建签名、真机安装与 Flutter 反编译风险4.1 用 hvigor 构建 HAP签名文件要认清楚Books 最终要产出 HAP 上传应用市场鸿蒙侧的构建工具是 hvigor命令行入口是 hvigorw。编译 Flutter 鸿蒙包的过程和 Android 略有不同大致是先用鸿蒙 Flutter 分支编译出libapp.so、libflutter.so和flutter_assets再由 hvigor 把它们组装进 HAP 包。签名环节是很多新手绕不过去的坎。鸿蒙签名文件主要有三个.p12密钥库、.cer证书文件、.p7bProfile 文件。打开 build-profile.json5把这三项配置进去就可以自动签名signingConfigs: [ { name: default, type: HarmonyOS, material: { certpath: ./sign/certificate.cer, storePassword: ******, keyAlias: debugKey, keyPassword: ******, profile: ./sign/profile.p7b, signAlg: SHA256withECDSA, storeFile: ./sign/keystore.p12 } } ]建议把签名文件统一放一个目录不要提交到 Git 仓库。Books 的 CI 机器配置了不同的签名路径本地打包用 debug 证书发布包用 release 证书这一点和 Android 双签名机制很像。4.2 真机安装与调试hdc 命令比 IDE 更快用 DevEco Studio 调试鸿蒙应用很方便但在 CI 环境或命令行场景hdc 是更趁手的工具。hdc 类似 Android 的 adb常用命令不多我整理了一下hdc list targets # 查看连接的设备 hdc install app.hap # 安装 HAP 包 hdc uninstall com.example.books # 卸载 hdc shell hilog # 查看应用日志真机调试时如果遇到安装失败最常见的错误是签名不一致。DevEco 调试签名只在特定设备上生效换一台新设备就要重新添加 UDID 并生成调试证书。另一个常见坑是 API 版本过低Books 初期用了一个比较老的 HarmonyOS SDK 编译测试包手头测试机系统版本比较高安装时报错“signature verification failed”升级 SDK 后解决。模拟器能覆盖大部分 UI 场景但涉及 IAP、华为账号登录这些系统级能力时模拟器的返回结果和真机差别很大容易造成“模拟器能付、真机付不了”的怪象。凡是支付和登录相关功能Books 都会强制在真机上回归一轮。4.3 反编译风险Flutter 产物在鸿蒙上同样能被人逆向说到 Flutter 安全很多人有个误解认为 AOT 编译后 Dart 代码就变成机器码了别人反编译不了。实际上Flutter 的 AOT 产物并非完全安全。现在社区里已经出现针对 Flutter AOT snapshot 的逆向工具链比如 blutter、reFlutter 这一类它们可以还原 Dart 堆快照、恢复符号名甚至定位到具体逻辑分支这在“反编译 Flutter”这个热搜词下面讨论得很激烈。Books 做的是内容付费图书正文就是核心资产所以防逆向这件事必须认真对待。我的防护清单大致如下构建时加--obfuscate --split-debug-infobuild/symbols混淆 Dart 符号同时把调试符号文件单独归档不放进安装包。敏感字符串不要明文写在 Dart 代码里。API Key、加密密钥等运行时拼接或从服务端下发最少也要做一层字节混淆。核心逻辑尽量下沉。内容解密、票据校验这些敏感操作放到鸿蒙原生层ArkTS/CDart 层只负责展示逆向者拿到的只是一层皮。图书文件在设备上加密存储密钥存放在服务端或安全硬件区解密只在内存中进行避免直接落盘明文。网络传输层启用证书校验不允许随便改 endpoint。把--obfuscate加进构建命令很简单但要注意一个副作用混淆后错误日志里的函数名会变成不可阅读的短符号调试 Release 包问题会变困难。所以 Books 的 CI 会把混淆后的 symbols 文件按版本号归档出问题对照符号文件来还原堆栈。这是个容易被忽略但很重要的习惯。还有一个细节鸿蒙侧原生的 ArkTS 代码默认也会被打包成字节码同样存在反编译风险。敏感算法放到 C 层并用系统能力做密钥保护比单纯写在 ArkTS 里安全得多。Books 的章节加密、离线授权校验都遵循这个原则。我在这次迁移里最大的体会是Flutter 在鸿蒙上的适配正在逐步变好但千万别把它想象成“把 target 一换就完事”。数据库、网络、登录、支付、逆向防护每一块都要重新审视一次。尤其是 Books 这种依赖原生能力和内容版权的应用花在鸿蒙工程上的时间不会比写 Flutter 代码少多少。好消息是一旦把数据库和网络这两条主干跑通剩下的插件适配问题都能找到替代方案整个工程的可维护性还是比较乐观的。
返回列表