鸿蒙系统应用基础开发

· 浏览次数 : 0

小编点评

**代码解析:** 该代码展示了如何使用路由模式路由和单例模式来实现页面导航和数据传递。 **路由模式:** * **单例模式 (Single):**每个页面仅创建一次,并使用 `router.pushUrl()` 方法在页面加载时自动加载。 * **单例模式 (Standard):**每个页面都会创建多次,并在页面加载之前创建一次。 **路由配置:** * `pages/A` 和 `pages/B` 是页面路由的地址。 * `router.pushUrl()` 方法用于在 `pages/B` 页面中打开 `pages/A` 页面。 **单例模式的实现:** * `A` 页面使用 `router.pushUrl()` 方法将地址 `pages/B` 传递给 `B` 页面。 * `B` 页面使用 `router.getParams()` 方法从请求参数中提取 `name` 和 `age` 属性,并将这些属性赋值给 `person` 对象。 **单例模式的缺点:** * 每个页面仅创建一次,可能会导致性能下降。 * 如果页面数量较多,单例模式可能会导致内存问题。 **双例模式的实现:** * `B` 页面使用 `router.pushUrl()` 方法将地址 `pages/B` 和 `params` 传递给 `A` 页面。 * `A` 页面使用 `router.getParams()` 方法从请求参数中提取 `name` 和 `age` 属性,并将这些属性赋值给 `person` 对象。 **双例模式的优点:** * 每个页面创建时只创建了一次,性能比单例模式更好。 * 如果页面数量较多,双例模式可以有效地利用内存。 **使用示例:** 假设我们有一个名为 `index` 的页面,它包含一个列表元素。我们可以使用以下代码导航到 `pages/B` 页面: ``` /index#page-b ``` **总结:** 该代码展示了如何使用路由模式和单例或双例模式实现页面导航和数据传递。单例模式适合于场景,其中页面数量较少且性能不敏感。双例模式适合于场景,其中页面数量较多且性能非常重要。

正文

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-

与鸿蒙系统应用基础开发相似的内容:

鸿蒙系统应用基础开发

0x01 概要叙述 (1)鸿蒙系统 鸿蒙是华为公司开发的操作系统,在多端使用 以手机为中心,包括手表、平板等 “万物互联”思想 各类应用间接为用户带来操作系统的用途 “鸿蒙应用千帆起,轻舟已过万重山” (2)准备工作 a. 语言 鸿蒙系统应用的开发语言:ArkTS 是 TypeScript 的超集

鸿蒙HarmonyOS实战-Stage模型(UIAbility组件)

一、UIAbility组件 1.概述 HarmonyOS中的Stage模型是一种基于UIAbility组件的应用程序架构。UIAbility是HarmonyOS系统中用于构建用户界面的基本组件之一。它负责处理应用程序界面的显示和交互。 在Stage模型中,每个应用程序都有一个或多个Stage

NodeJS 实战系列:DevOps 尚未解决的问题

本文将通过展示 NodeJS 应用里环境变量的提取过程,来一窥 DevOps 技术是如何应用在现在云平台上的运维工作中的。同时我也想让大家在这里看到 DevOps 的另外一面,即它并非全能,从本地开发到持续部署再到实际运行,有一些运维鸿沟依然还未被填平。“人工操作”依然是工作中的最大风险。

鸿蒙HarmonyOS实战-Web组件(基本使用和属性)

前言 Web是一种基于互联网的技术和资源的网络服务系统。它是指由许多互连的计算机组成的全球性计算机网络,使用户能够通过浏览器访问和交互式使用各种信息和资源,如网页、文档、图片、视频、音频等。通过Web,用户可以浏览网页、发送电子邮件、参与在线社交网络、进行在线购物等各种活动。Web的核心技术包括

鸿蒙HarmonyOS实战-Stage模型(开发卡片事件)

一、开发卡片事件 HarmonyOS元服务卡片页面(Metaservice Card Page)是指在HarmonyOS系统中,用于展示元服务的页面界面。元服务是指一组提供特定功能或服务的组件,例如天气服务、音乐播放服务等。元服务卡片页面可以显示元服务的相关信息和操作选项,用户可以通过点击卡片页

鸿蒙HarmonyOS实战-Stage模型(开发卡片页面)

一、开发卡片页面 HarmonyOS元服务卡片页面(Metaservice Card Page)是指在HarmonyOS系统中,用于展示元服务的页面界面。元服务是指一组提供特定功能或服务的组件,例如天气服务、音乐播放服务等。元服务卡片页面可以显示元服务的相关信息和操作选项,用户可以通过点击卡片页

鸿蒙HarmonyOS实战-Stage模型(ExtensionAbility组件)

一、ExtensionAbility组件 1.概念 HarmonyOS中的ExtensionAbility组件是一种能够扩展系统功能的能力组件。它可以通过扩展系统能力接口,为应用程序提供一些特定的功能,以满足应用程序的特殊需求。 ExtensionAbility组件的特点包括: 系统组件:E

鸿蒙HarmonyOS实战-ArkTS语言基础类库(并发)

一、并发 并发是指在一个时间段内,多个事件、任务或操作同时进行或者交替进行的方式。在计算机科学中,特指多个任务或程序同时执行的能力。并发可以提升系统的吞吐量、响应速度和资源利用率,并能更好地处理多用户、多线程和分布式的场景。常见的并发模型有多线程、多进程、多任务、协程等。 1.并发概述 Ha

鸿蒙HarmonyOS实战-ArkUI动画(放大缩小视图)

前言 在HarmonyOS中,可以通过以下方法放大缩小视图: 使用缩放手势:可以使用双指捏合手势来放大缩小视图。将两个手指放在屏幕上,并向内或向外移动手指,即可进行放大或缩小操作。 使用系统提供的缩放控件:在HarmonyOS的开发中,可以使用系统提供的缩放控件来实现视图的放大缩小功能。通过在布

厦门狄耐克:助推智慧医疗,需要夯实自身的技术底座

摘要:在推动医疗信息化发展的进程中,厦门狄耐克联合华为云DTSE团队,共同推出了智慧医护空间解决方案,将原有的Android系统替换成Open Harmony,打造了基于开源鸿蒙统一技术底座的智慧医院生态。 本文分享自华为云社区《华为云DTSE团队联合厦门狄耐克打造智慧医护空间解决方案》,作者:华为