写这篇博文的时候,我手上的OpenHarmony开发板刚好跑完一局三国杀的胜率统计。从Flutter工程创建到鸿蒙真机跑通,中间踩了不少坑,尤其是战绩记录功能涉及本地存储、列表刷新和统计计算,跟普通App开发完全是两回事。这篇文章是我用Flutter给OpenHarmony写三国杀攻略App时,实现战绩记录功能的全过程记录,包括环境配置、数据层设计、界面实现和问题排查。准备入坑OpenHarmony Flutter开发的兄弟,或者想在鸿蒙设备上做工具类应用的朋友,可以拿来做参考。
先说背景。三国杀攻略类App最核心的留存工具不是图鉴,而是战绩记录。玩家打完一局,需要快速记录身份、武将、胜负、评分,后续能看到自己总胜率、某个武将的胜负比,甚至连胜记录。这个功能玩法简单,但对数据存储、界面刷新和统计逻辑有明确要求,加上要跑在OpenHarmony这种非Android生态上,技术选型和适配方案就得提前想清楚。
1. 项目背景与技术选型:为什么用Flutter做OpenHarmony三国杀攻略App
1.1 三国杀攻略App的定位与战绩记录的核心诉求
三国杀攻略App的常规功能包括武将图鉴、卡牌查询、身份局规则速查,但这些都是静态内容,用户看完就走。真正能让用户留下来的,是一个“记录-查看-统计”闭环。战绩记录就是这个闭环的起点。
我设计的战绩记录功能,核心诉求有这么几条:
- 快速录入:打完一局后,10秒内完成一场记录,不能有复杂表单。
- 离线可用:不依赖后端服务,所有记录保存在本机,避免隐私和网络问题。
- 历史可查:按时间倒序展示所有对局,支持下拉刷新和长按删除。
- 统计分析:总场次、胜率、连胜、常用武将胜率,这些数字要有直接的计算结果。
这些诉求决定了技术方案不能太重。不需要云数据库,不需要用户系统,本地持久化加一套简单的统计逻辑就够。这也让我在选型时可以优先考虑开发效率和跨端兼容,而不是追求后端能力。
1.2 Flutter跨端能力与OpenHarmony的适配现状
选择Flutter,核心原因是跨端一致性和开发效率。一套Dart代码可以跑在Android、iOS、Web,以及OpenHarmony上。对于三国杀攻略这种中小型App,Flutter的UI开发效率和内置组件完全够用。
但OpenHarmony不是Android,它的API、文件系统、权限模型和应用沙盒都有自己的一套逻辑。官方Flutter SDK默认支持的是Android和iOS,要跑在OpenHarmony上,必须使用OpenHarmony社区维护的Flutter适配分支。目前流行的做法是使用flutter_flutter仓库的openharmony分支,配合DevEco Studio构建hap包。
我在搭建环境时踩过不少坑,后面会详细说。这里先提醒一句:不要直接拿官方Flutter主分支编译OpenHarmony目标,否则会出现大量找不到工程文件之类的问题。选型时一定要确认适配分支的版本号,并且尽量使用OpenHarmony 3.2以上的系统版本。
1.3 技术方案选型:路由、状态管理与本地存储
战绩记录功能涉及页面跳转、数据变更后刷新UI、本地读取与写入。我在技术选型时做了以下几项决策:
- 状态管理:使用Provider。理由是这个项目体量不大,Provider足够清晰,社区文档多,遇到问题也好搜。
- 路由:使用Navigator 1.0。为了降低适配风险,没有引入flutter_boost或go_router这些额外路由库,直接使用系统自带的Navigator,减少一层依赖。
- 本地存储:用JSON文件存储。这里我犹豫过,是使用shared_preferences、sqflite,还是直接写文件。后来实际尝试发现,OpenHarmony上sqflite的适配还不成熟,编译期容易报错,shared_preferences适合存储键值对,但对几千条战绩数据的查询和筛选支持较弱。最后选择了path_provider获取应用文档目录,用dart:io手动读写JSON文件,封装一个Store类来管理所有数据操作。
这个选型的好处是可控性强、依赖少、没有数据库版本升级的烦恼;缺点是每次增删改都要整体读写文件,性能上不如数据库。但战绩数据平时就是几百条到几千条,量级不大,JSON文件完全能扛住。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开发环境搭建与工程改造:把Flutter跑上OpenHarmony
2.1 环境准备:OpenHarmony SDK与Flutter适配分支
这一步是整个项目最容易劝退人的环节。环境版本不匹配,后面全白搭。我建议按以下顺序准备:
- 安装DevEco Studio最新版,自带的OpenHarmony SDK至少是3.2版本。
- 下载OpenHarmony Flutter适配SDK,通过git clone拿到openHarmony分支。
- 配置环境变量,包括flutter、dart、hdc和ohos_sdk的路径。注意环境变量配置后需要新开终端才会生效,我当时就因为这个白白折腾了十几分钟。
- 使用
flutter doctor检查环境,但在OpenHarmony环境下不要期待它全部绿勾,工具链本身没有对应的检测项,重点看Dart和Flutter版本是否正常。
版本匹配是重点。Flutter官方主分支迭代很快,OpenHarmony适配分支通常滞后几个版本。我用的组合是Flutter 3.7.12 + Dart 2.19.6 + OpenHarmony 3.2 Release,整体比较稳定。如果你用更新的Flutter版本,大概率会在编译阶段遇到各种API变更问题。
2.2 创建Flutter工程并集成鸿蒙平台支持
创建工程并不难,难在把它变成鸿蒙可构建的工程。步骤大致如下:
使用flutter create创建普通Flutter项目:
bash复制flutter create battle_record_app --org com.example
然后进入项目中,执行OpenHarmony适配脚本,或手动在工程根目录下增加ohos目录。常见做法是使用flutter_flutter提供的flutter create --platforms ohos参数,或者直接拷贝一个现成适配工程的ohos目录进来。
我手动改造时重点修改了这几个文件:
ohos目录下的build-profile.json5,配置hap包名和签名信息。AppScope/app.json5,修改应用名称和版本号。entry/src/main/module.json5,配置Ability的入口和权限声明。entry/oh-package.json5,声明依赖的Flutter插件和鸿蒙SDK组件。
配置完以后,用DevEco Studio打开ohos目录,等待Gradle和ohpm依赖同步完成。这时候不要着急编译,先检查一下签名是否配置好,否则真机安装会失败。
2.3 真机调试与日志输出
OpenHarmony真机调试和Android类似,但工具链不同。通过hdc命令行连接开发板,确保设备在线:
bash复制hdc list targets
如果设备列表为空,检查USB驱动和开发者模式,部分RK3568开发板还需要手动打开“USB调试”开关。
构建并安装hap包到设备,我一般用:
bash复制flutter build hap --debug
hdc install entry/build/default/outputs/default/entry-default-signed.hap
这里有个坑:flutter run命令在OpenHarmony分支上不一定支持完整的热重载,有时改动后不会自动同步。我在调试战绩记录页面时,更多是用flutter build hap配合hdc install,虽然编译慢一点,但结果可控。
日志查看用hilog命令,按标签过滤:
bash复制hilog | grep Flutter
Flutter侧打印的debugPrint和print日志都能在hilog里看到,但格式和Android的logcat不太一样,建议在代码里统一加个带[BattleRecord]前缀的日志,方便过滤。
3. 战绩记录功能的数据层设计
3.1 数据模型设计:从对局字段到Dart类
战绩记录的第一步是定义数据结构。我设计了BattleRecord模型类,字段如下:
dart复制class BattleRecord {
String id; // 唯一ID,用时间戳+随机数
String mode; // 模式:身份、国战、1v1
String roleCamp; // 阵营:主公、忠臣、反贼、内奸
String roleName; // 使用武将名称
bool isWin; // 本局是否胜利
int killCount; // 击杀数
int score; // 评分,1-10
int createTime; // 对局时间,Unix时间戳
String note; // 备注
}
为什么这样设计?几个关键字段的解释:
id不能省,列表刷新、删除操作都需要用它作为唯一标识。mode和roleCamp用于后续筛选统计,如果只存一个“胜负”标记,后面做各模式胜率就得重新解析字符串,太被动。createTime用时间戳而不是格式化后的字符串,排序和统计方便。score虽然主观,但玩家很喜欢看平均评分,这个字段能支撑后续的写轮眼功能。
Dart类的序列化我直接用toJson和fromJson手写,没有引入json_serializable,减少代码生成步骤。数据字段不多,手写完全够用。
3.2 本地持久化方案对比:shared_preferences、sqflite、还是JSON文件
这块我实际对比过,列个表说明:
| 方案 | 优点 | 缺点 | OpenHarmony适配 |
|---|---|---|---|
| shared_preferences | 接口简单,适合键值对 | 不适合存储大量列表数据,查询筛选能力弱 | 官方插件有移植版,基本可用 |
| sqflite | 支持SQL查询,适合复杂数据 | API较重,需要维护数据库版本和表结构 | 社区适配版本有编译问题和崩溃风险,我放弃了 |
| JSON文件 | 简单可控,无依赖,读写清晰 | 数据量大了以后性能会下降,并发写需要处理 | 完全可控,使用dart:io实现 |
最终选择JSON文件的另一个原因是我只有一套本地数据,不需要跨设备云同步。每次修改后直接序列化整个列表并写入文件,虽然看起来笨,但稳。对OpenHarmony这种生态还在成长中的平台,少一个依赖库就少一个踩坑点。
实际编码时,我封装了一个BattleRecordStore类,负责所有数据读写操作:
dart复制class BattleRecordStore {
File? _file;
List<BattleRecord> _records = [];
final ValueNotifier<List<BattleRecord>> notifier = ValueNotifier([]);
Future<void> load() async {
final dir = await getApplicationDocumentsDirectory();
_file = File('${dir.path}/battle_records.json');
if (_file!.existsSync()) {
final data = jsonDecode(_file!.readAsStringSync());
_records = data.map<BattleRecord>((e) => BattleRecord.fromJson(e)).toList();
}
notifier.value = List.of(_records);
}
Future<void> addRecord(BattleRecord record) async {
_records.add(record);
_records.sort((a, b) => b.createTime.compareTo(a.createTime));
await _save();
}
Future<void> deleteRecord(String id) async {
_records.removeWhere((r) => r.id == id);
await _save();
}
Future<void> _save() async {
if (_file == null) return;
await _file!.writeAsString(jsonEncode(_records.map((e) => e.toJson()).toList()));
notifier.value = List.of(_records);
}
}
注意notifier的设计。我放弃了setState式的页面刷新,改用ValueNotifier,让所有监听战绩列表的页面自动刷新。这个细节让UI层和数据层解耦,后面优化界面会方便很多。
3.3 数据操作封装:增删改查与统计计算
增删改查已经在上面的Store里体现了一部分。这里重点补充统计逻辑。
统计指标包括总场次、总胜场、总胜率、连胜纪录、按模式统计、按武将统计。我写了BattleStats类来承载计算结果:
dart复制class BattleStats {
int totalGames = 0;
int totalWins = 0;
double winRate = 0.0;
int maxStreak = 0;
Map<String, int> modeGames = {};
Map<String, double> roleWinRate = {};
}
计算连胜时,需要注意对局列表已经按时间倒序排列,连胜的定义是从最近一场往前连续胜利的场次。这个逻辑我一开始写错了,以为只要全局最大连续胜利,后来发现“当前连胜”才是玩家更关心的数据。所以代码里同时计算两种:当前连胜(maxCurrentStreak)和历史最长连胜(maxStreakAllTime)。
计算胜率时,要处理总场次为0的情况,避免除零异常。同时,如果玩家刚录入第一场且输了,胜率显示为0.0%而不是“--”,UI层要区分。
4. 战绩列表与统计界面实现
4.1 页面结构与交互设计
界面我采用底部TabTab结构,两个Tab:战绩列表和统计看板。这个结构简单清晰,符合工具型App的使用习惯。
战绩列表页的核心交互:
- 下拉刷新:重新加载Store数据。
- 长按删除:弹出删除确认框,防止误删。
- 点击进入详情:显示本局完整信息。
- 右下角悬浮按钮:快速添加新记录。
添加记录的页面用表单实现,模式选择用下拉框,阵营选择用SegmentedButton,胜负用Switch,评分用Slider,最后填一个备注文本框。整体交互尽量减少了输入次数,把常用选项默认值设置好,玩家只需调整个别字段。
4.2 战绩列表组件实现
列表使用ListView.builder,这是性能最好的方式,不会一次性构建所有条目。每个条目用Card包裹,左侧是胜负指示条,中间是对局信息,右侧是时间和删除入口。
这里遇到一个和热词相关的细节:Flutter的CheckboxListTile在控制文字与按钮距离时很麻烦,间距难调,尤其在OpenHarmony的字体渲染下会出现对齐问题。我在战绩记录里没有使用CheckboxListTile,而是用自定义Row套Checkbox和Text,这样间距完全可控。
列表项实现要点:
dart复制ListTile(
leading: CircleAvatar(
backgroundColor: record.isWin ? Colors.green : Colors.red,
child: Text(record.isWin ? '胜' : '负'),
),
title: Text('${record.roleName} · ${record.mode}'),
subtitle: Text('${record.roleCamp} 击杀${record.killCount} 评分${record.score}'),
trailing: Text(formatTime(record.createTime)),
onTap: () => Navigator.push(...),
onLongPress: () => showDeleteDialog(record.id),
)
formatTime用intl包或手写格式化函数都可以。为了减少依赖,我手写了一个yyyy-MM-dd HH:mm的格式化方法。
4.3 统计指标实现与数据可视化
统计看板我用了上中下三层布局:
- 顶部:胜率环形图,用
custom_paint绘制圆弧。 - 中部:三个数字卡片,总场次、胜场、当前连胜。
- 底部:按模式统计表格。
胜率环形图实现思路很简单:画一个背景圆环,再用Paint和drawArc画一个起始角为-90度、角度为胜率*360度的前景弧。这个视觉反馈很直观,比单调的数字效果好得多。
按武将胜率统计可以用表格展示,列出使用次数大于等于3的武将,按胜率排序。这一步需要从Records里聚合数据,性能上没问题。
统计页面每次进入时都需要刷新数据。我还是借助ValueNotifier,在页面打开时监听数据变化,或者在didChangeDependencies里调用refresh方法。实际测试下来,必须注意在Store加载完成前,不要构建统计计算,否则会出现空列表导致胜率计算异常。
5. 实战问题排查:从编译报错到状态管理坑
5.1 编译阶段常见错误:依赖版本、CMake与Gradle
编译是OpenHarmony Flutter开发最容易踩坑的地方,我整理了几个高频问题。
第一个是依赖版本不一致。Flutter各版本之间差异很大,如果直接使用最新版Flutter SDK,经常会遇到依赖包下载不下来或者编译不通过的情况。这时不要盲目升级,优先锁死SDK版本,并在pubspec.yaml里使用固定的依赖版本,不要用^浮动版本号。
第二个是CMake错误。在Windows上编译OpenHarmony工程时,可能报CMake Error at CMakeLists.txt:3 (project): Generator Visual Studio ...等,这是因为OpenHarmony的原生构建默认使用Ninja和Clang,不兼容Visual Studio生成器。解决办法是在构建命令中指定-G Ninja,或者直接在DevEco Studio里统一配置CMake参数。
第三个是Gradle插件应用问题。如果项目里还在用老式apply导入Gradle插件,会报类似“You are applying Flutter's main Gradle plugin imperatively using the apply”的错误。解决办法是改用plugin DSL方式:
gradle复制plugins {
id "com.android.application"
id "com.huawei.ohos.app"
}
5.2 运行期问题:页面主题颜色、字体和中文字符显示
运行时最让人头疼的是一些细节UI问题。
Flutter的showLicensePage页面主题颜色默认跟随MaterialApp的theme。如果你在应用里设置了深色主题,License页面会自动换成深色,但某个页面单独设置了浅色背景,打开License页就会显得很突兀。解决办法是在showLicensePage外层包裹一个Theme:
dart复制showLicensePage(
context: context,
applicationName: '三国杀攻略',
applicationVersion: '1.0.0',
);
它内部会使用根Theme,如果要强制颜色,就在调用前包一层Theme。
另外一个问题是中文字体。OpenHarmony系统默认字体和Android不完全一样,如果某些字符显示成方块,需要打包自定义字体文件,然后在MaterialApp的theme里设置fontFamily,或者使用fontFamilyFallback。我在统计页面使用了数字加粗字体,在OpenHarmony上个别数字显示偏细,最后选择了系统自带的sans-serif字体解决。
5.3 数据刷新与页面状态管理坑
状态管理这块我踩过一个典型的坑:页面A添加了一条战绩,返回到列表页B,但列表没有刷新。原因是我在页面B的initState里只加载了一次数据,没有监听Store的notifier。
解决办法是在initState里添加监听,并在dispose中移除:
dart复制@override
void initState() {
super.initState();
_store = context.read<BattleRecordStore>();
_store.addListener(_onStoreChanged);
}
void _onStoreChanged() {
if (mounted) {
setState(() {});
}
}
@override
void dispose() {
_store.removeListener(_onStoreChanged);
super.dispose();
}
这里必须检查mounted,因为Store刷新时页面可能已经销毁,直接setState会报错。另外,在异步加载数据后操作UI,也要小心context跨异步使用的问题。
还有一个坑是文件写入并发。如果用户快速连点保存按钮,可能会触发多次文件写入,导致数据错乱。我在Store内部加了一个简单的锁标记:
dart复制bool _saving = false;
Future<void> _save() async {
if (_saving) return;
_saving = true;
try {
await _file!.writeAsString(...);
} finally {
_saving = false;
}
}
这个实现虽然简陋,但已经能避免大部分连续点击导致的问题。
6. 后续优化与扩展方向
6.1 战绩分享与数据导出
战绩记录功能做扎实以后,可以增加分享能力。常见做法是把统计页面截屏,生成图片后通过系统分享发送出去。Flutter端可以用RepaintBoundary和dart:ui的toImage方法截取组件,然后在OpenHarmony上通过平台通道调用系统分享能力。
还可以把战绩导出为JSON或CSV文件,让用户备份到电脑。考虑到OpenHarmony的文件系统有自己的沙盒规则,这里建议使用FilePicker之类的插件,但要注意插件在OpenHarmony上的适配情况,不要盲目引入。
6.2 性能优化与首包体积控制
战绩记录这个功能对性能要求不算高,但App整体体感会影响用户留存。我做了两件事:
一是列表项分离。把Card构建逻辑抽成独立的BattleRecordListItem组件,减少列表项重建时的build范围。
二是懒加载统计页面。统计计算放到FutureBuilder里,先显示loading,再展示计算结果,避免进入页面时卡顿。
首包体积方面,Flutter App体积本身就不小,OpenHarmony的hap包更是要算上引擎体积。建议开启tree-shake-icons,只打包用到的Material图标,能省几百KB。在pubspec.yaml中加入:
yaml复制flutter:
uses-material-design: false
这样不会自动打包全部Material图标,但要注意代码里用到的图标需要显式导入。
6.3 鸿蒙能力接入:IAP、微信登录与图库选择
如果你想把这个App做成正式上架产品,还要考虑商业化能力和系统能力接入。
OpenHarmony上拉起支付,通常需要通过平台通道调用华为IAP SDK或AppGallery Connect能力。Flutter侧可以通过MethodChannel定义统一接口,鸿蒙侧用Java或Kotlin实现具体调用。我的建议是提前规划好支付回调逻辑,因为OpenHarmony的支付返回结果和Android略有差异,需要进行状态码兼容处理。
微信登录也是同理,需要同时适配Flutter侧和鸿蒙侧SDK。官方有flutter_wechat插件,但OpenHarmony适配得靠社区,稳妥做法是自己封装Channel,用鸿蒙原生SDK发起登录,再通过回调把code传给Flutter处理。
调用鸿蒙的图库选择头像,可以通过集成PhotoViewPicker原生能力,或者使用已有的image_picker插件,现在image_picker的OpenHarmony适配版本已经比较成熟,可以直接用。
除了这些,我还试过在OpenHarmony上通过蓝牙和ESP32设备通信,写了一个小demo,把三国杀计时器同步到桌面LED屏上。虽然从产品角度看有点“野”,但这正好说明Flutter for OpenHarmony的能力边界很宽,不止是做个静态攻略,也能玩出物联网的联动。
回到战绩记录功能本身,如果要我给一句总结,那就是:数据模型一定不要只考虑当前展示,要想到统计、筛选和未来扩展;存储方案优先选择可控性强的,少给平台适配添乱;界面和状态管理越简单越好,增加一层依赖就多一处翻车点。这次在OpenHarmony上用Flutter写三国杀攻略App的经历,让我对跨端开发有了新的感受。标准Flutter代码跑起来很爽,但真要把功能完整落到鸿蒙设备上,还是得耐心打磨平台差异。有几个晚上我几乎被编译错误折磨疯掉,但看到战绩列表在开发板上流畅滚动、胜率圆环正常渲染的那一刻,所有的折腾都值了。
最后分享一个小技巧:在开发这类跨端工具App时,一定要在项目早期就把数据层和UI层完全解耦,使用ValueNotifier或StreamBuilder一类的响应式机制,这样可以避免后期切换到鸿蒙真机调试时大量改页面代码。多写几行工具方法,少加几个全局依赖,这个投入绝不会亏。
