HarmonyOS实战
一、HarmonyOS介绍
课程目标
方舟编译器(ArkCompiler)核心原理
课程内容
(1)1+8+N布局
HarmonyOS的设计目标,是成为打通手机、PC、平板、电视、车机和智能穿戴等多种设备的统一操作系统。
其应用开发有多编程语言、多范式的支持需求,其中高级编程语言包括JavaScript、TypeScript、Java等,开发范式包括声明式UI范式、分布式编程范式
**我们需要相应的编译器和运行时来支撑这些高级应用编程语言的高效开发、部署和运行。**使应用开发者能使用同一套开发框架实现一次开发多端部署运行,并且让使用HarmonyOS设备的用户,能获得统一的用户体验。于是,ArkCompiler应运而生。
JS 弱类型语言:没有类型验证。
TS语言:类型静态检查,类型约束等等。
ArkTS:在TS基础上进行了封装,提出了声明式UI概念。
HTML+CSS+JS来实现页面整个交互。ArkTS 更多关注业务。实现业务需要用到组件和Api
(2)方舟编译器的原理
ArkCompiler是华为自研的统一编程平台,包含编译器、工具链、运行时等关键部件,支持高级语言在多种芯片平台的编译与运行,并支撑应用和服务运行在手机、个人电脑、平板、电视、汽车和智能穿戴等多种设备上的需求。
ArkCompiler(方舟编译器)是组件化、可配置的多语言编译和运行平台,它既能支撑单一语言运行环境,也能支撑多种语言组合的运行环境。它目前主要支持的语言是JavaScript、TypeScript和Java
相比常见的JavaScript运行时,可以把端侧的编译解析过程提前到发布前,提升程序的启动性能
以前写JS代码:写完了交给浏览器进行代码语法检测、编译、运行(出现问题,浏览器报错),回去修改源码
鸿蒙代码运行:在开发过程中,就会进行类型推断、语法检测、代码编译。编译为字节码。
字节码拿到执行器运行。
好处:代码在开发过程中遇到问题,直接在编辑器里面检测到。开发完成后,自动编译为字节码。
字节码可以移出编译,处处运行。
TS开发基础,ArkTS非常顺畅
什么是AOT:AOT编译器则是在运行前根据静态信息直接编译生成高质量的目标机器码
JIT编译器:JIT可以根据代码执行情况实时编译生成最优机器指令
课程小结
二、HarmonyOS开发环境搭建
课程目标
- 掌握mac平台下搭建鸿蒙的开发环境
- 掌握window平台下搭建鸿蒙的开发环境
- 安装DevEco Studio开发工具
- 介绍开发工具的基本设置
- 搭建第一个移动端项目,设计hello world
- 配置各平台模拟器的运行环境
课程内容
一、基本概念
DevEco Studio是开发鸿蒙应用的IDE软件,在正式进入软件开发之前,我们需要先本地先安装开发工具,工欲善其事,必先利其器。
软件下载地址:developer.harmonyos.com/cn/develop/...
如下图:
DevEco Studio提供了Windows版本和Mac版本选择,可以根据操作系统选择对应的版本进行下载。
二、安装步骤
在window系统下面的安装步骤参考官网
课程小结
三、鸿蒙项目创建
课程目标
- 通过DevEco Studio创建鸿蒙项目
- 介绍鸿蒙项目的目录结构
- 运行项目到模拟器
- 本地模拟器的下载运行
课程内容
(1)项目目录结构
项目目录结构如下:
- AppScope中存放应用全局所需要的资源文件。
- entry是应用的主模块,存放HarmonyOS应用的代码、资源等。
- ability:Ability 是应用最基本的抽象单位,是能够完成一个独立功能的应用组件,Ability 可能有用户界面(PA),也可能没有用户界面仅执行后台功能
- ets:arkts的文件类型。
- oh_modules是工程的依赖包,存放工程依赖的源文件。
- build-profile.json5是工程级配置信息,包括签名、产品配置等。
- hvigorfile.ts是工程级编译构建任务脚本,hvigor是基于任务管理机制实现的一款全新的自动化构建工具,主要提供任务注册编排,工程模型管理、配置管理等核心能力。
- oh-package.json5是工程级依赖配置文件,用于记录引入包的配置信息。
(2)项目运行
preview:临时预览项目
模拟器:可以在模拟器上运行我们的项目
课程小结
四、ArkTS基础
(1)ArkTS介绍
Mozilla创造了JS,Microsoft创建了TS,Huawei进一步推出了ArkTS。
JS特点
完成页面的基础逻辑交互能力,实现事件监听或者动画效果。
JS语言由Mozilla创造,最初主要是为了解决页面中的逻辑交互问题,它和HTML(负责页面内容)、CSS(负责页面布局和样式)共同组成了Web页面/应用开发的基础。随着Web和浏览器的普及,以及Node.js进一步将JS扩展到了浏览器以外的环境,JS语言得到了飞速的发展。在2015年相关的标准组织ECMA发布了一个主要的版本ECMAScript 6(简称ES6),这个版本具备了较为完整的语言能力,包括类(Class)、模块(Module)、相关的语言基础API增强(Map/Set等)、箭头函数(Arrow Function)等。从2015年开始,ECMA每年都会发布一个标准版本,比如ES2016/ES2017/ES2018等,JS语言越来越成熟。
TS特点
在JS基础上提供了,类型声明、类型系统、开发约束、团队开发协作高效。
大型的应用工程一般会涉及较复杂的代码以及较多的团队协作,对语言的规范性,模块的复用性、扩展性以及相关的开发工具都提出了更高的要求。为此,Microsoft在JS的基础上,创建了TS语言,并在2014年正式发布了1.0版本。
ArkTS特点
它在TypeScript(简称TS)的基础上,扩展了声明式UI、状态管理等相应的能力,让开发者可以以更简洁、更自然的方式开发高性能应用。ArkTS则是TS的超集。
传统应用,写一个应用需要了解三种语言(JS/TS、HTML和CSS)。这对Web开发者相对友好,但对非Web开发者来说,负担较重。
(2)ArkTS整体运行流程
官方推荐我们采用两种开发模式:
- 基于传统的HTML\CSS\JS来构建我们的应用,通过华为底层的编译器来进行代码的编译和渲染。这种模式我们成为
基于JS扩展的类Web范氏
。对于前端工程师来说,可以无缝切换到鸿蒙开发 - 基于 TS扩展的声明式UI范式,这种开发方式就是基于ArkTS来进行开发,提供声明式UI来进行页面设计和交互,无需自己在构建原生的HTML\CSS 代码。这种方式也是鸿蒙官方推荐的方式。
(3)ArkTS代码的基本结构
代码结构图:
核心内容说明
装饰器
用来装饰类、结构体、方法以及变量,赋予其特殊的含义,如上述示例中 @Entry 、 @Component 、 @State 都是装饰器。具体而言, @Component 表示这是个自定义组件; @Entry 则表示这是个入口组件; @State 表示组件中的状态变量,此状态变化会引起 UI 变更。
装饰器核心剖析可以查看文档:
自定义组件
可复用的 UI 单元,可组合其它组件,如上述被 @Component 装饰的 struct Hello。
UI 描述
声明式的方式来描述 UI 的结构,如上述 build() 方法内部的代码块。
内置组件
框架中默认内置的基础和布局组件,可直接被开发者调用,比如示例中的 Column、Text、Divider、Button。
事件方法
用于添加组件对事件的响应逻辑,统一通过事件方法进行设置,如跟随在Button后面的onClick()。
属性方法
用于组件属性的配置,统一通过属性方法进行设置,如fontSize()、width()、height()、color() 等,可通过链式调用的方式设置多项属性。
(4)组件基本结构
应用界面是由一个个页面组成,ArkTS是由ArkUI框架提供,用于以声明式开发范式开发界面的语言。
声明式UI构建页面的过程,其实是组合组件的过程,声明式UI的思想,主要体现在两个方面:
- 描述UI的呈现结果,而不关心过程
- 状态驱动视图更新
ArkUI作为HarmonyOS应用开发的UI开发框架,其使用ArkTS语言构建自定义组件,通过组合自定义组件完成页面的构建。
项目的第一个入口文件:使用@Entry和@Component装饰的自定义组件作为页面的入口,会在页面加载时首先进行渲染。
js
@Entry
@Component
struct Index {
@State message: string = 'Hello World 蜗牛'
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}
@Component:装饰器代表当前这个文件是自定义的一个组件
@Entry:装饰器代表当前组件是一个入口组件,有且仅有一个入口组件
build:build方法内可以容纳内置组件和其他自定义组件,Row、Column、Text都是内置组件
(5)行布局
我们最常使用的布局为行布局、列布局、弹性布局。
文档地址为:developer.harmonyos.com/cn/docs/doc...
沿水平方向布局容器。只要屏幕能放得下,多个Row盒子可以在一行显示,
默认支持弹性布局的方案
代码为:
js
@Component
export default struct RowLayout{
build(){
Column({ space: 5 }) {
// 设置子组件水平方向的间距为5
Text('行布局').width('90%')
Row({ space: 5 }) {
Row(){Text("第一行")}.width('30%').height(50).backgroundColor(0xAFEFFF)
Row(){Text("第二行")}.width('30%').height(50).backgroundColor(0xE0EEEE)
}.width('90%').height(207).border({ width: 1 })
}.width('100%')
}
}
效果为:
可以给Row组件设置弹性布局属性
js
Row({ space: 5 }) {
Row(){Text("第一行")}.width('30%').height(50).backgroundColor(0xAFEFFF)
Row(){Text("第二行")}.width('30%').height(50).backgroundColor(0xE0EEEE)
}.width('90%').height(207).border({ width: 1 }).justifyContent(FlexAlign.Center).alignItems(VerticalAlign.Center)
justifyContent:设置水平方向的布局模式,默认值要用FlexAlign枚举来控制;不支持字符串
alignItems:设置垂直方向的排列模式,默认要用VerticalAlign枚举来控制;不支持字符串
效果如下:
(6)列布局
组件Column沿垂直方向布局的容器。没一个Column组件独占一行。
默认支持弹性布局的方案。
代码如下:
js
@Component
export default struct ColumnLayout{
build(){
Column({ space: 5 }) {
// 设置子元素垂直方向间距为5
Text('垂直方向布局').width('90%')
// 设置子元素水平方向对齐方式
Text('设置子元素水平方向对齐方式').width('90%')
Column() {
Column().width('50%').height(30).backgroundColor(0xAFEEEE)
Column().width('50%').height(30).backgroundColor(0x00FFFF)
}.alignItems(HorizontalAlign.Center).width('90%').border({ width: 1 }).height(100)
// 设置子元素垂直方向的对齐方式
Text('设置子元素垂直方向的对齐方式').width('90%')
Column() {
Column().width('50%').height(30).backgroundColor(0xAFEEEE)
Column().width('50%').height(30).backgroundColor(0x00FFFF)
}.width('90%').height(100).border({ width: 1 }).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Start)
}.width('100%').padding({ top: 5 })
}
}
效果如下:
(7)弹性布局
以弹性方式布局子组件的容器组件。完美迁移了CSS3中的弹性布局方案
官方说明:
基础代码
js
@Component
export default struct Unit {
build() {
Column() {
Flex({ wrap: FlexWrap.Wrap,direction: FlexDirection.Row }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('4').width('20%').height(50).backgroundColor(0xD2B48C)
}.width('100%')
}
}
}
设置Flex盒子在水平方向和垂直方向居中显示
js
@Component
export default struct Unit {
build() {
Column() {
Flex({ wrap: FlexWrap.NoWrap,direction: FlexDirection.Row,justifyContent:FlexAlign.Center,alignItems:ItemAlign.Center}) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xF5DFE9)
}.width("100%").height(100).backgroundColor(0xF5EEE0)
Text('alignItems:Center').fontSize(9).fontColor(0xCCCCCC).width('90%')
Flex({ alignItems: ItemAlign.Center }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}.width("100%").height(100).backgroundColor(0xF5DFE9)
}
}
}
属性解释:
wrap: FlexWrap.NoWrap设置是否换行显示。这个地方有一个坑,如果设置wrap换行,alignItems属性将失效。
direction:设置弹性布局主轴的方向。
justifyContent:设置水平方向居中
alignItems:设置垂直方向居中,默认表示单行居中显示
(8)布局单位
鸿蒙为开发者提供4种像素单位,框架采用vp为基准数据单位。
我们推荐用vp来进行布局,当然你可以可以不写单位,默认就是vp
单位 | 说明 |
---|---|
px | 屏幕物理像素单位。 |
vp | 屏幕密度相关像素,根据屏幕像素密度转换为屏幕物理像素,当数值不带单位时,默认单位vp。 |
fp | 字体像素,与vp类似适用屏幕密度变化,随系统字体大小设置变化。 |
lpx | 视窗逻辑像素单位,lpx单位为实际屏幕宽度与逻辑宽度(通过designWidth配置)的比值 |
代码如下
js
@Component
export default struct Unit {
build() {
Column() {
Flex({ wrap: FlexWrap.Wrap }) {
//200vp布局
Column(){
Text("width(220vp)").width(200).height(40).backgroundColor(0xAFEEEE)
}
//200px布局
Column() {
Text("width(220px)")
.width("220px").height(40).backgroundColor(0xF9CF93)
}
Column() {
Text("width(220)")
.width(200).height(40).backgroundColor(0xF9CF93).fontSize("12fp")
}
}.width('100%')
}
}
}
五、布局实战
设置布局头部
js
@Component
export default struct FruitItemList {
build() {
Column(){
// 设置页面布局头部
Row() {
// 左侧的标题和返回按钮
Row(){
Image($r('app.media.ic_public_back'))
.width(30).height(30).margin(10)
Text("蜗牛水果摊排行")
.fontSize("16fp")
.fontWeight(600)
}
.width("50%")
.height(48)
.justifyContent(FlexAlign.Start)
.backgroundColor(0xF5DFE9)
// 右侧的刷新按钮
Row(){
Image($r('app.media.loading'))
.height(30)
.width(30)
}
.width("50%")
.height(48)
.justifyContent(FlexAlign.End)
.backgroundColor(0xD2B48C)
}.width('100%')
.padding({ left: 10, right: 10 })
.margin({ top: 10 })
.height(48)
.justifyContent(FlexAlign.SpaceAround)
.backgroundColor(0xF5DEB3)
}
}
}
效果如下:
设置导航区域
代码如下
js
// 设置页面布局头部
Row() {
// 左侧的标题和返回按钮
Row(){
...
}
// 右侧的刷新按钮
Row(){
...
}
}.width('100%')
.padding({ left: 10, right: 10 })
.margin({ top: 10 })
.height(48)
.justifyContent(FlexAlign.SpaceAround)
.backgroundColor(0xF5DEB3)
// 设置排名、种类、得票数
Row() {
Text("排名")
.fontSize("16fp")
.width("30%")
.fontWeight(500)
.fontColor("#989A9C")
Text("种类")
.fontSize("16fp")
.width("30%")
.fontWeight(500)
.fontColor("#989A9C")
Text("数量")
.fontSize("16fp")
.width("30%")
.fontWeight(500)
.fontColor("#989A9C")
}
.width("100%")
.padding(20)
效果如下:
内容渲染区域
代码如下
js
// 设置页面布局头部
Row() {
// 左侧的标题和返回按钮
Row(){
...
}
// 右侧的刷新按钮
Row(){
...
}
}
// 设置排名、种类、得票数
Row() {
Text("排名")
.fontSize("16fp")
.width("30%")
.fontWeight(500)
.fontColor("#989A9C")
Text("种类")
.fontSize("16fp")
.width("30%")
.fontWeight(500)
.fontColor("#989A9C")
Text("数量")
.fontSize("16fp")
.width("30%")
.fontWeight(500)
.fontColor("#989A9C")
}
.width("100%")
.padding(20)
// 内容渲染,沿垂直方向设计一个容器
Column(){
List({ space: 0, initialIndex: 0 }) {
ForEach(this.arr, (item) => {
ListItem() {
Text('' + item)
.width('100%').height(40).fontSize(16)
.textAlign(TextAlign.Center).backgroundColor(0xFFFFFF)
// .border({})
}
}, item => item)
}
.listDirection(Axis.Vertical) // 排列方向
.divider({ strokeWidth: 1 }) // 每行之间的分界线
.edgeEffect(EdgeEffect.Spring) // 滑动到边缘无效果
.onScrollIndex((firstIndex: number, lastIndex: number) => {
console.info('first' + firstIndex)
console.info('last' + lastIndex)
})
.width('100%')
.borderRadius(20)
}
.padding({
left:10,
right: 10
})
.width("90%")
.backgroundColor("#fff")
.borderRadius(20)
效果如下:
内容定义和渲染
List组件里面渲染的三个文本内容如下
js
List({ space: 0, initialIndex: 0 }) {
ForEach(this.arr, (item) => {
ListItem() {
// Text('' + item)
// .width('100%').height(40).fontSize(16)
// .textAlign(TextAlign.Center).backgroundColor(0xFFFFFF)
// .border({})
Row(){
Row(){
Text("1").width(28).height(28).backgroundColor("#007dff").borderRadius(50).textAlign(TextAlign.Center)
}.width("30%").height(50)
Row(){
Text("苹果").textAlign(TextAlign.Center)
}.width("40%").height(50)
Row(){
Text("1200").textAlign(TextAlign.Center)
}.width("30%").height(50)
}.width("100%")
}
}, item => item)
}
组件的内部数据
我们在组件内部定义了arr数组来供List组件渲染
js
export default struct FruitItemList {
private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
}
这个数据在组件内部并不属于内部状态数据。无法实现页面同步更新。只能在组件第一次加载的时候读取渲染
组件内部状态的定义如下:当点击了Button按钮过后可以动态修改myIndex变量
js
export default struct FruitItemList {
@State myIndex:number = 1
build(){
Column(){
Button(`按钮序号${this.myIndex}`).onClick(()=>{
this.myIndex = 100
})
}
}
}
如果遇到复杂的数据类型,开发流程如下
(1)创建ets/model/DataModel.ets
js
export class RankData {
name: string;
vote: string; // Number of votes
id: string;
constructor(id: string, name: string, vote: string) {
this.id = id;
this.name = name;
this.vote = vote;
}
}
const rankData1: RankData[] = [
new RankData('1', "苹果", '12080'),
new RankData('2', "凤梨", '10320'),
new RankData('3', "橘子", '9801'),
new RankData('4', "花生", '8431'),
new RankData('5', "菠萝", '7546')
];
export {rankData1}
- RankData:这个用于定义组件内部数据的约束和初始化
- rankData1这个变量定义的数据,由RankData来创建构建。
在组件中使用FruitItemList
js
import {RankData,rankData1} from "../model/DataModel"
export default struct FruitItemList {
@State myIndex:number = 10
@State myData:RankData[] = rankData1
@State chooseIndex:number = 1
}
定义组件内部数据myData用于做页面渲染。
js
List({ space: 0, initialIndex: 0 }) {
ForEach(this.myData, (item) => {
ListItem() {
Row(){
Row(){
if(item.id == this.chooseIndex){
Text(item.id).width(28).height(28).backgroundColor("#007dff").borderRadius(50).textAlign(TextAlign.Center)
}else{
Text(item.id).width(28).height(28).textAlign(TextAlign.Center)
}
}.width("30%").height(50).onClick(()=>{
this.chooseIndex = item.id
})
Row(){
Text(item.name).textAlign(TextAlign.Center)
}.width("40%").height(50)
Row(){
Text(item.vote).textAlign(TextAlign.Center)
}.width("30%").height(50)
}.width("100%")
}
}, item => item.id)
}
我们根据编号来判断是否需要选中这个文本。
给文本添加一个点击事件,点击过后修改chooseIndex 属性。当数据变化过后页面会更新
效果如下:
六、路由设置
页面路由指在应用程序中实现不同页面之间的跳转和数据传递。HarmonyOS提供了Router模块,通过不同的url地址,可以方便地进行页面路由,轻松地访问不同的页面。本文将从页面跳转、页面返回和页面返回前增加一个询问框几个方面介绍Router模块提供的功能。
(1)路由页面创建
选择创建page页面,默认会配置路由映射路径
在main/resources/profile/main_pages.json
js
{
"src": [
"pages/Index",
"pages/TodoList"
]
}
类似于小程序,我们需要在页面中配置路由映射。
(2)路由跳转
在IndexPage中设计一个跳转按钮
js
import router from '@ohos.router';
@Entry
@Component
struct Index {
@State message: string = 'Hello World'
build() {
Row() {
Column() {
Text("页面列表").margin({bottom:10}).fontSize(20).fontWeight(800)
Button("todoList").onClick((event: ClickEvent) => {
// console.info("ok")
// console.info(JSON.stringify(event))
router.pushUrl({
url: "pages/TodoList",
params: {
id:1
}
}).catch((error: Error) => {
// Logger.info(TAG, 'IndexPage push error' + JSON.stringify(error));
console.log("错误信息")
});
})
}
.width('100%')
}
.height('100%')
}
}
点击跳转过后,可以执行router.pushUrl()
完成的语法
js
router.pushUrl({
url: "pages/TodoList",
params: {
id:1
}
}).catch((error: Error) => {
console.log("错误信息")
});
跳转方式有两种:
- router.pushUrl():目标页不会替换当前页,而是压入页面栈。这样可以保留当前页的状态,并且可以通过返回键或者调用router.back()方法返回到当前页
- router.replaceUrl():目标页会替换当前页,并销毁当前页。这样可以释放当前页的资源,并且无法返回到当前页
- router.back():返回到之前历史记录的页面
同时,Router模块提供了两种实例模式,分别是Standard和Single。这两种模式决定了目标url是否会对应多个实例
-
Standard:标准实例模式,也是默认情况下的实例模式。每次调用该方法都会新建一个目标页,并压入栈顶。
-
Single:单实例模式。即如果目标页的url在页面栈中已经存在同url页面,则离栈顶最近的同url页面会被移动到栈顶,并重新加载;如果目标页的url在页面栈中不存在同url页面,则按照标准模式跳转。
js
router.pushUrl({
url: 'pages/Detail'
}, router.RouterMode.Standard, (err) => {
if (err) {
return;
}
});
标准实例模式下,router.RouterMode.Standard参数可以省略。
(3)路由参数传递
页面一:传递参数默认params对象
js
router.pushUrl({
url: "pages/TodoList",
params: {
id:1
}
})
页面二:
js
struct TodoList {
@State message: string = 'todolist'
onPageShow(){
console.info(JSON.stringify(router.getParams()))
}
...
}
获取到结果为{id:1}
(4)页面返回添加确认框
在返回的时候,可以添加确认框。
js
Button("返回").onClick(()=>{
try {
router.showAlertBeforeBackPage({
message:"确定返回?"
})
}catch(e){
console.error("返回失败")
}
router.back()
})
如果想要在目标界面开启页面返回询问框,需要在调用router.back()方法之前,通过调用router.showAlertBeforeBackPage()方法设置返回询问框的信息。例如,在支付页面中定义一个返回按钮的点击事件处理函数
(5)自定义弹出框
可以自定义弹出框来让用户选择释放返回
js
import promptAction from '@ohos.promptAction';
promptAction.showDialog({
message: '您还没有完成支付,确定要返回吗?',
buttons: [
{
text: '取消',
color: '#FF0000'
},
{
text: '确认',
color: '#0099FF'
}
]
}).then((result) => {
if (result.index === 0) {
// 用户点击了"取消"按钮
console.info('User canceled the operation.');
} else if (result.index === 1) {
// 用户点击了"确认"按钮
console.info('User confirmed the operation.');
// 调用router.back()方法,返回上一个页面
router.back();
}
}).catch((err) => {
console.error(`Invoke showDialog failed, code is ${err.code}, message is ${err.message}`);
})
七、todoList
(1)完成页面整体布局
页面:ToDoList.ets
ts
import router from '@ohos.router';
@Entry
@Component
struct TodoList {
onPageShow(){
}
build() {
Row() {
Column() {
Text($r("app.string.page_title"))
.fontSize(30)
.fontWeight(FontWeight.Bold)
Row({space:10}){
Button("全部").backgroundColor(0xF55A42)
Button("已完成")
Button("未完成")
}
}
.width('100%')
}
}
}
设置主页的布局,完成效果如下
(2)封装列表页面组件
列表页面的布局我们采用组件封装的形式来设计
js
@Component
export default struct Demo{
build(){
Column() {
Row({space:20}){
Toggle({ type: ToggleType.Checkbox, isOn: true }).onChange((event)=>{
})
Text("message")
.fontSize(20)
.opacity(1)
.decoration({type:TextDecorationType.LineThrough})
}.borderRadius(20)
.backgroundColor("#F0F0F0F0")
.width("90%")
.height(50)
.padding({left:20})
.margin({top:20})
}
}
}
封装todolist的一个列表组件
效果为:
(3)定义组件内部数据
todolist需要的数据为复杂数据类型
按照TS的规范来设计的话,如下
js
const dataSource:Array<{id:number,task:string,status:boolean}> = [
{id:1,task:"吃饭",status:true},
{id:2,task:"睡觉",status:false}
]
在ArkTS项目中,我们的复杂数据类型要基于类的方式来设计
- 创建ets/viewmodel/DataModel.ets
js
export class ObjectData{
private id:number
private task:string
private status:boolean
constructor(id:number,task:string,status:boolean) {
this.id = id
this.task = task
this.status = status
}
}
export class ListData{
private lists:Array<ObjectData> = [
new ObjectData(1,"吃饭",true),
new ObjectData(2,"睡觉",false),
new ObjectData(3,"打豆豆",false)
]
getList(){
return this.lists
}
}
定义ObjectData代表对象的类型约束。
定义ListData代表构造数组类型的数据,并初始化了三个数据
2)在TodoList页面中
js
import router from '@ohos.router';
import MyList from "../views/MyList"
import {ObjectData,ListData} from "../viewmodel/DataModel"
@Entry
@Component
struct TodoList {
private myLists:Array<ObjectData>
aboutToAppear(){
this.myLists = new ListData().getList()
}
build() {
Row() {
Column() {
Text($r("app.string.page_title"))
.fontSize(30)
.fontWeight(FontWeight.Bold)
Row({space:10}){
Button("全部").backgroundColor(0xF55A42)
Button("已完成")
Button("未完成")
}
}
.width('100%')
}
}
}
我们通过aboutToAppear生命周期获取定义好的数据,并保存起来。
4)进行动态渲染
js
import {ObjectData} from "../viewmodel/DataModel"
@Component
export default struct Demo{
@Link itemList:Array<ObjectData>
build(){
Column() {
Row({space:20}){
Toggle({ type: ToggleType.Checkbox, isOn: true }).onChange((event)=>{
})
Text("message")
.fontSize(20)
.opacity(1)
.decoration({type:TextDecorationType.LineThrough})
}.borderRadius(20)
.backgroundColor("#F0F0F0F0")
.width("90%")
.height(50)
.padding({left:20})
.margin({top:20})
}
}
}
在代码中使用ForEach
来实现对数据进行遍历渲染
子组件测试
js
import {ObjectData} from "../viewmodel/DataModel"
@Component
export default struct Demo{
@Link itemList:Array<ObjectData>
@Builder taskItem(message,checked,id){
Row({space:20}){
Toggle({ type: ToggleType.Checkbox, isOn: checked }).onChange((event)=>{
const object = this.itemList.find(item=>item.id == id)
object.status = event
})
Text(message)
.fontSize(20)
.opacity(checked?0.4:1)
.decoration({type:checked?TextDecorationType.LineThrough:TextDecorationType.None})
}.borderRadius(20)
.backgroundColor("#F0F0F0F0")
.width("90%")
.height(50)
.padding({left:20})
.margin({top:20})
}
build(){
Column() {
this.taskItem("测试",true,1)
}
}
}
改造一下build
js
build(){
Column() {
ForEach(this.itemList,item=>{
this.taskItem("测试",true,1)
},item=>item.id)
}
}
每次渲染加载MyList组件,并传递对应的message和checked属性
页面效果如下:
(4)使用@Builder提取公共模块
@Builder装饰器可以实现自定义构建函数
除了自定义组件以外,ArkUI还提供了一种更轻量的UI元素复用机制@Builder,@Builder所装饰的函数遵循build()函数语法规则,开发者可以将重复使用的UI元素抽象成一个方法,在build方法里调用。
自定义函数分为两种:
- 自定义组件内部定义自定义函数
js
import {ObjectData} from "../viewmodel/DataModel"
@Component
export default struct MyList{
private message:string = ""
private checked:boolean
//自定义函数
@Builder icons(icon:Resource,opcity:number){
Image(icon).width(35).opacity(opcity)
}
build(){
Row({space:20}){
if(this.checked){
this.icons($r("app.media.wancheng"),0.5)
}else{
this.icons($r("app.media.cha"),1)
}
。。。
}
.borderRadius(20)
.backgroundColor("#F0F0F0F0")
.width("90%")
.height(50)
.padding({left:20})
.margin({top:20})
}
}
允许在自定义组件内部定义多个自定义函数,调用的时候使用this.icons
来调用。
- 全局自定义函数
js
import {ObjectData} from "../viewmodel/DataModel"
//全局自定义函数
@Builder function MyBuilder(title:string) {
Row() {
Text(title)
}
}
@Component
export default struct MyList{
private message:string = ""
private checked:boolean
//组件内部的定义函数
@Builder icons(icon:Resource,opcity:number){
Image(icon).width(35).opacity(opcity)
}
build(){
Row({space:20}){
if(this.checked){
this.icons($r("app.media.wancheng"),0.5)
}else{
this.icons($r("app.media.cha"),1)
}
MyBuilder("测试")
}
.borderRadius(20)
.backgroundColor("#F0F0F0F0")
.width("90%")
.height(50)
.padding({left:20})
.margin({top:20})
}
}
当自定义函数内部的数据是静态的,不存在刷新的情况,那就适合哟过全局的
自定义函数的参数分类
- 按照值传递:默认就是按照值来传递,当外部的变量值发生变化,自定义函数内部不受影响
js
@State user:{name:string} = {name:"xiaowang"}
@Builder function MyBuilder(title:{name:string}) {
Row() {
Text(title.name)
}
}
build(){
MyBuilder(this.user)
Button("修改").onClick((event: ClickEvent) => {
this.user.name = "xiaofeifei"
})
}
点击按钮修改user的name属性,MyBuilder并不会发生页面变化。
- 按照引用类型传递:当外部的变量值发生数据变化,会影响组件内部的值
js
@Builder function MyBuilder($$:{name:string}) {
Row() {
Text($$.name)
}
}
@State label:string = "测试"
MyBuilder({name:this.label})
Button("修改").onClick((event: ClickEvent) => {
this.label = "测试"
})
$$
作为按引用传递参数的范式。
要求传递的每个值是单独控制的。如果 MyBuilder(this.user)传递一个对象,无法控制。
?打满了问号
八、生命周期
页面生命周期,即被@Entry装饰的组件生命周期,提供以下生命周期接口:
- onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景,仅@Entry装饰的自定义组件生效。
- onPageHide:页面每次隐藏时触发一次,包括路由过程、应用进入前后台等场景,仅@Entry装饰的自定义组件生效。
- onBackPress:当用户点击返回按钮时触发,仅@Entry装饰的自定义组件生效。
组件生命周期,即一般用@Component装饰的自定义组件的生命周期,提供以下生命周期接口:
- aboutToAppear:组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其build()函数之前执行。
- aboutToDisappear:在自定义组件析构销毁之前执行。不允许在aboutToDisappear函数中改变状态变量,特别是@Link变量的修改可能会导致应用程序行为不稳定。
流程图如下:
实战代码
页面代码:
js
import ChildrenCom from "../views/ChildrenCom"
@Entry
@Component
struct CompConnect {
@State message: string = 'Hello World'
// 只有被@Entry装饰的组件才可以调用页面的生命周期
onPageShow() {
console.info('Index onPageShow');
}
// 只有被@Entry装饰的组件才可以调用页面的生命周期
onPageHide() {
console.info('Index onPageHide');
}
// 只有被@Entry装饰的组件才可以调用页面的生命周期
onBackPress() {
console.info('Index onBackPress');
}
// 组件生命周期
aboutToAppear() {
console.info('MyComponent aboutToAppear');
}
// 组件生命周期
aboutToDisappear() {
console.info('MyComponent aboutToDisappear');
}
build() {
Column() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
ChildrenCom()
.width('100%')
}
.height('100%')
}
}
组件中的代码
js
@Component
export default struct Children {
// 组件生命周期
aboutToAppear() {
console.info('Children aboutToAppear');
}
// 组件生命周期
aboutToDisappear() {
console.info('Children aboutToDisappear');
}
build() {
Column() {
Text("children").fontSize(40)
}
}
}
九、组件状态
@State组件的内部状态
(1)组件内部定义基本数据类型
js
@Component
export default struct Children {
//基本数据类型
@State msg:string = "xiaowang"
// 组件生命周期
aboutToAppear() {
console.info('Children aboutToAppear');
}
// 组件生命周期
aboutToDisappear() {
console.info('Children aboutToDisappear');
}
build() {
Column() {
Text("children").fontSize(40)
}
}
}
基本数据类型定义,按照TS的约束规范来设计就可以了。
注意:@State 定义的变量必须初始化值
(2)引用类型的变量
js
@Component
export default struct Children {
//基本数据类型
@State msg:string = "xiaowang"
@State user:Array<{id:number,name:string}> = [{id:1,name:"xiaowang"},{id:2,name:"xiaofei"}]
// 定义在组件内的@Styles封装的样式
@Styles fancy() {
.width(200)
.height(40)
.backgroundColor(Color.Yellow)
}
// 组件生命周期
aboutToAppear() {
console.info('Children aboutToAppear');
}
// 组件生命周期
aboutToDisappear() {
console.info('Children aboutToDisappear');
}
build() {
Column() {
Text("children").fontSize(40)
ForEach(this.user,item=>{
Column(){
Row(){
Text(item.id.toString()).fancy()
Text(item.name).fancy()
}
}
},item=>item.id)
}
}
}
(3)面向对象类型
在ets/viewmodel/StudentModel.ets
封装一个对象模型。后续根据这个模型产生我们的数据约束
js
export default class Student{
public id:number
public name:string
public address:string
constructor(id,name,address) {
this.id = id;
this.name = name
this.address = address
}
}
组件中使用这个模式
js
import Student from "../viewmodel/StudentModel.ets"
@State students:Array<Student> = [
new Student(1,"王小二","成都"),
new Student(2,"张晓名","南京")
]
按照你构造的数据模型来设计我们组件内部数据
@Prop父子组件单向状态
@Prop装饰的变量和父组件建立单向的同步关系:
- @Prop变量允许在本地修改,但修改后的变化不会同步回父组件。
- 当父组件中的数据源更改时,与之相关的@Prop装饰的变量都会自动更新。如果子组件已经在本地修改了@Prop装饰的相关变量值,而在父组件中对应的@State装饰的变量被修改后,子组件本地修改的@Prop装饰的相关变量值将被覆盖。
子组件:
js
import Student from "../viewmodel/Student"
@Component
export default struct Children {
//基本数据类型
@State msg:string = "xiaowang"
@State user:Array<{id:number,name:string}> = [{id:1,name:"xiaowang"},{id:2,name:"xiaofei"}]
@State students:Array<Student> = [
new Student(1,"王小二","成都"),
new Student(2,"张晓名","南京")
]
@Prop username:string
@Prop classes:string
}
@Prop来定义的变量可以不用初始化,等待父组件传递过来初始化
父组件
js
import ChildrenCom from "../views/ChildrenCom"
@Entry
@Component
struct CompConnect {
@State message: string = 'Hello World'
@State username:string = "南京"
@State classes:string = "10期"
build() {
Column() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
//传递参数给子组件
ChildrenCom({username:this.username,classes:this.classes})
Text(`父组件的username${this.username}`)
}
.height('100%')
}
}
@Link父子组件双向状态
子组件
js
@Component
export default struct Children {
//基本数据类型
@State msg: string = "xiaowang"
@State user: Array<{
id: number,
name: string
}> = [{ id: 1, name: "xiaowang" }, { id: 2, name: "xiaofei" }]
@State students: Array<Student> = [
new Student(1, "王小二", "成都"),
new Student(2, "张晓名", "南京")
]
@Prop username:string
@Prop classes:string
@Link companyName:string
}
使用@Link来定义子组件双向通信的变量
父组件doubleData
属性用于传递给子组件
js
import ChildrenCom from "../views/ChildrenCom"
@Entry
@Component
struct CompConnect {
@State message: string = 'Hello World'
@State username:string = "南京"
@State classes:string = "10期"
@State doubleData:string = "成都蜗牛学院"
build(){
ChildrenCom({companyName:$doubleData})
}
}
其中变量前面必须传递$代表引用类型传递。