☰
Flutter+OpenHarmony迁移实战:手语学习App开发与踩坑记录
2026/10/9 6:01:27 网站建设 项目流程

1. 为什么把手语学习App跑在OpenHarmony上:选型逻辑与项目背景

先说结论:这半年我一直在做一件有点"折腾"的事,把手语学习App从传统的Android/iOS双端,迁移到Flutter + OpenHarmony这套组合上。项目名叫flutter_for_openharmony手语学习app,核心是完成整个App的实战开发,同时把"关于我们"这种看起来不起眼、实际坑不少的页面做到能打。

为什么要这么折腾?因为我发现手语学习这个场景,和OpenHarmony的适配度其实非常高。手语学习者最需要的是什么?是随时随地打开App看标准动作视频、反复跟练、记录自己的学习进度。这些功能完全可以跑在轻量终端上,比如学习平板、一体机,甚至以后的手语翻译穿戴设备。OpenHarmony作为面向全场景的操作系统,正好覆盖这些设备类型。如果只做Android版本,那些国产化终端、教育平板就全部放弃了。

再说为什么选Flutter而不是ArkTS。这个决定我们内部讨论过好几轮。OpenHarmony真正的原生语言是ArkTS,团队里也确实有人建议直接写ArkTS。但我们的底气在于:Flutter的跨端能力已经被验证过,一套Dart代码可以跑在Android、iOS、Web、Windows上,现在OpenHarmony社区也有专门的Flutter适配分支。对一个小团队来说,维护一套代码比维护三套代码要现实得多。更关键的是,Flutter的组件生态、状态管理方案、动画系统都成熟,手语学习这种视频+交互+动画密集型的App,用Flutter开发效率确实高。

项目基本信息也很简单:

  • 目标设备:OpenHarmony 4.1及以上的开发板/手机/平板
  • 技术栈:Flutter 3.22分支(OpenHarmony适配版)、Provider状态管理、sqflite本地存储
  • 核心功能:手语课程视频、动作拆解跟练、学习打卡、收藏分类、个人中心
  • 特殊页面:完整的"关于我们",包含版本信息、开源许可、意见反馈、主题切换

这个项目做下来,我的最大感受是:OpenHarmony + Flutter的组合已经过了"能不能跑"的阶段,现在拼的是"跑得稳不稳、细节到不到位"。而"关于我们"页面恰恰是这种细节的集中体现,它麻雀虽小,但涉及组件通信、状态管理、平台差异适配、插件兼容性,几乎每个Flutter项目里你都会碰到的典型问题,它全占齐了。

2. 手语学习App的需求拆解与工程规划

2.1 手语学习场景的特殊性

手语学习跟英语、编程这种传统线上课程不太一样,它有几个很明显的特征:

第一,教学载体是视频,而且视频要能慢放、能循环、能局部定格。手语的表达靠手形、位置、运动方向、面部表情共同完成,很多手语单词的差异非常细微,比如"谢谢"和"对不起"在手形上就有关键区别。所以App里的视频播放不能只做到"能放",最好还能做0.5倍速、逐帧暂停、循环播放。

第二,练习过程需要自我对照。标准的练习模式是:看视频学动作,然后自己对着摄像头练习,再跟标准视频对比。这就意味着App可能需要调用相机权限,做画中画对照。当然,初版我们没做实时姿态识别,那个模型成本太高,但我们预留了摄像头调用的入口。

第三,学习记录碎片化。用户可能每天只学10分钟,学三个新词、复习五个旧词,学习数据要细到"哪个词学过、掌握程度如何"。所以本地数据库是刚需。

2.2 页面架构与路由设计

基于以上场景,我把App的页面架构拆成了这样:

  • 首页:推荐课程、最近学习、热门手语分类
  • 课程列表:按主题分类,比如日常问候、餐饮、医疗、法律
  • 学习页:视频播放 + 动作拆解 + 跟练入口
  • 练习页:自我录制 + 标准动作对照
  • 我的页:学习统计、收藏夹、设置入口
  • 关于我们:App介绍、版本信息、反馈、开源许可

路由用的命名路由,统一在main.dart里管理:

routes: { '/': (context) => HomePage(), '/courses': (context) => CourseListPage(category: arguments), '/learn': (context) => LearnPage(courseId: arguments), '/practice': (context) => PracticePage(), '/mine': (context) => MinePage(), '/about': (context) => AboutPage(), '/feedback': (context) => FeedbackPage(), }

这里有个细节,手语课程分类场景下,课程列表页需要接收分类参数,我用的是命名路由的arguments传参。但"关于我们"页面我没走arguments,原因后面在第4章详细说,因为它涉及到跨页面的状态同步问题,是组件通信的典型场景。

2.3 数据层设计

手语学习的数据不复杂,但关系要理清楚。我设计了三个核心表:

CREATE TABLE category ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, icon TEXT ); CREATE TABLE course ( id INTEGER PRIMARY KEY, category_id INTEGER, title TEXT NOT NULL, video_url TEXT NOT NULL, gif_url TEXT, description TEXT, difficulty INTEGER, FOREIGN KEY (category_id) REFERENCES category(id) ); CREATE TABLE learning_progress ( id INTEGER PRIMARY KEY, course_id INTEGER UNIQUE, learned_count INTEGER DEFAULT 0, is_favorite INTEGER DEFAULT 0, last_time TEXT, mastery_level INTEGER DEFAULT 0 );

learning_progress表是核心,它记录用户对每个手语词的掌握程度,从0到5,0是未学习,5是完全掌握。后续打卡、学习统计都依赖这张表。

关于视频资源,初版我用的是本地assets里的mp4 + 远程URL两种方式。手语标准教学视频来自公开的通用手语词典,每个词一个短视频,几十秒到两分钟不等。实际开发中视频文件的体积控制是个问题,这里先按下不表,第6章我会说踩过的坑。

3. "关于我们"页面实战:一个看似简单实则五脏俱全的页面

3.1 页面需求清单:它到底要做多少事

先说个reverse直觉的事:很多新手觉得"关于我们"就是一张静态页,放个Logo、写两行介绍、加个版本号就完事了。实际在OpenHarmony + Flutter这个组合下,它至少要承担这些职责:

  1. App品牌展示:Logo、名称、口号、产品简介
  2. 版本信息展示:能从系统拿到真实的版本号和build号
  3. 功能特性预览:手语App的特色能力条列式展示
  4. 操作入口:意见反馈、检查更新、开源许可、隐私政策
  5. 主题状态联动:App切换到深色模式时,"关于我们"页面要即时响应
  6. 版权与法律信息:底部版权声明、备案信息(如果上架需要)
  7. 跨页面数据同步:比如在设置页改了主题,"关于我pages"要能感知

也就是说,"关于我们"其实是App信息的中枢页面,它要把分散的数据(版本信息来自插件、主题状态来自全局状态、开源许可来自静态数据、反馈入口来自页面跳转)汇总到同一个页面上。这个性质决定了它不能简单用setState写死。

3.2 页面UI结构与代码骨架

我规划的"关于我们"页面结构是:

  • 顶部:圆角Logo + App名称 + 版本号(小字灰体)
  • 中部:项目简介文字块,两到三句话说明App是做什么的
  • 特色功能卡片区:横向滚动的"课程体系""实时跟练""学习报告"三个小卡片
  • 列表功能区:意见反馈 / 检查更新 / 开源许可 / 隐私政策,四个ListTile
  • 底部:版权信息 + 备案占位 + 技术支持

整体用ListView包起来,保证小屏设备也能滚动。核心代码框架如下:

class AboutPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('关于我们'), centerTitle: true, ), body: ListView( children: [ _buildHeader(context), _buildIntro(context), _buildFeatureCards(), _buildActionList(context), _buildFooter(context), ], ), ); } }

这个页面我把StatelessWidget和状态管理结合起来用。表面上无状态,但内部通过context.watch来订阅全局状态的变化。这一点和OpenHarmony的声明式UI思路很像:状态在哪一层管理不重要,重要的是UI根据状态自动重建。

3.3 版本信息的正确获取姿势

版本号不能写死。在OpenHarmony上,Flutter应用获取版本信息有两条路:

第一条路是package_info_plus插件。这是Flutter社区标准做法,而且该插件有OpenHarmony的适配分支。用法很常规:

import 'package:package_info_plus/package_info_plus.dart'; Future<String> getAppVersion() async { final info = await PackageInfo.fromPlatform(); return '${info.version} (${info.buildNumber})'; }

第二条路是通过平台通道直接读OpenHarmony的配置文件。因为Flutter在OpenHarmony上的包管理信息和Android不完全一样,版本号可能存在于module.json5或app.json5里。如果package_info_plus在某个版本上返回空,可以走MethodChannel自己拿原生数据。

我在项目里用了package_info_plus成功拿到版本号,但要注意一个细节:OpenHarmony分支的package_info_plus版本号和Android的版本号格式可能不同,在展示前最好做一次格式化处理,避免出现null字符串。

3.4 开源许可列表:法律细节不能漏

手语学习App用了不少开源组件,包括Flutter SDK本身、sqflite、provider、video_player、dio等。"关于我们"页面里必须展示开源许可,这是合规要求,也是开发者素养的体现。

我用的是showLicensePage:

void _openLicensePage(BuildContext context) { showLicensePage( context: context, applicationName: '手语学习', applicationVersion: '1.0.0', applicationIcon: FlutterLogo(size: 56), applicationLegalese: '本项目遵循MIT协议,部分教学视频版权归原作者所有。', ); }

这个页面会遍历所有依赖包里的LICENSE文件并展示,非常省事。但注意,如果你的App在OpenHarmony上用了非Flutter的原生库(比如用ohpm装的OpenHarmony三方库),它们不会出现在showLicensePage里。所以我额外加了一个手动维护的许可列表:

final List<LicenseEntry> extraLicenses = [ LicenseEntry( 'ohos_camera', ['https://gitee.com/openharmony-sig/ohos_camera'], ['Apache License 2.0'], ), ];

这些细节在验收的时候很加分,而且对开源社区也是一种尊重。

3.5 意见反馈入口:不要只会写一个转跳

"关于我们"里的意见反馈,最容易做成"跳到一个表单页"就完事。我建议把这个入口做得更实用一点:

  • 点击"意见反馈"弹出底部弹窗,让用户选择反馈类型(功能异常、内容纠错、改进建议)
  • 内容纠错这个类型很有价值,因为手语教学视频里可能存在个别动作不标准的争议,用户纠错能帮我们持续改进内容
  • 提交后先写本地,等有网络再自动上报到服务端
void _showFeedbackSheet(BuildContext context) { showModalBottomSheet( context: context, builder: (ctx) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: Icon(Icons.report_problem), title: Text('功能异常'), onTap: () => Navigator.pushNamed(context, '/feedback', arguments: 'bug'), ), ListTile( leading: Icon(Icons.content_paste_go), title: Text('内容纠错'), onTap: () => Navigator.pushNamed(context, '/feedback', arguments: 'content'), ), ListTile( leading: Icon(Icons.lightbulb_outline), title: Text('改进建议'), onTap: () => Navigator.pushNamed(context, '/feedback', arguments: 'suggestion'), ), ], ), ), ); }

弹窗比直接跳转页面的好处是快、不打断学习节奏。用户在学习手语的过程中觉得"这个动作讲解不清楚",点两下就能反馈,停留成本很低。这个交互细节是我在真实使用中觉得最值得保留的一个设计。

4. 状态管理与组件通信:Provider在"关于我们"里的真实用法

4.1 为什么不是setState,也不是Bloc

"关于我们"页面如果要和设置页联动(比如深色模式切换),就有跨页面通信的需求。在Flutter里,跨页面通信的常见方案有三种:setState + 回调、Provider、Bloc。

setState的问题很明显:它只能管自己页面,跨页面要么层层回调,要么手动传参。如果设置页改了主题,你希望"关于我们"页立即跟着变,用setState就特别别扭,你得在页面层级上层层通知,代码很快就会被搅成一团。

Bloc则是反过来的问题,它很强大但样板代码太多。一个简单的主题切换,Bloc要写Event、State、Bloc、BlocProvider四层。对"关于我们"这种体量的页面,用Bloc属于杀鸡用牛刀。

Provider正好卡在中间:简单、灵巧、官方推荐,而且ChangeNotifier机制在学习成本上非常友好。我不会说 Provider比Bloc高明,但在"手语学习App"这种中小型项目里,Provider的逻辑清晰度和开发效率是最平衡的。

4.2 应用信息Provider的设计

我是这样设计Provider的。整个App的全局状态分为两块:一个是主题模式,一个是当前用户的学习数据。主题模式对全局所有页面可见,"关于我们"页面需要订阅它。

具体的Provider结构:

class ThemeProvider extends ChangeNotifier { bool _isDarkMode = false; bool get isDarkMode => _isDarkMode; void toggleTheme() { _isDarkMode = !_isDarkMode; notifyListeners(); } } class LearnerProvider extends ChangeNotifier { int _totalLearned = 0; int _favoriteCount = 0; int get totalLearned => _totalLearned; int get favoriteCount => _favoriteCount; Future<void> refreshStats() async { final db = await DatabaseHelper.instance.database; _totalLearned = await db.rawQuery( 'SELECT COUNT(*) AS cnt FROM learning_progress WHERE mastery_level > 0' ).then((rows) => rows.first['cnt'] as int? ?? 0); notifyListeners(); } }

ThemeProvider管UI层面的主题状态,LearnerProvider管学习统计。这两个Provider都注册在App的顶层,通过MultiProvider包裹:

MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => ThemeProvider()), ChangeNotifierProvider(create: (_) => LearnerProvider()), ], child: App(), )

注册位置很关键。一定要在MaterialApp之上包裹,这样不管是首页还是"关于我们"页面,都能通过context.watch拿到同一个Provider实例。

4.3 "关于我们"页面如何订阅数据

"关于我们"里的版本号展示,可以通过context.watch或context.select来精细化订阅。为什么要用select而不是watch?因为watch会监听整个Provider的所有字段变化,哪怕那个字段和本页面无关也会触发重建。

比如设置页切换了语言,ThemeProvider里的某个字段变了,如果"关于我们"用的是watch<ThemeProvider>,那整个页面会重建,这没必要。用select可以只监听我们关心的字段:

final isDarkMode = context.select<ThemeProvider, bool>( (theme) => theme.isDarkMode, );

这样当深色模式切换时,"关于我们"页面顶部Logo的背景色、文字颜色才会变化,其他部分不重建。在低端设备上,这种精细化订阅能明显减少重绘开销。

4.4 跨页面通信的具体联动链路

我项目里的联动链路是这样的:

  1. 用户在"我的"页面点击"深色模式"开关
  2. ThemeProvider.toggleTheme()被调用,状态翻转,notifyListeners()被触发
  3. Provider会通知所有订阅了这个Provider的组件
  4. 正在后台的"关于我们"页面由于已经被push了,它持有的context依然有效,数据变化会触发它重新build

关键点在于:Flutter的Provider是依赖InheritedWidget机制实现跨层通信的。所谓InheritedWidget,你可以理解为一个"全局广播站",顶层注册了状态之后,所有下级组件都能收到通知。这和OpenHarmony的@Provide/@Consume装饰器思路类似。我在开发OpenHarmony版本时,就直接类比了@Provide和@Consume来理解Provider,学习曲线一下就平了。

实际响应式效果见下面这个框架代码:

// AboutPage内部 Widget _buildHeader(BuildContext context) { final isDark = context.select<ThemeProvider, bool>((t) => t.isDarkMode); final info = context.watch<AppInfoData>(); // 这里用了watch,因为版本号变化应当重建整个头部 return Container( color: isDark ? Color(0xFF1F1F1F) : Color(0xFFF5F5F5), padding: EdgeInsets.symmetric(vertical: 32), child: Column( children: [ CircleAvatar( radius: 40, backgroundColor: isDark ? Colors.grey[800] : Colors.white, child: Text('手', style: TextStyle(fontSize: 36, color: Theme.of(context).primaryColor)), ), SizedBox(height: 12), Text('手语学习', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), SizedBox(height: 6), Text('v${info.version}', style: TextStyle(fontSize: 14, color: Colors.grey)), ], ), ); }

这里有两个细节:

  • 版本号数据我单独定义了一个AppInfoData类,存储在Provider里,因为版本号只有在启动时获取一次,不需要频繁更新
  • 深色模式用select,版本号用watch,因为版本号如果更新了,整个头部都应该重建,而主题切换只需要改背景色和文字颜色

这种拆分的思路,我认为是实际项目里组件通信的精髓:不是把所有的东西一股脑全塞进一个Provider,而是根据"变化频率"和"影响范围"来切割状态粒度。变化频率高的(主题)用小粒度监听,变化频率低的(版本号)用粗粒度监听。

5. OpenHarmony适配踩坑:从编译失败到真机运行的完整排错链路

5.1 环境配置和工程创建:网上教程大多教会你一半

先说环境。要在OpenHarmony上跑Flutter,不是随便下一个Flutter SDK就行的。OpenHarmony官方的Flutter分支维护在flutter_flutter的ohos分支上,你需要把SDK切换到那个分支,或者直接下载OpenHarmony发布的Flutter SDK压缩包。

我当时遇到的第一个坑:用普通Flutter的flutter doctor是检查不出OpenHarmony开发环境的。因为OpenHarmony用的是DevEco Studio,不是Android Studio。Flutter的ohos配置需要把DevEco Studio的SDK路径手动配置到flutter config里:

flutter config --ohos-sdk /path/to/devEco/sdk

配置完之后,创建工程的命令也和Android不太一样。普通Flutter用flutter create就能跑,但在OpenHarmony上,如果你直接flutter create再打开,会发现生成的目录结构里没有OpenHarmony平台相关的目录(比如ohos/)。需要额外使用OpenHarmony的模板,或者在工程创建后手动添加:

flutter create --platforms ohos .

加上--platforms ohos参数后,工程里会多出一个ohos目录,这个目录默认是DevEco Studio的工程结构,里面有entry、AppScope、hvigorfile等。

5.2 编译报错:签名配置缺失,最容易被忽略的一环

创建完工程,第一次执行flutter build hap(注意是hap,不是apk)时,我在编译最后阶段卡住了。错误提示大概是Failed to sign the hap package之类。

这个问题的根因是:OpenHarmony的HAP包必须签名才能安装到设备上,而DevEco Studio默认使用的是自动签名,需要你在DevEco Studio里登录并生成一个调试证书。但命令行flutter build hap不懂这个,它需要你显式指定签名配置文件。

解决方法是在ohos/entry/build-profile.json5里配置signingConfigs:

{ "signingConfigs": [ { "name": "default", "type": "HarmonyOS", "material": { "certpath": "path/to/your.cer", "storePassword": "your-password", "keyAlias": "your-alias", "keyPassword": "your-password", "profile": "path/to/your.p7b", "signAlg": "SHA256withECDSA" } } ], "buildOption": { "strictMode": { "caseSensitiveCheck": false } } }

这里有个容易混淆的点:你需要在DevEco Studio里生成一个.cer文件、一个.p7b文件,同时配置好keyAlias。如果直接复制网上示例里的storePassword,大概率会报password incorrect。因为这个密码是DevEco Studio生成证书时你设置的那个,每一台机器都不一样。

5.3 开发板上的渲染差异:Impeller和OpenHarmony不完全是朋友

Flutter从3.10起在Android上主推Impeller渲染引擎,不再是旧的Skia。但在OpenHarmony的Flutter分支上,Impeller的支持进度更快,因为OpenHarmony的图形栈是方舟图形引擎(DSGP),Flutter适配时直接对接的是OpenHarmony的统一的渲染接口。

我遇到的实际问题是:视频播放器在部分OpenHarmony设备上出现画面撕裂。排查链路如下:

  • 第一步怀疑video_player插件问题,换成手动支持Texture的方式,问题还在
  • 第二步怀疑是编解码器H.264的格式兼容问题,翻日志发现解码正常
  • 第三步将范围缩小到Flutter渲染层,打开flutter run --trace-skia检查绘制轨迹,发现视频帧的texture更新频率和设备VSync不一致

最终解决方案是在ohos工程里的entry/src/main/module.json5中配置了requestFullScreen和旋转模式,让视频播放Activity保持横屏等稳定渲染方向,再通过Flutter的video_player的mixWithOthers参数避免音频抢占。这套组合下来问题基本解决。

这个案例的典型意义在于:OpenHarmony机型杂,不同开发板的GPU驱动差异大,视频渲染相关的问题必须先定位是解码层、传输层还是绘制层,直接换播放器不是正确思路。

5.4 沙箱权限与文件路径:和Android不一样的游戏规则

手语练习功能需要录制视频,这里涉及相机权限和麦克风权限。在OpenHarmony上,权限声明不在AndroidManifest里,而是在module.json5的requestPermissions字段里:

{ "module": { "requestPermissions": [ { "name": "ohos.permission.CAMERA", "reason": "用于手语练习时录制自我对照视频", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }, { "name": "ohos.permission.RECORD_AUDIO", "reason": "用于录制练习视频的音轨", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }, { "name": "ohos.permission.INTERNET", "reason": "用于加载远程教学视频资源" } ] } }

注意reason是必填字段,而且描述要写清楚应用场景,否则部分机型在动态授权弹窗里会显示空内容。这个在申请时就要写规范。

文件路径的差异更隐蔽。Android上我们习惯用getExternalFilesDir()拿外部存储路径,但在OpenHarmony的Flutter分支上,path_provider插件返回的路径结构和Android完全不同。它通常返回的是类似/data/storage/el2/base/haps/entry/files/这样的沙箱路径。实测中,你在Android上用getApplicationDocumentsDirectory()写的文件,在OpenHarmony上对应的是应用的沙箱内路径,用户看它不到,备份也麻烦。所以手语练习的录像文件,我统一通过媒体库接口写入用户的公共视频目录,避免App卸载后数据全丢。

6. 手语学习App的调试与发布要点:从DevEco Studio联调到HAP安装

6.1 日志查看与联调技巧

OpenHarmony联调最实用的是hdc命令,作用相当于Android的adb。常用几个命令建议背下来:

# 查看连接的设备 hdc list targets # 安装hap包 hdc install path/to/your.hap # 启动应用 hdc shell aa start -a EntryAbility -b com.example.signlanguage # 查看Unity/Flutter运行日志 hdc shell hilog

hilog是OpenHarmony的日志系统,注意它和Android的logcat语法不一样。如果你想过滤Flutter的Dart层日志,记得在运行Flutter时加--verbose参数:

flutter run -d <device> --verbose

这样Dart侧的print和debugPrint输出会直接和OpenHarmony的hilog混在一起。我习惯在代码里统一用debugPrint而不是print,因为print在release模式下会被混淆且没有行号信息。

6.2 手语学习App的性能观察

手语视频播放对性能敏感,尤其是低端OpenHarmony开发板。我在开发板上观察到的数据是:视频播放时CPU占用约25%,内存峰值约450MB。这个水平对中低端设备偏高,分析后发现问题集中在视频解码组件上。优化手段主要是两处:

第一,视频列表页懒加载。首页的推荐课程列表原本一次性加载全部课程视频的封面图和预览信息,改成ListView.builder后,只有滚到可见区域才加载,内存立刻降了约80MB。

第二,本地数据库索引优化。手语学习App每次进入首页要从learning_progress表查最近学习记录,原来没有加索引,数据量到几百条时就开始变慢。后来加了course_id、last_time两个索引字段,查询速度提升一个数量级:

CREATE INDEX idx_progress_course ON learning_progress(course_id); CREATE INDEX idx_progress_last_time ON learning_progress(last_time);

6.3 发布前的自检清单

项目在发布前,我列了一个自检清单,每条都是实践中验证过的:

  • 版本号是否为真实获取,而不是写死。如果写死,用户反馈"检查更新说已是最新,但实际商店里版本已经更新",这种低级错误很败口碑
  • "关于我们"页面的开源许可列表是否完整。开源协议合规是一个App能否进入企业采购清单的基础门槛
  • 深色模式下所有图片是否有适配。手语课程里的GIF动图在深色模式下可能出现白底突兀,后续可以考虑统一加一层圆角底色
  • 退出登录、账户注销入口是否好找。在个人信息保护法规要求下,这个入口不能藏得太深
  • OpenHarmony设备和Android设备的屏幕适配。手语视频播放页在平板上的布局如果直接用手机版拉伸,观感会很差,可以考虑用AdaptiveVideoPage做响应式布局

发布HAP的最后一个环节是在DevEco Studio里配置app.json5的bundleName,这个包名是全应用唯一的,不能和Android包名混淆。打包命令是:

flutter build hap --release

生成产物在build/ohos/release目录下,后缀为.hap。把HAP文件拷贝到开发板或者通过应用市场分发,就完成了一个完整的发布流程。

回头再看这个项目,我最大的体会是:OpenHarmony生态的成熟度正在以肉眼可见的速度追赶Android,尤其是Flutter适配这部分,已经从"能跑Hello World"进化到了"能跑完整的业务App"。手语学习App的实战经历证明,只要踩对了环境配置、签名、权限这几个基础坑,剩下的Flutter开发体验和写Android几乎没有区别。而"关于我们"这个页面,恰恰是一个把平台差异、组件通信、状态管理这些知识点串起来的绝佳实践样本。如果你正在犹豫要不要投入OpenHarmony开发,别犹豫,动手搭个Flutter工程跑起来,比看十篇文章都管用。

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

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

立即咨询