最近在做Flutter应用往OpenHarmony上迁移的事,项目里需要一个标准的“关于”页面。Flutter的Material组件库里现成就有AboutDialog,心想这还不简单,几行代码就完事了。等到真机上一跑才发现,事情没那么顺利:对话框是弹出来了,可标题区域直接一片空白,版本号取不到,默认字体显示也怪怪的。
当时我就意识到,这种系统组件在跨到OpenHarmony平台之后,很多在Android上“约定俗成”的东西都会掉链子。这篇我就结合自己的实操,把Flutter for OpenHarmony里AboutDialog的完整用法、适配思路和踩坑记录都理一遍,给后面做跨端移植的朋友打个样。
1. 为什么偏偏要聊AboutDialog这个组件
很多人可能觉得“关于对话框”不就是个展示版本号的小弹窗嘛,有什么好单独拿出来讲的。但在我把Flutter应用移植到OpenHarmony之后,最大的感触恰恰是:越不起眼的系统组件,越容易在跨平台时暴露问题。
AboutDialog在Flutter里承担的事情其实不少。它不只是弹出一个“版本号+Logo”的窗口,还负责展示应用名称、版权声明(legalese)、开源许可证列表,甚至可以通过children属性塞入自定义的内容。在OpenHarmony应用上架的合规审核里,关于页面、开源许可声明、隐私政策入口这些都属于基础要求,很多厂商的XTS认证流程也会重点核对应用内是否提供了完整的信息披露入口。
再看当前社区的热度,Flutter开发者把大量精力放在组件通信、Provider状态管理、Impeller渲染引擎这些新玩意儿上,反倒忽略了系统组件在跨端场景里的适配问题。ArkTS和Flutter谁更流行的争论先放一边,实际迁移过代码的人都知道:状态管理写错了顶多页面白屏,AboutDialog这类组件适配不好,应用在真机上直接显示乱码,这更致命。
我用的方案也不是什么高深套路:用Flutter自带的AboutDialog作为骨架,针对OpenHarmony平台的差异做定制替换,并把获取版本信息、加载许可证、跳转隐私政策这些能力补全。 这套思路你在自己项目里也能直接照搬。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 跑通Flutter for OpenHarmony是第一道门槛
2.1 SDK分支别用错
首先要明确一点:官方下载页里的Flutter SDK默认不支持OpenHarmony平台。想让Flutter跑在OpenHarmony上,得用OpenHarmony SIG维护的flutter_flutter仓库,代码托管在Gitee上,里面有专门适配OpenHarmony的分支。这里我踩过的第一个坑就是分支选择:直接用master分支去flutter create,生成的工程根本找不到ohos目录。
正确做法是clone下仓库之后,先checkout到OpenHarmony对应的release分支。我当时用的是OpenHarmony 5.0系列的适配分支,这个分支的版本号命名和OpenHarmony系统版本是对齐的,你可以根据自己开发板的系统版本选择。具体分支名以仓库的README为准,因为这个项目更新比较快,写死分支名没有意义。
环境上除了Flutter SDK,还必须有DevEco Studio配套的OpenHarmony SDK、ohpm包管理工具以及hvigor构建工具。如果你在Windows上装过纯Flutter环境,那这套OpenHarmony环境的区别主要在SDK路径和命令行工具上,别把Android SDK的路径直接填进来,会报各种奇怪的错。
2.2 工程目录和命令都不一样
在Flutter for OpenHarmony下创建工程,和标准Flutter工程最大的区别就是多了一个ohos目录:
code复制project/
├── android/ # Android平台工程
├── ohos/ # OpenHarmony平台工程,替代了ios目录的位置
├── lib/ # Dart代码
└── pubspec.yaml
构建命令也变了,Android平台用flutter build apk,OpenHarmony平台得用flutter build hap。还有一个很常见的报错:You are applying Flutter's main Gradle plugin imperatively using the apply script。这个错误虽然看起来是Gradle插件的警告,但在我混合构建多平台工程时频繁出现。核心原因是你同时保留了android和ohos两套构建配置,而ohos侧走的是hvigor而不是Gradle,两套构建系统抢了同一个插件的配置。解决办法就是把构建命令分开、把Gradle相关的环境变量独立处理,不要在同一个shell会话里来回切换导致缓存污染。
如果你只需要OpenHarmony这一个平台,完全可以把android目录删掉,能省掉不少构建时的缓存问题。当然,如果你是要做双端打包,那就得靠CI里的不同任务去隔离了。
2.3 真机连接用hdc不是adb
连接OpenHarmony设备用的工具是hdc(OpenHarmony Device Connector),装完DevEco Studio之后在Sdk/toolchains目录下能找到。检查设备命令:
bash复制hdc list targets
设备连上之后,再用flutter devices确认Flutter能识别到设备。我在这个环节卡了将近一个小时,原因是开发板开着USB调试,hdc死活识别不到。后来发现OpenHarmony的“允许调试”开关不是默认开启的,需要在开发者选项里手动打开,部分开发板还要在hdc shell里执行一条授权命令把USB模式确认一遍。
热重载在OpenHarmony上也是可用的,但和Android上“改了Dart代码立刻刷新”的体验略有差别。修改纯Dart代码时热重载没问题,一旦动了原生插件或者修改了ohos目录下的配置文件,热重载就会失效,必须重新flutter run整包安装。这个限制我在后面调试AboutDialog的字体问题时反复体会到,每次改完原生代码就得等一遍完整构建。
3. 默认AboutDialog在OpenHarmony上暴露的三个问题
标准写法是这样:
dart复制showAboutDialog(
context: context,
applicationName: '我的应用',
applicationVersion: '1.0.0',
applicationIcon: const FlutterLogo(),
applicationLegalese: '© 2024 My Company',
children: const [
Text('这是一个关于页面的示例。'),
],
);
这段代码在Android和iOS上都没毛病,但到了OpenHarmony真机上,我遇到了三个明显的问题。
3.1 版本信息和应用名称拿不到
在桌面操作系统或者Android上,Flutter的AboutDialog会通过系统API去拿应用显示名称和版本号。但在OpenHarmony上,Flutter引擎层对“获取应用信息”的支持路径和Android完全不同。我最初用getApplicationLegacy拿版本名,结果返回空字符串;用package_info_plus插件去拿,在OpenHarmony平台上也不支持,直接抛MissingPluginException。
后来我没在这个接口上死磕,而是用了简单粗暴的降级方案:把版本号和应用名维护在工程内的一个Dart常量文件里,通过kRelease的性质手动管理。虽然听起来不优雅,但至少稳定。
3.2 默认字体的蜜汁渲染
OpenHarmony系统本身是有中文字体的,但Flutter引擎在OpenHarmony上的字体回退链跟Android不太一样。我在开发板上跑默认AboutDialog时,对话框标题和正文都显示正常,但某些特殊字符、版权符号、以及部分emoji直接变成了方框。这在Android上是从未出现过的。
排查下来基本可以确认是system font fallback的差异。解决办法是在MaterialApp的ThemeData里统一指定fontFamily,或者在对话框的Text样式里显式设置字体。你可以在项目里放一个开源字体文件,用FontLoader加载,这样能彻底绕开系统字体回退的问题。
3.3 Material主题和系统风格严重割裂
Flutter自带的AboutDialog在Android 12+上会跟随Material You动态取色,但OpenHarmony没有这套机制,默认主题色是Flutter标准蓝。在深色模式下,对话框背景、文字颜色、分割线的组合在OpenHarmony屏幕上显得很突兀。
如果你的应用本身已经适配了OpenHarmony的深浅色切换,建议在showAboutDialog外层包一个Theme,用自己设计好的色板覆盖Material默认值:
dart复制Theme(
data: Theme.of(context).copyWith(
colorScheme: ColorScheme.fromSeed(seedColor: brandColor),
textTheme: Theme.of(context).textTheme.apply(fontFamily: 'HarmonyOS Sans'),
),
child: const AboutDialogContent(),
)
这么做之后,在OpenHarmony下才能保证整体视觉一致。
4. 手写一个适配OpenHarmony的定制AboutDialog
既然默认组件在OpenHarmony上这么多毛病,我干脆基于Material的AboutDialog思路,写了一个自己的定制版本。这样不仅能彻底控制UI表现,还能把版本信息、开源许可、隐私政策这些内容全塞进一个数据模型里管理。
4.1 先定义数据模型
把关于页面的基础信息抽成类:
dart复制class AboutAppInfo {
final String name;
final String version;
final String buildNumber;
final String copyright;
final String privacyPolicyUrl;
final List<MapEntry<String, String>> licenses;
const AboutAppInfo({
required this.name,
required this.version,
this.buildNumber = '',
this.copyright = '',
this.privacyPolicyUrl = '',
this.licenses = const [],
});
}
然后定义一个常量实例,把版本号维护在这里:
dart复制const aboutInfo = AboutAppInfo(
name: 'My App',
version: '1.2.0',
buildNumber: '20241115',
copyright: '© 2024 My Company. All rights reserved.',
privacyPolicyUrl: 'https://example.com/privacy',
licenses: [...],
);
4.2 自定义对话框的UI结构
Material默认的AboutDialog结构是“图标+应用名+版本号+licenses入口”,我保留了同样的骨架,但把每个部分都改成可配置的。
dart复制class CustomAboutDialog extends StatelessWidget {
final AboutAppInfo info;
const CustomAboutDialog({Key? key, required this.info}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AlertDialog(
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
const FlutterLogo(size: 32),
const SizedBox(width: 12),
Expanded(
child: Text(
info.name,
style: theme.textTheme.titleLarge,
overflow: TextOverflow.ellipsis,
),
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('版本 $info.version ($info.buildNumber)'),
const SizedBox(height: 8),
Text(info.copyright, style: theme.textTheme.bodySmall),
const Divider(height: 24),
buildButtonList(context),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('关闭'),
),
],
);
}
Widget buildButtonList(BuildContext context) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
ActionChip(
avatar: const Icon(Icons.privacy_tip_outlined, size: 18),
label: const Text('隐私政策'),
onPressed: () => openPrivacyPolicy(context),
),
ActionChip(
avatar: const Icon(Icons.description_outlined, size: 18),
label: const Text('开源许可'),
onPressed: () => showLicensePage(context),
),
],
);
}
}
启动方式可以直接用showDialog包一层:
dart复制void showCustomAboutDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: (_) => const CustomAboutDialog(info: aboutInfo),
);
}
4.3 版本号和开源许可的数据来源
版本号这块我前面说了,直接用常量维护。但在更大一点的团队里,版本号总是跟构建流程绑定的,手动维护容易漏。我推荐的做法是在CI脚本里,用sed或者脚本工具在构建前把版本号写进aboutInfo对应的dart文件,或者在构建时根据环境变量动态生成。总比真机上显示空值强。
开源许可列表,我用了flutter_oss_licenses这个包,它可以直接扫描项目依赖生成oss_licenses.dart文件:
bash复制flutter pub run flutter_oss_licenses:generate.dart
生成的文件里包含所有用到的三方库的许可证文本,然后用一个页面去列表展示。这比死记硬背靠谱,也完全符合应用商店对开源许可证披露的要求。
要注意的是,这个工具是依赖pubspec.lock去扫描的,如果你用的是Flutter for OpenHarmony的SDK,扫出来的依赖可能和平常有点区别,但基本不影响使用。
4.4 一个是带链接的文章
隐私政策跳转这块,我一开始用的是url_launcher插件的标准API:
dart复制Future<void> _openUrl(String url) async {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
但url_launcher对OpenHarmony的支持,需要单独引入适配插件,没适配的话会直接报MissingPluginException。我没在这个插件上死磕,直接封装了一个MethodChannel,调用OpenHarmony的能力打开外部浏览器。
在工程ohos目录的Ability里,自己实现一个channel handler:
dart复制// 这里是Dart侧
static const _channel = MethodChannel('com.example.app/browser');
await _channel.invokeMethod('openBrowser', url);
原生侧用OpenHarmony的Want能力拉起浏览器应用,这是一个绕开插件依赖的办法。当然,如果你能找到已经适配Ohos的插件版本,直接用插件更省事。
4.5 搭配AboutListTile做设置页入口
Material库里的AboutListTile是专门放在设置页里用的,它会把整个AboutDialog包成一个ListTile:
dart复制AboutListTile(
applicationName: 'My App',
applicationVersion: '1.2.0',
applicationIcon: const FlutterLogo(),
applicationLegalese: '© 2024 My Company',
)
但在OpenHarmony上我建议把它替换成普通的ListTile,然后onTap回调里调用我上面写的showCustomAboutDialog。这样既保留了UI一致性,又绕开了默认组件的适配问题。
5. 从点击到显示:真实排错链路记录
这一节我专门复盘一下调试过程里遇到的两个最典型的故障场景,整条排查链路值得你自己走一遍。
5.1 现象一:对话框弹出但内容全部空白
第一次在OpenHarmony开发板上点击“关于”按钮,对话框能弹出来,但整个content区域一片空白,连默认的版本号都看不到。
我的排查链路是这样走的:
第一步,先在showAboutDialog前后加日志,确认child内容确实构建了。打印出来一切正常。
第二步,用flutter attach挂上调试器,在content的Text组件上打断点,发现Text的text内容不为空,但渲染区宽高为0。
第三步,怀疑是主题字体问题,把ThemeData里的fontFamily临时去掉,空白现象消失。这证实了我的猜测:OpenHarmony上,指定的字体族如果没有正确fallback到系统字体,文字的排版宽度会计算成0,导致整个区域不可见。
后续的处理方案是:不指定这个字体族,改用系统默认;如果必须在特定场景用该字体,那就用FontLoader提前加载,并等字体加载完再显示对话框。
5.2 现象二:hot reload之后AboutDialog点不出来
代码改完热重载,再点击按钮,这次连对话框都不弹了,控制台报了一堆异常。定位后发现:我新增了BuildContext跨异步使用的问题,在一个async函数里先await了隐私政策链接的校验,然后才用之前的context去showDialog,页面已经切换了。
修复方式就是所有弹窗操作都在build阶段同步抛出,异步操作的结果通过StatefulWidget的字段去管理,不要用context跨异步调用。
5.3 打包hap时卡的几个环节
flutter build hap和Android的flutter build apk流程类似,过程中会先构建原生部分。最容易忽略的是签名配置:OpenHarmony的build-profile.json5里需要配置签名证书,调试时用DevEco Studio的自动签名,发布时换正式的发布证书。
还有一个老生常谈的报错,就是你刚建完Flutter项目后直接跑起来的场景。如果你用的是OpenHarmony专用SDK分支,运行flutter run时加上-d指定设备id,不要让它自动选设备,能省掉很多设备干扰问题。
6. 关于页面也能成为状态管理和性能优化的实验场
别看就是一个小对话框,我后面在它上面做了不少扩展,还顺手解决了一些全局状态管理的问题。
6.1 用Provider管理“关于信息”的全局状态
如果你的应用支持多语言切换,那关于页面里的应用名称、隐私政策链接、版本说明都可能需要跟着语言变。这时候把AboutAppInfo放进Provider里就很合适。
dart复制class AboutModel extends ChangeNotifier {
AboutAppInfo _info = aboutInfoZh;
AboutAppInfo get info => _info;
void switchLanguage(String locale) {
_info = locale == 'zh' ? aboutInfoZh : aboutInfoEn;
notifyListeners();
}
}
然后顶层用ChangeNotifierProvider包一下,在CustomAboutDialog里通过context.watch<AboutModel>().info去读取数据。这样语言切换后,所有页面的关于信息都会同步刷新。这就是一个把Flutter Provider运用到真实场景的例子,比你单独练组件通信有意思多了。
6.2 打开对话框时的帧率问题
在OpenHarmony的低端开发板上,AboutDialog打开时偶尔会掉帧。原因是许可证列表加载太慢,主线程被卡住了。我的处理方法是把许可证数据拆成一个单独的异步加载任务,进入对话框页面时先展示loading,数据准备好了再刷新列表。整个过程用FutureBuilder就能搞定,完全没必要上重量级方案。
顺便说一句,Flutter的Impeller渲染引擎在OpenHarmony上属于较新的能力,如果你遇到奇怪的渲染闪烁问题,可以试试切换渲染模式。我在一台老旧开发板上就遇到过半透明阴影渲染异常的情况,切到软件渲染后问题解决。
6.3 后续还能扩展什么
现在这个定制对话框已经可以在OpenHarmony、Android、iOS三端统一使用了。后续如果想继续完善,我建议在以下几个方面做扩展:
- 增加“检查更新”入口,点击后请求服务端接口判断新版本。
- 接入应用内反馈渠道,让关于页面成为一个反馈入口。
- 针对儿童模式等特殊场景,隐藏隐私政策里的广告追踪说明。
- 将许可证列表做成搜索功能,应用依赖多的时候这个功能很实用。
最后分享一个我自己的经验:跨端适配的系统组件,不要一上来就追求“零改动”,先把默认行为在目标平台上跑一遍,把不符合预期的点列出来,再决定哪些要改。AboutDialog这套流程走下来,我对Flutter在OpenHarmony上的适配机制理解得比之前透彻多了。也建议你把手里的项目里所有用了Material标准组件的地方,都拿真机点一遍,你会有收获的。
