HarmonyOS 应用开发基础案例(九):我的任务清单

56 阅读5分钟

本篇案例介绍如何使用ArkTS声明式语法和基础组件,实现简易待办列表。效果为点击某一事项,替换标签图片、虚化文字。

1. 案例效果截图

2. 案例运用到的知识点

2.1. 核心知识点

  • Text组件:显示一段文本的组件。
  • Column组件:沿垂直方向布局的容器。
  • Row组件:沿水平方向布局的容器。

2.2. 其他知识点

  • ArkTS 语言基础
  • 自定义组件和组件生命周期
  • 内置组件:Column/Text/Row/Stack/Blank/Button
  • 日志管理类的编写
  • 常量与资源分类的访问
  • MVVM模式

3. 代码结构

├──entry/src/main/ets                  // ArkTS代码区
│  ├──common
│  │  └──constants
│  │     └──CommonConstants.ets   // 公共常量类
│  ├──entryability
│  │  └──EntryAbility.ets             // 程序入口类
│  ├──pages
│  │  └──ToDoListPage.ets           // 主页面
│  ├──view
│  │  └──ToDoItem.ets              // 自定义单项待办组件
│  └──viewmodel
│     └──DataModel.ets              // 列表数据获取文件
└──entry/src/main/resources	         // 资源文件目录

4. 公共文件与资源

本案例涉及到的常量类和工具类代码如下:

  1. 通用常量类
// entry/src/main/ets/common/constants/CommonConstant.ets
export default class CommonConstants {
  static readonly FULL_LENGTH: string = '100%'
  static readonly TITLE_WIDTH: string = '80%'
  static readonly LIST_DEFAULT_WIDTH: string = '93.3%'
  static readonly OPACITY_DEFAULT: number = 1
  static readonly OPACITY_COMPLETED: number = 0.4
  static readonly BORDER_RADIUS: number = 24
  static readonly FONT_WEIGHT: number = 500
  static readonly COLUMN_SPACE: number = 16
  static readonly TODO_DATA: Array<string> = [
    "看完1000本书",
    "一次无计划的旅程",
    "高空跳伞",
    "拿到驾照",
    "献血",
    "拥有自己的房子",
    "创业(无论成败)",
    "亲手种出食物并吃掉"
  ]
}

本案例涉及到的资源文件如下:

4.1. string.json

// entry/src/main/resources/base/element/string.json
{
  "string": [
    {
      "name": "module_desc",
      "value": "module description"
    },
    {
      "name": "EntryAbility_desc",
      "value": "description"
    },
    {
      "name": "EntryAbility_label",
      "value": "清单列表"
    },
    {
      "name": "page_title",
      "value": "我的清单"
    }
  ]
}

4.2. float.json

// entry/src/main/resources/base/element/float.json
{
  "float": [
    {
      "name": "checkbox_width",
      "value": "28vp"
    },
    {
      "name": "checkbox_margin",
      "value": "20vp"
    },
    {
      "name": "item_font_size",
      "value": "20fp"
    },
    {
      "name": "title_font_size",
      "value": "28fp"
    },
    {
      "name": "title_font_height",
      "value": "33vp"
    },
    {
      "name": "title_margin_top",
      "value": "24vp"
    },
    {
      "name": "title_margin_bottom",
      "value": "12vp"
    },
    {
      "name": "list_item_height",
      "value": "64vp"
    }
  ]
}

其他资源请到源码中获取。

5. 功能实现

5.1. 首页逻辑

// entry/src/main/ets/pages/ToDoListPage.ets
import DataModel from '../viewmodel/DataModel'
import CommonConstants from '../common/constant/CommonConstant'
import ToDoItem from '../view/ToDoItem'

@Entry
@Component
struct ToDoListPage {
  private totalTasks: Array<string> = []

  aboutToAppear() {
    this.totalTasks = DataModel.getData()
  }

  build() {
    Column({ space: CommonConstants.COLUMN_SPACE }) {
      Text($r('app.string.page_title'))
        .fontSize($r('app.float.title_font_size'))
        .fontWeight(FontWeight.Bold)
        .lineHeight($r('app.float.title_font_height'))
        .width(CommonConstants.TITLE_WIDTH)
        .margin({
          top: $r('app.float.title_margin_top'),
          bottom: $r('app.float.title_margin_bottom')
        })
        .textAlign(TextAlign.Start)

      ForEach(this.totalTasks, (item: string) => {
        ToDoItem({ content: item })
      }, (item: string) => JSON.stringify(item))
    }
    .width(CommonConstants.FULL_LENGTH)
    .height(CommonConstants.FULL_LENGTH)
    .backgroundColor($r('app.color.page_background'))
  }
}

5.2. 数据源获取

// entry/src/main/ets/viewmodel/DataModel.ets
import CommonConstants from '../common/constant/CommonConstant'

export class DataModel {

  private tasks: Array<string> = CommonConstants.TODO_DATA

  getData(): Array<string> {
    return this.tasks
  }
}

export default new DataModel()

5.3. 列表项视图

// entry/src/main/ets/view/ToDoItem.ets
import CommonConstants from '../common/constant/CommonConstant'

@Component
export default struct ToDoItem {
  private content?: string
  @State isComplete: boolean = false

  @Builder labelIcon(icon: Resource) {
    Image(icon)
      .objectFit(ImageFit.Contain)
      .width($r('app.float.checkbox_width'))
      .height($r('app.float.checkbox_width'))
      .margin($r('app.float.checkbox_margin'))
  }

  build() {
    Row() {
      if (this.isComplete) {
        this.labelIcon($r('app.media.ic_ok'))
      } else {
        this.labelIcon($r('app.media.ic_default'))
      }

      Text(this.content)
        .fontSize($r('app.float.item_font_size'))
        .fontWeight(CommonConstants.FONT_WEIGHT)
        .opacity(this.isComplete 
                 ? CommonConstants.OPACITY_COMPLETED 
                 : CommonConstants.OPACITY_DEFAULT)
        .decoration({ type: this.isComplete 
          ? TextDecorationType.LineThrough : TextDecorationType.None })
    }
    .borderRadius(CommonConstants.BORDER_RADIUS)
    .backgroundColor($r('app.color.start_window_background'))
    .width(CommonConstants.LIST_DEFAULT_WIDTH)
    .height($r('app.float.list_item_height'))
    .onClick(() => {
      this.isComplete = !this.isComplete
    })
  }
}

6.补充知识:自定义构建函数

ArkUI提供了一种轻量的UI元素复用机制@Builder,其内部UI结构固定,仅与使用方进行数据传递,开发者可以将重复使用的UI元素抽象成一个方法,在build方法里调用。

为了简化语言,我们将@Builder装饰的函数也称为“自定义构建函数”。

6.1 @Builder装饰器:自定义构建函数

@Builder装饰器有两种使用方式,分别是定义在自定义组件内部的私有自定义构建函数和定义在全局的全局自定义构建函数。

  1. 私有自定义构建函数

定义的语法:

@Entry
@Component
struct BuilderDemo {
  @Builder
  showTextBuilder() {
    Text('Hello World')
      .fontSize(30)
      .fontWeight(FontWeight.Bold)
  }
  @Builder
  showTextValueBuilder(param: string) {
    Text(param)
      .fontSize(30)
      .fontWeight(FontWeight.Bold)
  }
  build() {
    Column() {
      // 无参数
      this.showTextBuilder()
      // 有参数
      this.showTextValueBuilder('Hello @Builder')
    }
  }
}

使用方法:

this.showTextBuilder()
  • 允许在自定义组件内定义一个或多个@Builder方法,该方法被认为是该组件的私有、特殊类型的成员函数。
  • 私有自定义构建函数允许在自定义组件内、build方法和其他自定义构建函数中调用。
  • 在自定义函数体中,this指代当前所属组件,组件的状态变量可以在自定义构建函数内访问。建议通过this访问自定义组件的状态变量而不是参数传递。
  1. 全局自定义构建函数

定义的语法:

@Builder
function showTextBuilder() {
  Text('Hello World')
    .fontSize(30)
    .fontWeight(FontWeight.Bold)
}
@Entry
@Component
struct BuilderDemo {
  build() {
    Column() {
      showTextBuilder()
    }
  }
}

使用方法:

showTextBuilder()
  • 如果不涉及组件状态变化,建议使用全局的自定义构建方法。
  • 全局自定义构建函数允许在build方法和其他自定义构建函数中调用。
  1. 参数传递规则

自定义构建函数的参数传递有按值传递和按引用传递两种,均需遵守以下规则:

  • 参数的类型必须与参数声明的类型一致,不允许undefined、null和返回undefined、null的表达式。
  • 在@Builder修饰的函数内部,不允许改变参数值。
  • @Builder内UI语法遵循UI语法规则。
  • 只有传入一个参数,且参数需要直接传入对象字面量才会按引用传递该参数,其余传递方式均为按值传递。

6.2 @LocalBuilder装饰器: 维持组件父子关系

当开发者使用@Builder做引用数据传递时,会考虑组件的父子关系,使用了bind(this)之后,组件的父子关系和状态管理的父子关系并不一致。为了解决组件的父子关系和状态管理的父子关系保持一致的问题,引入@LocalBuilder装饰器。@LocalBuilder拥有和局部@Builder相同的功能,且比局部@Builder能够更好的确定组件的父子关系和状态管理的父子关系。

  1. 装饰器使用说明

定义的语法:

@LocalBuilder MyBuilderFunction() { ... }

使用方法:

this.MyBuilderFunction()
  • 允许在自定义组件内定义一个或多个@LocalBuilder方法,该方法被认为是该组件的私有、特殊类型的成员函数。
  • 自定义构建函数可以在所属组件的build方法和其他自定义构建函数中调用,但不允许在组件外调用。
  • 在自定义函数体中,this指代当前所属组件,组件的状态变量可以在自定义构建函数内访问。建议通过this访问自定义组件的状态变量而不是参数传递。
  1. 使用场景
  • 按引用传递参数

按引用传递参数时,传递的参数可为状态变量,且状态变量的改变会引起@LocalBuilder方法内的UI刷新。

特别说明,若@LocalBuilder函数和$$参数一起使用,子组件调用父组件的@LocalBuilder函数,传入的参数发生变化,不会引起@LocalBuilder方法内的UI刷新。

示例一:

组件Parent内的@LocalBuilder方法在build函数内调用,按键值对写法进行传值,当点击Click me 时,@LocalBuilder内的Text文本内容会随着状态变量内容的改变而改变。

class ReferenceType {
  paramString: string = ''
}

@Entry
  @Component
  struct Parent {
    @State variableValue: string = 'Hello World'

    @LocalBuilder
    citeLocalBuilder(params: ReferenceType) {
      Row() {
        Text(`UseStateVarByReference: ${params.paramString}`)
      }
    }

    build() {
      Column() {
        this.citeLocalBuilder({ paramString: this.variableValue })
        Button('Click me').onClick(() => {
          this.variableValue = 'Hi World'
        })
      }
    }
  }

按引用传递参数时,如果在@LocalBuilder方法内调用自定义组件,ArkUI提供$$作为按引用传递参数的范式。

示例二:

组件Parent内的@LocalBuilder方法内调用自定义组件,且按照引用传递参数将值传递到自定义组件,当Parent组件内状态变量值发生变化时,@LocalBuilder方法内的自定义组件HelloComponent的message值也会发生变化。

class ReferenceType {
  paramString: string = ''
}

@Component
struct HelloComponent {
  @Prop message: string

  build() {
    Row() {
      Text(`HelloComponent===${this.message}`)
    }
  }
}

@Entry
@Component
struct Parent {
  @State variableValue: string = 'Hello World'

  @LocalBuilder
  citeLocalBuilder($$: ReferenceType) {
    Row() {
      Column() {
        Text(`citeLocalBuilder===${$$.paramString}`)
        HelloComponent({ message: $$.paramString })
      }
    }
  }

  build() {
    Column() {
      this.citeLocalBuilder({ paramString: this.variableValue })
      Button('Click me').onClick(() => {
        this.variableValue = 'Hi World'
      })
    }
  }
}

子组件引用父组件的@LocalBuilder函数,传入的参数为状态变量,状态变量的改变不会引发@LocalBuilder方法内的UI刷新,原因是@Localbuilder装饰的函数绑定在父组件上,状态变量刷新机制是刷新本组件以及其子组件,对父组件无影响,故无法引发刷新。若使用@Builder修饰则可引发刷新,原因是@Builder改变了函数的this指向,此时函数被绑定到子组件上,故能引发UI刷新。

示例三:

组件Child将状态变量传递到Parent的@Builder和@LocalBuilder函数内,在@Builder的函数内,this指向Child,参数变化能引发UI刷新,在@LocalBuilder函数内,this指向Parent,参数变化不能引发UI刷新。若@LocalBuilder函数内引用Parent的状态变量发生变化,UI能正常刷新。

class Data {
  size: number = 0
}

@Entry
@Component
struct Parent {
  label: string = 'parent'
  @State data: Data = new Data()

  @Builder
  componentBuilder($$: Data) {
    Text(`builder + $$`)
    Text(`${'this -> ' + this.label}`)
    Text(`${'size : ' + $$.size}`)
    Text(`------------------------`)
  }

  @LocalBuilder
  componentLocalBuilder($$: Data) {
    Text(`LocalBuilder + $$ data`)
    Text(`${'this -> ' + this.label}`)
    Text(`${'size : ' + $$.size}`)
    Text(`------------------------`)
  }

  @LocalBuilder
  contentLocalBuilderNoArgument() {
    Text(`LocalBuilder + local data`)
    Text(`${'this -> ' + this.label}`)
    Text(`${'size : ' + this.data.size}`)
    Text(`------------------------`)
  }

  build() {
    Column() {
      Child({
        contentBuilder: this.componentBuilder,
        contentLocalBuilder: this.componentLocalBuilder,
        contentLocalBuilderNoArgument: this.contentLocalBuilderNoArgument,
        data: this.data
      })
    }
  }
}

@Component
struct Child {
  label: string = 'child'
  @Builder customBuilder() {}
  @BuilderParam contentBuilder: ((data: Data) => void) = this.customBuilder
  @BuilderParam contentLocalBuilder: ((data: Data) => void) = this.customBuilder
  @BuilderParam contentLocalBuilderNoArgument: (() => void) = this.customBuilder
  @Link data: Data

  build() {
    Column() {
      this.contentBuilder({ size: this.data.size })
      this.contentLocalBuilder({ size: this.data.size })
      this.contentLocalBuilderNoArgument()
      Button("add child size").onClick(() => {
        this.data.size += 1
      })
    }
  }
}
  • 按值传递参数

调用@LocalBuilder装饰的函数默认按值传递。当传递的参数为状态变量时,状态变量的改变不会引起@LocalBuilder方法内的UI刷新。所以当使用状态变量的时候,推荐使用按引用传递。

示例:

组件Parent将@State修饰的label值按照函数传参方式传递到@LocalBuilder函数内,此时@LocalBuilder函数获取到的值为普通变量值,所以改变@State修饰的label值时,@LocalBuilder函数内的值不会发生改变。

@Entry
@Component
struct Parent {
  @State label: string = 'Hello'

  @LocalBuilder
  citeLocalBuilder(paramA1: string) {
    Row() {
      Text(`UseStateVarByValue: ${paramA1}`)
    }
  }

  build() {
    Column() {
      this.citeLocalBuilder(this.label)
    }
  }
}

6.3 @BuilderParam装饰器:引用@Builder函数

当开发者创建了自定义组件,并想对该组件添加特定功能,例如想在某一个指定的自定义组件中添加一个点击跳转操作,此时若直接在组件内嵌入事件方法,将会导致所有该自定义组件的实例都增加了功能。为解决此问题,ArkUI引入了@BuilderParam装饰器,@BuilderParam用来装饰指向@Builder方法的变量(@BuilderParam是用来承接@Builder函数的)。我们可以在初始化自定义组件时,使用不同的方式(如:参数修改、尾随闭包、借用箭头函数等)对@BuilderParam装饰的自定义构建函数进行传参赋值,在自定义组件内部通过调用@BuilderParam为组件增加特定的功能。该装饰器用于声明任意UI描述的一个元素,类似slot占位符。

@BuilderParam装饰的方法只能被自定义构建函数(@Builder装饰的方法)初始化。

  • 使用所属自定义组件的自定义构建函数或者全局的自定义构建函数,在本地初始化@BuilderParam。
@Builder function overBuilder() {}

@Component
struct Child {
  @Builder doNothingBuilder() {}
  // 使用自定义组件的自定义构建函数初始化@BuilderParam
  @BuilderParam customBuilderParam: () => void = this.doNothingBuilder
  // 使用全局自定义构建函数初始化@BuilderParam
  @BuilderParam customOverBuilderParam: () => void = overBuilder
  build(){}
}
  • 用父组件自定义构建函数初始化子组件@BuilderParam装饰的方法。
@Component
struct Child {
  @Builder customBuilder() {};
  @BuilderParam customBuilderParam: () => void = this.customBuilder

  build() {
    Column() {
      this.customBuilderParam()
    }
  }
}

@Entry
@Component
struct Parent {
  @Builder componentBuilder() {
    Text(`Parent builder `)
  }

  build() {
    Column() {
      Child({ customBuilderParam: this.componentBuilder })
    }
  }
}
  • 需要注意this的指向。

以下示例对this的指向做了介绍。

@Component
struct Child {
  label: string = 'Child'
  @Builder customBuilder() {}
  @Builder customChangeThisBuilder() {}
  @BuilderParam customBuilderParam: () => void = this.customBuilder
  @BuilderParam customChangeThisBuilderParam: () => void 
    = this.customChangeThisBuilder

  build() {
    Column() {
      this.customBuilderParam()
      this.customChangeThisBuilderParam()
    }
  }
}

@Entry
@Component
struct Parent {
  label: string = 'Parent'

  @Builder componentBuilder() {
    Text(`${this.label}`)
  }

  build() {
    Column() {
      // 调用this.componentBuilder()时,this指向当前@Entry所装饰的Parent组件,
      // 即label变量的值为"Parent"。
      this.componentBuilder()
      Child({
        // 把this.componentBuilder传给子组件Child的@BuilderParam 
        // customBuilderParam,this指向的是子组件Child,即label变量的值为"Child"。
        customBuilderParam: this.componentBuilder,
        // 把():void=>{this.componentBuilder()}传给子组件Child的
        // @BuilderParam customChangeThisBuilderParam,
        // 因为箭头函数的this指向的是宿主对象,所以label变量的值为"Parent"。
        customChangeThisBuilderParam: (): void => { this.componentBuilder() }
      })
    }
  }
}

6.4 wrapBuilder:封装全局@Builder

当开发者在一个struct内使用了多个全局@Builder函数,来实现UI的不同效果时,多个全局@Builder函数会使代码维护起来非常困难,并且页面不整洁。此时,开发者可以使用wrapBuilder来封装全局@Builder。

  1. @Builder方法赋值给变量

把@Builder装饰器装饰的方法MyBuilder作为wrapBuilder的参数,再将wrapBuilder赋值给变量globalBuilder,用来解决@Builder方法赋值给变量后无法被使用的问题。

@Builder
function MyBuilder(value: string, size: number) {
  Text(value)
    .fontSize(size)
}

let globalBuilder: WrappedBuilder<[string, number]> = wrapBuilder(MyBuilder)

@Entry
@Component
struct Index {
  @State message: string = 'Hello World'

  build() {
    Row() {
      Column() {
        globalBuilder.builder(this.message, 50)
      }
      .width('100%')
    }
    .height('100%')
  }
}

2. @Builder方法赋值给变量在UI语法中使用

自定义组件Index使用ForEach来进行不同@Builder函数的渲染,可以使用builderArr声明的wrapBuilder数组进行不同@Builder函数效果体现。整体代码会较整洁。

@Builder
function MyBuilder(value: string, size: number) {
  Text(value)
    .fontSize(size)
}

@Builder
function YourBuilder(value: string, size: number) {
  Text(value)
    .fontSize(size)
    .fontColor(Color.Pink)
}

const builderArr: WrappedBuilder<[string, number]>[] 
  = [wrapBuilder(MyBuilder), wrapBuilder(YourBuilder)]

@Entry
@Component
struct Index {
  @Builder testBuilder() {
    ForEach(builderArr, (item: WrappedBuilder<[string, number]>) => {
      item.builder('Hello World', 30)
    }

    )
  }

  build() {
    Row() {
      Column() {
        this.testBuilder()
      }
      .width('100%')
    }
    .height('100%')
  }
}

3. 引用传递

通过按引用传递的方式传入参数,会触发UI的刷新。

class Tmp {
  paramA2: string = 'hello'
}

@Builder function overBuilder(param: Tmp) {
  Column(){
    Text(`wrapBuildervalue:${param.paramA2}`)
  }
}

const wBuilder: WrappedBuilder<[Tmp]> = wrapBuilder(overBuilder)

@Entry
@Component
struct Parent{
  @State label: Tmp = new Tmp()
  build(){
    Column(){
      wBuilder.builder({paramA2: this.label.paramA2})
      Button('Click me').onClick(() => {
        this.label.paramA2 = 'ArkUI'
      })
    }
  }
}

✋ 需要参加鸿蒙认证的请点击 鸿蒙认证链接