鴻蒙系統應用基礎開發

SRIGT發表於2024-04-26

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-

相關文章