第 16 章 一次开发多端部署

随着终端设备形态愈发多样,分布式技术打破单一硬件局限,应用和服务可在不同设备间自由调用、共享,为开发者带来庞大潜在用户群。但应用要在多设备上提供相同内容,需适配不同屏幕和硬件,开发成本高。HarmonyOS 系统的 "一次开发,多端部署"("一多")能力可解决此问题。其指一套代码工程一次开发上架,多端按需部署,旨在帮开发者快速开发兼容多设备形态的应用,提供跨设备分布式体验,不过要先解决页面适配和功能兼容问题。

16.1 一次开发,多端部署概述

方舟开发框架有类Web和声明式两种开发范式,前者适合简单中小应用,后者适合大型应用且更推荐。应用程序包的Module分 "Ability" 和 "Library",HAP分Entry和Feature。"一多" 有A、B两种部署模型,相同泛类设备优先选 A,不同泛类优先选B,实际常混合用且都需一次编译。"一多" 推荐 "三层工程结构",开发应考虑代码复用,最终应用以APP包形式发布。

16.1.1 定义和目标

  1. 定义

依托一套代码工程,完成一次开发并成功上架,随后依据不同终端的需求灵活进行多端部署。

  1. 目标

全力协助开发者以快速且高效的方式,开发出能够适配多种终端设备形态的应用程序。在实现对各类不同设备完美兼容的基础之上,为用户提供流畅无阻的跨设备流转、便捷的迁移以及高效协同的分布式优质体验。

为达成 "一次开发,多端部署(一多)" 的目标,必须攻克两个关键的基础问题:其一,鉴于不同设备在屏幕尺寸、色彩风格等方面存在明显差异,如何实现页面的精准适配;其二,由于不同设备的系统能力参差不齐,例如智能穿戴设备是否具备定位功能、智慧屏是否配备摄像头等情况,怎样保障功能在不同设备上都能有效兼容。

16.1.2 一多的挑战和关键问题解决思路

"一多"(一次开发,多端部署)面临着诸多挑战。设备方面,屏幕尺寸、色彩风格等差异使页面适配困难,系统能力如计算、存储、传感器配置不同导致功能兼容不易,且在开发成本与效率平衡上也存在难题。

解决这些关键问题,可采用响应式设计,依据设备参数自动调整页面布局,利用方舟开发框架灵活绘制 UI 以适配页面;开发时检测设备系统能力,依此启用或禁用功能,封装通用模块来保障功能兼容;借助 "三层工程结构",即common、features、products层,实现代码复用,减少重复开发,提高效率、降低成本,从而应对 "一多" 所面临的挑战。

16.2 界面级一多开发

方舟开发框架大力推荐开发者采用声明式开发范式来进行应用开发,因此,本章所涵盖的内容以及给出的示例,均主要围绕声明式开发范式展开。其中,布局能力在页面设计与开发中起着至关重要的作用,它决定了页面中各个元素的排布方式和显示效果,是开发者在着手开发时首先需要重点考量的关键因素,通常可依据页面(或自定义组件)内部的组件结构,包括组件的数量、组件之间的父子或兄弟关系、组件的具体类型以及组件的相对位置等信息,来精准判断并选择合适的布局能力,如在组件结构不随尺寸变化的场景可灵活运用自适应布局能力实现预期效果,对于组件结构会随着尺寸变化而有所不同的场景则更适合采用响应式布局能力来实现不同尺寸下的差异化显示效果,此外还包括交互归一、多态组件、资源使用这些内容。并且温馨提示,在开发适用于多设备的同一页面时,强烈建议开发者充分利用自定义组件,这不仅能够显著提升代码的可读性,使代码结构更加清晰易懂,便于后续的维护和修改,还能够最大程度地实现代码的复用,有效减少重复代码的编写,提高开发效率,降低开发成本。

16.2.1 布局能力

布局分为自适应布局和响应式布局。自适应布局指外部容器大小变化时,元素依相对关系(如占比等)自动变化,有拉伸等7种能力,可使界面连续变化,常用于页面区域内布局差异,常借助Row、Column、Flex组件实现。

响应式布局指元素根据断点、栅格等自动变化,有断点等3种能力,使界面不连续变化,常用于区域间布局差异,常与GridRow、Grid等组件搭配。

其中,GridRow 借助断点和栅格布局;Grid用单元格布局,配合断点改属性实现不同效果;List、Swiper、Tabs也都需配合断点,改变对应属性实现不同布局。

  1. sm
  1. md
  1. lg

16.2.2 交互归一

在多样化的智能设备使用场景中,用户的交互方式因设备而异,涵盖触摸屏、鼠标、触控板等多种操作方式。倘若针对每种交互方式分别进行适配开发,不仅会大幅增加开发工作量,还会导致大量重复代码的产生。为有效解决这一问题,我们对各种交互方式的API进行了统一整合,实现了交互归一,显著提升了开发效率并优化了代码结构。

  1. 基础输入

基础输入对应的开发接口,以及当前在各设备上的支持情况整理如下表:

输入 开发接口 触控屏 触控板 鼠标
悬浮 onHover NA
点击 onClick
双击 TapGesture
长按 LongPressGesture ×
上下文菜单 ContentMenu
拖拽 Drag
轻扫 SwipeGesture
滚动及平移 PanGesture
缩放 PinchGesture
旋转 RotationGesture NA

特别说明:

点击事件(onClick)本质上是点击手势(TapGesture)的一种特殊情况,即单指单次点击。由于这一场景在实际开发中应用极为广泛,为了契合开发者的使用习惯并提升开发便捷性,我们专门提供了独立的开发接口。此外,目前触控板对长按输入功能的支持尚在开发过程中。

  1. 拖拽事件

在应用开发实践中,拖拽是一个常见的交互场景。它通常发生在两个组件之间,并非简单的单次输入行为,而是一个连贯的过程。以将组件 A 拖拽到组件 B 中为例,一般包含以下步骤:

  • 长按或点击组件 A,从而触发拖拽操作。
  • 持续保持按压或点击状态,将组件 A 逐步向组件 B 的方向进行拖拽。
  • 当组件 A 到达组件 B 的范围内,释放按压或点击操作,完成整个拖拽过程。
  • 当然,也可以在组件 A 尚未到达组件 B 之前的任意时刻,释放按压或点击,以此取消本次拖拽操作。

一个完整的拖拽事件由多个拖拽子事件构成,具体情况如下表所示(如需了解详细用法,请访问拖拽事件相关内容)。目前,触控屏和鼠标的拖拽事件已经成功实现了" 交互归一",而对手写笔的支持功能还在紧锣密鼓的开发当中。

名称 功能描述
onDragStart 绑定 A 组件,在触控屏上长按或鼠标左键按下并移动时触发
onDragEnter 绑定 B 组件,当触控屏上的手指或鼠标移动进入 B 组件的瞬间触发
onDragMove 绑定 B 组件,在触控屏上手指或鼠标在 B 组件内部移动时触发
onDragLeave 绑定 B 组件,当触控屏上的手指或鼠标移动退出 B 组件的瞬间触发
onDrop 绑定 B 组件,在 B 组件内部,当触控屏上手指抬起或鼠标左键松开时触发

16.3 功能级一多开发

在应用开发过程中,至少涵盖两部分核心工作内容:UI 页面开发以及底层功能开发。对于部分依赖网络的应用而言,还会涉及到服务端开发的工作。此前的章节已详细阐述了页面适配问题的解决方法,而在本章节中,我们将重点聚焦于应用如何有效处理设备系统能力差异的兼容问题。不同设备的系统能力存在差异,这可能会影响应用的正常运行。例如,某些设备的系统版本较低,可能不支持应用的某些新功能;或者不同品牌设备的系统在底层实现上有所不同,导致应用在功能调用时出现异常。因此,解决设备系统能力差异的兼容问题,对于保障应用在各类设备上的稳定运行和良好体验至关重要。

16.3.1 SysCap 机制:能力集

系统能力(SystemCapability,简称为 SysCap),是指操作系统中各个相对独立的特性,诸如蓝牙、WIFI、NFC、摄像头等,均属于系统能力的范畴。每个系统能力都对应着多个API,这些API会随着目标设备对该系统能力的支持与否,同步存在或消失。

与系统能力紧密相关的,有支持能力集、联想能力集和要求能力集这三个核心概念:

  1. 支持能力集:即设备所具备的系统能力集合,需在设备配置文件中进行配置。
  2. 要求能力集:是应用运行所需要的系统能力集合,在应用配置文件中完成配置。
  3. 联想能力集:指在开发应用时,DevEco Studio能够联想的API所在的系统能力集合,同样在应用配置文件中配置。

特别说明:只有当应用的要求能力集是设备支持能力集的子集时,该应用才能够在对应设备上进行分发、安装与运行。若想了解全量的系统能力,可访问系统能力列表。

16.3.2 SysCap 机制:CanIUse 接口

在开发多设备应用时,工程内存在一些默认设置:默认的要求能力集是多个设备支持能力集的交集,而默认的联想能力集则是多个设备支持能力集的并集。同时,开发者具备在运行时动态判断某设备是否支持特定系统能力的能力,并且能够自行对联想能力集和要求能力集进行修改。

  1. 动态逻辑判断

若某个系统能力未被写入应用的要求能力集,那么在使用该能力前,必须先判断设备是否支持。这里有两种判断方法:

  • canIUse 接口法:开发者可借助 canIUse 接口判断设备对特定 syscap 的支持情况。例如:
ts 复制代码
if (canIUse("SystemCapability.Communication.NFC.Core")) {
   console.log("该设备支持SystemCapability.Communication.NFC.Core");
} else {
   console.log("该设备不支持SystemCapability.Communication.NFC.Core");
}
  • 模块导入法:开发者通过 import 方式导入模块,若设备不支持该模块,import 结果为 undefined。使用其 API 时需先判断是否存在,示例如下:
ts 复制代码
import { nfcController } from '@kit.ConnectivityKit';
try {
    nfcController.enableNfc();
    console.log("nfcController enableNfc success");
} catch (busiError) {
    console.log("nfcController enableNfc busiError: " + busiError);
}

特别说明:

若某系统能力是应用运行的必需条件,务必将其写入应用的要求能力集,以防止应用分发和安装到不满足要求的设备上;若某系统能力并非应用运行所必需,则可在运行时进行动态判断,这样能最大程度拓宽应用的适用设备范围。

16.4 工程级一多开发

本章主要介绍如何使用DevEco Studio进行多设备应用开发。

16.4.1 三层架构规范

通常我们使用DevEco Studio直接创建的工程目录是这样的,只包含一个的entry类型的模块。

如果直接使用如下所示的平级目录进行模块管理,工程逻辑结构较混乱且模块间的依赖关系不够清晰,不利于开发及后期维护。

arduino 复制代码
/monorepo
├── common
├── feature1
├── feature2
├── featureN
├── wearable
├── default
└── productN

更推荐使用的common、features、product三层工程结构。也就是"一多"推荐的"三层工程结构"。工程结构示例如下所示:

bash 复制代码
/monorepo
 ├── common                  # 公共特性目录
 │
 ├── features                # 功能模块目录
 │   ├── feature1            # 子功能
 │   ├── feature2            # 子功能2
 │   └── ...                 # 子功能n
 │
 └── product                 # 产品层目录
     ├── wearable            # 智能穿戴泛类目录
     ├── default             # 默认设备泛类目录
     └── ...

16.4.2 新建Module

新建三个ohpm模块,分别命名为common、feature1、feature2。新建一个entry类型的模块,假设命名为"wearable"(仅仅为了说明某一类产品)。

  1. 右键点击monorepo目录,选择模块

选择Static Library并点击Next,模块相关知识点可翻阅"开发ohpm"章节。注意,创建wearable时需要选择Empty Ability。

输入对应的模块名

  1. 创建完成后的目录结构

16.4.3 修改Module配置

  1. 修改Module名称

修改创建工程时默认的entry模块名称。在该模块上点击鼠标右键,依次选择"重构 -> 重命名",将名称修改为default。

  1. 修改Module类型及其设备类型

将default模块的deviceTypes配置为["phone", "tablet"],同时将其type字段配置为entry。 即default模块编译出的HAP在手机和平板上安装和运行

将wearable模块的deviceTypes配置为["wearable"],同时将其type字段配置为entry。

即wearable模块编译出的HAP仅在智能穿戴设备上安装和运行

16.4.4 调整目录结构

在工程根目录(MyApplication)上点击鼠标右键,依次选择"新建 -> 目录"新建子目录。创建product和features两个子目录。

用鼠标左键将default目录拖拽到新建的product目录中,在DevEco Studio弹出的确认窗口中,点击"重构r"即可。

按照同样的步骤,将wearable目录放到product目录中,将feature1和feature2放到features目录中。

16.4.5 修改依赖关系

我们推荐在common目录中存放基础公共代码,features目录中存放相对独立的功能模块代码,product目录中存放完全独立的产品代码。这样在product目录中依赖features和common中的公共代码来实现功能,可以最大程度实现代码复用。

配置依赖关系可以通过修改模块中的oh-package.json5文件。通过修改default模块中的oh-package.json5文件,使其可以使用common、feature1和feature2模块中的代码。

  1. 修改common以及feature模块的包名
  1. default模块中使用common以及feature

修改oh-package.json5文件后,请点击右上角的"Sync Now",否则改动不会生效。

16.4.6 引用ohpm包中的代码

在开发ohpm包中,仅介绍了如何使用ohpm包中的页面和资源,本小节以例子的形式补充介绍如何使用ohpm包中的类和函数。

  • 在common模块中新增ComplexNumber类,用于表征复数(数学概念,由实部和虚部组成),该类包含toString()方法,将复数转换为字符形式。
  • 在common模块中新增Add函数,用于计算并返回两个数字的和。
  • 在default模块中,使用common模块新增的ComplexNumber类和Add函数。
  • 在"common/src/main/ets"目录中,按照需要新增文件和自定义类和函数。
  • 在common模块中新增ComplexNumber类,用于表征复数(数学概念,由实部和虚部组成),该类包含toString()方法,将复数转换为字符形式。
  • 在common模块中新增Add函数,用于计算并返回两个数字的和。
  • 在default模块中,使用common模块新增的ComplexNumber类和Add函数。
  1. 在"common/src/main/ets"目录中,按照需要新增文件和自定义类和函数。
ts 复制代码
export class ComplexNumber {
  private real?: number
  private imaginary?: number

  constructor(real: number, imaginary: number) {
    this.real = real
    this.imaginary = imaginary
  }

  toString() {
    return '' + this.real + '+' + this.imaginary + 'i'
  }
}

export function Add (numA: number, numB: number) {
  return numA + numB
}
  1. 在"common/index.ets"文件中导出

申明需要export的类、函数的名称及在当前模块中的位置,否则其它模块无法使用。

  1. 实际使用

在default模块中import和使用这些类和函数。注意提前在default模块的oh-package.json5文件中配置对common模块的依赖关系。

ts 复制代码
import { ComplexNumber, Add } from '@raink/common'

@Entry
@Component
struct Index{
  complexNumber: string = new ComplexNumber(2,3).toString()

  build() {
    Row() {
      Column() {
        Text('复数:'+ this.complexNumber)
          .fontSize(30)
        Text('3 + 5 = '+ Add(3, 5))
          .fontSize(30)
      }
      .width('100%')
    }
    .height('100%')
  }
}
  1. 效果呈现

16.5 案例实战

当前案例截取自《润客点餐》app中的一多开发的具体实现。并根据手机、折叠屏、平板以及2in1不同的设备尺寸实现对应页面。

16.5.1 案例效果截图

  1. 手机运行效果图
  1. 折叠屏运行效果图
  1. 平板、2in1运行效果图

16.5.2 案例运用到的知识点

  • 一次开发,多端部署:一套代码工程,一次开发上架,多端按需部署。支撑开发者快速高效的开发支持多种终端设备形态的应用,实现对不同设备兼容的同时,提供跨设备的流转、迁移和协同的分布式体验。
  • 自适应布局:当外部容器大小发生变化时,元素可以根据相对关系自动变化以适应外部容器变化的布局能力。相对关系如占比、固定宽高比、显示优先级等。
  • 响应式布局:当外部容器大小发生变化时,元素可以根据断点、栅格或特定的特征(如屏幕方向、窗口宽高等)自动变化以适应外部容器变化的布局能力。
  • GridRow:栅格容器组件,仅可以和栅格子组件(GridCol)在栅格布局场景中使用。
  • GridCol:栅格子组件,必须作为栅格容器组件(GridRow)的子组件使用。
  • V2版状态管理:@ComponentV2/@Local/
  • Stage模型
  • 自定义组件和组件生命周期
  • @Builder装饰器:自定义构建函数
  • @Extend装饰器:定义扩展组件样式
  • if/else:条件渲染
  • 日志管理类的编写
  • MVVM模式

16.5.3 代码结构解读

less 复制代码
├──entry/src/main/ets                         // 代码区
│  ├──components
│  ├──model
│  │  └──core.ts                            	// 项目核心逻辑实现区
│  ├──entryability
│  │  └──EntryAbility.ets
│  ├──model
│  │  └──core.ts                            	// 项目核心逻辑实现区
│  └──pages
│  │   └──home.ets                            // 首页
│  │   └──...     
│  └──utils
│  │   └──BreakpointType.ets                  // 获取当前断点适合的长度
│  │   └──...     
│  └──utils
│     └──BreakpointType.ets                   // 获取当前断点适合的长度
│     └──...     
└──entry/src/main/resources                   // 应用资源目录

16.5.4 首页代码实现

  1. 导入模块
ts 复制代码
import { FoodListHeader } from '../components/FoodListHeader';
import { AppStorageV2, window } from '@kit.ArkUI';
import { BreakpointConstants } from '../constants/BreakpointConstant';
import { BreakpointType } from '../utils/BreakpointType';
import { FoodItem } from '../components/FoodItem';
import { OrderDishes } from '../views/orderDishes/orderDishs';
import { TabBar } from '../components/TabBar';
import { BreakpointModel, WindowPositionModel } from '../models/core';
  1. 定义组件状态
  • @Local 装饰器定义了本地状态,如windowPositionModel和breakpointModel,用于存储窗口位置和断点信息。
  • @Provider装饰器用于提供数据给子组件,如pageInfo和ifShowSides。
  • chooseIndex存储当前选中的索引。
  • scroller是一个滚动器实例。
ts 复制代码
@Local windowPositionModel:WindowPositionModel = 
  AppStorageV2.connect(WindowPositionModel, () => new WindowPositionModel(0,0))!
@Local breakpointModel:BreakpointModel = 
  AppStorageV2.connect(BreakpointModel, () => 
    new BreakpointModel(BreakpointConstants.BREAKPOINT_LG))!
@Provider('pageInfo') pageInfo: NavPathStack = new NavPathStack();
@Local chooseIndex: number = 0
private scroller: Scroller = new Scroller();
@Provider('ifShowSides') ifShowSides: boolean = false;
  1. 监视器方法

@Monitor 装饰器标记的方法会在changeBreakpoint事件触发时执行,当断点为BREAKPOINT_SM时,隐藏侧边栏。

ts 复制代码
@Monitor('changeBreakpoint')
changeBreakpoint() {
  if (
    this.breakpointModel.currentBreakpoint === BreakpointConstants.BREAKPOINT_SM
  ) {
    this.ifShowSides = false;
  }
}
  1. 页面映射构造器
ts 复制代码
@Builder
PageMap() {
  OrderDishes();
}
  1. Navigation组件
  • FoodListHeader组件:显示食物列表的头部,通过BreakpointType类根据当前的断点(如 sm、md、lg)动态设置左右内边距,实现不同设备下的布局适配。
  • Scroll组件:用于实现滚动效果,内部包含一个GridRow组件,用于展示食物列表。滚动条宽度设置为0以隐藏滚动条,layoutWeight(1) 表示该组件会填充剩余的空间。底部内边距根据 windowPositionModel.windowBottom设置,以确保安全距离。
  • Navigation 组件属性:
  • height('100%'):设置导航栏高度为100%。
  • hideTitleBar(true):隐藏标题栏。
  • navBarWidthRange:根据当前断点设置导航栏的宽度范围。
  • mode:根据是否显示侧边栏和当前断点设置导航模式,支持分屏模式(NavigationMode.Split)和堆叠模式(NavigationMode.Stack)。
  • navDestination:设置导航的目标页面,这里使用this.PageMap方法返回的组件。
ts 复制代码
Navigation(this.pageInfo) {
  Column() {
    FoodListHeader()
      .padding({
        left: 
          new BreakpointType($r('app.float.padding_small'), 
                             $r('app.float.padding_food_md'),
          $r('app.float.common_padding'))
          .getValue(this.breakpointModel.currentBreakpoint),
        right: new BreakpointType($r('app.float.padding_small'),
                                  $r('app.float.padding_food_md'),
          $r('app.float.common_padding'))
          .getValue(this.breakpointModel.currentBreakpoint)
      })
    Scroll(this.scroller) {
      // 滚动内容
    }
    .scrollBarWidth(0)
    .layoutWeight(1)
    .padding({
      bottom: this.windowPositionModel.windowBottom
    })
  }
  .padding({
    top: this.windowPositionModel.windowTop
  })
}
.height('100%')
.hideTitleBar(true)
.navBarWidthRange(this.breakpointModel.currentBreakpoint 
                  === BreakpointConstants.BREAKPOINT_MD ?
  ['50%', '50%'] :
  ['40%', '40%'])
.mode(this.ifShowSides && this.breakpointModel.currentBreakpoint 
      !== BreakpointConstants.BREAKPOINT_SM ?
  NavigationMode.Split : NavigationMode.Stack)
.navDestination(this.PageMap)
  1. GridRow 和 GridCol 组件
  • GridRow组件:用于实现网格布局,根据当前断点和是否显示侧边栏设置每行的列数。gutter属性设置网格之间的间距。
  • GridCol组件:每个GridCol组件包含一个FoodItem组件,用于显示食物项。点击GridCol组件时,会显示侧边栏,替换当前导航路径,并更新选中的索引。
  • 内边距设置:根据是否显示侧边栏和当前断点动态设置左右内边距。
ts 复制代码
GridRow({
  columns: this.breakpointModel.currentBreakpoint 
    === BreakpointConstants.BREAKPOINT_LG && !this.ifShowSides ?
    BreakpointConstants.GRID_ROW_COLUMNS[5]
    : 1,
  gutter: { x: BreakpointConstants.GRID_ROW_GUTTER, y: 
    BreakpointConstants.GRID_ROW_GUTTER }
}) {
  ForEach(Array(10).fill(''), (item: string, index) => {
    GridCol() {
      FoodItem({ listIndex: index, chooseIndex: this.chooseIndex })
    }
    .onClick(() => {
      this.ifShowSides = true;
      this.pageInfo.replacePath({ name: 'default' }, false);
      this.chooseIndex = index;
    })
  })
}
.padding({
  left: this.ifShowSides ? $r('app.float.padding_small') :
    new BreakpointType($r('app.float.padding_small'), 
                       $r('app.float.padding_food_md'),
      $r('app.float.common_padding'))
    .getValue(this.breakpointModel.currentBreakpoint),
  right: this.ifShowSides ? 
    new BreakpointType($r('app.float.padding_small'), 
                       $r('app.float.padding_small'),
    $r('app.float.common_padding'))
    .getValue(this.breakpointModel.currentBreakpoint) :
    new BreakpointType($r('app.float.padding_small'),
                       $r('app.float.common_padding'),
      $r('app.float.common_padding'))
    .getValue(this.breakpointModel.currentBreakpoint)
})
.layoutWeight(1)

16.5.5 断点核心代码实现

  1. BreakpointModel类
  • 可观察类:使用 @ObservedV2装饰器,表明这个类是可观察的。当类中被观察的属性发生变化时,框架会自动更新依赖这些属性的 UI 组件。
  • 可观察属性:currentBreakpoint属性使用@Trace装饰器标记为可观察属性。它存储当前的屏幕尺寸断点信息,初始值设为BreakpointConstants.BREAKPOINT_LG,代表默认是大尺寸屏幕。
  • 构造函数:接受一个字符串参数curBp,在创建BreakpointModel实例时,会用这个参数的值来初始化currentBreakpoint属性。
  1. WindowPositionModel类
  • 可观察类:同样使用@ObservedV2装饰器,意味着这个类也是可观察的。
  • 可观察属性:windowTop和windowBottom属性都使用@Trace装饰器标记为可观察属性。它们分别用于存储窗口顶部和底部的位置信息。
  • 构造函数:接受两个数字类型的参数windowTop和windowBottom,在创建WindowPositionModel实例时,会用这两个参数的值来初始化对应的属性。
ts 复制代码
/**
 * 全局流转的数据
 */
import { display } from "@kit.ArkUI";
import { BreakpointConstants } from "../constants/BreakpointConstant";


@ObservedV2
export class BreakpointModel {
  @Trace currentBreakpoint?: string = BreakpointConstants.BREAKPOINT_LG;

  constructor(curBp: string) {

    this.currentBreakpoint = curBp
  }
}

@ObservedV2
export class WindowPositionModel {
  @Trace
  windowTop?: number
  @Trace
  windowBottom?: number

  constructor(windowTop: number, windowBottom: number) {
    this.windowTop = windowTop
    this.windowBottom = windowBottom
  }
}

16.5.6 utils实现

构建一个泛型类BreakpointType,它可以存储不同断点(如小屏幕、中等屏幕、大屏幕)对应的值,并且能依据当前的断点信息返回合适的值。

ts 复制代码
import { BreakpointConstants } from '../constants/BreakpointConstant';

/**
 * 注意类型的问题
 * 所以我们这里先进行类型转换
 * 当前T 可以不传 框架会自动传入
 */
export class BreakpointType<T> {
  sm: T;
  md: T;
  lg: T;

  constructor(sm: T, md: T, lg: T) {
    this.sm = sm;
    this.md = md;
    this.lg = lg;
  }

  /**
   * 根据当前的断点选择合适长度
   * @param currentBreakpoint
   * @returns
   */
  getValue(currentBreakpoint: string): T {
    if (currentBreakpoint === BreakpointConstants.BREAKPOINT_MD) {
      return this.md;
    }
    if (currentBreakpoint === BreakpointConstants.BREAKPOINT_LG) {
      return this.lg;
    } else {
      return this.sm;
    }
  }
}

16.5.7 BreakpointConstants 常量定义

公共端点布局逻辑,后续我们的布局主要基于Grid布局 会应用上去 这里先进行定义。

ts 复制代码
/**
 * 公共端点布局逻辑
 * 后续我们的布局主要基于Grid布局 会应用上去 这里先进行定义
 */
export class BreakpointConstants {
  /**
   * 小型设备断点
   * 一些屏幕较小的鸿蒙系统的设备
   */
  static readonly BREAKPOINT_SM: string = 'sm';

  /**
   * 中型设备断点
   * 普通手机 meta60 meta70等
   */
  static readonly BREAKPOINT_MD: string = 'md';

  /**
   * 大型设备断点
   * 比如 iPad 三折叠 等
   */
  static readonly BREAKPOINT_LG: string = 'lg';

  /**
   * 网格 行列 列表。
   */
  static readonly GRID_ROW_COLUMNS: number[] = [4, 8, 12, 15, 11, 2, 5];

  /**
   * 网格列跨度列表。
   */
  static readonly GRID_COLUMN_SPANS: number[] = [2, 3, 4, 8, 12, 5, 7, 6];

  /**
   * 中等设备的宽度
   * 注意啊 这只是一个阈值 并不是 只有600的才是 中等设备
   *
   */
  static readonly MIDDLE_DEVICE_WIDTH: number = 600;

  /**
   * 大型设备宽度
   */
  static readonly LARGE_DEVICE_WIDTH: number = 840;

  /**
   * 栅格我们分为16个
   */
  static readonly GRID_ROW_GUTTER: number = 16;
}

16.5.8 FoodItem 组件实现

  1. 组件定义及状态
  • @ComponentV2:这是ArkTS中用于标记组件的装饰器,表明FoodItem是一个组件。
  • @Param:用于接收父组件传递过来的参数。
  • chooseIndex:可选参数,默认值为0,可能表示当前选中的项目索引。
  • listIndex:必传参数,默认值为0,代表当前美食项目在列表中的索引。
  • @Consumer:用于从上下文获取数据,ifShowSides可能是一个布尔值,用于判断是否显示侧边栏或者是否处于两栏显示模式。
ts 复制代码
@ComponentV2
export struct FoodItem {
  @Param chooseIndex?: number = 0
  @Param  listIndex: number = 0;
  @Consumer() ifShowSides?: boolean
  1. 样式方法
  • @Styles:这是ArkTS中定义样式方法的装饰器。
  • orangeText方法:定义了一组样式,包括背景颜色、内边距和边框圆角,这些样式值通过$r函数从资源文件中获取。
ts 复制代码
  @Styles orangeText(){
    .backgroundColor($r('app.color.orange_background'))
    .padding($r('app.float.orange_padding'))
    .borderRadius($r('app.float.orange_border'))
  }
  1. build方法

按照UI进行整体布局

ts 复制代码
  build() {
    Row() {
      Image($r('app.media.bjky'))
        .width($r('app.float.food_data_image'))
        .aspectRatio(1)
      Column() {
        Row() {
          Text('北京烤鸭')
            .fontWeight(700)
            .fontSize($r('app.float.food_data_font'))
          Blank()
          Text('订')
            .fontWeight(400)
            .fontColor($r('app.color.currency_symbol'))
            .fontSize($r('app.float.filtering_font'))
            .orangeText()
            .margin({
              right: $r('app.float.filtering_margin_right')
            })
          Text('外面')
            .fontWeight(400)
            .fontColor($r('app.color.currency_symbol'))
            .fontSize($r('app.float.filtering_font'))
            .orangeText()
        }
        .width('100%')
        .margin({
          bottom: $r('app.float.food_data_bottom')
        })

        Row() {
          // 评级组件 打星星的
          Rating({rating:4.3, indicator: true})
            .height($r('app.float.rating_height'))
            .margin({
              right: $r('app.float.rating_margin_right')
            })
          Text('4.3')
            .fontWeight(700)
            .fontColor($r('app.color.currency_symbol'))
            .fontSize($r('app.float.reviews_font'))
            .margin({
              right: $r('app.float.filtering_margin_right')
            })
          Text('¥98/人')
            .fontWeight(400)
            .fontSize($r('app.float.delivery_font'))
            .textAlign(TextAlign.Center)
        }
        .alignItems(VerticalAlign.Center)
        .width('100%')
        .margin({
          bottom: $r('app.float.food_margin_bottom')
        })

        Row() {
          Text('甜点/蛋糕')
            .fontWeight(500)
            .fontColor($r('app.color.sixty_black'))
            .fontSize($r('app.float.delivery_font'))
          Blank()
          Text('1.3km')
            .fontWeight(500)
            .fontSize($r('app.float.delivery_font'))
        }
        .width('100%')
        .margin({
          bottom: $r('app.float.text_margin_bottom')
        })

        Row() {
          ForEach(['新中餐榜第三名','地道老北京'],(item:string) =>{
            Text(item)
              .fontWeight(400)
              .fontColor($r('app.color.filtering_font'))
              .fontSize($r('app.float.filtering_font'))
              .orangeText()
              .margin({
                right: $r('app.float.filtering_margin_right')
              })
          })
        }
        .width('100%')
        .margin({
          bottom: $r('app.float.text_margin_bottom')
        })

        Row() {
          Text($r('app.string.preference_flag'))
            .fontWeight(400)
            .fontColor($r('app.color.filtering_font'))
            .fontSize($r('app.float.filtering_font'))
            .orangeText()
            .margin({
              right: $r('app.float.flag_margin_right')
            })
          Text('¥78')
            .fontWeight(500)
            .fontColor($r('app.color.filtering_font'))
            .fontSize($r('app.float.delivery_font'))
            .margin({
              right: $r('app.float.symbol_margin_right')
            })
          Text('¥' + ' ' + 88)
            .fontWeight(400)
            .fontColor($r('app.color.forty_black'))
            .fontSize($r('app.float.filtering_font'))
            .decoration({type:TextDecorationType.LineThrough})
          Text('美味烤鸭地道风味')
            .fontWeight(500)
            .fontSize($r('app.float.filtering_font'))
            .layoutWeight(1)
            .maxLines(1)
            .margin({left: 4})
            .textOverflow({
              overflow:TextOverflow.Ellipsis
            })
        }
        .width('100%')
      }
      .padding({
        left: $r('app.float.twelves_padding')
      })
      .layoutWeight(1)
    }
    .alignItems(VerticalAlign.Top)
    .width('100%')
    .padding($r('app.float.twelves_padding'))
    .borderRadius($r('app.float.dish_border'))

    // 当两栏是出现高亮
    .backgroundColor(this.listIndex  
                     === this.chooseIndex && this.ifShowSides ?
    $r('app.color.food_background') : $r('app.color.three_black'))
  }

16.7.9 代码视频教程

完整案例代码与视频教程请参见:

代码:code-16.zip。

视频:《润客点餐一多开发》。

16.6 总结

随着终端设备形态日益多样,分布式技术虽带来庞大潜在用户群,但应用适配多设备的开发成本高。HarmonyOS 系统的 "一次开发,多端部署" 能力可有效解决该问题。它指一套代码一次开发上架后多端按需部署,能助力开发者快速开发兼容多设备形态的应用,提供跨设备的优质分布式体验,不过需先攻克页面适配和功能兼容难题。

方舟开发框架提供类 Web 和声明式两种开发范式,后者更适合大型应用且被推荐。应用程序包的Module分 "Ability" 和 "Library",HAP分Entry和Feature。"一多" 有A、B两种部署模型,常混合使用且都需一次编译,还推荐 "三层工程结构" 以实现代码复用,最终应用以 APP 包形式发布。

在开发方面,界面级开发推荐声明式范式,布局分自适应和响应式,前者元素依相对关系变化,后者依断点等变化;交互归一整合了多种交互方式API。功能级开发需解决设备系统能力差异的兼容问题,通过SysCap机制的能力集和CanIUse接口判断设备对系统能力的支持情况。工程级开发使用DevEco Studio,遵循三层架构规范,经新建Module、修改配置、调整目录结构、修改依赖关系、引用 ohpm 包代码等步骤进行。

以《润客点餐app为例的案例实战,展示了其在手机、折叠屏、平板及2in1设备上的运行效果,运用到"一次开发,多端部署"、自适应布局、响应式布局等众多知识点,还对其代码结构、首页代码、断点核心代码、utils实现等进行了解读。

相关推荐
掘金安东尼18 分钟前
上周前端发生哪些新鲜事儿? #407
前端·面试·github
小谭鸡米花27 分钟前
ECharts各类炫酷图表/3D柱形图
前端·javascript·echarts·大屏端
郝晨妤32 分钟前
【鸿蒙5.0】向用户申请麦克风授权
linux·服务器·前端·华为·harmonyos·鸿蒙
神秘代码行者42 分钟前
使用 contenteditable 属性实现网页内容可编辑化
前端·html5
小鱼人爱编程43 分钟前
Look My Eyes 最新IDEA快速搭建Java Web工程的两种方式
java·前端·后端
郝晨妤44 分钟前
【鸿蒙5.0】鸿蒙登录界面 web嵌入(隐私页面加载)
前端·华为·harmonyos
小鱼人爱编程1 小时前
当上小组长的第3天,我裁掉了2年老员工
前端·后端·面试
晓得迷路了1 小时前
栗子前端技术周刊第 74 期 - 2025 Vue.js 现状报告、Element Plus X、Material UI v7...
前端·javascript·vue.js
知识分享小能手1 小时前
CSS3学习教程,从入门到精通, CSS3 变形效果(2D 和 3D)的详细语法知识点及案例代码(22)
前端·javascript·css·学习·3d·css3·html5
花之亡灵1 小时前
.net 6 + vue3中使用SignaIR实现双向通信功能
前端·javascript·笔记·websocket·.net·信息与通信