ARTICLE DETAIL

资讯详情

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

Flutter for OpenHarmony商城实战:从选型到商品详情页落地

Flutter for OpenHarmony商城实战:从选型到商品详情页落地 做OpenHarmony商城App的时候团队第一个争论就是跨端框架选什么。倒不是OpenHarmony原生不行——ArkUI的声明式语法这两年进步很快——关键是团队刚从Flutter项目里出来怀里揣着一套已经打磨好的商城组件和状态管理方案。真要推倒重写光商品详情页这种交互密集的页面布局、滚动、图片缓存、SKU联动这些老坎就得再过一遍工期乐观估也要两三周。所以看到Flutter for OpenHarmony这条路线时我们几乎没怎么纠结就定了一份Dart代码跑在Android、iOS、OpenHarmony三端。这篇文章就用商城App里最典型的商品详情页做载体从选型理由、环境搭建、数据层设计、UI落地到OpenHarmony上特有的适配问题和几段排错复盘完整讲一遍。适合两类人一类是刚接触Flutter、想搞明白它在OpenHarmony上到底怎么跑的新手另一类是已经入了Flutter的坑、正准备把现有App迁到OpenHarmony的团队。内容偏实战尽量少讲虚的直接上能用的代码和思路。1. 为什么在OpenHarmony上做商城App我最终选了Flutter1.1 三个路线的实际对比选型那会儿摆在我们面前的主要是三条路纯原生ArkUI、React Native、以及Flutter for OpenHarmony。纯原生ArkUI的优势是性能和系统能力调用最直接HarmonyOS的声明式UI写起来也确实顺手。但劣势同样明显团队里没人写过ArkUI。商城这种以列表、详情、购物车为主的业务页面数量不少每个页面都用原生重写一遍人力成本是明摆着的。再加上ArkUI的生态起步时间不长第三方组件、图表库、图片加载这些常用能力都得自己造轮子。React Native那条路更坎坷主要问题在于RN对OpenHarmony的适配一直没有形成稳定的社区版本JS引擎和原生桥接的兼容性需要自己维护的东西太多。试了两天就放弃了——团队没有精力养一套非主流的桥接层。Flutter for OpenHarmony反而是当时最稳妥的。OpenHarmony SIG组在Gitee上有专门的flutter引擎移植分支虽然不能跟Android官方支持比但Dart侧代码基本不用改UI自绘机制天然不依赖原生控件商城页面99%的自绘UI可以直接复用。我们线上Android版商城已经在用Flutter写几千行Dart代码平移过来成本低得惊人。三个方案摆在一起差异其实就一句话ArkUI和RN都是在学一门新语言新生态Flutter只是在换一个运行平台。1.2 Flutter在OpenHarmony上到底怎么跑起来的很多人第一次接触Flutter for OpenHarmony会误以为它是把Android的Flutter引擎打包直接搬过去的。实际不是。OpenHarmony上跑Flutter本质是把Flutter的引擎层替换成了适配OHOS图形子系统的版本。我拿电视盒子打比方Dart代码就是剧本Flutter引擎是播放器而OpenHarmony的图形栈是电视屏幕。Flutter的UI并不渲染成ArkUI的组件树而是自己通过Skia后续也在往Impeller迁移直接绘制到屏幕上。也就是说在OpenHarmony上打开一个Flutter页面原生侧其实只提供了一个宿主容器剩下的画面都是Flutter引擎自己画出来的。系统能力这块Flutter通过PlatformChannel跟OHOS侧通信。比如要在详情页调相机、存图片、拿设备信息Dart侧发起一个MethodChannel调用OHOS侧用同一套平台通道做实现注册。我们当时做了一个商品晒图功能相机调用就是在OHOS侧实现了一个CameraPluginDart侧跟Android/iOS一样用image_picker这个接口只是换了底层的实现注册。理解了这个结构就能解释很多后续踩到的问题凡是Flutter能自己画的在OpenHarmony上都相对顺畅凡是必须依赖原生组件的就要小心platform view和插件适配这两道坎。1.3 这套组合适合什么项目聊完选型必须泼一盆冷水。Flutter for OpenHarmony不是万能药。适合做的就是商城、工具、资讯这类以自绘UI为主、原生能力依赖少的应用。我们当时把商品详情页、列表页、购物车全部迁过来页面级别几乎零改动依赖的原生能力只有WebView、相机、相册这几个都有现成或可开发的通道。不适合做的是重度依赖系统原生交互的应用比如AR、复杂地图、系统设置类。OpenHarmony侧的原生控件封装还不像Android那么丰富PlatformView的成熟度也有限硬做会砸在适配阶段。选型建议就一句话先盘一下自己现有页面对原生控件的依赖程度再决定要不要走Flutter for OpenHarmony。依赖原生越少这条路越香。2. 环境搭建与项目初始化文档之外的那点坑2.1 从Gitee拉引擎分支版本匹配是第一道坎环境搭建最容易被忽视的就是版本匹配。Flutter for OpenHarmony不是从pub.dev或者flutter.dev官方分发渠道拿的而是OpenHarmony SIG组维护的flutter仓库的openharmony分支托管在Gitee上。我们当时踩的第一个坑就是用官方Flutter SDK建工程然后跑flutter build结果编出来的产物在OpenHarmony真机上根本起不来。查了半天发现是版本分支不对——OpenHarmony侧的引擎用的是OHOS SDK的图形接口跟官方Flutter引擎的OpenHarmony适配是绑定具体版本的。实操建议直接clone Gitee上的flutter_flutter仓库切到openharmony分支用这个SDK去跑项目。同时OpenHarmony SDK用DevEco Studio配套的版本别追新两个SDK的版本组合以能跑通hello world为准。我们当时固化的版本组合是这样的版本号会更新关键看稳定性组件建议说明Flutter SDKGitee openharmony分支不要用官方原版OpenHarmony SDKDevEco Studio配套稳定版别用预览版构建工具hvigor替代Android的gradleIDEDevEco Studio VS Code原生侧用DevEcoDart侧用VS Code版本匹配这里没有太多捷径最快的验证方式就是脚手架建一个默认工程真机上跑起来再继续。第一步跑不起来就升级版本跑起来了就固化锁死后面所有开发都基于这一套。2.2 创建工程从Flutter命令行到OHOS工程目录环境配好后创建工程本身不复杂但有个关键点Flutter工程的ohos目录不是自动生成的。我当时先用flutter命令创建纯Dart工程然后跑到Gitee仓库在openharmony分支下提供的模板里把ohos平台目录拷贝进项目。这一步官方文档提得很隐晦很多人卡在这里。实际流程是这样# 1. 用openharmony分支的flutter SDK创建工程 flutter create --org com.example mall_app # 2. 从模板仓库拷贝ohos平台目录到工程根目录 cp -r flutter_templates/ohos mall_app/ # 3. 进入ohos目录配置包名和应用名 cd mall_app/ohos # 修改module.json5和build-profile.json5里的包名 # 4. 回到工程根目录拉取Dart依赖 flutter pub get之所以要手动拷贝ohos目录是因为OpenHarmony侧的应用工程结构entry、hvigorfile、module.json5这些跟Flutter默认的平台目录不共享一套生成逻辑。手动拷完注意把ohos工程的包名跟Flutter侧的applicationId保持一致否则后面混编时签名和渠道会乱。2.3 首次构建的报错排查镜像、依赖与DartVMInitializer第一次构建时我们遇到了两个相当经典的报错。第一个是依赖下载慢到怀疑人生。OpenHarmony的hvigor依赖默认走外网下载不搭镜像的话几个小时都下不完。解决办法是把hvigor的仓库地址替换成国内镜像然后在ohos/build-profile.json5里配置好仓库源。这个不展开说了团队内部文档里都有核心就是一旦涉及下载第一反应就是换镜像源别傻等。第二个是运行期一直刷E/flutter (31173): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)]这样的日志然后页面卡住不动。这个报错是Flutter引擎的Dart VM初始化阶段的全局异常捕获在打印未处理异常通常意味着应用里某个异步操作抛错但没有被任何try-catch接住。我们后来定位到是商品数据接口的网络层在OpenHarmony真机上用了LocalSocket方式但代码里还在用旧版的IOClient导致连接失败后抛了异常没人管。这个后面第六章专门讲这里先记住这个E/flutter日志是有异常没处理的信号不是Flutter引擎本身坏了。2.4 混合开发把Flutter模块打成AAR/HAR给原生工程用项目后期我们走的是混合开发模式原生ArkUI页面比如设置、账号安全保留不变商品端首页、搜索、详情、购物车全部塞进Flutter模块。这种模式下Flutter侧要产出供原生工程引用的二进制包。这里就涉及到热词里的flutter aar。在Android世界叫AAR在OpenHarmony侧对应的是HAR或者直接产物so/hap的组合。操作上跟Android的集成方式类似先把Flutter模块用hvigor构建出release产物然后在原生工程里通过har依赖引进来。踩得比较深的坑是资源合并Flutter产的so库和assets资源在原生工程打包成hap时偶尔会冲突。我们的规避方式是把Flutter模块构建产物放到一个独立的存储路径原生工程只引用不覆盖。另外混编时Dart侧的入口要配置成可被外部的flutterEngine拉起否则原生页面跳Flutter页面时找不到入口。混合开发建议放到项目稳定后再做。前期还是全Flutter页面单跑把业务逻辑跑通再考虑把原生页面嵌进来。一开始就混编会同时面临两套构建体系和两套调试环境的复杂度排查故障时非常痛苦。3. 商品详情的数据层设计JSON、异步与状态管理3.1 数据模型先把商品结构定清楚商品详情页的UI再花哨底子都是数据。页面从上到下要展示轮播图、标题、价格、规格、库存、图文详情这些字段我按接口返回的JSON结构建了对应的Dart模型。我们用的模型没有上json_serializable那套代码生成器因为商城接口字段不算特别多手写fromJson反而更直观也方便针对OpenHarmony侧的特殊字段做容错。核心模型大概是这样的class ProductDetail { final int id; final String title; final String subtitle; final ListString images; final double price; final double originalPrice; final int stock; final ListSkuGroup skuGroups; final String detailHtml; ProductDetail({ required this.id, required this.title, required this.images, required this.price, ... }); factory ProductDetail.fromJson(MapString, dynamic json) { return ProductDetail( id: json[id] as int, title: json[title] as String? ?? , images: (json[images] as List?)?.map((e) e.toString()).toList() ?? [], price: (json[price] as num?)?.toDouble() ?? 0, skuGroups: (json[skuGroups] as List?) ?.map((e) SkuGroup.fromJson(e as MapString, dynamic)) .toList() ?? [], detailHtml: json[detailHtml] as String? ?? , ); } } class SkuGroup { final String groupName; final ListSkuOption options; SkuGroup({required this.groupName, required this.options}); factory SkuGroup.fromJson(MapString, dynamic json) { return SkuGroup( groupName: json[name] as String, options: (json[options] as List) .map((e) SkuOption.fromJson(e as MapString, dynamic)) .toList(), ); } }这里有个容易忽略的点商品接口返回的字段经常是null比如部分商品没有originalPrice、没有subtitle。Dart的强类型不会容忍你直接json[stock] as int一旦null就崩。所以所有字段都要给空值兜底这是详情页稳定性的第一道防线。3.2 Repository模式为什么不能直接在页面里发请求很多新手写详情页习惯在State里面直接建Dio对象然后_http.get(...)一把梭。页面少了还好商城这种多业务复用的项目十个页面各写各的请求后面改个接口地址、加个公共请求头就是个灾难。我们统一用了Repository模式。所有网络请求收敛到ProductRepository里页面只跟仓库打交道class ProductRepository { final Dio _dio; ProductRepository(this._dio); FutureProductDetail fetchDetail(int productId) async { final resp await _dio.get(/api/product/detail, queryParameters: { productId: productId, }); return ProductDetail.fromJson(resp.data[data]); } }Dio的实例在入口处统一配置baseUrl、超时时间、拦截器统一加签名、统一解析错误码都在这一个地方做。页面拿到的永远是解析好的ProductDetail模型不关心网络细节。3.3 Dart异步三件事事件循环、微任务与错误边界商品详情页的数据加载绕不开Dart的异步机制。热搜词里有个问题问得很好flutter future的then回调是放入微任务队列吗。答案是then回调确实会被放入微任务队列microtask queue它在Dart单线程事件循环中优先级比事件队列高当前同步代码执行完后会立即执行微任务。这个机制在详情页有个很典型的坑你在then回调里更新状态时如果回调里做了什么耗时计算会直接卡住UI事件的调度因为微任务会连续执行完才回到事件循环。所以两个经验then回调里只做状态赋值和轻量操作重活扔到compute或者切到异步事件里去。FutureBuilder里不要嵌套async逻辑尤其不要在builder里直接await网络请求会导致重复请求和状态错乱。页面加载数据的正确姿势是集中管理请求生命周期override void initState() { super.initState(); _load(); } Futurevoid _load() async { setState(() _loading true); _errorMessage null; try { _detail await _repository.fetchDetail(widget.productId); } catch (e) { _errorMessage 网络异常请稍后重试; } finally { if (mounted) { setState(() _loading false); } } }这里最容易被忽略的是那个if (mounted)。异步请求返回时页面可能已经销毁了用户快速退出详情页还继续调setState会触发setState() called after dispose()的异常。电商场景里用户点进点出很频繁这个检查必须写。3.4 状态管理为什么没上Bloc选了ChangeNotifier详情页的状态包括加载状态、商品数据、当前选中的SKU组合、收藏状态。这些状态分布在轮播图、信息区、SKU弹窗、底部操作栏多个组件里不是单纯用setState能管好的。我们没有上Bloc因为详情页的状态复杂度还没到需要事件流维度来管理的程度。Bloc那套的样板代码在这种中小型页面上反而是负担。选的是ChangeNotifier ListenableBuilder的组合简单直接。class DetailState extends ChangeNotifier { ProductDetail? detail; bool loading false; String? errorMessage; int selectedSkuIndex -1; bool favorited false; Futurevoid load(int productId) async { loading true; errorMessage null; notifyListeners(); try { detail await _repository.fetchDetail(productId); } catch (e) { errorMessage 加载失败请重试; } finally { loading false; notifyListeners(); } } void toggleFavorite() { favorited !favorited; notifyListeners(); } }详情页根组件用ListenableBuilder包住子组件通过局部监听拿到自己关心的字段。收藏按钮只监听favoritedSKU弹窗只监听selectedSkuIndex和detail.skuGroups。这样改一个字段不会全页刷新滚动性能要好很多。4. 商品详情页UI落地的全过程4.1 首屏结构一个CustomScrollView打底详情页整体框架我用的是CustomScrollView而不是SingleChildScrollView Column。原因有两条一是页面内部有多段滚动内容轮播图、信息区、SKU区、图文详情CustomScrollView天然支持给特定区块添加吸顶效果二是后面要接下拉刷新RefreshIndicator跟CustomScrollView配合最顺。页面骨架RefreshIndicator( onRefresh: _onRefresh, child: CustomScrollView( slivers: [ SliverAppBar( title: Text(商品详情), pinned: true, ), SliverToBoxAdapter(child: _BannerSection(detail: detail)), SliverToBoxAdapter(child: _InfoSection(detail: detail)), SliverToBoxAdapter(child: _SkuEntrySection(state: state)), SliverToBoxAdapter(child: _DetailWebView(html: detail.detailHtml)), ], ), )每一段UI都拆成独立组件父组件只负责把数据传下去。这样页面不会因为某个区域逻辑变复杂而膨胀成几千行的巨型build方法。4.2 轮播图PageView 缩放手势 指示器商品轮播图是比较容易做崩的模块尤其图片一多内存和滚动体验问题就来了。我用的方案是PageView.builder好处是懒加载 —— 屏幕外的商品图不会提前解码占用内存。每张图外面包一层InteractiveViewer支持双指缩放和拖动查看细节SizedBox( height: 375, child: PageView.builder( controller: _pageController, itemCount: detail.images.length, onPageChanged: (index) { setState(() _currentImage index); }, itemBuilder: (context, index) { return InteractiveViewer( maxScale: 3.0, child: Image.network( detail.images[index], fit: BoxFit.cover, frameBuilder: (context, child, frame, wasSyncLoaded) { if (frame null) { return Container(color: Colors.grey[200]); } return child; }, ), ); }, ), )frameBuilder在这里做了占位灰底图片没加载完时不至于白屏闪烁。4.3 价格区营销信息怎么做得不廉价价格区做起来不难但电商文案里那些促销标签、划线价、满减信息一多UI就容易乱。我们定的规则是当前售价最大最醒目原价做删除线且弱化营销标签用圆角小色块排在一行最多三个多了换行。页面里的价格不是一个简单的Text而是一个RichText组合RichText( text: TextSpan( children: [ TextSpan( text: ¥${detail.price.toStringAsFixed(2)}, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: PriceColor), ), if (detail.originalPrice detail.price) TextSpan( text: ¥${detail.originalPrice.toStringAsFixed(2)}, style: TextStyle(fontSize: 14, color: Colors.grey, decoration: TextDecoration.lineThrough), ), TextSpan( text: ${_promotionTag(detail)}, style: TextStyle(fontSize: 12, color: Colors.white, backgroundColor: PriceTint), ), ], ), )这里有个细节坑RichText里的backgroundColor和文字的圆角没法统一控制促销标签文字背景是直角的看起来突兀。后来我把标签单独抽成Container组件包一层ClipRRect放进去观感才正常。4.4 SKU规格选择弹窗、联动与防抖SKU选择是详情页交互最复杂的模块。用户点选择规格弹出底部弹窗弹窗里列出颜色、尺码等规格分组用户点选组合后回显对应SKU的价格和库存再点确认加购。核心逻辑在SkuSelector组件里维护一个已选规格的映射class SkuSelector extends StatefulWidget { final DetailState state; ... } class _SkuSelectorState extends StateSkuSelector { MapString, String _selectedOptions {}; SkuMatch? _matchCurrentSelection() { final selections _selectedOptions.entries.toList() ..sort((a, b) a.key.compareTo(b.key)); // 与服务端返回的SKU组合匹配返回对应价格库存 return widget.state.detail!.skuList.firstWhere( (sku) _selectedOptions.keys.every( (groupName) sku.optionValues[groupName] _selectedOptions[groupName], ), orElse: () skuList.first, ); } }这里容易出问题的是库存为0的规格要不要置灰。我们的做法是该规格组合下任何SKU有库存就可选完全无库存的置灰。用户选到置灰的规格时按钮直接不可点避免提交后服务端报错。加购按钮的防抖我们用了简单的_submitting布尔开关请求没回来前不响应第二次点击。千万别小看这个商城秒杀场景下用户连点两下加购订单就可能下重了。4.5 图文详情与下拉刷新图文详情这块我用的是平台View加载商品介绍的HTML页面而不是把一张动辄几千像素的长图塞进列表。原因很简单长图在SingleChildScrollView里一次性渲染整个坐标系滚动和内存开销都大HTML交给平台侧的WebView去解析和渲染对这些内容的处理更成熟。下拉刷新则复用RefreshIndicator刷新回调里重新走一遍_load()流程。注意刷新时不要直接清空页面数据再等接口返回那样会闪空白。我们的做法是先保留旧数据等新数据返回后一次性替换。4.6 底部操作栏加购、收藏、立即购买底部操作栏是固定定位在页面底部的用SafeArea包住防止全屏设备底部Home条遮挡。里面四个按钮收藏心形图标、客服、加入购物车、立即购买。收藏点击走DetailState.toggleFavorite()加购和立即购买则把当前选中的SKU提交到购物车模块。这里有个导航上的细节立即购买不能直接用Navigator.push跳支付页因为订单是在服务端创建的。正确流程是先调创建订单接口拿到订单号再跳支付页。这个流程写错的话用户在弱网环境下会看到订单提交失败的诡异提示。5. OpenHarmony平台特有的适配与进阶5.1 PlatformView与ArkWeb图文详情的适配真相前面提到图文详情用平台View。在Android上Flutter嵌WebView用AndroidView在OpenHarmony上对应的是UiViewFactory和ArkWeb组件的桥接。问题是PlatformView本来就是Flutter各平台适配里最闹心的一块因为原生控件和Flutter自绘UI不在同一个渲染坐标系里触摸事件、键盘弹出、页面滚动都有边界情况。OpenHarmony上的实现还不像Android那么久经考验我们实际遇到的坑是WebView滚动时Flutter侧的外层CustomScrollView偶尔会吞掉手指事件导致WebView内部滚动和页面滚动手感不一致。解决思路有两个第一图文详情尽量独立成页不进主列表滚动流第二如果必须在滚动列表里内嵌WebView给WebView固定高度内部滚动自己处理外部列表只负责WebView整体滚动。5.2 Impeller与Skia渲染引擎在OHOS上别乱切Flutter 3.x之后在iOS和Android上逐渐默认启用Impeller渲染引擎但OpenHarmony移植版目前主要还是走Skia路径。我们没改任何渲染配置原因很简单OpenHarmony上的Flutter测试覆盖集中在Skia切到Impeller没人给你兜底。真机上如果遇到动画掉帧不要第一反应是换渲染引擎。先把flutter run --profile跑起来用PerformanceOverlay定位是栅格化瓶颈还是布局瓶颈。我们遇到过详情页打开弹出SKU弹窗时掉帧最后定位到是弹窗里规格选项数量太多一次性build了几十个嵌套圆角容器用RepaintBoundary包了一层就缓解了。渲染引擎切换是最后的手段轻易别动。5.3 系统能力相机、权限与隐私弹窗商品晒图功能要调相机。Android上直接申请CAMERA权限OpenHarmony侧用ohos.camera接口实现但权限弹窗逻辑要适配OHOS的应用权限模型。第一次调的时候因为我们没在module.json5里声明相机权限服务端校验通过但真机上弹窗就是不出现代码报permission denied。排查结果OpenHarmony的权限声明除了要在module.json5的requestPermissions字段里写还需要在页面侧通过abilityAccessCtrl接口做运行时授权申请。两步缺一不可。这个坑在Android上不存在因为manifest声明后系统会自动提示。到了OHOS运行时授权要手动调用很多Android转过来的人会漏。5.4 XTS认证视角上架前要过的兼容门槛如果项目做完打算走OpenHarmony的应用认证流程XTS兼容性测试是绕不开的。XTS那套测试用例会覆盖到应用生命周期、权限管理、IPC通信、应用沙箱等多个维度。我们当时在认证前跑了一遍用例暴露出来的主要问题集中在应用在静默后台被系统回收后Flutter引擎重启逻辑没处理好导致返回前台白屏。部分原生权限声明了却没实际使用被判定为权限滥用。这两类问题都不难修但如果在开发阶段就按XTS的规范来后期能省大量返工时间。建议在项目初期就看一下XTS的用例范围。6. 三处耗时最长的排错复盘6.1 未处理异常导致页面白屏DartVMInitializer报错的真相文章开头提到E/flutter DartVMInitializer报错我们第一次遇到时整个详情页冷启动直接白屏。当时团队里没人知道这个日志什么意思查了半天资料最后定位链路是这样的先加全局异常捕获void main() { runZonedGuarded(() { FlutterError.onError (details) { // 接入自己日志系统 reportError(details.exception); }; runApp(const MallApp()); }, (error, stack) { reportError(error, stack); }); }加了捕获之后日志里能看到真正崩的异常堆栈了。问题出在商品数据接口返回的skuList字段在部分商品上是个空数组但我们的解析代码用了firstWhere(orElse: () skuList.first)空数组上取.first就直接抛Bad state: No element。这个异常发生在Future的调用链上没有catch直接被引擎当作未处理异常打印出来了。这件事给我们的教训是两层第一解析层必须对所有集合做空值兜底不能想当然地认为返回一定有值第二生产环境一定要全局挂异常捕获不然你会看到一屏DartVMInitializer日志但完全不知道业务代码哪里炸了。6.2 图片资源过大导致的OOM详情页轮播图和图文详情图片都是后台编辑上传的编辑上传时没有做压缩好几张图单张4MB以上。在低配真机上轮播图滑两次就OOM。排查过程先用flutter run --profile观察内存曲线发现图片组件一加载内存直接往上窜。原因在于Flutter引擎对网络图片解码后默认保留原始尺寸的位图4MB的图解码成位图后内存可能膨胀到20MB以上。修复方案是双管齐下代码层给图片服务加?imageMogr2/thumbnail/800x800这样的缩略图参数轮播图和列表只拉缩略图不拉原图内存层把ImageCache的默认上限调低防止缓存池被大图撑爆PaintingBinding.instance.imageCache.maximumSize 200; PaintingBinding.instance.imageCache.maximumSizeBytes 80 20;图文详情那个页面因为本身就是WebView渲染不受Flutter内存池影响倒没出问题。轮播图、商品列表这种Flutter侧直接解码的地方是OOM重灾区。6.3 购物车角标的组件通信Stream比全局State好用详情页点了加购之后底部导航栏的购物车角标要立刻更新。这个跨页面通信的场景我们没有引入全局状态管理框架用了一个轻量级EventBus。Dart侧定义一个全局的StreamController作为事件通道class CartEvents { static final StreamControllerint _controller StreamController.broadcast(); static void add(int count) _controller.add(count); static Streamint get stream _controller.stream; }详情页加购成功后发事件购物车页签的宿主在initState里监听Stream、更新角标。用Stream而不是直接拿全局变量好处是事件天然支持多订阅者以后列表页、搜索页也要更新角标时加监听就行不用改详情页的代码。这里要小心一个坑StreamController用完之后要close不然一直挂在内存里。我们用broadcast全局单例的话一般不手动close监听端在State里监听时要在dispose里取消订阅。StreamSubscription? _sub; override void initState() { super.initState(); _sub CartEvents.stream.listen((count) { setState(() _cartCount count); }); } override void dispose() { _sub?.cancel(); super.dispose(); }这套模式在商品详情页、购物车、首页三个模块之间通用代码量不大但把跨页面通信解耦得很干净。6.4 防抖在加购场景下的必要性第三个排错其实是我的失误。加购按钮那会儿没有做请求防抖测试在弱网环境下双击加购购物车里出现了两条相同的订单商品。虽然服务端可以合并但用户体验很怪。后来在_submitOrder和_addToCart两个方法入口都加了_submitting开关请求未返回期间按钮置灰并显示一个轻量loading。这个改动十五分钟搞定但直接避免了一类商城高频投诉。顺带说一个细节加购成功后的Toast提示在OpenHarmony上不要用Android的Toast通道Flutter侧用SnackBar或者Overlay自己渲染比走平台通道要稳。因为部分OHOS真机的Toast高度跟Flutter页面不共享状态容易顶掉底部操作栏的位置。最后分享一个小技巧商品详情页的骨架屏。最开始的版本里数据加载期间整个页面是白底加一个居中的转圈在OpenHarmony真机上冷启动加载商品数据要等两秒左右用户反馈感觉像卡死了。后来我在加载期间放了一套骨架屏布局灰色圆角块模拟轮播图、标题、价格条的位置数据回来后切换成真实内容。表面看只是UI细节但电商场景里详情页的加载时间是用户流失率的高敏指标骨架屏的成本非常低收益却很明显。骨架屏、错误重试、下拉刷新这三件套加完商品详情页才算真正达到上线标准。Flutter for OpenHarmony这条路走到现在我最深的体会是跨端框架移植真正的难点从来不是Dart代码本身而是每一层平台对接的边界问题。把这些边界问题摸熟这套组合的性价比确实很高。
返回列表