搞了十来天的OpenHarmony跨平台折腾,今天终于可以来点真正的实战了。这一篇我们直接落地一个电商场景中最核心、也最能检验功底的商品详情页,重点把轮播图从零手写出来,并把点击跳转的完整闭环跑通。这篇文章适合两种人看:一是已经在OpenHarmony设备上把Flutter环境跑通、想找点真实业务场景练手的开发者,二是对Flutter跨平台开发有一定基础、想了解在鸿蒙生态上做页面有哪些坑和差异的同学。我会带着你从工程配置开始,一步一步把整个页面架起来,代码直接可抄,坑直接帮你踩平。
1. 为什么选商品详情页作为Flutter实战的DAY11主题
1.1 电商业务里最复杂的单页面场景
做了这么多年客户端,我一直有一个观点:商品详情页是移动端最能锻炼页面组织能力的场景,没有之一。一个像样的详情页,集合了图片轮播、多级联动滚动、复杂信息展示、规格弹窗、底部操作栏、路由跳转等等几乎你能想到的所有UI交互形态。
列表页再复杂,核心也就是一个item的复用和刷新;个人中心再花哨,也无非是分组列表加几组入口。但详情页不一样,它要求开发者同时处理沉浸式视觉、交互流畅度、数据加载状态管理,还得兼顾业务上的转化诉求。
从业务角度讲,用户在详情页的每一个操作——看主图、滑参数、选规格、点加购,背后都是一条完整的行为链路。所以把这个页面吃透了,你基本上就掌握了电商类Flutter应用的骨架。我把这个主题放在DAY11,正好是一个承上启下的位置:前面我们已经把工程环境、路由架构、基础组件都梳理过了,今天开始用真实业务把这些能力串起来。
1.2 Flutter做OpenHarmony跨平台的真实价值
在OpenHarmony生态里开发应用,绕不开一个选择:用ArkUI原生开发,还是用Flutter走跨平台方案?
我的观点很明确:如果你的团队已经有一套Flutter代码库,或者你的目标平台除了OpenHarmony还包括Android和iOS,那Flutter的投入产出比要远高于用ArkUI重写一套。Flutter用一套Dart代码接管了UI渲染层,在OpenHarmony上通过适配层对接系统能力,业务代码的复用率可以达到百分之八十以上。
这也是我为什么坚持用Flutter来做这个系列的原因。我们不是在讨论理论上的可能性,而是真的在OpenHarmony设备上跑Flutter应用。整个DAY11的页面,从架构设计到UI组件,全部是标准的Flutter写法,这意味着你在Android、iOS上积累的经验可以无差别迁移过来。当然,过程中会有一些OpenHarmony特有的适配细节,这个在后面的实操和踩坑章节里我会专门展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开局准备:在OpenHarmony上跑起Flutter工程
2.1 工具链版本与环境说明
在动手写代码之前,先把环境说清楚。我这里用的是Dayu200开发板,搭载的是RK3568芯片,OpenHarmony版本是4.0 Release。Flutter这边用的OpenHarmony官方适配分支,SDK版本是3.16.5,在OpenHarmony环境里跑Flutter,本质上跟你跑普通Flutter工程是一样的,只是编译部署的目标设备变成了鸿蒙开发板。
这里必须提一个热搜词里大家问得特别多的问题:RK3568的设备树到底怎么选。说真的,这个问题我在DAY2系列里聊过一次,但后台还是不停有人问。设备树选错的表现非常典型:开发板能通电、串口有日志、但屏幕就是黑屏不出界面。你在编译OpenHarmony系统镜像的时候,不同屏幕模组对应不同的dts文件,比如默认的MIPI屏、HDMI输出、LVDS屏,它们的初始化参数完全不一样。
选设备树没有捷径,只能对照你手里那块屏幕的硬件型号去找相应的dts配置。比如Dayu200套件常见的是默认MIPI DSI接口的屏幕,那你就得在编译配置里指定对应的产品型号和设备树文件。你要是拿默认配置去适配一块HDMI屏幕,大概率只能看到串口日志在跑,屏幕永远不亮。解决思路就一条:先确认屏幕物理接口,再去找对应的dts文件烧录,别让系统层的问题干扰你对Flutter层的判断。
2.2 创建Flutter工程并配置OpenHarmony插件
环境确认没问题之后,就进入正题了。先创建一个Flutter工程:
bash复制flutter create --org com.example --project-name product_detail_app product_detail_app
工程创建完,打开pubspec.yaml,添加相关的依赖。商品详情页我们会用到网络图片加载,所以dio和cached_network_image是少不了的。当然,为了展示效果,这里也可以用本地资源图代替,但真实项目里肯定要接网络图片,所以网络请求库我建议现在就加上:
yaml复制dependencies:
flutter:
sdk: flutter
# 网络请求
dio: ^5.3.3
# 图片加载和缓存
cached_network_image: ^3.3.0
这里需要特别说明的一点是,OpenHarmony的Flutter适配分支对pub.dev上的第三方包兼容情况参差不齐。由于鸿蒙适配层实现的是大部分Flutter框架层API,凡是纯Dart实现的包基本都能直接用,但凡是依赖了原生插件(通过MethodChannel调用Android/iOS原生代码)的包,就必须等鸿蒙那边的插件适配。所以我这里选的dio和cached_network_image,一个是纯Dart实现,一个虽然内部有原生部分但官方做了OpenHarmony的适配,实际用下来问题不大。
2.3 OpenHarmony应用编译部署的方式
工程建好之后,编译部署这一步会有两个分支。如果你是在Windows或macOS上开发,可以走远程连接开发板的方式,用flutter run直接跑到设备上。我在实际调试中更推荐先在模拟器或者本地用flutter run -d windows / -d macos把页面效果调好,再部署到OpenHarmony设备上验证兼容性。
为什么这么建议?因为在开发机上用桌面端调试,热重载的反应速度最快,基本秒级更新UI。而部署到OpenHarmony开发板,每一次构建打包都要走OpenHarmony的编译链路,一次完整编译经常要几分钟起步。开发期间频繁烧板子,效率太低了。先把逻辑和UI全部在桌面端验证通过,再上板子做真机适配,这个流程能省下大量时间。
3. 商品详情页布局拆解:先把骨架搭出来
3.1 页面整体结构设计与数据模型定义
商品详情页看着复杂,其实拆开来看就几个区域:顶部沉浸式导航栏、图片轮播区、商品信息区、店铺与优惠区、规格选择入口、用户评价区、商品图文详情区,最后还有底部固定的操作栏。这么多内容要在一个页面里共存,如何协调滚动是关键。
我的做法是用CustomScrollView来组织整个页面,而不是简单地在一个ListView里套多个Widget。为什么?因为详情页中不同区块有不同的滚动表现:轮播图区可以跟着内容一起滚出去,商品参数区可能需要吸顶,底部图文详情可能要有独立的滚动逻辑。CustomScrollView配合Sliver家族组件,可以精确控制每一个区块的滚动行为,这是ListView做不到的。
先定义商品的数据模型:
dart复制class ProductModel {
final String title;
final String subtitle;
final double price;
final int salesCount;
final List<String> imageUrls;
final List<String> galleryUrls;
const ProductModel({
required this.title,
required this.subtitle,
required this.price,
required this.salesCount,
required this.imageUrls,
required this.galleryUrls,
});
}
这里imageUrls是轮播图主图列表,galleryUrls是详情页的图片长廊,两者业务上可能重叠,但数据源分开会灵活一些。实际项目中,这些字段通常来自后端接口,序列化和解析可以交给Dio配合json_annotation来完成,我这里就先用固定数据模拟,方便聚焦UI实现。
3.2 搭建CustomScrollView的页面骨架
页面的主体结构是这样组织的:
dart复制@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
_buildSliverAppBar(),
SliverToBoxAdapter(
child: _buildBannerSection(product),
),
SliverToBoxAdapter(
child: _buildProductInfoSection(product),
),
SliverToBoxAdapter(
child: _buildSpecSection(),
),
SliverToBoxAdapter(
child: _buildCommentSection(),
),
SliverToBoxAdapter(
child: _buildDetailSection(product),
),
SliverToBoxAdapter(
child: SizedBox(height: 80),
),
],
),
bottomNavigationBar: _buildBottomToolBar(),
);
}
这个结构已经趋近一个成熟的详情页形态。我把整个页面拆成了可独立维护的区块方法:_buildBannerSection负责轮播图,_buildProductInfoSection负责价格和标题,_buildSpecSection是规格入口,_buildCommentSection是评价摘要,_buildDetailSection是图文详情。每个区块都是一个独立的Widget构建方法,后续要改哪一个区域,直接定位过去就行,不会牵一发而动全身。
为什么要用SliverToBoxAdapter而不是直接返回一个普通的Column?因为CustomScrollView只接受Sliver家族组件作为children。SliverToBoxAdapter的作用,就是把一个普通的Widget包装成Sliver,让它能参与CustomScrollView的滚动布局。这是Flutter一个比较容易绕晕的点,搞清楚了就一通百通了。
3.3 沉浸式导航栏的实现细节
电商详情页的导航栏和普通页面不同,它要做成沉浸式的,也就是背景透明悬浮在轮播图上方。用户向上滑动之后,导航栏再渐渐变成半透明或者纯色背景。
这个效果我用了SliverAppBar来实现:
dart复制Widget _buildSliverAppBar() {
return SliverAppBar(
expandedHeight: 0,
pinned: true,
backgroundColor: Colors.white.withOpacity(0.9),
leading: const BackButton(color: Colors.black),
actions: [
IconButton(
icon: const Icon(Icons.share_outlined, color: Colors.black),
onPressed: () {
// 分享逻辑
},
),
],
);
}
实际项目中这里更精细的做法是监听滚动偏移量,动态控制导航栏的背景透明度。Rolling的时候透明度低,展示更多图片内容;滚到商品信息区了就把透明度拉满。实现方式是用NotificationListener监听ScrollNotification,取出pixels值来计算透明度比例。
4. 轮播图的完整实现:从零手写一个可用的Banner组件
4.1 为什么不用三方库,而是自己用PageView实现
聊到轮播图,很多人第一反应是直接引入flutter_swiper或者carousel_slider。这两个库在Android和iOS上确实很好用,但是到了OpenHarmony环境,问题就来了:第三方库内部可能依赖了一些原生插件或者未适配的API,导致在鸿蒙设备上运行异常。
所以这一期我选择用Flutter自带的PageView来实现轮播图。这样做有几个好处:首先,PageView是Flutter框架层的东西,OpenHarmony适配分支已经完整支持,不存在插件兼容问题;其次,手写轮播图的核心逻辑并不复杂,也就几十行代码;最后,自己实现的组件在样式定制上完全可控,后续想加什么效果直接改自己的代码就行。
这在工程上也是一个很成熟的决策思路:核心组件能自己掌控就不要依赖第三方,尤其是跨平台开发场景,每多一个依赖就多一个潜在的适配风险。
4.2 PageView实现轮播图的核心代码
轮播图的核心功能包括三个部分:左右滑动切换、自动播放、底部指示器。先看完整实现:
dart复制class ProductBanner extends StatefulWidget {
final List<String> imageUrls;
final ValueChanged<int> onBannerTap;
const ProductBanner({
super.key,
required this.imageUrls,
required this.onBannerTap,
});
@override
State<ProductBanner> createState() => _ProductBannerState();
}
class _ProductBannerState extends State<ProductBanner> {
late PageController _pageController;
Timer? _autoPlayTimer;
int _currentPage = 0;
bool _isDragging = false;
@override
void initState() {
super.initState();
_pageController = PageController(viewportFraction: 1.0);
_startAutoPlay();
}
void _startAutoPlay() {
_autoPlayTimer?.cancel();
_autoPlayTimer = Timer.periodic(const Duration(seconds: 3), (timer) {
if (!_isDragging && widget.imageUrls.length > 1) {
_pageController.nextPage(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
);
}
});
}
@override
void dispose() {
_autoPlayTimer?.cancel();
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final height = MediaQuery.of(context).size.width; // 宽高比1:1
return SizedBox(
height: height,
child: Stack(
children: [
PageView.builder(
controller: _pageController,
itemCount: widget.imageUrls.length,
onPageChanged: (index) {
setState(() {
_currentPage = index % widget.imageUrls.length;
});
},
onPanDown: (_) => _isDragging = true,
onPanCancel: () => _isDragging = false,
onPanEnd: (_) => _isDragging = false,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () => widget.onBannerTap(index),
child: CachedNetworkImage(
imageUrl: widget.imageUrls[index],
fit: BoxFit.cover,
placeholder: (context, url) => Container(
color: Colors.grey.shade200,
child: const Center(
child: CircularProgressIndicator(),
),
),
),
);
},
),
// 指示器
Positioned(
bottom: 12,
right: 16,
child: _buildIndicator(),
),
],
),
);
}
Widget _buildIndicator() {
return Row(
children: List.generate(widget.imageUrls.length, (index) {
final isActive = index == _currentPage;
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(left: 4),
width: isActive ? 16 : 6,
height: 6,
decoration: BoxDecoration(
color: isActive ? Colors.white : Colors.white54,
borderRadius: BorderRadius.circular(3),
),
);
}),
);
}
}
这段代码里有两个细节值得注意。第一个是PageView.builder的onPanDown、onPanCancel、onPanEnd三个回调。我在这里用手势回调维护了一个_isDragging标志位,目的是在用户手动滑动图片的时候暂停自动播放,等手指松开再恢复。如果不做这个处理,会出现自动播放和手动滑动打架的情况,用户体验非常差。
第二个细节是指示器的实现。我用的是AnimatedContainer,活跃项是一个16像素宽的圆角条,非活跃项是6像素宽的圆点,切换的时候会有平滑的宽度动画。这个设计在很多电商App里都能看到,视觉上比普通的圆点更有设计感,而且实现成本极低。
4.3 轮播图实现中的几个关键细节决策
关于轮播图的数量处理,很多人会问要不要做无限循环。我的建议是,如果图片数量只有三到五张,做不做无限循环影响不大,轮播到最后一页往回滑就好。如果数量比较少,为了无限循环去玩取模运算和大数法,反而增加了复杂度。
我这里用的是一个更稳妥的方案:不劫持PageView的原始索引去实现无限循环,而是让PageView按真实数量滚动,用户滑到哪一页就看到哪一页。自动播放到最后一页之后,调用nextPage会怎样?它的行为是到达末尾边界就停下来,不再继续。从体验上讲,商品详情页的轮播图到边界停住是完全合理的,用户本来就可以手动往回滑,所以我不在这个环节做过度的处理。
那如果产品经理非要让你做无限循环怎么办?实现方式倒也不复杂。思路一:把itemCount设置成一个很大的数(比如图片数量的1000倍),初始页码设置为中间某个位置,然后用取模运算映射到真实的图片索引。思路二:在PageView前后各补两页,到边界时用jumpToPage做无缝跳转。这两种方案在OpenHarmony上都能跑通,但会增加代码复杂度,对商品详情的场景来说收益不高,所以我这边先不做,属于典型的过度设计。
4.4 轮播图的点击事件如何设计
轮播图肯定要支持点击跳转,但是跳去哪里?业务上通常是这几种:跳转到商品详情页的不同Tab、跳转到图片全屏预览页、跳转到一个H5活动页。我这里的做法就是向上抛一个onBannerTap的回调,携带当前点击的图片索引,由上层页面来决定处理逻辑。
为什么不直接在Banner组件内部跳转?因为Banner组件属于通用组件,它不应该感知具体的业务路由逻辑。你把这个组件复用到其他页面时,点击行为可能完全不同。设计成回调的形式,让使用方自己决定,这才是组件复用的正解。
5. 点击跳转的完整闭环:从Banner到图片预览
5.1 商品详情页的路由设计
在讲跳转之前,先铺垫一下路由设计。我建议在项目里独立维护一个路由配置文件,把所有页面统一管理起来,而不是在详情页里直接Navigator.push一个MaterialPageRoute直接创建页面。跳转页面的代码集中管理之后,后续无论做动态路由还是深度链接,都有很多操作空间。
dart复制class AppRoutes {
static const String productDetail = '/product_detail';
static const String imagePreview = '/image_preview';
static const String webViewPage = '/webview_page';
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case productDetail:
return MaterialPageRoute(
builder: (context) => ProductDetailPage(
productId: settings.arguments as String,
),
);
case imagePreview:
final args = settings.arguments as ImagePreviewArgs;
return MaterialPageRoute(
builder: (context) => ImagePreviewPage(
imageUrls: args.imageUrls,
initialIndex: args.initialIndex,
),
);
default:
return MaterialPageRoute(
builder: (context) => const NotFoundPage(),
);
}
}
}
路由参数的类型安全很容易被忽略。如果你用Map来传参,在页面上取值的时候做类型转换,一旦传参的类型和你预期的对不上,运行期就直接抛异常。所以我这里定义了一个ImagePreviewArgs的普通类,参数类型在编译期就确定了,IDE也能自动补全,好用得多。
5.2 图片预览页的完整实现
商品详情页的轮播图点击,最常见的跳转目标是图片预览页。用户点击某一张主图,进入全屏可以左右滑动浏览大图的页面,并且支持双指缩放查看细节。这是一个很基础但使用频率很高的功能。
用到的组件是PageView配合InteractiveViewer。PageView负责左右滑动切换,InteractiveViewer负责图片的缩放:
dart复制class ImagePreviewPage extends StatefulWidget {
final List<String> imageUrls;
final int initialIndex;
const ImagePreviewPage({
super.key,
required this.imageUrls,
required this.initialIndex,
});
@override
State<ImagePreviewPage> createState() => _ImagePreviewPageState();
}
class _ImagePreviewPageState extends State<ImagePreviewPage> {
late PageController _pageController;
late int _currentIndex;
@override
void initState() {
super.initState();
_currentIndex = widget.initialIndex;
_pageController = PageController(initialPage: widget.initialIndex);
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
title: Text('${_currentIndex + 1}/${widget.imageUrls.length}'),
),
body: PageView.builder(
controller: _pageController,
itemCount: widget.imageUrls.length,
onPageChanged: (index) {
setState(() {
_currentIndex = index;
});
},
itemBuilder: (context, index) {
return Center(
child: InteractiveViewer(
maxScale: 4.0,
child: CachedNetworkImage(
imageUrl: widget.imageUrls[index],
fit: BoxFit.contain,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(color: Colors.white),
),
),
),
);
},
),
);
}
}
这个页面非常轻量,核心逻辑就两个:当前页点击的是哪张图,预览页就把哪张图作为初始页展示;用户左右滑动时,顶部的页码跟随变化。代码量不大,但完整覆盖了图片预览页的所有必备功能。
5.3 透明度渐变的页面切换动画
商品详情页的轮播图点击进入预览页,Flutter自带的MaterialPageRoute默认是从右侧滑入。但在电商场景中,从图片点击进入大图预览,更有质感的过渡是透明度渐变加上轻微缩放,营造一种图片“放大焦点”的感觉。
实现方式是用PageRouteBuilder自定义过渡动画:
dart复制Navigator.push(
context,
PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 250),
pageBuilder: (context, animation, secondaryAnimation) => ImagePreviewPage(
imageUrls: product.imageUrls,
initialIndex: index,
),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final curved = CurvedAnimation(
parent: animation,
curve: Curves.easeOut,
);
return FadeTransition(
opacity: curved,
child: child,
);
},
),
);
这里只有透明度渐变,没有做缩放。原因很简单,缩放过渡动画涉及图片尺寸的动态计算,处理不当会显得很生硬。项目初期先用透明度渐变保证交互的自然感,后续如果视觉稿有要求,再在这个基础上加ScaleTransition也不迟。
5.4 页面跳转在OpenHarmony上的兼容性注意点
在OpenHarmony环境里使用Navigator.push做跳转,整体是没有问题的。但我遇到过一个比较隐蔽的场景:当详情页的图片还没来得及加载完成,用户快速点击轮播图进入预览页时,偶发出现预览页白屏。
排查下来的原因是这样:预览页依赖cached_network_image去加载图片,但图片裸地址网络请求尚未完成,页面就渲染了占位图。而在OpenHarmony开发板上,如果网络请求线程调度慢,占位图可能还没有来得及替换成真正的图片,页面就已经被用户滑动切换了,视觉上就会出现短暂的白屏闪动。
解决方案是在预览页的图片加载占位阶段,用一个浅灰色的Container做为底色,同时给CachedNetworkImage配置一个loadingBuilder,加载过程中显示一个居中的进度圈。这样即使在网络缓慢的情况下,也不会出现刺眼的白屏,视觉上是可接受的。
6. 踩坑实录:我在OpenHarmony上遇到的那些真实坑
6.1 坑一:RK3568设备树选错,白屏背锅给了Flutter层
这是折腾OpenHarmony必踩的一个大坑,我在DAY2的文章里提过,但今天还得再强调一遍,因为真的太多人栽在这里了。
当时我拿到Dayu200开发板,烧录完编译好的OpenHarmony镜像,发现串口日志显示系统在正常运行,但HDMI接口接的显示器始终黑屏,完全没有画面输出。我下意识以为是Flutter渲染层出了问题,折腾了半天Dart代码,后来才通过排查确认,问题出在编译镜像时选择的设备树和我的屏幕类型不匹配。
OpenHarmony的RK3568芯片平台,设备树里对显示驱动部分的初始化是分屏幕类型的。默认配置走的是MIPI DSI接口的屏幕驱动,而HDMI输出则需要单独的dts文件支持。我编译镜像的时候没有改默认的设备树配置,烧录到板子上接HDMI,系统起来了但是显示驱动无法初始化,结果就是黑屏。
排查和解决的思路在这里:先用串口工具连接开发板,查看启动日志中显示相关的初始化记录,看有没有报错。一旦日志里出现类似hdmi probe failed或者是dsi panel not found这样的关键信息,基本可以确认是设备树的问题。然后去OpenHarmony源码里找到对应你屏幕类型的dts文件,重新编译镜像,烧录验证。
这类问题最大的痛点在于:它的表现和Flutter层渲染黑屏太像了,很容易误导初学者把时间浪费在错误的方向上。我给的建议是,买开发板之前一定确认好屏幕接口类型,烧录系统遇到屏幕不亮,先查设备树,不要一上来就怀疑自己的代码。
6.2 坑二:Flutter依赖下载缓慢导致构建失败
在OpenHarmony上做Flutter开发,网络环境是绕不开的坎。Flutter工程创建之后,第一次执行flutter pub get,依赖要从pub.dev拉取;编译OpenHarmony产物时,Gradle又要从远端拉取OpenHarmony SDK。如果网络不好,这些步骤分分钟让你怀疑人生。
我的建议是,在动手之前先把pub和Gradle的镜像源都配置好。pub源可以配置成本地镜像仓库或者公司内部的Nexus;Gradle仓库地址也改成镜像地址。这些配置虽然在Android开发的机器上大家都会做,但到了OpenHarmony环境里,很多人只关注OpenHarmony本身,反而忽略了基础的构建环境配置,结果卡在漫长的等待上。
另外还有一个非常小的细节,容易让人抓狂:Flutter自带的Dart SDK在下载依赖时,如果之前已经配置过一个路径环境变量,新开终端窗口后这个变量可能没有生效。特别是Windows环境下,改完path环境变量之后必须新开终端窗口,否则命令提示符里死活找不到flutter命令。别看这是小问题,我见过不止一个开发新手在这里卡了半个多小时不知所措。
6.3 坑三:网络图片加载失败的权限问题
在OpenHarmony设备上运行Flutter应用,从网络加载图片失败是一个比较高概率的事件,而且原因往往出在权限配置上而不是代码本身。
OpenHarmony的权限模型和Android类似,应用要访问网络,必须在配置文件中申请网络权限。如果你创建的Flutter工程没有显式声明网络访问权限,跑起来之后dio请求和CachedNetworkImage的图片下载都会失败,报错信息就是一堆网络异常。
处理的细节是你要去模块的ohos配置文件里加上网络权限声明。这一步不是Flutter工程文件里的内容,而是OpenHarmony工程侧文件。它对新手不友好在于:Flutter代码层不会有任何提示告诉你缺少系统权限,你只能通过排查错误日志才能定位到根因。这就意味着,你在OpenHarmony上做Flutter开发之前,必须对鸿蒙工程的结构有一个基本的认知,至少要能看懂几个关键配置文件的作用。
6.4 坑四:PageView图片数量边界处理
最后分享一个纯Flutter层的小坑。PageView在itemCount为一的时候,如果还用自动轮播,Timer依然会周期性触发nextPage,此时PageView会尝试滑动到一个不存在的页面。虽然不会崩溃,但控制台会打出一些不友好的异常信息,也有可能造成页面闪一下。
解决办法就是在启动自动轮播之前判断一下图片数量:
dart复制if (widget.imageUrls.length > 1) {
_startAutoPlay();
}
这是一个极小的边界条件,但开发者很容易忽略。图片数量为1在很多后台系统里是完全正常的,比如上传主图的时候就传了一张。这种边界处理没有技术难度,纯粹考的是你代码写的够不够细。
7. 更多值得尝试的进阶方向
商品详情页跑通之后,后续可以扩展的方向其实非常多。最近后台讨论比较多的有几个方向,我简单展开说说。
一是Flutter内嵌数据库。详情页里的商品参数、用户最近浏览记录,都可以用sqflite这类数据库缓存起来,用户在弱网环境下也能正常打开页面。OpenHarmony上的sqflite已经有适配方案,纯Dart层的数据库逻辑是通用的,就是需要注意文件存储路径在鸿蒙系统里的差异。
二是Flutter如何调用鸿蒙的图库和系统能力。详情页需要上传用户图片评价、实现类似微信登录这类第三方授权能力时,都需要通过平台通道去调用OpenHarmony的系统API。这个方向的关键是理解Flutter的MethodChannel和OpenHarmony侧的接入方式,本质上是跨语言调用,搞清楚了原理,具体业务都能扩展。
三是Flutter兼容鸿蒙拉起应用内支付。电商应用最终要打通交易链路,支付是逃不开的话题。OpenHarmony有自己的一套支付能力开放接口,Flutter层怎样去对接这些能力,需要关注鸿蒙SDK的Flutter适配进展。这块目前还在快速迭代中,建议先做好页面层和交互层,支付这类系统能力最后再接入。
四是Flutter端的性能优化。详情页图片量大、滚动频繁,内存占用和帧率表现需要重点关注。在开发板上跑Flutter应用,性能优化比在PC上要敏感得多。图片懒加载、列表缓存、动画性能监控这些都是值得深挖的方向。
8. 把页面完整跑通后的几点真实体会
整个DAY11的实践做下来,我最大的感受是Flutter在OpenHarmony上已经不是“能不能跑”的问题,而是“怎么跑得更顺”的问题。页面开发、路由跳转、图片加载、状态管理这些核心能力,OpenHarmony的Flutter适配分支都已经覆盖到位了,日常开发中的绝大多数需求都能在鸿蒙设备上实现。
当然,这并不意味着没有坑。第三方依赖的兼容筛选、系统权限的配置、开发板上的网络环境调试,这些跨端适配的额外成本,是每一个想在OpenHarmony上做跨平台开发的团队都必须承担的。我的建议是,在选型阶段就把这些成本算进去,而不是等项目走到一半再回头补课。
如果你正在做OpenHarmony上的Flutter开发,希望这一篇的细节能帮你省下几个晚上的排查时间。后面我会继续更新这个系列,把商品详情页涉及的规格选择弹窗、购物车流程、订单结算这些链路一个个补齐,最终把一套完整的电商功能在OpenHarmony上跑通。
