鸿蒙系统应用基础开发

0x01 概要叙述

(1)鸿蒙系统

  • 鸿蒙是华为公司开发的操作系统,在多端使用
    • 以手机为中心,包括手表、平板等
    • “万物互联”思想
  • 各类应用间接为用户带来操作系统的用途
    • “鸿蒙应用千帆起,轻舟已过万重山”

(2)准备工作

a. 语言

  • 鸿蒙系统应用的开发语言:ArkTS
    • 是 TypeScript 的超集
  • 统一了编程体验
    • ArkTS 包括 HTML、CSS、JavaScript

区别:ArkTS 是语言,ArkUI 是框架

b. 工具

开发工具:DevEco Studio

  1. 官网下载安装包并打开
  2. 遇到多选可以全选
  3. 安装完成后不需要重启
  4. Basic Setup 中全部使用 Install
    • 如果本地存在 Node.js、Ohpm,可以选择 Local 并定位到相关目录
    • 建议使用 Install 安装官方推荐版本
    • Ohpm(Open Harmony Package Management)是开放鸿蒙包管理器
  5. SDK Setup 中全部选择 Accept
  6. 选择 Create Project 创建项目
  7. 选择 Empty Ability
  8. 配置项目信息
    1. Project name:项目名称
    2. Bundle name:应用上线的唯一标识
      • 公司域名翻转与应用名称,如 com.example.application
    3. Save location:项目保存路径
    4. Compile SDK:编译开发工具包
    5. Model:选择模型(FA 是鸿蒙开发早期模型)
    6. Device type:选择应用适配设备

c. 项目目录

graph TB app-->AppScope & entry entry-->src-->main & ohosTest main-->ets & resource ets-->entryability & pages entryability-->EntryAbility.ts pages-->Index.ets resource-->en_US & zh_CN & module.json5
  • 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") }) 
  • 可以使用 bindthis 绑定到普通函数中

    @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)     }   } } 

    完整代码:https://gitee.com/srigt/harmony/blob/master/图片宽度自定义

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. 自定义对话框

  1. 构建自定义对话框组件

    @CustomDialog struct MyDialog {   controller: CustomDialogController   build() {     Column() {       Text("自定义对话框")       Button("关闭对话框")         .onClick(() => {           this.controller.close()         })     }   } } 
  2. 将对话框组件注册到页面中

    @Entry @Component struct Index {   controller: CustomDialogController = new CustomDialogController({     builder: MyDialog({})   }) } 
  3. 绑定点击事件触发对话框

    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  })       // ...     }   } } 

    完整代码:https://gitee.com/srigt/harmony/tree/master/商品列表

(3)页面布局

a. 线性布局

  • Row:行布局,从左至右
    • 主轴:从左至右
    • 侧轴(交叉轴):从上至下
  • Column:列布局,从上至下
    • 主轴:从上至下
    • 侧轴(交叉轴):从左至右
  • 主轴使用 justifyContent(FlexAlign.*) 调整对齐,FlexAlign 枚举包括:
    • Start:从开始处(默认)
    • Cneter:居中
    • End:从结束处
    • SpaceBetween:均分且开始和结束处不留空间
    • SpaceAround:均分且间隔比为 (0.5:1:1: ldots :1:0.5)
    • SpaceEvenly:均分且间隔空间相同
  • 侧轴使用 aligmItems 调整对齐,分为:
    • VerticalAlignRow 行布局,其枚举包括:
      • Top:从顶部
      • Center:居中(默认)
      • Bottom:从底部
    • HorizontalAlignColumn 列布局,其枚举包括:
      • 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)数据请求

  • 一般数据请求步骤

    1. 导入对应模块

      import http from '@ohos.net.http'; 
    2. 在方法中,创建 HTTP 请求对象

      import http from '@ohos.net.http';  @Component struct Index {   httpHandler() {     let httpRequest = http.createHttp()   }   // ... } 
    3. 调用 request(url, options) 发送请求

      httpHandler() {   let httpRequest = http.createHttp()   let promise = httpRequest.request(     "http://example.com",     {       method: http.RequestMethod.GET     }   ) } 
      • url 为请求地址、options 为请求配置
      • 一个 HTTP 请求对象仅能调用一次 request() 方法
    4. 获取响应结果

      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());     }   ) } 
  • 默认采用异步方式请求

    • 可以使用 asyncawait 变为同步

      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)条件渲染

  • 使用 ifelseelse if 语句

  • ifelse 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 会监视对象和其中属性值的变化

  • 如果属性值是对象,且该对象的值发生了变化,则可以使用以下方法监视:

    • 重新 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)             })           }         })       }     }   } } 

      完整代码:https://gitee.com/srigt/harmony/tree/master/待办列表

(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 的原理是:将父组件的属性值复制一份到子组件

  • @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)使用步骤

  1. src/main/resources/base/profile/main_pages.json 中注册路由

    {   "src": [     "pages/A",     "pages/B"   ] } 
  2. 在页面中引入路由模块:import router from '@ohos.router'

  3. 在 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 方法返回上一个页面
  4. 在 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%')   } } 
  5. 使用 A 页面传递参数

    router.pushUrl({   url: 'pages/B',   params: {     name: "张三",     age: 18   } }) 
  6. 在 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-

发表评论

相关文章