1. 搭建开发环境:让Flutter在OpenHarmony设备上真正跑起来
1.1 版本选型与仓库说明:为什么不能用官方Flutter SDK直接编鸿蒙包
先聊一个很多人会踩的坑。Flutter官方SDK目前并不会直接支持OpenHarmony平台,如果你直接flutter build apk那当然没问题,但想编出能在鸿蒙设备上安装的hap包,就得换一套工具链。社区里目前最主流的方案是OpenHarmony SIG维护的flutter_flutter仓库和配套的flutter_ohos引擎仓。简单说,前者是Flutter框架层的鸿蒙适配分支,后者负责把Dart代码和Flutter引擎编译成OpenHarmony可以识别和调用的so库与jar包。
我第一次接触这个项目的时候,也想当然地以为装个Flutter 3.22的官方SDK,加一行flutter config --enable-ohos就能搞定,结果折腾了一整天,编译出来的产物在DevEco Studio里完全无法识别。后来才搞清楚,OpenHarmony的Flutter适配分支是基于特定版本拉出来的,目前社区主线稳定在Flutter 3.22版本左右,你需要把整个Flutter SDK替换成sig维护的那套源码,然后重新编译工具链。建议直接在gitee上搜索“openharmony-sig/flutter_flutter”,按README里的分支说明切换到对应的适配版本,不要自己随便挑个老版本用,否则后面跑示例工程时会出现一堆莫名其妙的API不兼容问题。
提示:如果公司网络访问gitee比较慢,建议提前用git把仓库完整clone下来,并加上
--recursive参数拉取子模块。flutter_flutter的子模块比较多,漏掉任何一个都会导致后续构建阶段失败。
1.2 创建Flutter工程并配置OpenHarmony平台产物
环境准备好之后,创建工程的方式和普通Flutter项目并没有区别,依然是flutter create。但请注意,这一步你用的是适配过OpenHarmony的flutter_flutter分支,所以创建的工程会多出一个ohos目录,这才是关键。
创建完成后,需要用DevEco Studio打开ohos目录,它会自动识别为OpenHarmony工程。我建议首次打开时先让DevEco Studio自动同步gradle依赖,同步完成后再用命令行执行编译,不然两边各自下载依赖很容易冲突。这里有一个容易被忽视的点:OpenHarmony工程的编译并不是直接flutter build apk,而是先flutter build hap --debug或flutter build hap --release生成hap安装包,然后再用hdc工具安装到设备上。如果你直接在DevEco Studio里点Build按钮,也是可以的,但那个走的不是Flutter的构建链路,很可能会出现“Dart代码没有被打进hap”的情况,页面白屏半天却找不到原因。
另外,工程的local.properties文件里需要指定SDK路径,和Android工程类似。如果使用DevEco Studio自带的SDK,路径一般可以在安装目录的Sdk子目录里找到。我习惯把它显式写进local.properties,避免命令行构建时找不到SDK。注意这个文件不要提交到git。
1.3 真机调试:hdc连接设备、查看系统版本与运行日志
构建好hap包之后,安装过程用的是hdc而不是adb。hdc是OpenHarmony提供的设备连接调试工具,语法和adb非常像。连上设备后,可以用hdc list targets查看当前设备是否被识别,然后安装hap包:
bash复制hdc install ohos/entry/build/default/outputs/default/entry-default-unsigned.hap
安装完成后,需要启动应用。这里推荐用hdc shell aa start命令直接拉起EntryAbility,避免手动去设备上点图标,尤其在实际开发中经常要反复安装启动,命令行效率高很多。
热词里有一个很典型的需求:“hdc 查看 openharmony 系统版本 param get”。实际命令是:
bash复制hdc shell param get const.product.software.version
hdc shell param get const.product.name
第一条返回的是系统版本号,第二条返回的是设备型号。调试前确认这两项,可以避免样例工程编译目标版本和设备系统版本不匹配的问题。比如我手头的开发板是RK3566和RK3588两块,它们的系统版本不同,有的API level和引擎预编译产物对不上,跑起来直接黑屏。查阅日志用hdc shell hilog,可以加过滤条件,比如只输出flutter相关的日志:
bash复制hdc shell "hilog | grep flutter"
这样能看到Dart层打印的日志和引擎报错信息,排查效率会高很多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 剧本杀组队的业务建模:邀请好友功能到底要解决什么问题
2.1 核心业务对象拆解:房间、成员与邀请状态
剧本杀组队App的核心场景是“开一局剧本杀,凑齐人数,开始游戏”。邀请好友功能看起来简单,但它不是单纯地“把链接发出去”,需要用一组清晰的对象来承载业务逻辑。我在项目里拆了三个核心模型:房间Room、成员Member、邀请Invitation。
房间是组队的容器,它至少包含roomId、hostId、当前成员列表和房间状态。房间状态我用了一个枚举:创建中、等待成员、开局准备、游戏中、已结束。为什么需要状态机?因为剧本杀组队的核心矛盾是“人数齐不齐”和“房主什么时候点开始”,如果不对状态做严格约束,邀请逻辑很容易出现错乱,比如人已经满了还可以继续进房、房主已经点开局但成员列表还没同步完。
成员模型对应的是当前房间里的每个玩家,包含userId、昵称、头像、角色(在剧本杀里是侦探/凶手/平民)、是否准备就绪等字段。这里尤其要注意“加入状态”和“准备状态”的区分:邀请好友进入房间只是第一步,好友必须再点一次“准备”才算真正就绪,房主才能开局。
邀请模型是邀请功能的灵魂。一条邀请记录至少包含inviteCode、roomId、fromUserId、toUserId、createTime、expireTime、status(待接受/已接受/已过期/已拒绝)。把这个模型单独抽出来,是为了支持后面要讲的多种邀请方式共用同一套数据状态,不会出现邀请码和深链各写一套逻辑的情况。
2.2 邀请链路选型:邀请码、邀请链接与拉起App三种方案对比
在实际做技术选型的时候,我列了三种邀请方案:邀请码、普通邀请链接、系统级深链拉起App。很多人会想“我全都做”,但这里建议先想清楚场景优先级。
邀请码最通用,好友只要把倒计时内的6位短码填进App就能加入房间。实现成本最低,适合现场组队,比如一群人在同一个线下剧本杀店里凑队。缺点是输入麻烦,不适合远程发送。
邀请链接方案适合社交分享场景。好友点开链接,Web页面展示“XX邀请你加入剧本杀房间”,再一键跳转App。这需要你有一个H5落地页加服务端,开发量中等。在OpenHarmony上,这一步还需要处理浏览器拉起应用的兼容性问题。
深链拉起App(Deep Link)是最贴近原生体验的方案。好友点链接后如果已安装App,直接拉起App并自动加入指定房间,无需手动输入邀请码。这个方案在产品体验上是最好的,但在OpenHarmony上需要自己在module.json5中配置Ability的skills匹配规则,实现起来比Android的intent-filter要稍微绕一点,不过思路完全一致。
我最终的做法是“邀请码+深链”两条路并行:邀请码作为兜底,深链作为主流路径。普通Web链接先用一个极简H5页做中转,通过页面上按钮再次发起深链。这个结构足够覆盖线上和线下两种组队场景,而且服务端数据模型只有一张invitation表,不用为不同方式重复设计。
3. 邀请好友功能的工程实现:从选人、生成邀请码到自动入队
3.1 好友选择器:本地数据列表与选中状态管理
第一步是让房主选人。我们做的是轻量级剧本杀组队,不依赖通讯录权限,所以好友列表是从服务端拉取的“关注关系”或“最近组队队友”。在Flutter端,我直接用Provider做状态管理,好友选择器的核心状态就两个:好友列表List<Friend>和已选中的用户ID集合Set<String>。
UI层是常规的ListView.builder加Checkbox,但要注意的一个细节是:不要在每个item里直接用FriendCard内部维护自己的选中状态,否则滚动列表时会出现勾选状态错乱。正确做法是让Checkbox的选中值完全受外部selectedUserIds控制,点击时回调到页面顶层的ViewModel统一更新,这算是列表选择器的一个通用经验,不只是OpenHarmony项目会遇到。
好友列表数据量如果不大,推荐一次性拉全量,然后用AnimatedList做简单的展示。如果数据量大,再加分页,但那样还要处理“选中了第二页的人,翻回第一页状态要保留”的问题,需要维护一个全局selection set。我是直接全量拉的,一次组队最多也就几十个候选好友,性能完全没问题。
3.2 生成邀请码:短码规则、过期时间与幂等性设计
邀请码我设计为6位,由大写字母和数字组成,排除了容易混淆的0/O、1/I。生成逻辑很简单,用随机数在字符集里取6个字符即可。但这里有两个关键点。
第一,邀请码必须和房间ID、创建者ID绑定,保证同一个房间只有一条有效邀请记录。如果房主反复点击“生成邀请码”,服务端不能每次生成新码把旧的作废,否则之前已经把邀请码发出去的好友就全部失效了。我做了幂等处理:如果房间已存在未过期的邀请记录,直接复用并重置过期时间。如果已过期,则生成新码覆盖旧记录。
第二,过期时间默认10分钟。过期时长不能太长也不能太短:太长老老实实躺库里的脏数据多,而且人齐之后邀请码还一直有效会带来安全问题;太短用户在微信里转一圈再回来就过期了,体验很差。实测10分钟是一个比较合理的值。
校验入队时,服务端需要做三层检查:邀请码是否存在、是否过期、房间是否还有空位。这里建议再校验一下“发起邀请的人和房间房主是否一致”,防止有人拿别人的邀请码往别人房间里塞人。
3.3 深链拉起App:OpenHarmony Ability的Uri配置与参数解析
深链功能是整个邀请体验里最“高级”也最容易出问题的地方。先看OpenHarmony侧的配置,在ohos/entry/src/main/module.json5中,EntryAbility的skills里添加uris匹配规则:
json复制{
"skills": [
{
"uris": [
{
"scheme": "jubensha",
"host": "join",
"path": "room",
"linkFeature": "OpenLink"
}
]
}
]
}
这样配置后,系统会把形如jubensha://join/room?code=XXXXXX&roomId=123的链接路由到App的EntryAbility。不过配置只解决“App能被拉起来”的问题,真正麻烦的是参数传递。
当App已经处于前台或后台运行时,深链拉起App不一定会重新走Flutter首页的init流程,而是触发Ability的onNewWant回调,OpenHarmony会把新的Want参数带进来。Flutter端需要通过平台通道才能拿到这个参数。我封装了一个ohos_deep_link插件,在原生侧监听onNewWant,把Uri字符串通过EventChannel推给Flutter侧,Flutter侧订阅后解析出inviteCode和roomId,再执行自动入队逻辑。
这里有个比较隐蔽的坑:如果App是被冷启动拉起的,Flutter侧在main()里初始化时原生侧可能还没准备好EventChannel。我一开始的写法是直接在main()里调用deepLink.getInitialLink(),但鸿蒙上这个方法经常返回null。后来改成“先等待实例绑定,再主动查询一次初始参数,再订阅后续的onNewWant”,双管齐下才稳定。道理和Android的冷启动intent处理很相似。
3.4 入队状态同步:轮询、WebSocket与消息回调的取舍
邀请码和深链都解决“把好友带到房间门口”的问题,好友点击加入后,房主这边的成员列表需要实时刷新。组队场景有“人数少、状态变化不频繁”的特点,所以技术选型上轮询和WebSocket都可以。
我用的是WebSocket加轻量消息协议。房主创建房间后,客户端会连上房间频道,频道内广播成员加入、成员准备、房主开局等事件。好友入队的流程是:解析出inviteCode和roomId → 调用加入接口 → 成功后通过WebSocket向房间频道推送MEMBER_JOINED。房主端收到事件后更新本地成员列表并播放一个轻微的音效或震动提示。
如果不想引入WebSocket,用1到2秒一次的轮询也能实现同样的效果,只是房主端眼睁睁看着成员列表每两秒“跳”一次,体验会比较差。而且剧本杀组队过程中,准备状态、发言阶段等后续功能都需要实时性,与其以后再换方案,不如一开始就上WebSocket。
注意:OpenHarmony设备上WebSocket连接属于网络操作,需要在module.json5中申请
ohos.permission.INTERNET权限。忘记申请的话,连接会静默失败,控制台里只有底层socket的报错,排查起来非常费劲。
4. 真机联调中的真实问题与排查记录
4.1 编译期问题:Gradle插件报错与SDK路径不匹配
这个项目的构建链是“Flutter侧构建生成中间产物 + OpenHarmony侧gradle组装hap”,两个工具链叠在一起,问题多也正常。热词里提到的“you are applying flutter's main gradle plugin imperatively using the apply s”就是一个典型的gradle插件应用方式的报错。
出现这条错误,通常是因为工程里混用了不同版本的Flutter/OpenHarmony gradle插件。我们项目里OpenHarmony侧的build.gradle用的是com.huawei.ohos:chdplugin,而Flutter侧又带了dev.flutter.flutter-plugin-loader,两者如果配置顺序或版本不对,就会触发这种“imperatively using apply script”的兼容性提示。解决方式是检查根目录build.gradle中插件声明和settings.gradle中的pluginManagement仓库顺序,确保两个插件严格按官方示例的写法配置,不要自己“优化”。
另一个高频问题是我在local.properties里指定了错误的SDK版本。OpenHarmony SDK是按API版本分的,如果DevEco Studio的SDK版本比项目要求的低,构建时不会直接报“版本不够”,而是报一堆R类符号找不到。这种错误很迷惑。建议先跑hdc shell param get const.product.software.version确认设备系统版本,再反推SDK版本,不要盲目升级。
4.2 深链跳转与状态恢复问题:页面没刷新、参数丢失
深链接入后的第一轮联调,我遇到最奇怪的问题:从系统桌面点App图标冷启动,能正常进入首页;但从微信H5页面通过深链拉起App,App虽然被拉起来了,Flutter端却长时间停留在“启动页白屏”。后来定位到原因是启动参数没有及时传给Flutter引擎,导致Flutter侧一直没触发路由跳转逻辑。
第二个深链问题是热启动时参数丢失。App已经从后台被拉起到前台,但Flutter页面的initState不会重新执行,所以如果你只在initState里读取Deep Link参数,那大概率会漏掉这次拉起的邀请参数。我最终的做法是:在Flutter根组件里监听AppLifecycleState.resumed,同时接收原生侧onNewWant推送的EventChannel事件,两者同时处理,保证热启动也能正确读取邀请参数。这段逻辑建议封装成一个单例的DeepLinkService,避免页面之间相互传参造成状态混乱。
4.3 邀请状态不同步问题:重复入队、过期码还能用
联调过程中,我和后端同事一起发现了一个比较典型的业务问题:好友在A页面已经成功加入房间,但服务端因为网络原因没返回给客户端成功结果,用户以为没加入成功,又点了第二次加入,导致同一个人在同一房间里出现了两条成员记录。虽然这个问题的根子在接口幂等性上,但客户端也可以通过“入队按钮一旦点击就进入loading状态,且短时间内禁止再次点击”来缓解。
另外邀请码过期问题,我们最初是在客户端判断过期时间,结果用户只要稍微修改一下手机时间就绕过了限制,所以后来改成服务端校验为准,客户端只负责界面提示。这个点对于做组队类App的人特别重要:所有状态类的校验,信任边界都在服务端,客户端永远不会是安全的边界。邀请码校验结果分为三类:无效码、已过期、已入队,客户端需要分别给出不同的toast提示,不要统一弹“邀请码错误”,否则用户不知道到底是输错了还是过期了。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| flutter build hap失败,找不到SDK | local.properties未指定SDK | 配置sdk.dir为DevEco Studio的Sdk路径 |
| 安装hap成功后点图标闪退 | 设备系统版本低于SDK target | 用hdc shell param get const.product.software.version确认版本 |
| 页面白屏,Dart层无日志 | Flutter引擎未被正确加载 | 用hilog过滤flutter关键字,观察引擎启动日志 |
| 深链拉起App后无跳转 | 冷启动初始参数获取失败 | 主动查询一次初始链接,再订阅onNewWant |
| 热启动时邀请参数丢失 | 没有监听resumed状态 | 在AppLifecycleState.resumed中重新解析参数 |
| 重复入队产生脏数据 | 加入接口缺少幂等校验 | 服务端按userId+roomId去重;客户端做loading防抖 |
| 客户端时间被修改导致邀请码不过期 | 过期校验放在客户端 | 过期时间统一由服务端校验 |
| WebSocket连接不上 | 缺少INTERNET权限 | 在module.json5中检查权限声明 |
5. 几个值得再深入的方向
邀请好友功能做完之后,项目其实还有很多可以扩展和优化的点。就我个人的实际体验来说,有几个方向非常适合在“剧本杀组队”这个场景里继续深耕。
5.1 扫码组队:离线场景的补充
OpenHarmony设备上有很多自带扫码能力的硬件,剧本杀门店如果给每个房间配一个二维码,玩家到店后扫码即入队,就不需要手动输入邀请码了。实现上可以在Flutter侧接入扫码插件,扫码结果解析出roomId后,走和深链完全相同的入队逻辑,复用度非常高。
5.2 开局后角色秘密分发
剧本杀和普通组队最大的不同,是开局的瞬间需要给每个玩家分发秘密角色(凶手、侦探、平民、特殊身份),而且不能提前在房主的手机上展示。这个功能依赖实时消息通道,我的建议是把上一节实现的WebSocket消息协议扩展出ROLE_ASSIGN事件类型,服务端在房主点“开局”时把角色信息逐人下发,而不是一次性广播全量角色列表,这样才能保证每个玩家只能看到自己的身份。
5.3 组件化改造:把邀请逻辑沉淀成独立模块
如果后续要做多个OpenHarmony应用复用这套邀请功能(比如狼人杀App、密室逃脱组队App),建议现在就把它抽成一个独立的Flutter插件包,对外只暴露inviteService和InviteCallback接口。内部拆成invite_api、invite_code、invite_deeplink三个子模块。这样业务层可以完全隔离,也方便做单元测试。我在实际开发中体会最深的一点是:邀请功能虽然看起来只是几条接口和两个页面,但它横跨了配置、路由、状态同步、异常兜底多个层面,任何一环断了,用户感知都非常直接。把这一环做扎实了,后面加再多的功能心里都有底。
