2024年下半年我们团队接了一个共享社区类App的项目,内部代号叫“享+”。要做的事情不复杂:房源短租、邻里互助、二手闲置发布、社区活动报名,再加上IM聊天和支付。复杂的是终端。要求一出来就得支持Android和iOS,华为鸿蒙设备的需求也在产品计划里排上了。客户端团队只有三个人,后端占了大头,没有任何一个端能分出两个人来维护原生代码。所以技术选型就成了整个项目最先决定、也最不能错的一件事。
我们最终的选择是Flutter,并且在一台鸿蒙设备上把整个应用跑通、完成了平台插件的适配和打包。这个过程中有架构设计上的权衡,也有大量堆在文档之外的坑。我在这篇文章里把整个思考路径、架构分层、鸿蒙适配流程和踩坑排查过程都写出来,希望能给后面做同样选择的人一些参考。
1. 为什么“享+”最终选择Flutter作为三端统一方案
1.1 共享社区App的产品形态与端侧需求
先说一下“享+”到底要做什么。它不是一个单纯的租房App,而是把“房源信息”和“社区关系”绑在一起:你可以在上面发布自己的空余房间,也可以查看邻居发布的闲置物品,报名周末社区活动,甚至发起拼车、借工具这类轻量互助。这个产品形态决定了客户端有几个很典型的特征:
- 信息流和列表页非常多,图片占比高,需要流畅的滚动体验;
- 表单和状态多,比如发布房源要填好几页信息、活动报名要选择时间段;
- 页面之间需要频繁传递数据,而且很多页面在切换回去时要保留当时的筛选条件和滚动位置;
- 有IM和推送,需要原生能力参与,同时Dart侧要能收到实时事件。
这些特征对跨端框架提出了比较具体的要求:列表性能不能差、状态管理要灵活、原生通道要稳定、页面路由要能支持复杂嵌套。而“鸿蒙适配”这件事,在选型阶段就必须认真考虑,不能等Android和iOS上线之后再补。
1.2 跨端方案的横向对比:Flutter、RN、原生
团队内部做过一次比较系统的技术选型,时间大概花了两周。参与对比的方案有三个:Flutter、React Native、以及Android/iOS双原生(鸿蒙单独再评估)。
| 对比维度 | Flutter | React Native | 双原生 |
|---|---|---|---|
| UI一致性 | 自绘引擎,跨端渲染结果几乎一致 | 依赖原生控件,样式细节需要各端调 | 完全一致但工作量大 |
| 鸿蒙适配成熟度 | 有社区维护的OpenHarmony Flutter SDK,适配路线清晰 | 社区有React Native for OpenHarmony,但生态和插件成熟度较弱 | 需要单独的鸿蒙原生团队 |
| 性能 | 自绘渲染,长列表可用RepaintBoundary优化,整体稳定 | JS桥接有性能损耗,复杂页面容易掉帧 | 原生最优 |
| 关键插件支持 | 主流插件都有Dart/native版本,个别需要自己写适配 | 依赖原生模块,鸿蒙侧往往要自己再包一层 | 不涉及跨端插件问题 |
| 团队学习成本 | Dart语言从零学起,一周内能上手写页面 | JavaScript门槛低,但工程规范需要建设 | Android/iOS各自是独立技术栈,人力要求高 |
RN在JavaScript生态上有天然优势,但当时的社区鸿蒙适配还不太稳定,社区版的React Native鸿蒙分支更新频率不高,一旦踩到核心问题,排错的成本可能很高。双原生不用多说,产品希望三端同时上线,三个人根本维护不过来。Flutter虽然也需要学Dart,但UI代码的跨端一致性确实省掉了大量联调时间,所以我们最终把票投给了Flutter。
1.3 一套代码覆盖鸿蒙的核心判断依据
光靠“Flutter能跑鸿蒙”的结论还不足以说服所有人,我们当时还列了三个判断依据:
第一,渲染链路。Flutter在鸿蒙上并不是直接调ArkUI绘制,而是通过OpenHarmony的Flutter引擎桥接层,把Flutter的UI合成到鸿蒙的Surface上。只要这个桥接层维护得够勤快,Flutter的Widget在鸿蒙上的渲染效果就会和Android/iOS基本一致。
第二,平台通道。Flutter和原生交互的MethodChannel、EventChannel在鸿蒙SDK里是完整支持的。也就是说,我们已有的原生业务逻辑,比如登录、支付、定位,都可以通过同样的方式迁移到鸿蒙,不需要另起一套通信机制。
第三,插件生态。我们整理了一份“享+”需要用到的原生能力清单:相机、相册、定位、推送、支付、分享、扫码、电话。逐个去OpenHarmony社区查插件支持情况后,发现覆盖率大概在80%左右。剩下的如华为推送和华为账号登录,本身就是用鸿蒙原生API封一层,工作量可控。
评估完这三点之后,我们才真正定下来。接下来的架构设计和鸿蒙适配都是在“Flutter作为统一框架、鸿蒙作为一等公民”这个前提下展开的。
2. “享+”客户端的模块化架构与状态管理落地
2.1 分层架构:从UI到数据层的边界划分
“享+”的工程没有做iOS和Android两个壳工程里塞Flutter的做法,而是从一开始就按照一个完整的Flutter应用来搭,原生工程只作为平台壳存在。Dart侧的分层非常明确:
- 展示层:存放所有页面Widget、路由表、局部组件;
- 应用层:存放Riverpod的Provider、页面状态、事件分发;
- 领域层:定义数据模型、仓储接口、业务规则;
- 数据层:实现网络请求、本地数据库、平台通道调用。
模块划分上,业务按功能域拆:房源模块、社区模块、IM模块、个人中心模块、支付模块。每个模块内部可以单独引入Riverpod和网络层,但模块之间不允许直接import对方的页面对象,只能通过领域接口或事件总线通信。
这里有一个容易踩的坑:很多人会把“模块化”做成“把文件放进文件夹”。我们要求Dart侧对上层的依赖方向必须是单向的,展示层可以依赖应用层,但领域层不能反过来依赖展示层。否则鸿蒙适配时你往往会发现,某个原生能力被写死在了一个页面Widget里,根本没法复用。
Dart语法层面的part、library关键字我们也用过。当时有一个房源表单文件特别长,UI、校验、提交逻辑全在一起,团队里有人提议用part拆文件。但后来发现part本质上是同一个库的文件拆分,虽然能减少文件长度,却会让隐式依赖变得更难追踪,甚至导致循环引用只在编译期才暴露。最终我们放弃了用part拆分代码,改用严格的目录分层和Riverpod来解耦状态逻辑。part适合简单的代码整理,不适合拿来当模块边界用。
2.2 状态管理选型:Provider还是Riverpod
状态管理我们对比过setState、Provider、Riverpod和Bloc。最终选择Riverpod,理由其实很实际:
- Provider在大型项目里容易依赖BuildContext,测试和复用都会受限;
- Bloc样板代码量偏大,对“享+”这种表单密集型应用反而拖慢速度;
- Riverpod的Provider是编译期类型安全的,支持异步状态和自动重试,写起来也接近普通函数。
举个例子:房源列表页的数据是分页加载的,筛选条件有价格区间、入住时间、排序方式。我们用AsyncNotifier来承载这个列表状态,筛选条件改变时调用ref.invalidate重新拉取数据。这个过程中页面只关心AsyncValue的加载态、数据态和错误态,完全不关心缓存和网络层怎么实现。
对于申请加入社区、发布二手物品这类表单,用的是NotifierProvider<FormState>。因为表单的每个字段都需要局部刷新,如果全部塞进全局Store,会导致整个页面频繁重建。这里加了一个优化:字段级别的ValueListenable和TextEditingController都放在Widget内部管理,只有表单提交时才通知上层状态。换句话说,我们的原则是“能不用全局状态就不用,但一旦页面间需要共享状态,必须走Riverpod”。
2.3 路由设计:Navigator 2.0与页面状态保活
有一个很经典的问题:Flutter的Navigator切换页面后,页面状态会不会丢失?这个问题的答案取决于你怎么做路由跳转。
如果使用普通的Navigator.push,原页面会被压在路由栈里,状态确实保留。但“享+”的场景远比这个复杂:
- 底部Tab之间切换,我们希望每个Tab保存滚动位置;
- 从房源详情页返回列表页,列表页要保留筛选条件和滚动位置;
- IM模块有新消息时,要能跳转到会话页,同时不破坏首页的浏览状态;
- 收到深链时,要能直接定位到二手房详情页,且返回栈应该是“首页→搜索结果→详情页”。
所以我们没有用Navigator 1.0的样板写业务,而是直接上了Navigator 2.0。确切说是用Router+RouteInformationParser+RouterDelegate做了一个轻量封装。深链和页面跳转都统一走路由配置表,而不是直接Navigator.push("xxx")。
对于底部Tab的保活,用IndexedStack一次性创建所有Tab子树。加上PageStorageKey,每个Tab里的ListView滚动位置都能被独立记住。这里有个容易被忽略的细节:IndexedStack会同时构建所有Tab,所以每个Tab页面一定要自己管好数据拉取时机,不能一进首页就把全部Tab的数据请求都打出去。我们在外层套了一个VisibilityDetector,只有在Tab可见时才允许页面发起首屏请求。
2.4 组件通信:EventChannel与跨模块事件总线
“享+”里有一个比较典型的跨模块通信场景:IM模块收到新消息后,首页“消息”Tab的角标、社区模块的报名状态、资产模块的红点都要同步变化。如果这些页面各自写一遍监听,会和IM模块强耦合。
我们的方案是引入一个统一事件总线,底层基于StreamController.broadcast()。IM模块只负责往总线里发一个MessageUnreadEvent,首页和个人中心各自订阅自己关心的事件类型。这个总线本身放在应用层,不属于任何业务模块。
但这里要特别强调:Dart侧的事件总线和原生的EventChannel是两个层面的事情。Dart侧总线解决的是Flutter进程内的跨模块通信,而EventChannel解决的是原生和Dart之间的通信。比如华为推送SDK在鸿蒙侧收到一条新通知,原生代码需要通过EventChannel把这个消息推到Dart侧,Dart侧收到后再转发到事件总线,模块之间才能解耦。
EventChannel的用法也很固定,Dart侧:
class PushMessageChannel { static const EventChannel _channel = EventChannel('com.xshare/push_message'); Stream<Map<Object?, Object?>> receive() { return _channel.receiveBroadcastStream().map((event) => Map<Object?, Object?>.from(event)); } }原生侧Android是在MainActivity里注册:
new EventChannel(getEngine().getDartExecutor().getBinaryMessenger(), "com.xshare/push_message") .setStreamHandler(new PushStreamHandler());鸿蒙侧后面会讲到,本质是一样的,只是注册API不同。
3. HarmonyOS适配:从环境搭建到平台通道移植
3.1 适配前的技术预研:Flutter SDK for OpenHarmony
“享+”进入鸿蒙适配时,我们面对的第一个问题不是代码,而是Flutter SDK本身。鸿蒙上不能直接用Google官方发布的Flutter SDK,因为官方SDK没有耕植鸿蒙的platform embedder。我们用的是OpenHarmony社区维护的Flutter分支,通常称为flutter_flutter的ohos分支,或者直接叫flutter_ohos。
安装方式并不复杂,本质上就是换一个Flutter SDK目录。当时我们的开发机配了FVM,所以可以在项目里指定使用ohos分支。关键点在于,不要试图把官方Flutter SDK和ohos分支混在一个项目里,两者的命令行参数和产物目录都有区别。
环境变量确认正确后,在Flutter工程目录下执行flutter doctor,就能看到多出一个设备类型。需要注意鸿蒙的构建命令不是flutter build app,而是类似flutter build hap。HAP就是鸿蒙的安装包格式,类比Android的APK。
3.2 项目迁移步骤:修改哪几个关键文件
鸿蒙工程的结构和Android工程差别很大,但不是从零开始。Flutter的ohos分支会在flutter create --platforms=ohos .之后生成一个ohos目录,里面是标准DevEco Studio工程。我们项目是从已有Flutter工程上增加鸿蒙平台,因此执行的是:
flutter create --platforms=ohos --org com.xshare .这一步会自动创建鸿蒙侧壳工程。之后要改的文件主要有这几个:
| 文件 | 作用 | 必改项 |
|---|---|---|
ohos/build-profile.json5 | 模块签名、产品配置 | app_signature、products名称 |
ohos/entry/src/main/module.json5 | 应用入口配置、权限声明 | abilities名称、权限列表 |
ohos/entry/src/main/ets/entryability/EntryAbility.ets | 加载FlutterEngine的入口 | 配置FlutterModule |
ohos/entry/src/main/ets/pages/Index.ets | 承载Flutter的容器页面 | 初始化FlutterFragment |
ohos/oh-package.json5 | 依赖包管理 | 引用Flutter引擎相关包 |
最关键的改动在EntryAbility.ets。它类似于Android的MainActivity,需要继承FlutterAbility,并在onWindowStageCreate里加载Flutter模块。我们当时还为了把Flutter容器嵌入到现有鸿蒙Tab页面中,改用了FlutterFragment方式,由ArkUI侧统一管理底部导航。
这一步如果只是新建工程很简单,但在老Flutter项目上会遇到各种版本不一致的问题。比如flutter create --platforms=ohos生成出来的模板flavor名字是默认的,可能和项目现有的Dart entrypoint不匹配,需要到build-profile.json5里把main.dart路径和编译模式对齐。
3.3 平台插件的鸿蒙适配流程(以第三方登录SDK为例)
“享+”的第三方登录模块一开始只有Android和iOS实现,鸿蒙平台上Dart侧没有任何变化,因为登录按钮的UI是Flutter渲染的,但底层调用的是原生SDK。我们当时对接的第三方身份服务商提供了鸿蒙SDK的ArkTS版本,于是这块就是一个典型的MethodChannel适配。
Dart侧原本的登录代码:
class AuthService { static const MethodChannel _channel = MethodChannel('com.xshare/auth'); Future<String?> loginWithProvider(String provider) async { return await _channel.invokeMethod<String?>('login', {'provider': provider}); } }Android侧对应一个MethodCallHandler,鸿蒙侧则需要新建一个类,实现MethodChannel.Plugin接口。核心方法是onMethodCall:
import { MethodChannel } from '@ohos/flutter_ohos'; import { authProvider } from '@ohos/hms-auth'; // 举例,具体以实际SDK为准 export class AuthPlugin implements MethodChannel.Plugin { private channel: MethodChannel | null = null; onAttach(id: string, plugin: MethodChannelPlugin) { this.channel = new MethodChannel(plugin.binaryMessenger, 'com.xshare/auth'); this.channel.setMethodCallHandler((call) => { if (call.method === 'login') { const provider = call.argument<string>('provider'); if (provider === 'huawei') { const controller = new authProvider.HUAWEI(); controller.execute().then((result) => { call.result.success(result); }); } } }); } onDetach() { this.channel?.setMethodCallHandler(null); } }这个适配流程可以总结为五步:
- 在Dart侧确认MethodChannel的channel名和协议,尽量保持和Android/iOS一致;
- 在鸿蒙侧实现
MethodChannel.Plugin; - 在插件类里调用鸿蒙原生SDK;
- 把SDK返回对象转换成可被dart:ui识别的Map或String;
- 在Ability或Fragment容器里注册插件实例。
有一个容易忽略的点:鸿蒙侧通过call.argument<T>()取到的参数类型,要和Dart侧invokeMethod传入Map时保持一致。我们遇到过Dart侧传的是Map<String, dynamic>,鸿蒙侧拿到的却是Record的情况,原因是Dart的Map在序列化到鸿蒙侧时会被转成Record,需要先做一次转换再取key,否则运行时不报错但永远取到undefined。
3.4 双端平台通道的统一封装
平台通道最大的问题在于很容易写出一堆重复代码。如果每个页面都直接用MethodChannel('com.xshare/xxx'),那适配鸿蒙时就要把所有调用点都改一遍。
我们的做法是,在Dart侧定义抽象接口,然后按平台做条件导入。比如定义LocationService接口:
abstract class LocationService { Future<LocationResult> getCurrentLocation(); }然后有两个实现文件:location_service_io.dart和location_service_ohos.dart,其中ohos文件调用鸿蒙侧的定位通道。调用端只依赖抽象接口,不关心当前跑在哪个平台:
import 'location_service_io.dart' if (dart.library.ohos) 'location_service_ohos.dart';这样做的好处是,业务层完全感知不到平台差异,新增一个平台能力时只需要写对应的service实现。对我们这种需要同时维护Android、iOS、鸿蒙三类原生的项目来说,这个模式避免了大量if (Platform.isAndroid)式的散弹代码。
4. 渲染性能与Impeller引擎在鸿蒙设备上的实际表现
4.1 为什么要关注Impeller
“享+”的页面里图片和特效不少,特别是房源图的5张以上缩略图、社区活动的毛玻璃背景、列表项滑动时的阴影。Flutter的渲染引擎从一开始的Skia到后来的Impeller,在Android和iOS上经历了比较大的迁移。Impeller的最大卖点是提前编译所有着色器,解决传统Skia在首次绘制时的jank问题。
对我们来说,用户的感知就是一个字:滑。房源列表滚动是否流畅、详情页图片缩放是否跟手,这些都是评分和留存的关键。所以在做鸿蒙适配时,我们特意关注了ohos分支的渲染引擎状态。
4.2 Skia与Impeller在鸿蒙上的差异
当时鸿蒙适配的Flutter SDK,默认渲染后端仍然是Skia,但要测试Impeller也不难。Android上开启Impeller的常规做法是在AndroidManifest.xml里设置:
<meta-data android:name="io.flutter.embedding.android.Engine.EnableImpeller" android:value="true" />鸿蒙侧则是在初始化Flutter引擎时传参,让Flutter Engine切换到Impeller后端。我们在一台HarmonyOS 4.2的设备上跑了同样的房源列表页,对比结果是这样的:
| 指标 | Skia模式 | Impeller模式 |
|---|---|---|
| 冷启动到首帧 | 约1.72秒 | 约1.48秒 |
| 列表快速滑动掉帧率 | 平均3.6% | 平均1.2% |
| 图片多尺寸切换响应 | 偶发明显掉帧 | 基本平滑 |
| 文本渲染稳定性 | 稳定 | 个别字体字重显示偏细 |
Impeller在冷启动和连续滚动上确实有优势,但也不是没有代价。我们的社区活动页有一个自定义的渐变背景圆角卡片,在Impeller模式下出现了偶尔的半透明叠加异常,最后查到是Impeller对某些ClipRRect+BackdropFilter组合的栅格化策略和Skia不同。这个页面最终用RepaintBoundary隔离了背景层,问题才消失。
4.3 性能调优的实际数据与操作
除了引擎层面的切换,还有很多更基础的优化在“享+”里起到了实实在在的作用:
- 图片列表使用
cached_network_image时,强制指定合适的缓存宽高,避免在列表页加载原图; - 对每个商品卡片用
RepaintBoundary隔离,滚动时只重绘卡片内部变化; - 房源详情页的页面切换动画从默认的Cupertino过渡改成更轻的自定义Fade+Slide,减少转场期间的raster线程压力;
- IM消息列表的Item用
const构造,让Widget重建时尽可能复用Element。
我们用Flutter DevTools里的Timeline做了一次系统分析,发现房源列表在快速滑动时,Raster线程偶尔会冲到16ms以上。定位下来主要是图片解码占用的资源。在开启Impeller之后,图片解码走了新的Impeller的纹理上传路径,整体GPU压力反而更平均了。
从我们的实测结果看,鸿蒙适配版的首屏速度已经逼近Android原生的表现,虽然还达不到iOS原生那种“随点随开”的手感,但用户基本感知不到差异。
5. 适配过程中的关键踩坑与排查链路
5.1 Gradle插件命令式应用报错:you are applying flutter's main gradle plugin imperatively
鸿蒙适配过程中我们并没有完全抛弃Android构建,但Android和鸿蒙共用一个Flutter工程,所以Android构建配置也需要保持健康。升级Android Gradle Plugin版本后,构建立刻报了一个错:
You are applying Flutter's main Gradle plugin imperatively using the apply script method, which is not supported. Use the plugins { } block in your project's settings.gradle or build.gradle file instead.这个问题的原因是Flutter新版插件要求使用声明式plugins {}方式加载,而老项目习惯在根build.gradle里用apply plugin:命令式加载。特别是我们在集成第三方SDK时,为了处理依赖顺序,写了很多apply plugin: 'com.android.library'这样的语句。
解决方式分两步。先在根settings.gradle中引入:
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.10' apply false }然后在app/build.gradle里同样使用plugins {}声明,而不是apply plugin:。这里有一个很隐蔽的坑:如果项目里还有其他module,那么每个module的plugins {}里的id都必须和根目录声明的version对应,否则Gradle会报“plugin not found”。我们在适配时把其中一个build.gradle漏改,结果错误信息完全一样,浪费了半小时。
5.2 HarmonyOS构建时找不到Flutter SDK支持的版本
鸿蒙构建时遇到一个很典型的版本校验错误:
The current configured Flutter SDK is not known to be fully supported.这句话本身不是致命错误,但会打断自动化的CI脚本。原因是flutter_ohos分支需要匹配特定的OpenHarmony API版本,而项目里fvm flutter --version指向的SDK版本过于旧。我们查看flutter/config/ohos相关配置后,发现需要升级到支持OpenHarmony 5.0的SDK分支。
处理方式:
- 确认当前项目使用的Flutter SDK是ohos fork版本;
- 升级FVM里对应的flutter版本到项目指定的tag;
- 清理
pubspec.lock并重新flutter pub get; - 最后执行
flutter build hap --release验证。
不要为了跳过校验去手动删版本检查代码,后面其他构建行为会一起崩。
5.3 打包时出现AssertionError的定位过程
在Windows构建机上打鸿蒙HAP包时,出现了这样一个错误:
java.lang.AssertionError: java.lang.Exception: could not close i...日志后半截被截断了,但核心是AAPT2缓存无法关闭。当时第一反应是磁盘文件占用。我们试了删除build目录、清空.gradle缓存,仍然复现。后来发现是Gradle daemon的并发构建导致多个进程同时操作同一个AAPT2 cache文件。
最终的解决方法是在gradle.properties里限制并发worker:
org.gradle.workers.max=1同时保证命令行构建时关闭其他DevEco Studio进程。这个问题在单台开发机上很难遇到,但在CI和本地同时触发构建时特别容易踩。
还有一个相关的坑:“享+”在打release版HAP时,因为代码混淆导致反射调用鸿蒙SDK失败。Flutter的Dart层打包不影响,但鸿蒙原生的ArkTS代码混淆了MethodChannel的plugin类名,导致运行时找不到插件。需要在混淆规则里keep住所有实现MethodChannel.Plugin的类。这个在文档里几乎没有,我们是通过半天的崩溃日志才定位到的。
5.4 鸿蒙端原生页面与Flutter页面的嵌套跳转
适配“享+”最后一个大块是混合跳转。产品里有一个“扫码加入社区”功能,扫描的是社区门禁二维码。我们用的是一个原生扫码页面,在Android上是startActivity,在鸿蒙上是startAbility。从Flutter页面跳转到原生页面本身不难,难的是原生页面返回扫码结果后,Dart侧要能拿到并继续走业务流程。
我们在鸿蒙侧的做法是:
- Flutter通过MethodChannel调用
openScanner; - 鸿蒙原生使用
AbilityContext.startAbility启动扫码页面; - 扫码页面拿到结果后,通过EventChannel把
scanResult事件回传给Dart侧; - Dart侧监听事件,收到结果后关闭loading、跳转到社区详情页。
代码结构上,扫描结果传递用的是鸿蒙侧的emit方法:
this.eventChannel.sendEvent('scan_result', { result: qrText });Dart侧:
_eventChannel.receiveBroadcastStream().listen((event) { final result = (event as Map<Object?, Object?>)['result']?.toString(); // 处理扫码结果 });这里我们踩过一个生命周期坑:原生页面没有关闭时,Dart侧收到了结果,但Flutter容器已经被压到后台,无法弹窗。所以我们在Dart侧等结果事件过来后,要先确保回到前台,或直接通过Navigator.push替换当前路由。不可共用同一个Navigator context时的Correction是使用全局navigatorKey,而不是页面context。
这个场景也引出一个经验:凡是涉及原生跳转返回结果的业务,一定要在平台上定义好“请求-响应-取消”三态,不能只靠一个成功回调。我们后来在推送跳转、支付回调中都套用了同样的模式。
整个“享+”项目从架构定型到鸿蒙适配跑完,前后花了大约一个半月。回看这个过程,最值得强调的不是某个具体API用法,而是从一开始就把平台差异留给适配层,而不是散落在业务代码里。正因为Dart侧模块边界清晰,鸿蒙接入时才只需要处理底层通道和原生插件,不用回头改页面。
最后分享一个小经验:如果你打算在项目里同时兼容Android、iOS和鸿蒙,尽量在第一天就把三端构建脚本和CI流程搭好。我们前期Android和iOS构建一直顺畅,鸿蒙适配开始时才发现CI机器上没有一个能跑hap构建的环境,最后手动在Windows上打包了一周,这是完全可以提前避免的时间开销。