鸿蒙系统应用基础开发
0x01 概要叙述
(1)鸿蒙系统
- 鸿蒙是华为公司开发的操作系统,在多端使用
- 以手机为中心,包括手表、平板等
- “万物互联”思想
- 各类应用间接为用户带来操作系统的用途
- “鸿蒙应用千帆起,轻舟已过万重山”
(2)准备工作
a. 语言
- 鸿蒙系统应用的开发语言:ArkTS
- 是 TypeScript 的超集
- 统一了编程体验
- ArkTS 包括 HTML、CSS、JavaScript
区别:ArkTS 是语言,ArkUI 是框架
b. 工具
开发工具:DevEco Studio
- 在官网下载安装包并打开
- 遇到多选可以全选
- 安装完成后不需要重启
- Basic Setup 中全部使用 Install
- 如果本地存在 Node.js、Ohpm,可以选择 Local 并定位到相关目录
- 建议使用 Install 安装官方推荐版本
- Ohpm(Open Harmony Package Management)是开放鸿蒙包管理器
- SDK Setup 中全部选择 Accept
- 选择 Create Project 创建项目
- 选择 Empty Ability
- 配置项目信息
- Project name:项目名称
- Bundle name:应用上线的唯一标识
- 公司域名翻转与应用名称,如 com.example.application
- Save location:项目保存路径
- Compile SDK:编译开发工具包
- Model:选择模型(FA 是鸿蒙开发早期模型)
- Device type:选择应用适配设备
c. 项目目录
- app:应用模块
- entry:入口模块
- 在一个项目中可能会包含多个模块,但 entry 是唯一的主模块
- AppScope:应用全局配置
- 各个模块可以共享的配置
- src:开发的源代码目录
- main:主目录
- ohosTest:测试目录
- EntryAbility.ts:当前模块入口文件
- pages:页面目录
- Index.ets:主页面,每个页面的文件的后缀为 ets
- resources:资源目录,包括文本、图片、音视频等;还包括国际化相关功能子目录,如 en-US、zh-CN
- module.json5:当前模块配置文件
d. 页面代码结构
-
一个应用中往往包含多个页面
-
一个页面就是一个结构描述
- 关键字
struct
用于描述一个自定义组件,名称与文件名相同,使用驼峰命名法 - 页面是组件,而组件不一定是页面,
- 一个页面可以拆分成多个组件,提高组件复用率(组件化)
- 关键字
-
一个组件中必须包含以下内容:
build()
,用于构建组件 UI 界面,其中:- 一般编写 ArkTS 提供的内置组件
- 只能包含一个根组件
@Component
/@CustomDialog
,组件装饰器/自定义对话框装饰器
-
@Entry
,该装饰器可以将组件作为单独页面在 Preview 中进行预览 -
@State
,该装饰器作用于组件的内部变量,当变量修改后(数据监视),页面会自动重新渲染;声明时必须初始化 -
组件可以不断进行嵌套
build() { Row() { Column() { Row() { // ... } } } }
-
组件是一个封装好的对象,包括属性(样式)、方法(事件)
build() { Column() { Text("Item 1") Text("Item 2") Text("Item 3") }.width(300) }
300
是虚拟像素,根据屏幕换算- 对于列容器,默认宽高由内容决定
-
举例:
@Entry @Component struct Index { @State message: string = 'Hello World' build() { Row() { Column() { Text(this.message) .fontSize(50) .fontWeight(FontWeight.Bold) } .width('100%') } .height('100%') } }
e. 调试
- 工具中的 Preview 提供单页面预览功能
- 工具中的 Device Manager 中允许连接并使用模拟真机进行调试
(3)常用样式
a. 文本样式
-
fontSize
:字号Text("Hello, world!").fontSize(60)
-
fontColor
:文本颜色Text("Hello, world!").fontColor(Color.Red) // 或 Text("Hello, world!").fontColor("#f00")
-
fontWeight
:字重Text("Hello, world!").fontWeight(FontWeight.Bold) // 或 Text("Hello, world!").fontWeight(800)
-
fontStyle
:字样Text("Hello, world!").fontStyle(FontStyle.Normal) // 常规 Text("Hello, world!").fontStyle(FontStyle.Italic) // 斜体
-
fontFamily
:字体 -
textAlign
:对齐方向Text("Hello, world!") .width("100%") .textAlign(TextAlign.Start)
- 必须指定宽度后,才可以设置对齐方向
-
lineHeight
:行高Text("Hello, world!").lineHeight(300)
-
decoration
:划线Text("Hello, world!").decoration({type: TextDecorationType.Underline}) // 下划线 Text("Hello, world!").decoration({type: TextDecorationType.Overline}) // 上划线 Text("Hello, world!").decoration({type: TextDecorationType.LineThrough}) // 删除线
b. 背景样式
backgroundColor
:背景颜色backgroundImage
:背景图片
c. 盒子模型
-
width
:宽度 -
height
:高度 -
padding
:内边距Text("Hello, world!").padding({top:10})
-
border
:边框Text("Hello, world!").border({style: BorderStyle.Solid, color: Color.Red, radius: 50})
-
margin
:外边距Text("Hello, world!").margin(10)
-
列间距(行间距同理)
Column({space: 16}) {}
(4)常用事件
-
事件三要素:事件源、事件类型、事件处理
- 事件处理推荐使用箭头函数:
(参数列表) => 函数体
,方便访问组件内其他属性与方法
Button("Click").onClick(()=>{ console.log("Log") })
- 事件处理推荐使用箭头函数:
-
可以使用
bind
将this
绑定到普通函数中@Entry @Component struct Index { text: string = "This is a piece of text." handle() { console.log(this.text); } build() { Column() { Button("Click").onClick(this.handle.bind(this)) } .width("100%") .height("100%") } }
0x02 页面设计
(1)ArkUI 常用内置组件
a. Text 文本组件
-
语法:
Text(content?: string | Resource)
-
长文本最大行数与省略显示
Text("This is a long long sentence.") .width(100) .maxLines(1) .textOverflow({overflow:TextOverflow.Ellipsis})
-
国际化
-
src/main/resources/base/element/string.json
{ "string": [ { "name": "username", "value": "用户名" }, { "name": "password", "value": "密码" } ] }
-
src/main/resources/en_US/element/string.json
{ "string": [ { "name": "username", "value": "username" }, { "name": "password", "value": "password" } ] }
-
src/main/resources/zh_CN/element/string.json
{ "string": [ { "name": "username", "value": "用户名" }, { "name": "password", "value": "密码" } ] }
-
Index.ets
Column() { Row() { Text($r('app.string.username')) .fontSize(50) } Row() { Text($r('app.string.password')) .fontSize(50) } } .width('100%') .height('100%')
-
b. TextInput 输入框组件
-
语法:
TextInput(value?:{placeholder?: ResourceStr, text?: ResourceStr, controller?: TextInputController})
-
登录表单
Column({space: 20}) { Row() { Text($r('app.string.username')) .fontSize(22) .width("20%") TextInput({placeholder: "输入账号"}) .width("70%") } Row() { Text($r('app.string.password')) .fontSize(22) .width("20%") TextInput({placeholder: "输入密码"}) .width("70%") .type(InputType.Password) } }
c. Button 按钮组件
-
语法:
Button(options?: {type?: ButtonType, stateEffect?: boolean})
-
登录表单按钮组
Row({ space: 20 }) { Button($r('app.string.login')) .fontSize(22) Button($r('app.string.reset')) .fontSize(22) .type(ButtonType.Normal) }
-
完善登录页面
@Entry @Component struct Index { @State username: string = "" @State password: string = "" build() { Column({space: 20}) { Row() { Text("登录 Login") } Row() { Text($r('app.string.username')) TextInput({placeholder: "输入账号", text: this.username}) .onChange(content => this.username = content) } Row() { Text($r('app.string.password')) TextInput({placeholder: "输入密码", text: this.password}) .type(InputType.Password) .onChange(content => this.password = content) } Row({ space: 20 }) { Button($r('app.string.login')) .onClick(() => { console.log("username:" + this.username) console.log("password:" + this.password) }) Button($r('app.string.reset')) .onClick(() => { this.username = "" this.password = "" }) } } } }
d. Blank 空白组件
-
语法:
Blank(min?: number | string)
-
占据父容器中剩余空间
-
调整表单对齐
Column({ space: 20 }) { Row() { Text("Item1") Blank() TextInput() .width(200) } .width("80%") Row() { Text("Item2") Blank() TextInput() .width(200) } .width("80%") } .width('100%') .height('100%')
e. Image 图片组件
-
语法:
Image(src: string | PixelMap | Resource)
-
可以渲染与展示本地图片和网络图片
Column({ space: 20 }) { Image($r('app.media.logo')) .width("50%") Image("https://developer.huawei.com/allianceCmsResource/resource/HUAWEI_Developer_VUE/images/HW-LOGO.svg") .width("50%") } .width('100%') .height('100%')
f. Slider 滑块组件
-
语法:
Slider(options?: {value?: number, min?: number, max?: number, step?: number, style?: SliderStyle, direction?: Axis, reverse?: boolean})
-
举例
Column({ space: 20 }) { Slider({ min: 0, // 最小值 max: 20, // 最大值 value: this.value, // 当前值 step: 2, // 步长 style: SliderStyle.InSet // 样式 }) .trackColor(Color.Red) // 轨道颜色 .selectedColor(Color.Pink) // 选中颜色 .trackThickness(9) // 轨道厚度 .onChange(value => this.value = value) Text(this.value.toString()) } .width('100%') .height('100%') .backgroundColor("#ccc")
-
图片尺寸设置案例:
@Entry @Component struct Index { @State widthValue:number = 100 minWidth:number = 50 maxWidth:number = 340 build() { Column({ space: 20 }) { Text("图片尺寸设置") Row() { Image($r("app.media.image")) .width(this.widthValue) } Row() { Text("图片宽度 ") TextInput({ text: parseInt(this.widthValue.toFixed(0)).toString() }) .onChange(value => this.widthValue = widthValue) } Row() { Button("缩小").onClick(() => this.widthValue -= 1) Button("放大").onClick(() => this.widthValue += 1) } Slider({ min: this.minWidth, max: this.maxWidth, value: this.widthValue, step: 1 }) .onChange(value => this.widthValue = value) } } }
g. List 列表组件
-
语法:
List(value?:{space?: number | string, initialIndex?: number, scroller?: Scroller})
-
其子组件只能是
ListItem(value?: string)
或ListItemGroup(options?: {header?: CustomBuilder, footer?: CustomBuilder, space?: number | string})
- 在
ListItem
中可以使用其他组件 ListItem
组件的swipeAction()
支持侧滑手势,其中传入组件用于设置侧滑的内容
- 在
-
举例 1:电商平台商品列表
import router from '@ohos.router' interface IProduct { id: number, imageURL: string, name: string, price: number, discounted?: number } @Entry @Component struct Page { titleBgColor: string = "#fafafa" contentBgColor: string = "#eee" products: Array<IProduct> = [ { id: 1, imageURL: "", name: "Product 1", price: 7599, discounted: 500 } ] build() { Column() { Row() { Button() { Image($r('app.media.arrow')) } .onClick(() => { router.back() }) Text("商品列表") Blank() Button() { Image($r('app.media.refresh')) } .onClick(() => { console.log("Refresh") }) } .backgroundColor(this.titleBgColor) List({ space: 20 }) { ForEach(this.products, (item) => { ListItem() { Row() { Image(item.imageURL) Column({ space: 10 }) { Text(item.name) if(item.discounted) { Text("价格:¥" + item.price) .fontColor("#aaa") .decoration({ type: TextDecorationType.LineThrough}) Text("折后价:¥" + (item.price - item.discounted)) Text("优惠:¥" + item.discounted) } else { Text("价格:¥" + item.price) } } .layoutWeight(1) } } .border({ width: 2, style: BorderStyle.Solid, color: this.contentBgColor, radius: 20 }) }) } } .backgroundColor(this.contentBgColor) } }
-
举例 2:通讯录
interface IAddressItem { group: string, contactList: string[] } @Entry @Component struct Index { addressBook: IAddressItem[] = [ { group: "家人", contactList: ["张三", "李四"] }, { group: "朋友", contactList: ["王五", "赵六"] }, { group: "同事", contactList: ["田七"] } ] @Builder groupHeader(group: string) { Text(group) .fontSize(30) .fontWeight(FontWeight.Bold) } build() { Column() { Text("通讯录") .fontSize(50) .fontWeight(FontWeight.Bolder) List({ space: 20 }) { ForEach(this.addressBook, (item:IAddressItem) => { ListItemGroup({ header: this.groupHeader(item.group) }) ForEach(item.contactList, (item:string) => { ListItem() { Text(item) .fontSize(20) } }) }) } } .width("100%") .height('100%') .padding({ left: 10, right: 10 }) } }
h. 自定义对话框
-
构建自定义对话框组件
@CustomDialog struct MyDialog { controller: CustomDialogController build() { Column() { Text("自定义对话框") Button("关闭对话框") .onClick(() => { this.controller.close() }) } } }
-
将对话框组件注册到页面中
@Entry @Component struct Index { controller: CustomDialogController = new CustomDialogController({ builder: MyDialog({}) }) }
-
绑定点击事件触发对话框
build() { Column() { Button("开启对话框") .onClick(() => { this.controller.open() }) } .width('100%') .height('100%') }
i. 自定义导航
-
语法:
Tabs(value?: {barPosition?: BarPosition, index?: number, controller?: TabsController})
-
举例:
@Component struct AComponent { build() { Text("A 组件内容") } } @Component struct BComponent { build() { Text("B 组件内容") } } @Component struct CComponent { build() { Text("C 组件内容") } } @Entry @Component struct Index { @State currentIndex: number = 0 @Builder customTabBarContent(icon: Resource, title: string, index: number) { Column({ space: 6 }) { Image(icon) .width(20) .fillColor(this.currentIndex == index ? Color.Green : Color.Black) Text(title) .fontSize(16) .fontColor(this.currentIndex == index ? Color.Green : Color.Black) } } build() { Column() { Tabs() { TabContent() { AComponent() }.tabBar(this.customTabBarContent($r("app.media.icon"), "A 组件", 0)) TabContent() { BComponent() }.tabBar(this.customTabBarContent($r("app.media.icon"), "B 组件", 1)) TabContent() { CComponent() }.tabBar(this.customTabBarContent($r("app.media.icon"), "C 组件", 2)) } .barPosition(BarPosition.End) .vertical(false) // 不使用垂直布局 .scrollable(false) // 关闭页面滑动切换 .onChange((index: number) => { this.currentIndex = index }) } .width("100%") .height("100%") } }
(2)组件化开发
- 组件化:将整个页面分割为多个部分,并使用单独的组件描述每个部分,使得一个页面由多个组件构成
a. @Builder 自定义构建函数
-
构建函数中只能写入组件
-
语法:
@Builder 函数名(参数) { 函数体; }
-
自定义构建函数可以在
build()
中调用 -
完善电商平台商品列表
@Builder header() { Row() { Button() { Image($r('app.media.arrow')) } .backgroundColor(this.titleBgColor) .onClick(() => { router.back() }) Text("商品列表") Blank() Button() { Image($r('app.media.refresh')) } .backgroundColor(this.titleBgColor) .onClick(() => { console.log("Refresh") }) } .backgroundColor(this.titleBgColor) } @Builder productCard(item:IProduct) { Row() { Image(item.imageURL) Column({ space: 10 }) { Text(item.name) if(item.discounted) { Text("价格:¥" + item.price) .fontColor("#aaa") .decoration({ type: TextDecorationType.LineThrough}) Text("折后价:¥" + (item.price - item.discounted)) Text("优惠:¥" + item.discounted) } else { Text("价格:¥" + item.price) } } } } build() { Column() { this.header() List({ space: 20 }) { ForEach(this.products, (item) => { ListItem() { this.productCard(item) } .border({ width: 2, style: BorderStyle.Solid, color: this.contentBgColor, radius: 20 }) }, (item:IProduct) => { return item.id.toString() }) } } .backgroundColor(this.contentBgColor) }
b. @Component 自定义组件
-
自定义构建函数仅能在当前组件中使用,无法复用到其他组件,因此需要自定义组件
-
一般写在 ets/components 目录下
-
完善电商平台商品列表
-
components/Header.ets
import router from '@ohos.router' @Component export default struct Header { title: string = "Undefined" titleBgColor: string = "#fafafa" contentBgColor: string = "#eee" build() { Row() { Button() { Image($r('app.media.arrow')) } .backgroundColor(this.titleBgColor) .onClick(() => { router.back() }) Text(this.title) Blank() Button() { Image($r('app.media.refresh')) } .backgroundColor(this.titleBgColor) .onClick(() => { console.log("Refresh") }) } .backgroundColor(this.titleBgColor) } }
-
entry/Page.ets
build() { Column() { Header({ title: "商品列表" }) // ... } }
-
-
自定义组件使用成本更高,但复用性更强,且其中数据独立
c. @BuilderParam 构建参数
-
将自定义构建函数作为参数传递到自定义组件
-
完善电商平台商品列表
-
components/Header.ets
@Component export default struct Header { // ... @BuilderParam rightItem: () => void build() { Row() { // ... this.rightItem() } } }
-
entry/Page.ets
import Header from '../components/Header' @Entry @Component struct Page { // ... @Builder refreshButton() { Button() { Image($r('app.media.refresh')) } .onClick(() => { console.log("Refresh") }) } build() { Column() { Header({ title: "商品列表", rightItem: this.refreshButton }) // ... } } }
-
(3)页面布局
a. 线性布局
Row
:行布局,从左至右- 主轴:从左至右
- 侧轴(交叉轴):从上至下
Column
:列布局,从上至下- 主轴:从上至下
- 侧轴(交叉轴):从左至右
- 主轴使用
justifyContent(FlexAlign.*)
调整对齐,FlexAlign
枚举包括:Start
:从开始处(默认)Cneter
:居中End
:从结束处SpaceBetween
:均分且开始和结束处不留空间SpaceAround
:均分且间隔比为 \(0.5:1:1:\ \ldots\ :1:0.5\)SpaceEvenly
:均分且间隔空间相同
- 侧轴使用
aligmItems
调整对齐,分为:VerticalAlign
:Row
行布局,其枚举包括:Top
:从顶部Center
:居中(默认)Bottom
:从底部
HorizontalAlign
:Column
列布局,其枚举包括:Start
:从开始处Center
:居中(默认)End
:从结束处
layoutWeight
:填充父容器主轴方向的空闲空间
b. 层叠布局
-
子组件按照顺序依次入栈,后一个子组件覆盖前一个子组件
-
语法:
Stack(value?: { alignContent?: Alignment })
-
举例:
@Entry @Component struct Index { build() { Stack({}) { Column() {} .width('100%') .height('100%') .backgroundColor(Color.Red) Row() {} .width("50%") .height("50%") .backgroundColor(Color.Green) } } }
c. 网格布局
-
由行列分割的单元格所组成,通过指定项目所在的单元格完成布局
-
语法:
Grid(scroller?: Scroller)
-
类似 List 组件,Grid 要求其中每一项的子组件包含在
GridItem
中 -
常用属性:
rowsTemplate()
:行模板,设置每行的模板,包括列数与列宽columnsTemplate()
:列模板,设置每列的模板,包括行数与行宽rowsGap()
:行间距columnsGap()
:列间距
-
举例:
@Entry @Component struct Page { array: number[] = [1, 2, 3, 4, 5, 6] build() { Grid() { ForEach(this.array, (item: number) => { GridItem() { Text(item.toString()) .width("100%") .height(100) .border({ width: 2, color: Color.Black }) .fontSize(30) } }) } .width("100%") .height(220) .rowsTemplate("1fr 1fr") .columnsTemplate("1fr 1fr 1fr") .rowsGap(10) .columnsGap(10) } }
(4)数据请求
-
一般数据请求步骤
-
导入对应模块
import http from '@ohos.net.http';
-
在方法中,创建 HTTP 请求对象
import http from '@ohos.net.http'; @Component struct Index { httpHandler() { let httpRequest = http.createHttp() } // ... }
-
调用
request(url, options)
发送请求httpHandler() { let httpRequest = http.createHttp() let promise = httpRequest.request( "http://example.com", { method: http.RequestMethod.GET } ) }
url
为请求地址、options
为请求配置- 一个 HTTP 请求对象仅能调用一次
request()
方法
-
获取响应结果
httpHandler() { let httpRequest = http.createHttp() let promise = httpRequest.request( "http://example.com", { method: http.RequestMethod.GET } ) promise.then( (httpResponse:http.HttpResponse) => { console.log('Result: ' + httpResponse.result.toString()); } ) }
-
-
默认采用异步方式请求
-
可以使用
async
与await
变为同步Button("登录") .fontSize(22) .onClick(async () => { let httpRequest = http.createHttp() let response = await httpRequest.request( `http://10.200.21.163:8080/login?username=${this.username}&password=${this.password}`, { method: http.RequestMethod.GET } ) console.log(response.result.toString()) })
-
-
完善登录页面
Button("登录") .fontSize(22) .onClick(() => { let httpRequest = http.createHttp() let promise = httpRequest.request( `http://localhost:8080/login?username=${this.username}&password=${this.password}`, { method: http.RequestMethod.GET } ) promise.then((httpResponse:http.HttpResponse) => { console.log(httpResponse.result.toString()) }) })
(5)动画效果
-
通过设置关键帧实现动画效果
-
使用
animateTo(value: AnimateParam, event: () => void): void
方法value
:对象类型,用于配置动画参数,包括延时、变化曲线等event
:回调函数,用于配置动画关键帧的数据
-
举例:
@Entry @Component struct Index { @State scaleX: number = 0 @State scaleY: number = 0 build() { Column({ space: 30 }) { Button("开始动画") .margin(30) .onClick(() => { animateTo({ duration: 500 }, () => { this.scaleX = 1 this.scaleY = 1 }) }) Row() .width(200) .height(200) .backgroundColor(Color.Red) .scale({ x: this.scaleX, y: this.scaleY }) } .width("100%") .height("100%") } }
0x03 渲染控制
(1)条件渲染
-
使用
if
、else
和else if
语句 -
if
、else if
后跟随的条件语句可以使用状态变量 -
调整登录按钮
Button() { Row() { if(this.isLoading) { LoadingProgress() .width(30) .color(Color.White) } else { Text("登录") .fontSize(22) .fontColor(Color.White) } } }
(2)循环渲染
-
使用
ForEach
语句 -
语法:
ForEach( arr: Array, itemGenerator: (item: any, index: number) => void, keyGenerator?: (item: any, index: number) => string )
arr
:数组,数组包含多个元素,数组长度决定组件渲染个数itemGenerator
:子组件生成函数,用于生成页面组件,参数分别为数组每项的值与索引keyGenerator
:(可选)键值生成函数,用于指定每项的id:string
,参数分别为数组每项的值与索引
-
举例:
@Entry @Component struct Index { students:string[] = ["Alex", "Bob", "Charles", "David"] build() { Column() { ForEach(this.students, (item:string, index:number) => { Row() { Text(index.toString()) .fontSize(50) Blank() Text(item) .fontSize(50) } .width("80%") }) } .width('100%') .height('100%') } }
(3)数据懒加载
-
使用
LazyForEach
语句 -
语法:
LazyForEach( dataSource: IDataSource, itemGenerator: (item: any, index: number) => void, keyGenerator?: (item: any, index: number) => string ): void
-
用法与
ForEach
类似,其中数据源为IDataSource
interface IDataSource { totalCount(): number; getData(index: number): Object; registerDataChangeListener(listener: DataChangeListener): void; unregisterDataChangeListener(listener: DataChangeListener): void; }
totalCount
:获得数据总数getData
:获取索引值对应的数据registerDataChangeListener
:注册数据改变的监听器unregisterDataChangeListener
:注销数据改变的监听器
0x04 状态管理
(1)@State
-
状态:组件中的需要
@State
修饰器修饰的数据 -
特点:状态数据会通过声明式 UI 组件的方式展示到页面中,并且数据的变化会被 ArkUI 底层实时监控
-
如果
@State
装饰的变量是对象,则 ArkUI 会监视对象和其中属性值的变化
(2)@Observed 与 @ObjectLink
-
如果属性值是对象,且该对象的值发生了变化,则可以使用以下方法监视:
-
重新
new
一个对象 -
使用
@Observed
搭配@ObjectLink
@Observed class Car { name: string price: number constructor(name: string, price: number) { this.name = name this.price = price } } class Person { name: string car: Car constructor(name: string, car: Car) { this.name = name this.car = car } } @Component struct CarInfo { @ObjectLink car: Car build() { Text(`车名:${this.car.name}\n车价:${this.car.price}`) } } @Entry @Component struct Index { person: Person = new Person("张三", new Car("智界S7", 210000)) build() { Column() { Text(`姓名:${this.person.name}`) CarInfo({ car: this.person.car }) Button("车价减 1000") .onClick(() => { this.person.car.price -= 1000 }) } } }
-
-
如果传递方法,需要绑定
this
-
举例:待办列表
@Observed class TodoItem {} @Component struct TodoComponent { @ObjectLink item: TodoItem index: number remove: (index: number) => void customSize: number build() { Row() { Button() { Image($r("app.media.todo")) .width(this.customSize) } Text(this.item.name) Blank() Button() { Image($r('app.media.remove')) .width(this.customSize) } .onClick(() => { this.remove(this.index) }) } } } @Entry @Component struct Index { @State TodoList: TodoItem[] = [] customSize: number = 25 newItemName: string = "" remove(index: number) { this.TodoList.splice(index, 1) } @Builder Header() {} build() { Column({ space: 20 }) { Text("待办列表") this.Header() List({ space: 16 }) { ForEach(this.TodoList, (item: TodoItem, index: number) => { ListItem() { TodoComponent({ customSize: this.customSize, item: item, index: index, remove: this.remove.bind(this) }) } }) } } } }
-
(3)@Prop
-
@Prop
专门用于处理父子组件的之间单向的数据传递- 子组件数据变化不会影响父组件
-
举例:
@Component struct Child { @Prop message: string build() { Text(`Child: ${this.message}`) .fontSize(20) } } @Entry @Component struct Parent { @State message: string = 'Hello World' build() { Column({ space: 30 }) { Text(`Parent: ${this.message}`) .fontSize(20) .onClick(() => { this.message = 'Changed' }) Child({ message: this.message }) } .width('100%') .height('100%') } }
- 当触发点击事件后,父组件
message
的值发生了变化,在@Prop
的作用下,子组件也随着变化重新渲染
- 当触发点击事件后,父组件
-
区别于
@State
,@Prop
不需要在子组件初始化,而是等待来自父组件的数据 -
@Prop
只能装饰简单类型的属性 -
@Prop
的原理是:将父组件的属性值复制一份到子组件
(4)@Link
-
@Link
与@Prop
作用相同- 相同:都专门用于处理父子组件的之间的数据传递
- 不同:
@Link
可以双向数据传递,并且可以装饰任何类型的属性
-
举例:
@Component struct Child { @Link message: string build() { Text(`Child: ${this.message}`) .fontSize(20) .onClick(() => { this.message = 'Changed' }) } } @Entry @Component struct Parent { @State message: string = 'Hello World' build() { Column({ space: 30 }) { Text(`Parent: ${this.message}`) .fontSize(20) Child({ message: $message }) } .width('100%') .height('100%') } }
- 当触发点击事件后,子组件
message
的值发生了变化,在@Link
的作用下,父组件也随着变化重新渲染
- 当触发点击事件后,子组件
-
如果在子组件使用
@Link
,则父组件传递时,需要使用$
,如子组件({ 子组件属性名: $父组件属性名 })
-
@Link
的原理是,将父组件属性的地址值传递给子组件
(5)@Provide 与 @Consume
-
@Provide
与@Consume
搭配使用,实现任意组件之间双向的数据传递 -
采用隐式数据传递
- 提供方组件仅负责提供数据,而不指定目标组件
- 消费方组件直接消费来自提供方组件的数据
-
提供方可以为数据配置别名
-
举例:
@Component struct Grandchild { @Consume msg: string build() { Text(`Grandchild: ${this.msg}`) .fontSize(20) .onClick(() => { this.msg = 'Change from grandchild' }) } } @Component struct Child { build() { Grandchild() } } @Entry @Component struct Parent { @Provide('msg') message: string = 'Hello World' build() { Column({ space: 30 }) { Text(`Parent: ${this.message}`) .fontSize(20) .onClick(() => { this.message = 'Change from parent' }) Child() } .width('100%') .height('100%') } }
-
此方法对性能有所损耗(缺点)
(6)@Watch 监视器
-
监视对象是组件中的数据,当数据发生改变,监视器就会触发相应的方法
-
举例:
@Entry @Component struct Index { @State @Watch('calcTotal') array: number[] = [0, 1, 2, 3] @State total: number = 0 calcTotal(): void { this.total = 0 this.array.forEach(element => this.total += element); } aboutToAppear() { this.calcTotal() } build() { Column() { Text(`数组全部元素和为:${this.total}`) Button("向数组添加元素") .onClick(() => { this.array.push(10) }) } .width("100%") .height('100%') } }
0x05 页面路由
(1)概念
- 路由:一种实现在一个应用程序中页面之间相互跳转与数据传递的技术
- 页面栈:一个类似栈的页面容器,当前页面为栈底页面
- 为防止页面栈溢出,页面栈中最多存放 32 个页面
(2)使用步骤
-
在 src/main/resources/base/profile/main_pages.json 中注册路由
{ "src": [ "pages/A", "pages/B" ] }
-
在页面中引入路由模块:
import router from '@ohos.router'
-
在 A 页面调用方法:
pushUrl(options: RouterOptions): Promise<void>
-
其中,
RouterOptions
为:interface RouterOptions { url: string; params?: Object; }
import router from '@ohos.router' @Entry @Component struct A { build() { Column() { Text("A 页面") .fontSize(50) .fontWeight(FontWeight.Bold) Button("跳转到 B 页面") .onClick(() => { router.pushUrl({ url: 'pages/B' }) }) } .width("100%") .height("100%") } }
- 也可以使用
replaceUrl(options: RouterOptions): Promise<void>
,区别在于replaceUrl
方法会替换栈顶页面,导致无法使用下述back
方法返回上一个页面
-
-
在 B 页面调用方法:
back(options?: RouterOptions ): void
import router from '@ohos.router' @Entry @Component struct B { build() { Column() { Text("B 页面") .fontSize(50) .fontWeight(FontWeight.Bold) Button("返回") .onClick(() => { router.back() }) } .width('100%') .height('100%') } }
-
使用 A 页面传递参数
router.pushUrl({ url: 'pages/B', params: { name: "张三", age: 18 } })
-
在 B 页面接受参数
interface IParams { name: string age: number } @Entry @Component struct B { @State person: IParams = { name: "", age: 0 } aboutToAppear() { this.person = router.getParams() as IParams } build() { Column() { // ... Text(`${this.person.name} - ${this.person.age}`) } } }
(3)路由模式
-
路由模式有两种,包括:
- 单例模式:Single,每个页面仅创建一次
- 标准模式:Standard,每次都创建新的页面实例
-
路由模式在
pushUrl
方法的第二参数中指定,如:router.pushUrl( { url: 'pages/B' }, router.RouterMode.Single )
综合案例
认证
-End-