拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Flutter for OpenHarmony实战:从选型到上线的完整指南

Flutter for OpenHarmony实战:从选型到上线的完整指南

大概半年前,团队接到一个让我印象很深的任务:在OpenHarmony生态上做一款游戏中心App,第一版集中火力做“精选游戏”模块。当时手头的情况有点尴尬——团队里没有人写过ArkTS,原生适配层经验也基本是空白,好在Flutter的底子足够扎实。翻来覆去对比了一个多星期,最终拍板走 Flutter for OpenHarmony 这条路。正好最近不少朋友在问这条路能不能走、坑多不多,我就把这个项目从选型、环境搭建、数据层设计、精选页实现,到 EventChannel/PlatformView 桥接、Impeller 渲染优化、XTS 认证打包的完整过程整理成一篇文章,给准备在 OpenHarmony 上做 Flutter 应用的同学一份能直接照着走的参考。

1. 项目从0到1:游戏中心App的选型思路与整体架构

1.1 项目目标与功能范围

“游戏中心”这个词在各大应用市场里都很常见,本质上是一个游戏内容的聚合分发入口。我们这一版的核心目标是“精选游戏”模块的完整体验,功能范围包括:

  • 首页顶部 Banner 轮播:展示 3~5 个重点推荐位,图片带跳转参数
  • 分类筛选标签:休闲、策略、射击、角色扮演、竞速等
  • 游戏卡片列表:封面图、名称、评分、大小、下载量、标签、下载按钮
  • 下拉刷新与分页加载更多
  • 游戏详情页:截图轮播、介绍、相关推荐、下载安装入口
  • 下载进度实时反馈:进度条、下载状态、安装完成后的启动入口

功能看起来不多,但牵扯的面很广:既有常规列表性能问题,又要处理 Flutter 和系统侧的安装、下载、推送这类能力打通,还得考虑真机兼容性和认证问题。后面对应这些问题,各自都有不少坑。

1.2 为什么选Flutter而不是ArkTS原生

很多人在群里问过我这个话题。其实单论 OpenHarmony 生态,ArkTS 声明式 UI 是官方主力,这一点没有疑问。但具体到我们这个团队和这个项目,Flutter 有几个点更占优势:

对比维度Flutter for OpenHarmonyArkTS 原生
UI 一致性自绘渲染引擎,跨端表现一致依赖系统组件规范,各版本有差异
团队学习成本团队已有两年 Flutter 经验需要从头学声明式 UI 和状态管理
代码复用一套 Dart 代码可复用到 Android/iOS仅限 OpenHarmony/HarmonyOS 生态
渲染性能Impeller 编译期着色器,卡顿少系统原生组件,性能也不弱
插件生态上游社区插件需要适配,桥接机制成熟原生接口直接调用,生态逐步建设中
包体积含引擎约 18MB较小

我们当时的判断逻辑是:这个 App 未来大概率不止 OpenHarmony 一个平台,团队技术栈集中在 Flutter,硬切 ArkTS 意味着整个团队要重学一遍 UI 体系,项目周期会明显拉长。而 Flutter for OpenHarmony 虽然在 OpenHarmony SIG 的维护下还谈不上“完全平级”,但主链路——渲染、平台通道、包管理——已经能跑,配合隔离好的桥接层,风险是可控的。

这里有一条明确的教训:选型不能只看技术好不好,要看团队和技术的适配度。如果团队本来就是 ArkTS 主力,没必要绕一圈用 Flutter;反过来,团队是 Flutter 出身,硬写 ArkTS 的隐性成本比想象中大得多。

1.3 整体架构与目录规划

工程结构上,我们延续了“数据层、桥接层、业务层”三层分离的习惯。重点说一下桥接层:这是 Flutter 在 OpenHarmony 上的生命线,所有和系统能力的交互都必须收敛到统一的 bridge 目录里,绝不允许原生能力相关的调用散落在业务代码中。这一点在后面适配插件时会体现出巨大价值。

game_center/ ├── lib/ │ ├── main.dart │ ├── core/ # 主题、网络封装、常量、工具 │ ├── data/ # 模型、仓库、接口服务 │ │ ├── models/ │ │ ├── repository/ │ │ └── services/ │ ├── modules/ # 按业务模块划分 │ │ ├── featured/ # 精选游戏首页 │ │ ├── detail/ # 游戏详情 │ │ ├── download/ # 下载状态管理 │ │ └── profile/ │ ├── bridge/ # MethodChannel/EventChannel封装 │ └── widgets/ # 通用组件:加载态、空态、错误态 ├── harmony/ # OpenHarmony原生工程 │ └── entry/src/main/ └── pubspec.yaml

为什么要单独讲这个结构?因为后面精选页的 UI、EventChannel 桥接、性能优化都是在这个目录约定下展开的。模块化不是为了好看,是因为 OpenHarmony 生态下 Flutter 插件的适配很可能会频繁变动,模块边界越清晰,改动影响面越小。

2. 开发环境搭建:Flutter for OpenHarmony 的工具链准备

2.1 需要的工具与版本选择

先直接给结论:不能用官方 Flutter SDK,必须用 OpenHarmony SIG 维护的 fork 版本。原因是 OpenHarmony 的引擎、平台通道、打包工具链和上游有所不同,官方 SDK 不认识ohos平台。

我当时准备的环境如下:

  • DevEco Studio 5.0+(对应 OpenHarmony API 12),用于原生侧工程管理和 HAP 打包签名
  • ohpm(OpenHarmony 包管理器),安装原生依赖
  • Node.js 18+
  • Flutter for OpenHarmony SDK fork:拉取 gitee 上 openharmony-sig/flutter_flutter 仓库
  • hdc 调试工具,连接真机/模拟器

克隆 SDK 并配置:

git clone -b master https://gitee.com/openharmony-sig/flutter_flutter.git export PATH=$PWD/flutter_flutter/bin:$PATH flutter doctor flutter precache --ohos

flutter precache --ohos会下载 OpenHarmony 平台需要的引擎产物,这步不能省。如果网络条件一般,建议配置PUB_HOSTED_URL和FLUTTER_STORAGE_BASE_URL指向国内镜像,否则依赖拉取容易超时。

这里要特别强调版本对齐:Flutter fork 版本、OpenHarmony SDK 版本、DevEco 版本三者必须匹配。我们一开始图省事用了最新的 SIG master 分支,结果和 DevEco 4 的 SDK 对不上,编译期报了一堆奇怪的错误,后来统一切到 release 分支才算消停。

2.2 创建工程与验证Hello World

环境配好后,创建工程:

flutter create --platforms ohos game_center

与标准 Flutter 工程的区别是:会生成harmony/原生目录,同时pubspec.yaml里不需要额外配置,flutter 工具链能识别ohos平台。第一次构建建议直接在 DevEco Studio 里打开harmony目录,等 ohpm 依赖同步完成后,选择 entry 模块跑一个模拟器实例验证 Hello World。

验证通过的标准是:模拟器上出现 Flutter 默认计数器页面,且热重载(按r键)能正常工作。如果热重载不生效,多半是引擎产物版本和工程版本不匹配,重新执行flutter precache --ohos再试。

2.3 环境搭建最常见的三个坑

第一个坑是SDK 版本咬合。OpenHarmony 的 Flutter 版本对 API Level 很敏感,API 12 配 Flutter 3.7.x 系列是当时比较稳的组合。SIG 的 release note 会写明支持的 API 版本,升级前务必看一眼,别盲追新。

第二个坑是模拟器和真机的行为差异。模拟器上 Banner 轮播、列表滑动都没问题,但 PlatformView 在部分模拟器上渲染黑屏,真机反而正常。所以环境搭建阶段就把真机联调通道跑通很有必要,后面排查问题会省很多事。

第三个坑是ohpm 依赖缓存问题。OpenHarmony 原生的第三方包偶尔会拉到损坏缓存,典型表现是构建报could not resolve ...。处理方式很粗暴但有奇效:删掉~/.ohpm缓存和工程的oh_modules目录,重新ohpm install。

提示:这个阶段别急着写业务代码。把 Hello World 在模拟器和真机都跑通、热重载验证稳定,再往下走,否则后面出了问题你根本分不清是环境问题还是代码问题。

3. 精选游戏功能的数据层设计:从模型到接口

3.1 数据模型定义

精选页的数据并不复杂,关键是模型划分要清晰。我们拆成了三个模型:BannerInfo(轮播位)、GameInfo(游戏实体)、PageResult(分页包装)。

class BannerInfo { final int id; final String title; final String imageUrl; final String targetGameId; BannerInfo({ required this.id, required this.title, required this.imageUrl, required this.targetGameId, }); factory BannerInfo.fromJson(Map<String, dynamic> json) => BannerInfo( id: json['id'] as int, title: json['title'] as String, imageUrl: json['imageUrl'] as String, targetGameId: json['targetGameId'] as String, ); } class GameInfo { final String gameId; final String name; final String iconUrl; final List<String> screenshots; final double rating; final String category; final String desc; final int downloadCount; final int sizeMb; final bool isHot; final bool isRecommended; GameInfo({ required this.gameId, required this.name, required this.iconUrl, required this.screenshots, required this.rating, required this.category, required this.desc, required this.downloadCount, required this.sizeMb, required this.isHot, required this.isRecommended, }); factory GameInfo.fromJson(Map<String, dynamic> json) => GameInfo( gameId: json['gameId'] as String, name: json['name'] as String, iconUrl: json['iconUrl'] as String, screenshots: List<String>.from(json['screenshots'] ?? []), rating: (json['rating'] as num).toDouble(), category: json['category'] as String, desc: json['desc'] as String, downloadCount: json['downloadCount'] as int, sizeMb: json['sizeMb'] as int, isHot: json['isHot'] as bool, isRecommended: json['isRecommended'] as bool, ); }

PageResult就是经典的{list, page, hasMore}三字段结构,不加花哨的东西。把这部分单独拎出来讲,是因为Dart 手写 fromJson 虽然繁琐,但在没有代码生成工具的条件下,比依赖反射方案更稳。OpenHarmony 上部分第三方序列化库的适配并不完善,别在这个环节引入不可控变量。

3.2 接口层与Mock方案

精选页需要两个接口:Banner 列表和推荐游戏分页列表。我们在 repository 层做了抽象:

abstract class FeaturedRepository { Future<List<BannerInfo>> getBanners(); Future<PageResult<GameInfo>> getRecommendGames({int page = 1, int pageSize = 20}); }

然后分别实现MockFeaturedRepository和RemoteFeaturedRepository。Mock 数据把 Banner、卡片、分页、空态、错误态都提前做出来,UI 开发完全不受后端排期影响。实际项目里后端接口晚了一周半,但 UI 和交互已经全部调完,这个节奏让整个项目舒服很多。

Remote 用 Dio 封装,设置统一的超时、拦截器、错误码处理。需要特别提醒的是:OpenHarmony 网络权限要在原生侧声明,不只改 Dart 侧代码。ohos.permission.INTERNET必须加在 module.json5 里,否则 release 包所有接口全部超时,debug 包可能因为签名调试策略不同表现不一致,这个坑特别隐蔽。

3.3 状态管理:Provider与FutureBuilder的取舍

精选页的状态其实不算复杂,我用ChangeNotifier + Provider就能覆盖,没必要上 Bloc 那一套。Banner 这种一次性数据用FutureBuilder足够,游戏列表这种需要“刷新、加载更多、错误重试”的状态,用 ViewModel 更顺手。

class FeaturedViewModel extends ChangeNotifier { final FeaturedRepository _repository; List<GameInfo> _games = []; int _page = 1; bool _hasMore = true; bool _loading = false; String? _error; List<GameInfo> get games => _games; bool get hasMore => _hasMore; bool get loading => _loading; String? get error => _error; // 初始化和下拉刷新统一走这里,避免两套逻辑 Future<void> refresh() async { _page = 1; _hasMore = true; _error = null; _loading = true; notifyListeners(); try { final result = await _repository.getRecommendGames(page: _page, pageSize: 20); _games = result.list; _hasMore = result.hasMore; } catch (e) { _error = e.toString(); } finally { _loading = false; notifyListeners(); } } Future<void> loadMore() async { if (_loading || !_hasMore) return; _loading = true; notifyListeners(); try { final result = await _repository.getRecommendGames(page: _page + 1, pageSize: 20); _page++; _games.addAll(result.list); _hasMore = result.hasMore; } catch (e) { // 加载更多失败不打断已有列表,保留重试入口 } finally { _loading = false; notifyListeners(); } } }

两个细节:一是刷新和加载更多共用_loading做互斥,避免用户在快速下拉时重复请求;二是加载更多失败时不清空列表,只在底部显示重试入口。数据层这些习惯在 OpenHarmony 上同样适用,平台差异不会体现在这一层。

4. 精选页UI实现:Banner、分类标签与游戏卡片

4.1 页面骨架与组件拆分

精选页最让我纠结的不是单个组件的实现,而是“Banner + 分类标签(吸顶)+ 游戏列表”混排的滚动体验。用普通ListView加多个 child 当然能实现,但一旦列表长起来,整页重建的成本很扎眼。

最终选型是CustomScrollView+ Sliver 家族:

  • SliverToBoxAdapter承载 Banner 轮播
  • SliverPersistentHeader做分类标签,支持吸顶
  • SliverList生成游戏卡片列表
  • 底部再用一个SliverToBoxAdapter放加载状态

这样所有区域共享同一个滚动视口,吸顶、滚动联动、列表懒加载都是原生行为,不用自己算偏移量。这也是后面列表性能优化能顺利展开的基础。

组件拆分上,Banner、分类标签、GameCard 分别独立成 widget,互不依赖。每个 widget 尽量用const构造,让 Flutter 能复用 Element,减少不必要的 build。

4.2 Banner轮播自研而不是引第三方库

按惯例,轮播图肯定先找包。但在这个项目里我坚决不引第三方轮播库,原因很简单:OpenHarmony 生态上第三方 Flutter 包的适配质量参差不齐,轮播又恰好是精选页的门面,出问题影响面太大。自研成本并不高,一个 PageView 加一个定时器而已。

核心实现:

class BannerCarousel extends StatefulWidget { final List<BannerInfo> banners; const BannerCarousel({super.key, required this.banners}); @override State<BannerCarousel> createState() => _BannerCarouselState(); } class _BannerCarouselState extends State<BannerCarousel> { final PageController _controller = PageController(viewportFraction: 0.92); Timer? _timer; int _current = 0; @override void initState() { super.initState(); _startAutoPlay(); } void _startAutoPlay() { _timer?.cancel(); _timer = Timer.periodic(const Duration(seconds: 4), (_) { if (!_controller.hasClients) return; final next = (_current + 1) % widget.banners.length; _controller.animateToPage( next, duration: const Duration(milliseconds: 350), curve: Curves.easeOut, ); }); } @override void dispose() { _timer?.cancel(); _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return SizedBox( height: 160, child: PageView.builder( controller: _controller, itemCount: widget.banners.length, onPageChanged: (index) => setState(() => _current = index), itemBuilder: (context, index) => _BannerCard(banner: widget.banners[index]), ), ); } }

几个容易忽略的点:

  1. dispose里必须取消定时器,否则页面销毁后定时器还在跑,配合 Navigator 来回切换会出现轮播跳变
  2. 手动滑动时自动播放要重启,否则容易产生“刚划走又自动切回来”的冲突感
  3. viewportFraction: 0.92是为了让相邻 Banner 露出一点边缘,视觉上更立体,这也是现在主流应用市场的做法
  4. 指示点用_current驱动,不需要单独的状态管理

4.3 游戏卡片与图片缓存

游戏卡片是这个页面的核心组件,信息密度高:封面图、名称、评分、分类、大小、下载量、下载按钮。布局上我用了传统的卡片式:上面是大图(16:9 裁切),下面是信息区。做得越像“应用市场里的游戏卡片”,用户认知成本越低。

图片加载直接用cached_network_image有个隐患:它依赖的flutter_cache_manager原生侧在部分 OpenHarmony 版本上有权限适配问题。我的处理方式是封装了一层AppNetworkImage,内部优先走 cached_network_image,一旦检测到缓存目录不可用,就退化为Image.network加内存缓存。这样既保证图片体验,又不至于因为一个缓存插件让整个列表挂掉。

列表性能上做了三件事:

  • 每张卡片包RepaintBoundary,滑动时避免无关区域重绘
  • SliverList配合固定卡片高度(itemExtent),滚动布局开销降到最低
  • 卡片 child 尽量const化,状态变化只重建最小范围

4.4 下拉刷新、加载更多与页面跳转

下拉刷新直接RefreshIndicator包CustomScrollView,注意设置physics: AlwaysScrollableScrollPhysics(),否则列表内容不够一屏时下拉手势不触发。

加载更多用NotificationListener监听滚动到底部:

NotificationListener<ScrollUpdateNotification>( onNotification: (notification) { if (notification.metrics.pixels >= notification.metrics.maxScrollExtent - 200) { viewModel.loadMore(); } return false; }, child: CustomScrollView(...), )

阈值设 200 像素,提前触发加载,避免用户滑到底后还要白等一个网络往返。底部加载状态做三种:加载中转圈、没有更多了显示文案、加载失败显示点击重试。这三个状态都做成独立 widget,复用性好。

点击卡片跳详情页用的是标准Navigator.push:

Navigator.push(context, MaterialPageRoute( builder: (_) => GameDetailPage(gameId: game.gameId), ));

这里提一个我在实际项目中踩过的点:OpenHarmony 上前几个版本对MaterialPageRoute的页面转场动画兼容有小问题,表现为部分设备上转场白屏一闪。如果遇到,给MaterialPageRoute配PageTransitionsTheme,或者直接用FadeUpwardsPageTransitionsBuilder替代,就能绕过去。这也是“Flutter 在 OpenHarmony 上不是完全和 Android 一致”的典型例子。

5. Flutter与OpenHarmony能力打通:EventChannel和PlatformView实战

5.1 为什么必须做桥接:游戏中心的能力边界

精选页不是纯展示,游戏下载、安装、进度反馈这些能力必须落到系统侧。Flutter 自身封装不到的系统能力,都要通过平台通道(Platform Channel)桥接。我们项目里实际用到三类:

  1. MethodChannel:一次性调用,比如触发 HAP 安装、拉起系统分享
  2. EventChannel:持续事件流,比如下载进度实时回传
  3. PlatformView:嵌入原生视图,比如广告 SDK 渲染

桥接层在架构上做了统一封装,Dart 侧业务代码只面对一个GameCenterBridge抽象类,屏蔽掉“当前跑的是 Android 还是 OpenHarmony”的差异。这是整个项目里最值得的投资之一。

5.2 EventChannel实战:下载进度实时回传

下载进度的场景很典型:原生侧发起下载任务,通过 EventChannel 把进度持续推给 Flutter 侧。Dart 侧代码如下:

class DownloadBridge { static const EventChannel _downloadChannel = EventChannel('com.example.gamecenter/download'); Stream<DownloadProgress> get downloadProgress { return _downloadChannel .receiveBroadcastStream() .map((event) => DownloadProgress.fromMap(event as Map)); } }

原生侧(OpenHarmony 的 entry 模块)注册对应的 EventChannel:

// OpenHarmony 原生侧,简化示例,具体 API 以本地引擎版本为准 import { EventChannel } from 'flutter_ohos'; const eventChannel = new EventChannel('com.example.gamecenter/download'); eventChannel.setStreamHandler({ onListen: (args, sink) => { downloadManager.onProgressChange((data) => { sink.success({ gameId: data.gameId, progress: data.progress, status: data.status, }); }); }, onCancel: () => { downloadManager.removeListener(); }, });

注意一个细节:receiveBroadcastStream()返回的是单订阅 Stream,原生侧每次 set 的 sink 都会投递。多页面同时监听同一个下载进度时,要由 Flutter 侧的 DownloadManager 做统一分发,把“全局通知总线”的活干好,不能指望每个页面各自开一个 Channel。

5.3 PlatformView实战:嵌入原生广告位

精选页底部插了一个原生广告位。Flutter 侧用 PlatformViewLink 接入:

PlatformViewLink( viewType: 'game-center-ad/view', onCreate: (PlatformViewCreationParams params) { return PlatformViewsService.initSurfaceAndroidView( id: params.id, viewType: 'game-center-ad/view', creationParams: const {'slotId': '10086'}, creationParamsCodec: const StandardMessageCodec(), onPlatformViewCreated: params.onPlatformViewCreated, ); }, onPlatformViewCreated: (id) { // 拿到 viewId,后续可下发广告位参数 }, child: const SizedBox(height: 90), )

PlatformView 在 OpenHarmony 的 Flutter fork 里 API 和 Android 侧基本对齐,这段代码几乎可以直接复用。但有两个性能提醒:

  1. PlatformView 不要滥用。它本质上是把原生视图合成到 Flutter 画布上,每次合成都有额外开销。广告位、视频位这类必要场景才用,普通 UI 别碰。
  2. 创建时机。PlatformViewLink 是懒创建的,页面进入后如果广告准备慢,会出现空白区域一闪。我这边加了一个 Skeleton 占位,等onPlatformViewCreated回调触发后再把占位移除,体验好很多。

5.4 现有Flutter插件适配OpenHarmony的通用流程

做这个项目过程中,我们适配了 shared_preferences、path_provider、一个登录 SDK 插件,还有一个内部广告插件。流程沉淀下来基本是四步:

  1. 找到上游插件源码,保留pubspec.yaml里 Android/iOS 的默认实现,新增ohos实现目录
  2. 在原生侧实现 MethodChannel/EventChannel 对应的方法,替换掉 Android 特有 API(比如SharedPreferences换成 OpenHarmony 的Preferences接口)
  3. 在插件注册入口增加ohos分支,让 Flutter 引擎启动时能加载到 OpenHarmony 实现
  4. 跑一遍 Dart 侧用例,确认对外接口行为不变

整个过程最有意思的发现是:只要 Dart 侧接口设计得干净,适配 OpenHarmony 时业务代码一行都不用改。登录插件适配时,原生侧租户权限弹窗的处理差异,我们通过桥接层把“登录结果回调”和“权限请求回调”做了归一化,Flutter 侧完全无感。这也是我反复强调桥接层重要的原因——它不仅是技术隔离,还是生态差异的缓冲垫。

6. 渲染与性能调优:从Skia到Impeller

6.1 Impeller在OpenHarmony上到底带来了什么

OpenHarmony 的 Flutter 引擎过去默认用 Skia 渲染,实际痛点在于:复杂页面首次打开或滚动时,着色器编译会导致明显卡顿,英文社区管这叫 shader jank。Impeller 的思路是在编译期把着色器提前编译好,运行时不再现编,从根上消灭这类卡顿。

SIG 把 Impeller 移植到了 OpenHarmony 上,我们在精选页实测的体感是:列表快速滑动时的帧间隔明显更稳定,设备发热和掉帧也比 Skia 版本好。粗略的日志统计对比,滚动场景下平均帧耗时从 22ms 左右降到 12ms 上下,虽然不如官方宣传那么夸张,但体感差异是实打实的。

要验证自己跑的引擎是 Skia 还是 Impeller,可以在启动 Flutter 引擎时打开调试开关,或者看编译产物的引擎标识。更简单粗暴的办法:同样一段列表滚动,开开发者模式看帧率统计,Impeller 版本几乎不出现长时间掉到 30fps 以下的情况。

6.2 精选列表的渲染优化实践

除了引擎层面的收益,代码层面的优化同样重要。我在这块总结了五个动作,按性价比排序:

  1. 固定 item 高度:SliverList设置itemExtent后,滚动时不需要对每个 item 做布局计算,这是列表性能最大头的一块
  2. RepaintBoundary 隔离:每张卡片包一层,滚动时卡片内部的图片加载、进度条变化不会向上污染整个页面的绘制
  3. 图片资源分级:Banner 用大图、卡片封面用中图、头像用缩略图,接口层直接下不同规格,避免一张大图撑爆内存
  4. 避免 shrinkWrap:内部不要出现嵌套的shrinkWrap: true列表(比如卡片里套横向滚动截图),必要场景用PageView或SingleChildScrollView替代
  5. 收敛不必要的动画:RefreshIndicator、PageView在列表滚动时都会触发合成,能收敛的动画尽量收敛

这些小动作单拎出来都不起眼,但叠在一起,滚动流畅度的差距就出来了。尤其是 OpenHarmony 真机生态比较杂,低端机型的性能余量远不如 Android/iOS 旗舰,优化一定要前置。

6.3 Navigator页面状态保活:不要被“开新页丢状态”吓住

“Flutter Navigator 切换页面后,会丢失状态吗”这个问题被问过很多次。我的结论是分情况:

  • 普通Navigator.push进入新路由,老路由的 State 对象还在栈里,不会被销毁,切回来时状态自然还在
  • 但如果路由被系统回收(比如 Ability 重建、低内存被杀后台),initState会重新执行,之前的内存状态确实没了
  • 你以为“丢状态”的另一个情况是列表滚动位置丢了,其实不是 State 没了,而是 Scrollable 的偏移量没有恢复

针对精选页,我做了三道保险:

第一,列表和页面状态存在 ViewModel 里,不依赖 Widget 生命周期,Ability 重建后数据还能拉回来;第二,CustomScrollView加PageStorageKey,滚动位置能恢复;第三,Tab 类场景用AutomaticKeepAliveClientMixin保活:

class _FeaturedPageState extends State<FeaturedPage> with AutomaticKeepAliveClientMixin<FeaturedPage> { @override bool get wantKeepAlive => true; @override Widget build(BuildContext context) { super.build(context); // 别忘了调用父类 build,否则保活不生效 ... } }

注意super.build(context)这行不能少,否则保活不生效。这也是很多人抄了代码却不生效的原因。

7. 质检与发布:XTS认证、HAP打包与上线

7.1 XTS认证是什么,为什么重要

XTS(X Test Suite)是 OpenHarmony 官方的兼容性测试套件,用来验证设备或者应用是否满足 OpenHarmony 的兼容性规范。对应用开发者来说,通过 XTS 意味着你的 App 在 OpenHarmony 真机上能稳定运行、正确调用系统 API、符合权限和行为规范。

这个认证的价值在分发端特别明显:应用市场对未通过 XTS 的应用兼容性和审核会严格很多,商店方也更有信心收录。说白了,XTS 就是 OpenHarmony 生态的“入场券”。

测试时可以直接用 DevEco Studio 自带的 XTS 测试工程跑。我们当时的做法是:准备一台出厂状态的 OpenHarmony 真机,把 XTS 的 acts 测试用例跑完,重点看两个维度——API 行为是否一致、后台切前台/Ability 重建是否异常。精选页这种重度使用列表和网络的应用,重点盯的就是页面重建和事件通道重连。

7.2 测试阶段的几个重点

XTS 是基础线,项目自己的测试才是大头。三轮测试下来,我们重点盯了四个场景:

  1. Ability 重建:Flutter 页面栈很深时,原生 Ability 被系统重建,Flutter 侧路由栈能不能恢复。手机上可以用开发者模式的“不保留活动”做压力测试
  2. EventChannel 断线重连:下载进行中,系统把后台进程杀掉,重新进入 App 后下载进度必须能重新连接,不能卡在 50% 永远不动
  3. 低端机滚动性能:精选页在低内存机型上快速滑动,关注掉帧和 OOM
  4. 平台差异测试:同样的功能在 Android 和 OpenHarmony 两侧跑,UI 细节保持一致

自动化方面,我们用integration_test写了两条冒烟链路:一条是“进精选页→点卡片进详情→返回”,一条是“刷新→加载更多→点击下载”。这两条链路在每次 release 前必跑,成本低收益高。

7.3 HAP打包、签名与分发

打包命令在 OpenHarmony 的 Flutter fork 下是:

flutter build hap --release

产物是带harmony/工程的 HAP 包。签名环节在 DevEco Studio 里完成:配置签名材料(证书、描述文件),勾选自动签名。这一步有个容易踩的坑:debug 和 release 的证书要分开,release 证书的包名、签名信息必须和 XTS 认证时一致,否则市场侧校验会失败。

分发上,OpenHarmony 生态的应用市场对 HAP 的收录要求和各渠道不完全一致,但基本流程是上传 HAP、提交商店截图和隐私说明、等待审核。精选游戏类目还需要额外提供游戏的版号、资质信息,建议法务和商务提前介入,别等技术侧都搞完了才发现材料不全。

8. 收笔:几个值得反复记住的实操经验

项目收尾后我复盘过几次,有三条体会最值得说出来。

第一条,桥接层是 Flutter 在 OpenHarmony 上安身立命的根本。凡是和系统能力沾边的调用,全部走 bridge;凡是可以不用平台特有的能力,就不要用。比如下载进度,理论上可以用原生通知栏展示,但为了和 Flutter 侧 UI 统一,还是走 EventChannel 回传,这样 Android 和 OpenHarmony 两端 UI 完全一致。

第二条,升级 Flutter 版本前,必须看 SIG 的 release note。OpenHarmony 的 Flutter 版本和引擎、SDK、DevEco 之间的绑定关系很紧,不是简单换个版本号就能升的。我们中途想从 3.7 升到 3.10,结果发现要对齐好几层依赖,最后评估性价比后先搁置了。在生态成熟前,“稳定优先”永远是对的。

第三条,别把 Android/iOS 的思维惯性带过来。比如 PlatformView、权限声明、后台进程限制、Ability 销毁策略,OpenHarmony 都有自己的一套。很多问题不是 Flutter 层面的 bug,而是对平台运行机制理解不够。遇到“奇怪”的现象,先怀疑平台差异,再怀疑自己的代码。

这篇文章看起来在讲一个精选游戏功能,实际上它覆盖了 Flutter 在 OpenHarmony 上从选型到交付的整个闭环。如果你正准备在 OpenHarmony 上做 Flutter 应用,最务实的路径就是:架构上把桥接层当第一公民,环境版本严格对齐,UI 层扎实做优化,发布前老老实实跑 XTS。把这个链路走一遍,后面再开新项目就等于有了模板,速度和稳定性都会好很多。

返回列表