【鸿蒙 5.0 实战开发】一次开发多端部署开发实例(购物比价)
本文从目前流行的垂类市场中,选择购物行业应用作为典型案例详细介绍 “一多” 在实际开发中的应用。购物行业应用的核心功能为浏览商品、商品比价和直播购等。根据这些核心功能,本文选择首页、商品分类页、商品详情页、商品支付页、咨询客服页、直播间页等作为典型页面进行开发,遵从多设备的“差异性”、“一致性”、“灵活性”和“兼容性”,能够让开发者快速高效地掌握“一多”能力并实现购物比价应用的相关功能。
📝往期推文全新看点(文中附带最新·鸿蒙全栈学习笔记)
🚩 鸿蒙(HarmonyOS)北向开发知识点记录~
🚩 鸿蒙(OpenHarmony)南向开发保姆级知识点汇总~
🚩 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?
🚩 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~
🚩 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?
🚩 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?
🚩 记录一场鸿蒙开发岗位面试经历~
📃 持续更新中……
概述
本文从目前流行的垂类市场中,选择购物行业应用作为典型案例详细介绍 “一多” 在实际开发中的应用。购物行业应用的核心功能为浏览商品、商品比价和直播购等。根据这些核心功能,本文选择首页、商品分类页、商品详情页、商品支付页、咨询客服页、直播间页等作为典型页面进行开发,遵从多设备的“差异性”、“一致性”、“灵活性”和“兼容性”,能够让开发者快速高效地掌握“一多”能力并实现购物比价应用的相关功能。
购物类应用为了向用户展示更多的商品选择,对垂类内的核心功能进行了独特设计:
-
商品分类页 主要用于快速查找目标商品,采用分栏的布局提升查找效率。
-
商品支付页,为避免大面积页面跳转和遮挡商品的关键信息,采用浅层窗口-半模态的方式进行支付。
-
在查看 商品详情 或 直播 时,通过侧边面板显示其他的辅助信息,增加浏览效率。
-
直播间页 推荐的商品信息,在多端基于设备屏幕尺寸进行响应式适配,避让直播的关键信息。
-
退出直播间页时,能够使用画中画小窗口观看直播。
当前系统的产品形态主要有手机、折叠屏、平板和2in1四种,下文的具体实践也将围绕这几种产品形态展开,同时将分别从UX设计、架构设计、页面开发三个角度给出符合“一多”的参考样例,介绍“一多”购物比价应用在开发过程中的最佳实践。
- UX设计 章节介绍购物比价应用的交互逻辑和通用的设计要点,对于类似的设计要点,开发者可以直接拿来使用。
- 架构设计 章节推荐“一多”应用使用目录结构更加清晰的三层架构。
- 页面开发 章节会将页面划分为不同区域,按照区域的开发顺序,介绍如何使用自适应布局和响应式布局实现不同的UI效果。
架构设计
HarmonyOS的分层架构主要包括三个层次:产品定制层、基础特性层和公共能力层,为开发者构建了一个清晰、高效、可扩展的设计架构。
页面开发
本章介绍购物比价应用中如何使用“一多”的布局能力,完成页面层级的一套页面、多端适配。下文将从不同页面展开,介绍每个页面区域使用到具体的布局能力,帮助开发者从0到1进行购物比价应用的开发。
首页
首页通常有入口图标和商品卡片等丰富的商品信息,帮助解决用户浏览及挑选商品的核心需求。观察首页在不同设备上的UX设计图,可以进行如下设计:
- 将首页划分为7个区域,效果图如下:
| sm | md | lg | |
|---|---|---|---|
| 效果图 | ![]() |
![]() |
![]() |
| sm | ![]() |
![]() |
![]() |
- 首页区域2在小设备上呈两行显示,在中设备和大设备上单行显示,断点变化时切换显示效果。
- 首页区域3使用自适应布局延伸能力随不同设备尺寸延伸或隐藏,区域4、5使用自适应布局占比能力和均分能力。
- 首页区域1、5-8使用响应式布局中的栅格断点系统,根据断点变化改变组件内属性,从而实现相应的布局效果。
| 区域编号 | 简介 | 实现方案 |
|---|---|---|
| 1 | 底部/侧边页签 | 借助 栅格布局 监听断点变化改变位置,代码可参考 一多开发实例(长视频)。 |
| 2 | 顶部页签及搜索框 | 栅格布局监听断点变化实现折行显示,List组件 实现延伸能力,layoutWeight实现拉伸能力,代码可参考 一多开发实例(长视频) |
| 3 | 商品分类图标 | List组件实现延伸能力,代码可参考 一多开发实例(长视频)。 |
| 4 | 商品卡片 | Swiper组件,指定displayCount属性实现占比能力,设置aspectRatio属性实现缩放能力,代码可参考 一多开发实例(长视频)。 |
| 5 | 福利专区 | Row组件 的justifyContent属性设置为FlexAlign.SpaceBetween实现均分能力,代码可参考 一多开发实例(长视频) |
| 6 | 甄选推荐 | 响应式布局的栅格布局,设置aspectRatio属性实现缩放能力,代码可参考 一多开发实例(长视频) |
| 7 | 限时秒杀 | 响应式布局的栅格布局,设置aspectRatio属性实现缩放能力,同甄选推荐。 |
商品分类页
商品分类页主要用于快速查找目标商品。观察商品分类页在不同设备上的UX设计图,可以进行如下设计:
- 将商品分类页划分为4个区域,效果图如下:
| sm | md | lg | |
|---|---|---|---|
| 效果图 | ![]() |
![]() |
![]() |
商品分类页的4个基础区域介绍及实现方案如下表所示:
| 区域编号 | 简介 | 实现方案 |
|---|---|---|
| 1 | 顶部搜索框 | 在sm断点下占满行宽,在md、lg断点下设置justifyContent属性为End。 |
| 2 | 侧边导航 | Navigation组件 实现,设置mode属性为Split分栏显示,使用navBarWidthRange约束不同断点下的固定导航栏宽度。 |
| 3 | 广告卡片 | Swiper组件设置display在不同断点下为1、2、3,在md断点下设置nextMargin露出后边距,实现自适应布局的占比能力,代码可参考 一多开发实例(长视频)。 |
| 4 | 商品小图 | 使用 List组件+ 栅格布局 实现每行显示固定个数的商品图,代码可参考 一多开发实例(长视频) |
- 侧边导航
使用Navigation组件实现分栏显示,设置mode为NavigationMode.Split双栏显示,同时设置不同断点下导航栏的最小和最大宽度一致,约束固定的导航栏宽度。
// features/home/src/main/ets/view/ClassifyContent.ets
Navigation(this.pageInfo) {
...
}
// 设置Navigation组件双栏显示
.mode(NavigationMode.Split)
// 初始化导航栏宽度
.navBarWidth(new BreakpointType($r('app.float.classify_navigation_bar_width_sm'),
$r('app.float.classify_navigation_bar_width_md'), $r('app.float.classify_navigation_bar_width_lg'))
.getValue(this.currentBreakpoint))
// 设置不同断点下导航栏的最小宽度与最大宽度一致
.navBarWidthRange([new BreakpointType($r('app.float.classify_navigation_bar_width_sm'),
$r('app.float.classify_navigation_bar_width_md'), $r('app.float.classify_navigation_bar_width_lg'))
.getValue(this.currentBreakpoint), new BreakpointType($r('app.float.classify_navigation_bar_width_sm'),
$r('app.float.classify_navigation_bar_width_md'), $r('app.float.classify_navigation_bar_width_lg'))
.getValue(this.currentBreakpoint)])
购物袋页
购物袋页通常用于快速查看并支付待购买的商品,在大屏上采用右侧露出辅助信息确保页面的使用效率。观察购物袋页在不同设备上的UX设计图,可以进行如下设计:
- 将购物袋页划分为4个区域,效果图如下:
| sm | md | lg | |
|---|---|---|---|
| 效果图 | ![]() |
![]() |
![]() |
购物袋页的4个基础区域介绍及实现方案如下表所示:
| 区域编号 | 简介 | 实现方案 |
|---|---|---|
| 1 | 顶部标题栏 | 剩余空间全部分配给中间空白区,用 Blank组件 实现自适应布局拉伸能力,同 首页顶部页签及搜索框 |
| 2 | 购物袋商品 | List组件 实现。 |
| 3 | 结算工具栏 | 剩余空间全部分配给中间空白区,用Blank组件实现自适应布局拉伸能力,同顶部标题栏。 |
| 4 | 优惠明细 | 购物袋主区域与优惠明细辅助区域在Row组件中呈左右布局,sm和md断点下只显示购物袋主区域、隐藏优惠明细区域,lg断点下全部显示,代码可参考 一多开发实例(长视频) |
商品详情页
商品详情页展示商品大图及详细信息。观察商品详情页在不同设备上的UX设计图,可以进行如下设计:
- 将商品详情页划分为4个区域,效果图如下:
| sm | md | lg | |
|---|---|---|---|
| 效果图 | ![]() |
![]() |
![]() |
商品详情页的4个基础区域介绍及实现方案如下表所示:
商品详情页的4个基础区域介绍及实现方案如下表所示:
| 区域编号 | 简介 | 实现方案 |
|---|---|---|
| 1 | 商品大图 | Swiper组件,指定displayCount属性实现延伸能力,设置aspectRatio属性实现缩放能力,代码可参考 一多开发实例(长视频) |
| 2 | 商品详细信息 | 商品大图区域与商品详细信息区域在sm和md断点下使用Column组件呈上下布局,在lg断点下使用Row组件呈左右布局,同 商品详情侧边面板页 |
| 3 | 购买工具栏 | 剩余空间按比例分配给加入购物袋与购买按钮,用layoutWeight属性实现自适应布局占比能力,同 首页顶部页签及搜索框。 |
| 4 | 画中画 | 使用 PiPWindow 实现画中画功能,启动、停止小窗直播及控制视频播放。 |
商品详情页在大屏设备上提供分屏功能,满足同时查看两个商品的详细参数进行购物比价的诉求。分屏通过创建一个新的UIAbility,并设置窗口显示为分屏模式实现。分屏后左右屏幕的宽度为1:1,在折叠屏上的效果图如下:

创建新的UIAbility,需要在phone目录下创建SecondAbility.ets,注册与EntryAbility相同的UIAbility生命周期回调。下一步需要在phone目录的module.json5配置文件,修改abilities属性注册SecondAbility,详情可参考源码。启动分屏时,调用UIAbilityContext的StartAbility接口,设置窗口模式为分屏并启动SecondAbility。关闭分屏时,调用UIAbilityContext的terminateSelf接口。
// features/detail/src/main/ets/views/ProductDetail.ets
Image(this.isSplitMode ? $r("app.media.icon_split") : $r('app.media.ic_mate_pad_2'))
...
.onClick(() => {
if (deviceInfo.deviceType === CommonConstants.DEVICE_TYPES[0]) {
return;
}
if (!this.isSplitMode) {
// 设置启动SecondAbility
let want: Want = {
bundleName: 'com.huawei.multishoppingpricecomparison',
abilityName: 'SecondAbility'
};
// 设置分屏的窗口启动模式
let option: StartOptions = { windowMode: AbilityConstant.WindowMode.WINDOW_MODE_SPLIT_PRIMARY };
// 启动分屏
(getContext(this) as common.UIAbilityContext).startAbility(want, option);
} else {
// 关闭分屏
(getContext(this) as common.UIAbilityContext).terminateSelf();
}
})
另外,为了增强在大设备上的浏览效率,用户点击全部评论,页面三分栏展示右侧的全部评价页面,使用SideBarContainer组件实现。
效果图如下:

// features/detail/src/main/ets/views/ProductHome.ets
SideBarContainer() {
// 右侧全部评论
Column() {
Image($r('app.media.icon_close_4'))
AllComments()
}
.alignItems(HorizontalAlign.End)
.height(CommonConstants.FULL_PERCENT)
.padding({
top: deviceInfo.deviceType === CommonConstants.DEVICE_TYPES[0] ? 0 : this.topRectHeight,
left: $r('app.float.three_column_page_padding'),
right: $r('app.float.three_column_page_padding')
})
// 左侧商品详情
Row() {
...
}
}
// 控制全部评论区是否显示
.showSideBar(this.isShowingSidebar)
.showControlButton(false)
.sideBarPosition(SideBarPosition.End)
.divider({
strokeWidth: $r('app.float.sidebar_divider_width'),
color: ResourceUtil.getCommonDividerColor()
})
// 固定右侧全部评论区宽度
.minSideBarWidth(px2vp(this.windowWidth) / CommonConstants.THREE)
.maxSideBarWidth(px2vp(this.windowWidth) / CommonConstants.THREE)
// 设置全部评论区是否跟随窗口宽度自动隐藏
.autoHide(false)
- 为了方便用户浏览其他页面时能够继续观看直播内容,购物直播设计了额外的画中画功能。点击直播间页的关闭按钮,返回上一页并以小窗模式呈现直播内容。画中画功能的实现分为以下步骤:
使用@ohos.PiPWindow模块的create接口创建画中画控制器,使用startPiP接口启动画中画,启动后返回上一页。其中画中画播放的视频内容需要使用XComponent+AVPlayer组件实现,读者可以自行查看源码。
// commons/base/src/main/ets/utils/PipWindowUtil.ets
async startPip(navId: string, mXComponentController: XComponentController, context: Context): Promise<void> {
if (!PiPWindow.isPiPEnabled()) {
Logger.error(`picture in picture disabled for current OS`);
return;
}
let config: PiPWindow.PiPConfiguration = {
context: context,
// 绑定XComponent直播播放组件
componentController: mXComponentController,
// 当前页面的导航ID
navigationId: navId,
// 画中画直播媒体类型
templateType: PiPWindow.PiPTemplateType.VIDEO_LIVE
};
// 创建画中画控制器
let promise : Promise<PiPWindow.PiPController> = PiPWindow.create(config);
await promise.then((controller: PiPWindow.PiPController) => {
this.pipController = controller;
// 初始化画中画控制器
this.initPipController();
// 通过startPip接口开启画中画功能
this.pipController.startPiP().then(() => {
Logger.info(`Succeeded in starting pip.`);
if (this.avPlayerUtil === undefined) {
return;
}
this.avPlayerUtil.play();
pageInfos.pop();
}).catch((err: BusinessError) => {
Logger.error(`Failed to start pip. Cause: ${err.code}, message: ${err.message}`);
});
}).catch((err: BusinessError) => {
Logger.error(`Failed to create pip controller. Cause: ${err.code}, message: ${err.message}`);
});
}
初始化画中画控制器时,分别注册画中画生命周期状态和直播控制事件的监听。
// commons/base/src/main/ets/utils/PipWindowUtil.ets
initPipController(): void {
if (!this.pipController) {
return;
}
// 注册画中画生命周期状态监听
this.pipController.on('stateChange', (state: PiPWindow.PiPState, reason: string) => {
this.onStateChange(state, reason);
});
// 注册直播控制事件监听
this.pipController.on('controlPanelActionEvent', (event: PiPWindow.PiPActionEventType) => {
this.onActionEvent(event);
});
}
使用stopPiP接口关闭画中画。
// commons/base/src/main/ets/utils/PipWindowUtil.ets
// 通过调用stopPip来关闭画中画
async stopPip(): Promise<void> {
if (this.pipController) {
let promise: Promise<void> = this.pipController.stopPiP();
promise.then(() => {
this.isShowingPip = false;
Logger.info(`Succeeded in stopping pip.`);
try {
this.pipController?.off('stateChange');
this.pipController?.off('controlPanelActionEvent');
} catch (exception) {
Logger.error('Failed to unregister callbacks. Code: ' + JSON.stringify(exception));
}
}).catch((err: BusinessError) => {
Logger.error(`Failed to stop pip. Cause: ${err.code}, message: ${err.message}`);
});
}
}
商品详情侧边面板页
在查看商品详情时,经常会有咨询客服或查看购物车的诉求,可采用侧边面板显示客服对话等辅助信息,提升浏览效率,实现边看商品边聊天咨询等体验。
- 侧边面板咨询客服,效果图如下:
| sm | md | lg | |
|---|---|---|---|
| 设计能力点 | ![]() |
![]() |
![]() |
| 侧边面板-咨询客服 | ![]() |
![]() |
![]() |
| 侧边面板-购物袋 | ![]() |
![]() |
![]() |
-
观察商品详情侧边面板的设计,在sm断点下只显示侧边辅助面板,在md和lg断点下使用Row组件呈左右布局,设置layoutWeight属性实现自适应布局的占比能力。在md断点时商品详情与侧边面板宽度为1:1,在lg断点时为5:3。
-
观察商品详情侧边面板的设计,在sm断点下只显示侧边辅助面板,在md和lg断点下使用Row组件呈左右布局,设置layoutWeight属性实现自适应布局的占比能力。在md断点时商品详情与侧边面板宽度为1:1,在lg断点时为5:3。
// features/detail/src/main/ets/views/ProductMoreDetail.ets
Row() {
Column() {
...
}
// 设置商品详情与侧边面板宽度比
.layoutWeight(new BreakpointType(0, CommonConstants.THREE, CommonConstants.FIVE)
.getValue(this.currentBreakpoint))
// 在sm断点下隐藏商品详情页
.visibility(this.currentBreakpoint === BreakpointConstants.BREAKPOINT_SM ? Visibility.None : Visibility.Visible)
Column() {
// 判断侧边面板的辅助信息页面
if (this.isShoppingBag) {
DetailShoppingBagView({ isMoreDetail: this.isMoreDetail })
}
if (this.isCustomerService) {
CustomerServiceView()
}
}
// 设置商品详情与侧边面板宽度比
.layoutWeight(3)
}
商品支付页
商品支付页采用浅层窗口展示商品支付信息。观察商品支付页在不同设备上的UX设计图,效果图如下:
| sm | md | lg | |
|---|---|---|---|
| 设计能力点 | ![]() |
![]() |
![]() |
| 效果图 | ![]() |
![]() |
![]() |
商品支付页的浅层窗口,在sm断点下使用bindSheet为购买按钮绑定底部半模态页面,在md和lg断点下使用居中半模态自定义弹窗居中显示。
// features/detail/src/main/ets/view/ProdutUtilView.ets
Button(DetailConstants.BUTTON_NAMES[1])
// sm断点下绑定底部半模态页面
.bindSheet($$this.isDialogOpen,
this.PayCardBuilder(), {
height: $r('app.float.pay_bind_sheet_height'),
preferType: SheetType.CENTER,
dragBar: false,
enableOutsideInteractive: true,
onDisappear: () => { this.isDialogOpen = false },
showClose: false,
backgroundColor: $r('app.color.pay_bind_sheet_background')
})
.onClick(() => {
if (this.isLivePage) {
return;
}
if (this.currentBreakpoint === BreakpointConstants.BREAKPOINT_SM) {
// sm断点下打开半模态页面
this.isDialogOpen = true;
} else {
if (this.dialogController === null) {
return;
}
// md和lg断点下弹出自定义弹窗
this.dialogController.open();
this.isDialogOpen = false;
}
})
半模态页面使用@Builder注解构建,绑定到bindSheet事件。
// features/detail/src/main/ets/view/ProdutUtilView.ets
// 构建底部半模态页面
@Builder
PayCardBuilder() {
Column() {
PayCard({...})
}
}
自定义弹窗使用@CustomerDialog注解构建,绑定到自定义弹窗控制器。
// features/detail/src/main/ets/view/ProdutUtilView.ets
// 绑定到自定义弹窗控制器
private dialogController: CustomDialogController | null = new CustomDialogController({
builder: PayCardDialog(),
customStyle: true
});
// 构建自定义弹窗页面
@CustomDialog
struct PayCardDialog {
...
build() {
Column() {
PayCard({...})
}
}
}
直播间页
直播画面和推荐的商品信息,在多端基于设备屏幕尺寸进行响应式适配。观察直播间页在不同设备上的UX设计图,可以进行如下设计:
| sm | md | lg | |
|---|---|---|---|
| 设计能力点 | ![]() |
![]() |
![]() |
| 效果图 | ![]() |
![]() |
![]() |
直播间页的3个基础区域介绍及实现方案如下表所示:
| 区域编号 | 简介 | 实现方案 |
|---|---|---|
| 1 | 直播内容 | Stack组件 控制子组件的显示层级,在sm断点下aspectRatio属性控制直播图片等比放大实现自适应能力的缩放能力,在md和lg断点下固定大小,同 商品详情页商品大图。 |
| 2 | 直播弹幕及推荐商品 | 使用 Flex组件 + List组件,在sm和md断点下呈上下结构,显示在下方,在lg断点下呈左右结构,显示在两侧并尾部对齐。 |
| 3 | 发表弹幕 | TextInput组件 设置layoutWeight实现自适应布局拉伸能力,同 首页顶部页签及搜索框 |
- 直播弹幕及推荐商品
Flex组件的direction和justifyContent属性控制子组件在容器主轴上的位置,sm和md断点下在容器底部,lg断点下在容器两侧。List组件控制列表的排列方向,sm和md断点下水平,lg断点下垂直。
// features/detail/src/main/ets/view/LiveMaskLayer.ets
Flex({
// 设置子组件在Flex容器的主轴方向,sm和md断点下垂直,lg断点下水平
direction: this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ? FlexDirection.Row : FlexDirection.Column,
// 设置主轴的对齐格式,sm和md断点下均分,lg断点下尾部对齐
justifyContent: this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ? FlexAlign.SpaceBetween : FlexAlign.End
}) {
Comment({ currentBreakpoint: this.currentBreakpoint })
LiveShopList({
currentBreakpoint: this.currentBreakpoint,
detailType: this.detailType,
isMoreDetail: this.isMoreDetail
})
}
// features/detail/src/main/ets/view/LiveShopList.ets
List({...}) {
...
}
// 设置List组件的排列方向,sm和md断点下水平,lg断点下垂直
.listDirection(this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ? Axis.Vertical :
Axis.Horizontal)
直播侧边面板页
在看直播时,经常需要一边听商品讲解一边浏览商品信息,可利用侧边辅助面板查看商品详情、口袋宝贝或支付页面。直播侧边面板页在不同设备上的UX设计图如下:
| sm | md | lg | |
|---|---|---|---|
| 侧边面板-商品详情页 | ![]() |
![]() |
|
| 侧边面板-口袋宝贝页 | ![]() |
![]() |
![]() |
| 侧边面板-支付页 | ![]() |
![]() |
![]() |
- 侧边面板-商品详情页,在sm断点下不显示,在md和lg断点下使用Row组件呈左右布局,设置layoutWeight属性实现自适应布局的占比能力,同 商品详情侧边面板页。在md断点时商品详情与侧边面板宽度为1:1,在lg断点时为5:3。
- 观察直播侧边面板-口袋宝贝页和支付页的设计,在sm断点下使用bindSheet为组件绑定半模态页面,同 商品支付页,在md和lg断点下使用Row组件呈左右布局,设置layoutWeight属性实现自适应布局的占比能力,同 商品详情侧边面板页。
更多推荐













































所有评论(0)