【HarmonyOS】鸿蒙入门学习

发布于:2024-06-16 ⋅ 阅读:(14) ⋅ 点赞:(0)

一、开发前的准备

(一)HarmonyOS 开发套件介绍

(二)开发者主要使用的核心套件

主要为代码开发阶段会使用到的 DevEco StudioArkTSArkUI三个工具。

(三)熟悉鸿蒙官网

1、网址

https://developer.huawei.com/consumer/cn/

2、开发工具的下载、安装与创建

(1)下载

https://developer.huawei.com/consumer/cn/deveco-studio/

找到如下图所示位置,点击下载对应版本的DevEco Studio安装包:

(2)安装

    1. 双击安装包

     2. 安装引导下载与安装软件-快速开始-DevEco Studio使用指南-工具

    3. 环境配置配置开发环境-快速开始-DevEco Studio使用指南-工具

ps: HarmonyOS SDK 路径中不能包含中文字符

(3)创建
  1. 创建一个新项目并运行创建和运行Hello World-快速开始-DevEco Studio使用指南-工具

二、开发学习

(一)ArkTS 语言

1、了解 ArkTS 语言特点

2、TypeScript 语言

官网地址:https://www.typescriptlang.org/

(1)变量声明

静态类型检查功能:每一个变量都有固定的数据类型。

(2)条件控制

支持基于 if-else 和 switch 的条件控制。

ps:在 TS 中,空字符串、数字0、null、undefined 都被认为是 false ,其他值则为 true。

(3)循环迭代

支持基于 for 和 while 循环,并且为一些内置类型(如 Array)提供快捷迭代语法。

(4)函数

通常利用 function 关键字声明函数,并且支持可选参数、默认参数、箭头函数等特殊语法。

(5)类和接口

具备面向对象编程的基本语法,如: interface、class、enum 等;

具备 封装、继承、多态 等面向对象基本特征;

ps:凡是在 类 和 接口 内部定义的函数不需要加 function 关键字。

(6)模块开发

应用复杂时,可以把通用功能抽取到单独的ts文件中,每个文件都是一个模块(module)。

模块可以相互加载,提高代码复用性。

(二)ArkUI 组件

1、基本信息说明

2、基础组件

用法、resources资源访问

(1)Image 图片显示组件

    1. 声明 Image 组件并设置图片源

Image(src:string|PixelMap|Resource)

      2. 添加图片属性

        组件通用属性:widthheightborderRadius...

        图片插值:interpolation(ImageInterpolation.High)

    3. 申请网络访问权限:访问控制授权申请-访问控制-安全-开发

(2)Text 文本显示组件

  1. 声明 Text 组件并设置文本内容

    Text(content?:string|Resource)
  2. 添加文本属性

(3)TextInput 文本输入框

  1. 声明 Text 组件

    TextInput({placeholder?:ResourceStr, text?:ResourceStr})
  2. 添加属性和事件

(4)Button 按钮组件

  1. 声明 Button 组件,laber 按钮文字

    Button(laber?:ResourceStr)
  2. 添加属性和事件

(5)Slider 滑动条组件

  1. 声明 Silder 组件

    Silder(options?:SilderOptions)
  2. 添加属性和事件

(6)Progress 进度条组件
  1. 声明 Silder 组件

Progress(options: {value: number, total?: number, type?: ProgressType})

// value:当前进度值
// total:进度总长,默认值:100
// type:进度条类型,默认值:ProgressType.Linear 线性

(7)Checkbox 多选框组件
Checkbox(options?: {name?: string, group?: string })

3、容器组件

(1)Column 和 Row 组件

  1. 声明 Column 组件 和 Row 组件

    // 纵向布局使用 Column 容器 
    Column({space?:number}){} 
    
    // 横向布局使用 Row 容器 
    Row({space?:number}){}
  2. 添加属性和事件

(2)List 列表布局组件

    1. 列表项(即 ListItem )特点

        数量过多超出屏幕后,会自动提供滚动功能;

        既可以纵向排列,也可以横向排列;

    2. 代码示例

List({ space: 8 }) {
  ForEach(
    [1,2,3,4],
    (item) => {
      ListItem() {
        Row({ space: 10 }) {
          Image($r('app.media.icon'))
            .width(100)

          Column({ space: 4 }) {
            if (item>4) {
              Text(item)
                .fontSize(20)
                .fontWeight(FontWeight.Bold)
            } else {
              Text(item)
                .fontSize(20)
                .fontWeight(FontWeight.Bold)

              Text('¥' + item)
                .fontSize(18)
                .fontColor('#f36')
            }

          }
          .height('100%')
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%')
        .height(120)
        .padding(10)
        .backgroundColor('#fff')
        .borderRadius(20)
      }
    }
  )
}
.width('100%')
.layoutWeight(1) // 页面中除其他元素外,剩下高度占满
.alignListItem(ListItemAlign.Center) // 列表中的元素左右居中

ps:

  • List 容器内部不能直接跟其他容器的,必须跟 ListItem,ListItem再去套别的元素;

  • ListItem 内部只能有一个根组件,在有多组件的情况下,必须用一个容器组件包起来;

(3)Stack 堆叠容器
Stack(value?: { alignContent?: Alignment })

// alignContent:设置子组件在容器内的对齐方式,默认值:Alignment.Center

3、循环控制:ForEach、if-else

(1)ForEach:循环遍历数组,根据数组内容渲染页面组件
ForEach(
    arr:Array, // 要遍历的数据数组
    (item: any, index?: number) => {
        // 页面组件生成函数
    },
    // 默认自带
    keyGenerator?: (item: any, index?: number): string => {
        // 键生成函数,为数组每一项生成一个唯一标示,组件是否重新渲染的判断标准
    }
)

  示例:

(2)if/else:条件控制,根据数据状态的不同,渲染不同的页面组件
if (判断条件) {
  // 成立时
}else {
  // 不成立时
}

4、自定义组件

(1)自定义组件——可以定义在全局或组件内

    a. 未抽出组件时,完整代码如下:

@Entry
@Component
struct ItemPage {
  build() {
    Column({ space: 8 }) {
      Row() {
        Text('商品列表')
          .fontSize(30)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
      .margin({ bottom: 20 })
    }
  }
}

    b. 抽出组件后:

        情况一:组件内使用——只有当前页面使用的组件(抽出至当前页面),代码如下:

// 抽出至当前页面最上面
@Component
struct Header {
  private title: ResourceStr

  build() {
  // 组件内容如下
    Row() {
        Text('商品列表')
          .fontSize(30)
          .fontWeight(FontWeight.Bold)
     }
     .width('100%')
  }
}

@Entry
@Component
struct ItemPage {
  build() {
    Column({ space: 8 }) {
    // 使用
      Header({ title: '商品列表' })
        .margin({ bottom: 20 })
    }
  }
}

        情况二:全局使用——其他页面也会使用的组件(抽出至组件文件夹),代码如下:

                组件的封装

// 文件路径:entry/srcmain/ets/components/CommonComponents

// 1.封装
@Component
export struct Header {
  private title: ResourceStr

  build() {
  // 组件内容如下
    Row() {
        Text('商品列表')
          .fontSize(30)
          .fontWeight(FontWeight.Bold)
     }
     .width('100%')
  }
}

                封装组件的使用

// 文件路径:entry/srcmain/ets/pages/ItemPage.ets

// 2.引入封装的自定义组件
import { Header } from '../components/CommonComponents'

@Entry
@Component
struct ItemPage {
  build() {
    Column({ space: 8 }) {
      // 3.对应的地方使用组件
      Header({ title: '商品列表' })
        .margin({ bottom: 20 })
    }
  }
}
(2)自定义构建函数——可以定义在全局或组件内
  • 全局定义

// 1、全局自定义构建函数:全局都可以使用
@Builder ItemCardJB(item: string) {
    // UI描述
    Row({ space: 10 }) {
      Text(item)
    }
    .width('100%')
    .height(120)
    .padding(10)
    .backgroundColor('#fff')
    .borderRadius(20)
}

@Entry
@Component
struct ItemPage {
  build() {
    Column({ space: 8 }) {
       // 2、使用——局部写法
      ItemCardJB('111')
    }
  }
}
  • 组件内定义

@Entry
@Component
struct ItemPage {
// 1、局部自定义构建函数:只能该组件内部使用
@Builder ItemCardJB(item: string) {
    // UI描述
    Row({ space: 10 }) {
      Text(item)
    }
    .width('100%')
    .height(120)
    .padding(10)
    .backgroundColor('#fff')
    .borderRadius(20)
}
 
  build() {
    Column({ space: 8 }) {
       // 2、使用——局部写法
      this.ItemCardJB('111')
    }
  }
}

(3)@Styles 装饰器——仅可封装组件通用属性
  • 全局定义

// 1、定义全局公共样式函数
@Styles function fillScreen(){
  .width('100%')
  .height('100%')
  .backgroundColor('#efefef')
  .padding(20)
}

@Entry
@Component
struct ItemPage {
  build() {
    Column({ space: 8 }) {
      Row() {
        Text('商品列表')
      }
      // 2、使用
      .fillScreen()
    }
  }
}
  • 组件内定义

@Entry
@Component
struct ItemPage {
// 1、定义局部公共样式函数
 @Styles fillScreen(){
   .width('100%')
   .height('100%')
   .backgroundColor('#efefef')
   .padding(20)
 }
 
  build() {
    Column({ space: 8 }) {
      Row() {
        Text('商品列表')
      }
      // 2、使用
      .fillScreen()
    }
  }
}
(4)@Extend 装饰器——仅可定义在全局,可以设置组件特有属性
// 1、定义——继承模式
// 1、组件特有属性的样式函数:@Extend(Text),即继承text属性;
// 2、@Extend() 不能写在组件内,即只能写在全局,即页面顶部
@Extend(Text) function priceText(){
  .fontSize(18)
  .fontColor('#f36')
}

@Entry
@Component
struct ItemPage {
  build() {
    Column({ space: 8 }) {
      Row() {
        Text('商品列表')
        // 2、使用
          .priceText()
      }
      .width('100%')
      .margin({ bottom: 20 })
    }
  }
}

(三)状态管理器

1、@State 装饰器

(1)在声明式UI中,以状态驱动视图更新

  • 状态(Stste):驱动视图更新的数据——被装饰器标记的变量;

  • 视图(View):基于UI描述渲染得到用户界面;

(2)代码示例
@Entry
@Component
struct Index {
// 使用
  @State message: string = 'Hello World'

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .fontColor('#36D')
          .onClick(()=>{
            this.message = '小明!'
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

ps:

  • @Stste 装饰器 标记的变量必须初始化,不能为空值;

  • @Stste 支持Object、class、string、number、boolean、enum 类型以及这些类型的数据;

  • 嵌套类型以及数组中的对象属性无法触发视图更新

2、@Prop 和 @Link

当父子组件之间需要数据同步时,可以使用 @Prop 和 @Link 装饰器。

(1)使用说明

@Prop

@Link

同步类型

单向同步

双向同步

允许装饰的

变量类型

  • 支持类型:string、number、boolean、enum;

  • 父组件对象类型,则子组件使用其对象属性;

  • 不可以是数组、any

  • 父子类型一致,且支持类型:string、number、boolean、enum、object、class,及他们的数组;

  • 数组中元素增、删、替换会引起刷新;

  • 嵌套类型及数组中的对象属性无法触发视图更新;

初始化方式

不允许子组件初始化

父组件传递,禁止子组件初始化

 

ps:

  • @Link 和 @State 很像,区别在于:@Link 用于子组件来实现双向同步,@State 用于父组件

(2)代码示例

    a. 传递数据类型——普通类型

// 任务类
class Task {
  static id: number = 1
  //   任务名称
  name: string = `任务${Task.id++}`
  //   任务状态:是否完成
  finished: boolean = false
}

// 统一卡片样式
@Styles function card() {
  .width('90%')
  .padding(20)
  .backgroundColor(Color.White)
  .borderRadius(15)
  .shadow({ radius: 6, color: '#1f000000', offsetX: 2, offsetY: 4 })
}

@Entry
@Component
struct PropPage {
  //  总任务数量
  @State totalTask: number = 0
  // 已完成任务数量
  @State finishTask: number = 0

  build() {
    Column({ space: 10 }) {
      // 1、任务进度卡片
      TaskStatistics({ finishTask: this.finishTask, totalTask: this.totalTask })
      //  3、任务列表
      // 传递引用使用 $ ,传递的不是变量值,传递的是引用
      TaskList({ finishTask: $finishTask, totalTask: $totalTask })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f1f2f3')
  }
}

@Component
struct TaskStatistics {
  @Prop finishTask: number
  @Prop totalTask: number

  build() {
    Row() {
      Text(`任务进度:`)
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
      Stack() {
        Progress({
          value: this.finishTask,
          total: this.totalTask,
          type: ProgressType.Ring
        })
          .width(100)
        Row() {
          Text(this.finishTask.toString())
            .fontSize(24)
            .fontColor('#36d')
          Text('/' + this.totalTask.toString())
            .fontSize(24)
            .fontColor('#36d')
        }
      }
    }
    .card()
    .margin({ top: 20, bottom: 20 })
    .justifyContent(FlexAlign.SpaceAround)
  }
}

@Component
struct TaskList {
  //  总任务数量
  @Link totalTask: number
  // 已完成任务数量
  @Link finishTask: number
  // 任务数组
  @State tasks: Task[] = []

  handleTaskChange() {
    // 更新任务总数
    this.totalTask = this.tasks.length
    // 更新已完成任务总数
    this.finishTask = this.tasks.filter(item => item.finished).length
  }

  build() {
    // 子组件只能有一个根元素
    Column() {
      //  2、新增任务按钮
      Button('新增任务')
        .width(200)
        .margin({ bottom: 20 })
        .onClick(() => {
          this.tasks.push(new Task()) // 在数组中添加一条数据
          this.handleTaskChange()
        })
      //  3、任务列表
      List({ space: 10 }) {
        ForEach(
          this.tasks,
          (item: Task, index: number) => {
            ListItem() {
              Row() {
                Text(`任务${item.name}`)
                Checkbox()
                  .select(item.finished)
                  .onChange((val) => {
                    item.finished = val //更新任务状态
                    this.handleTaskChange()
                  })
              }
              .card()
              .justifyContent(FlexAlign.SpaceBetween)
            }
            .swipeAction({ end: this.DeleteButton(index) })

          }
        )
      }
      .width('100%')
      .alignListItem(ListItemAlign.Center)
      .layoutWeight(1)
    }

  }

  @Builder DeleteButton(index: number) {
    Button('删')
      .width(40)
      .height(40)
      .backgroundColor(Color.Red)
      .margin(10)
      .onClick(() => {
        this.tasks.splice(index, 1)
        this.handleTaskChange()
      })
  }
}

    b. 传递数据类型——对象类型

// 任务类
class Task {
  static id: number = 1
  //   任务名称
  name: string = `任务${Task.id++}`
  //   任务状态:是否完成
  finished: boolean = false
}

// 统一卡片样式
@Styles function card() {
  .width('90%')
  .padding(20)
  .backgroundColor(Color.White)
  .borderRadius(15)
  .shadow({ radius: 6, color: '#1f000000', offsetX: 2, offsetY: 4 })
}

// 任务统计信息
class StatInfo{
  totalTask: number = 0 //  总任务数量
  finishTask: number = 0 // 已完成任务数量
}

@Entry
@Component
struct PropPage {
  //  统计信息
  @State stat: StatInfo = new StatInfo()

  build() {
    Column({ space: 10 }) {
      // 1、任务进度卡片
      TaskStatistics({ finishTask: this.stat.finishTask, totalTask: this.stat.totalTask })
      //  3、任务列表
      // 传递引用使用 $ ,传递的不是变量值,传递的是引用
      TaskList({ stat: $stat })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f1f2f3')
  }
}

@Component
struct TaskStatistics {
  @Prop finishTask: number
  @Prop totalTask: number

  build() {
    Row() {
      Text(`任务进度:`)
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
      Stack() {
        Progress({
          value: this.finishTask,
          total: this.totalTask,
          type: ProgressType.Ring
        })
          .width(100)
        Row() {
          Text(this.finishTask.toString())
            .fontSize(24)
            .fontColor('#36d')
          Text('/' + this.totalTask.toString())
            .fontSize(24)
            .fontColor('#36d')
        }
      }
    }
    .card()
    .margin({ top: 20, bottom: 20 })
    .justifyContent(FlexAlign.SpaceAround)
  }
}

@Component
struct TaskList {
  //  任务
  @Link stat: StatInfo
  // 任务数组
  @State tasks: Task[] = []

  handleTaskChange() {
    // 更新任务总数
    this.stat.totalTask = this.tasks.length
    // 更新已完成任务总数
    this.stat.finishTask = this.tasks.filter(item => item.finished).length
  }

  build() {
    // 子组件只能有一个根元素
    Column() {
      //  2、新增任务按钮
      Button('新增任务')
        .width(200)
        .margin({ bottom: 20 })
        .onClick(() => {
          this.tasks.push(new Task()) // 在数组中添加一条数据
          this.handleTaskChange()
        })
      //  3、任务列表
      List({ space: 10 }) {
        ForEach(
          this.tasks,
          (item: Task, index: number) => {
            ListItem() {
              Row() {
                Text(`任务${item.name}`)
                Checkbox()
                  .select(item.finished)
                  .onChange((val) => {
                    item.finished = val //更新任务状态
                    this.handleTaskChange()
                  })
              }
              .card()
              .justifyContent(FlexAlign.SpaceBetween)
            }
            .swipeAction({ end: this.DeleteButton(index) })

          }
        )
      }
      .width('100%')
      .alignListItem(ListItemAlign.Center)
      .layoutWeight(1)
    }

  }

  @Builder DeleteButton(index: number) {
    Button('删')
      .width(40)
      .height(40)
      .backgroundColor(Color.Red)
      .margin(10)
      .onClick(() => {
        this.tasks.splice(index, 1)
        this.handleTaskChange()
      })
  }
}

ps:子组件只能有一个根元素

3、@Provide 和 @Consume

@Provide 和 @Consume 可以跨组件双向同步(类似于 @Prop 和 @Link 的双向同步):

  • 父子间双向同步;

  • 爷孙间双向同步;

(1)使用说明

(2)代码示例
// 任务类
class Task {
  static id: number = 1
  //   任务名称
  name: string = `任务${Task.id++}`
  //   任务状态:是否完成
  finished: boolean = false
}

// 统一卡片样式
@Styles function card() {
  .width('90%')
  .padding(20)
  .backgroundColor(Color.White)
  .borderRadius(15)
  .shadow({ radius: 6, color: '#1f000000', offsetX: 2, offsetY: 4 })
}

// 任务完成样式
@Extend(Text) function finishedTask() {
  .decoration({ type: TextDecorationType.LineThrough })
  .fontColor('#b1b2b1')
  .fontSize(20)
}

// 任务统计信息
class StatInfo{
  totalTask: number = 0 //  总任务数量
  finishTask: number = 0 // 已完成任务数量
}

@Entry
@Component
struct PropPage {
  //  统计信息
  @Provide stat: StatInfo = new StatInfo()

  build() {
    Column({ space: 10 }) {
      // 1、任务进度卡片
      TaskStatistics()
      //  3、任务列表
      TaskList()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f1f2f3')
  }
}

@Component
struct TaskStatistics {
  @Consume stat: StatInfo

  build() {
    Row() {
      Text(`任务进度:`)
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
      Stack() {
        Progress({
          value: this.stat.finishTask,
          total: this.stat.totalTask,
          type: ProgressType.Ring
        })
          .width(100)
        Row() {
          Text(this.stat.finishTask.toString())
            .fontSize(24)
            .fontColor('#36d')
          Text('/' + this.stat.totalTask.toString())
            .fontSize(24)
            .fontColor('#36d')
        }
      }
    }
    .card()
    .margin({ top: 20, bottom: 20 })
    .justifyContent(FlexAlign.SpaceAround)
  }
}

@Component
struct TaskList {
  @Consume stat: StatInfo
  // 任务数组
  @State tasks: Task[] = []

  handleTaskChange() {
    // 更新任务总数
    this.stat.totalTask = this.tasks.length
    // 更新已完成任务总数
    this.stat.finishTask = this.tasks.filter(item => item.finished).length
  }

  build() {
    // 子组件只能有一个根元素
    Column() {
      //  2、新增任务按钮
      Button('新增任务')
        .width(200)
        .margin({ bottom: 20 })
        .onClick(() => {
          this.tasks.push(new Task()) // 在数组中添加一条数据
          this.handleTaskChange()
        })
      //  3、任务列表
      List({ space: 10 }) {
        ForEach(
          this.tasks,
          (item: Task, index: number) => {
            ListItem() {
              Row() {
                Text(`任务${item.name}`)
                Checkbox()
                  .select(item.finished)
                  .onChange((val) => {
                    item.finished = val //更新任务状态
                    this.handleTaskChange()
                  })
              }
              .card()
              .justifyContent(FlexAlign.SpaceBetween)
            }
            .swipeAction({ end: this.DeleteButton(index) })

          }
        )
      }
      .width('100%')
      .alignListItem(ListItemAlign.Center)
      .layoutWeight(1)
    }

  }

  @Builder DeleteButton(index: number) {
    Button('删')
      .width(40)
      .height(40)
      .backgroundColor(Color.Red)
      .margin(10)
      .onClick(() => {
        this.tasks.splice(index, 1)
        this.handleTaskChange()
      })
  }
}

ps:

  • @Provide 和 @Consume 不需要显式传递数据,内部会自己做数据同步,即不用传参(@Provide 提供, @Consume 直接消费);

4、@Observed 和 @ObjectLink

@Observed 和 @ObjectLink 装饰器用于在涉及嵌套对象数组元素为对象的场景中进行双向数据同步

(1)使用说明

        a. 嵌套对象-使用说明

        嵌套对象的类型上添加 @Observed 装饰器,不管嵌套几个类型,凡是嵌套的类型都要加上 @Observed 装饰器;

        给嵌套对象内部的对象加上 @ObjectLink 装饰器;

        如果嵌套对象内部的对象是作为一个方法的参数传递的就不能加,解决办法:定义一个组件,然后将内部嵌套对象传给这个组件,那么这个组件可以作为一个变量,就可以加 @ObjectLink 装饰器了

        b. 数组元素对象-使用说明

        如图:gfs: Person[]

(2)代码示例
// 任务类
@Observed
class Task {
  static id: number = 1
  //   任务名称
  name: string = `任务${Task.id++}`
  //   任务状态:是否完成
  finished: boolean = false
}

// 统一卡片样式
@Styles function card() {
  .width('90%')
  .padding(20)
  .backgroundColor(Color.White)
  .borderRadius(15)
  .shadow({ radius: 6, color: '#1f000000', offsetX: 2, offsetY: 4 })
}

// 任务完成样式
@Extend(Text) function finishedTask() {
  .decoration({ type: TextDecorationType.LineThrough })
  .fontColor('#b1b2b1')
  .fontSize(20)
}

// 任务统计信息
class StatInfo {
  totalTask: number = 0 //  总任务数量
  finishTask: number = 0 // 已完成任务数量
}

@Entry
@Component
struct PropPage {
  //  统计信息
  @Provide stat: StatInfo = new StatInfo()

  build() {
    Column({ space: 10 }) {
      // 1、任务进度卡片
      TaskStatistics()
      //  3、任务列表
      TaskList()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f1f2f3')
  }
}

@Component
struct TaskStatistics {
  @Consume stat: StatInfo

  build() {
    Row() {
      Text(`任务进度:`)
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
      Stack() {
        Progress({
          value: this.stat.finishTask,
          total: this.stat.totalTask,
          type: ProgressType.Ring
        })
          .width(100)
        Row() {
          Text(this.stat.finishTask.toString())
            .fontSize(24)
            .fontColor('#36d')
          Text('/' + this.stat.totalTask.toString())
            .fontSize(24)
            .fontColor('#36d')
        }
      }
    }
    .card()
    .margin({ top: 20, bottom: 20 })
    .justifyContent(FlexAlign.SpaceAround)
  }
}

@Component
struct TaskList {
  @Consume stat: StatInfo
  // 任务数组
  @State tasks: Task[] = []

  handleTaskChange() {
    // 更新任务总数
    this.stat.totalTask = this.tasks.length
    // 更新已完成任务总数
    this.stat.finishTask = this.tasks.filter(item => item.finished).length
  }

  build() {
    // 子组件只能有一个根元素
    Column() {
      //  2、新增任务按钮
      Button('新增任务')
        .width(200)
        .margin({ bottom: 20 })
        .onClick(() => {
          this.tasks.push(new Task()) // 在数组中添加一条数据
          this.handleTaskChange()
        })
      //  3、任务列表
      List({ space: 10 }) {
        ForEach(
          this.tasks,
          (item: Task, index: number) => {
            ListItem() {
            /*
             * 1、传递的是方法: this.handleTaskChange ,因为不是调用方法所以不能写 this.handleTaskChange()。
             * 2、因为this.handleTaskChange这个方法是在子组件里执行,所以这个方法的this就不是父组件的this,而是子组件的this;
             *    因为子组件没有tasks这个数组,所以会找不到;
             * 3、传递的过程中想要保证 this 不要变的办法:
             *    函数在传递时有一个方法 bind(this),即:
             *    把当前父组件中的 this 绑定到这个函数里面,那么这个函数里的 this 就永远都是当前这个父组件
             */
              TaskItem({ item: item, onTaskChange: this.handleTaskChange.bind(this) })
            }
            .swipeAction({ end: this.DeleteButton(index) })

          }
        )
      }
      .width('100%')
      .alignListItem(ListItemAlign.Center)
      .layoutWeight(1)
    }

  }

  @Builder DeleteButton(index: number) {
    Button('删')
      .width(40)
      .height(40)
      .backgroundColor(Color.Red)
      .margin(10)
      .onClick(() => {
        this.tasks.splice(index, 1)
        this.handleTaskChange()
      })
  }
}

@Component
struct TaskItem {
  @ObjectLink item: Task
  /*
   * 1、子组件想要调父组件的方法:
   * 可以将这个父组件的方法作为一个参数传递进来,但是在传递过程中会出现this丢失
   * 2、解决this丢失的办法:
   * 父组件传递参数时使用 .bind(this) ,将父组件的this绑定给子组件,即:this.handleTaskChange.bind(this)
   */
  onTaskChange: () => void // 这是一个变量,变量类型是一个函数

  build() {
    Row() {
      if (this.item.finished) {
        Text(`任务${this.item.name}`)
          .finishedTask()
      } else {
        Text(`任务${this.item.name}`)
      }

      Checkbox()
        .select(this.item.finished)
        .onChange((val) => {
          this.item.finished = val //更新任务状态
          this.onTaskChange() // 更新已完成的任务数量
        })
    }
    .card()
    .justifyContent(FlexAlign.SpaceBetween)
  }
}

ps:

        子组件想要调父组件的方法会造成 this 丢失,解决办法:父组件传递参数时使用 .bind(this) ,将父组件的this绑定给子组件,即:this.handleTaskChange.bind(this)

(四)页面路由

1、基本概念

(1)定义

页面路由:指在应用程序中实现不同页面之间的跳转和数据传递。

(2)基本原理及常见api

a.  页面栈的最大容量上限为32个页面,使用 router.clear() 方法可以清空页面栈,释放内存;

b. Router 有两种页面跳转模式,分别是:

    router.pushUrl():目标页不会替换当前页,而是压入页面栈,因此可以用 router.back() 返回当前页;

    router.replaceUrl():目标页替换当前页,当前页会被销毁并释放资源,无法返回当前页;

c. Router 有两种页面实例模式,分别是:

    Standard:标准实例模式,每次跳转都会新建一个目标页并压入栈顶,默认模式;

    Single:单实例模式,如果目标页已经在栈中,则离栈顶最近的同Url页面会被移动到栈顶并重新加载;

2、使用方法

(1)第一步:导入 HarmonyOS 提供的 router 模块
// 在页面顶部引入
import router from '@ohos.router';

ps:一般在页面中使用 router 时,会自动引入,所以这一句可以不用自己写,跳过;

(2)第二步:使用 router 的 API
  • 跳转到指定页面

// 跳转到指定路径,并传递参数
router.pushUrl(
  // 1、RouterOptions
  // - url:目标页面路径
  // - params:传递的参数,可选
  {
    url: 'pages/ImagePage',
    params: {id: 1},
  },
  // 2、页面模式:RouterMode 枚举
  router.RouterMode.Single,
  // 3、异常响应的回调函数错误码:
  // 100001:内部错误,可能是渲染失败
  // 100002:路由地址错误
  // 100003:路由栈中页面超过32个
  err => {
    if(err){
      console.log('路由失败')}
  }
)
  • 获取参数,返回上一页或指定页

// 获取传递过来的参数
params: any = router.gerParams()

// router.back()不传参,则返回上一页
router.back()

// router.back()传参,则返回到指定页,并携带参数
router.back(
  {
    url: 'pages/ImagePage',
    params: {id: 10},
  }
)

(1)创建页面,路由配置文件文件自动添加路径的方法

ps:

如果不是上述方法创建页面,则需要在 base/main_pages.json 文件中配置页面路径,格式如下:

{

    "src": [

        "pages/Index",

        "pages/ImagePage", // 对应页面的路径

    ]

}

(2)返回前的二次确认弹窗
// 返回前的二次确认弹窗
router.showAlertBeforeBackPage({
  message:'确认返回吗?'
})
// 返回上一页
router.back()

点击返回后,会前弹出确认弹窗,具体流程如图所示:

  • 点击“确定”,则走 router.back() 返回上一页;

  • 点击“取消”,则停留在当前页面;

 

(五)动画

1、属性动画

属性动画:是通过设置组件的 animation 属性来给组件添加动画,当组件的 width、height、Opacity、backgroundColor、scale、rotate、translate 等属性变更时,可以实现渐变过渡效果。

ps:animation 属性,放在需要变化的样式属性后面,否则不会生效。

2、显示动画

显示动画:是通过全局 animateTo 函数来修改组件属性,实现属性变化时的渐变过渡效果。(使用更多的一种方式)

3、组件转场动画

 

(六)Stage 模型

HarmonyOS为开发者提供了Stage应用模型,是应用程序所需能力的抽象提炼,它提供了应用程序必备的组件和运行机制。有了应用模型,开发者可以基于一套统一的模型进行应用开发,使应用开发更简单、高效。

1、Stage 模型概述

 

(1)项目结构 

Ability Module 可以创建多个,可以将不同的能力放到不同的模块中开发;其中,通用的功能可以抽取出来放至 Library Module 模块中(Library Module 顾名思义:共享依赖类型的模块)。

 

 (2)编译期——项目编译打包的方式
  1. 源码编译——>打包码——>App 安装包;

  2. 所有的 Ability 模块都会被编译成HAP文件(鸿蒙能力类型包),所有的 Library 模块都会被编译成HSP文件(鸿蒙共享类型包);HAP包在运行过程中可以去引用和依赖HSP包;

  • 一个应用内部可能会有很多不同的能力,就会有很多不同的Ability Module,所以会有多个HAP文件;

  • HAP 文件间的差异:

    • Entry类型的HAP,是开发应用的主要能力模块,入口HAP文件,只能有一个

    • Feature类型的HAP,是拓展功能可以有多个

  • 采用这种多HAP文件打包模式的目的:

    • 为了降低不同功能模块间的耦合,每一个模块都可以独立编译和运行

    • 应用在下载安装时,首先安装核心模块Entry,其他Feature可以选择性安装,这样能降低应用安装时的体积

    3. 很多HAP合并在一起叫Bundle,Bundle有一个自己的名字叫 bundleName ,是应用的唯一标识;

    4. Bundle合并打包会变成一个APP,即.app的安装包;

(3)运行期——运行时的一些概念
  1. 每一个HAP在运行时都会创建一个AbilityStage实例,来展示应用能力组件;

  2. 应用能力组件有很多类型,比较常见的有: UIAbility、ExtensionAbility;

  • UIAbility:包含UI界面的应用组件,是系统调度的基本单元。

      在展示组件时,首先会持有一个 WindowStage 实例对象,WindowStage 会持有 Window对象(即:用来绘制UI页面的窗口),窗口里会展示ArkUI Page(UI界面);

  • ExtensionAbility:拓展的能力组件,如应用卡片、输入法;

2、应用及组件配置

 

(1)应用的全局配置信息

 

  • 文件路径:AppScope > app.json5

  • 代码说明:

{
  "app": {
    // 应用唯一标识(在创建应用时就设置了),项目发布、打包、部署时都会用到
    // 格式要求:域名倒置的方式进行定义,类似“包名”
    "bundleName": "com.example.myapplication",
    "vendor": "example",
    // 版本——数字格式,versionName、versionCode需同步变化更新
    "versionCode": 1000000,
    // 版本——字符串格式
    "versionName": "1.0.0",
    // 应用列表中的图标
    "icon": "$media:app_icon",
    // 应用列表中的描述字符
    "label": "$string:app_name"
  }
}
(2)Module的配置信息

 

  • 文件路径:entry > src > main > module.json5

  • 代码说明:

{
  "module": {
    // 申请系统权限
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET",
      },
    ],
    // 当前模块的名字
    "name": "entry",
    // 当前模块的类型,一共有三种:entry、feature、shared(共享,即Library模块的类型)
    "type": "entry",
    // 当前模块的描述
    "description": "$string:module_desc",
    // 当前模块的入口名字(默认启动的 ability)
    "mainElement": "EntryAbility",
    // 设备类型:如果有多个模块也可以供给不同的设备类型使用
    "deviceTypes": [
      "phone",
      "tablet"
    ],
    // 当前模块是否跟随应用一起安装
    "deliveryWithInstall": true,
    "installationFree": false,
    // 当前模块包含的所有页面
    "pages": "$profile:main_pages",
    // 一个模块下如果有多个ability,则都需要填入
    "abilities": [
      {
        "name": "EntryAbility",
        // 当前 ability对应的源码路径
        "srcEntry": "./ets/entryability/EntryAbility.ts",
        // 当前 ability的描述
        "description": "$string:EntryAbility_desc",
        /* 注意:
              因为:该模块是入口模块,且该ability是入口ability,
              所以:ability的图标和描述(icon、label)就是当前应用的图标和描述。
              =》就是操作系统桌面上的那个应用图标和名称
        */
        // 当前ability的图标
        "icon": "$media:icon",
        // 当前ability的名称
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:icon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        // 当前ability负责的功能需要在skills下指定,与ability之间的跳转有关系
        "skills": [
          // home:代表入口的意思
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          }
        ]
      }
    ]
  }
}
  • 修改string文件便捷方法:打开编辑器

 

(3)更多详细说明 

官方文档:app.json5配置文件-应用配置文件(Stage模型)-开发基础知识-入门

  3、UIAbility 生命周期

(1)图解

 

(2)详细说明
  • 启动应用的生命周期:onCreate —— onWindowStageCreate —— onForeground —— 加载页面;

  • 将应用程序后台的生命周期:onBackground

  • 将应用程序前台的生命周期:onForeground

  • 清理应用的生命周期:onWindowStageDestroy —— onDestroy

(3)hilog 日志输出使用
// 带上级别的日志
/*
1、16进制的数字,用来标识不同模块的参数
2、tag标记,模块下的功能标识
3、日志输出的内容:线上部署时是否公开(public公开 private不公开)
4、参数3处占位符具体的值是什么,取决于参数4传递的内容
*/
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

4、页面及组件生命周期

 PageA:组成当前页面的入口组件;ComponentA:子组件;

(1)加载组件的步骤:创建组件实例 ——> 执行build函数

ps:

  1. 所有的组件做加载,都是上述步骤;

  2. build函数全部执行完毕后,组件才算绘制完成;

(2)销毁页面 & 销毁组件
  • 只要页面被销毁,里面的组件肯定被销毁;反过来,如果页面还在,里面的子组件不一定还在,因为用户在互动操作中,里面的部分子组件如果不需要显示了就会被销毁(因为组件不存在隐藏,组件不显示就销毁)。

  • 页面隐藏,不一样页面就被销毁,replaceUrl页面被销毁,pushUrl页面没有被销毁。

  • 入口组件被销毁,里面的子组件也会被销毁。

(3)生命周期钩子:组件 & 页面
  1. 组件的生命周期钩子

   aboutToAppear创建组件实例之后,且执行build函数之前触发;

 可以在这个钩子里面进行数据的初始化和准备工作,然后build函数中就可以利用这些数据,完成渲染了;

   aboutToDisappear销毁组件之前触发;

可以在这个钩子里面完成一些数据持久化、数据保存、资源释放等操作;

    2. 页面的生命周期钩子

   onPageShow页面展示之后触发;

   onBackPress点击“返回”按钮时触发;

   onPageHide页面隐藏之前触发;

    3. 注意

        页面的生命周期钩子,只能在加了 @Entry 的入口组件中使用,普通的自定义组件中不能使用;

        组件的生命周期钩子,都可以使用;

    4. 执行顺序验证

        演示步骤——动图

        演示步骤——图示

 

 

        演示步骤——详细说明


  (1)加载IndexPage页面

  触发入口组件的创建 —— 执行 aboutToAppear 函数(执行完毕,即组件创建完毕)—— 执行build函数(执行完毕后,展示page)—— 触发onPageShow

  (2)加载LifeCirclePage页面

ps1:从IndexPage页面,通过pushUrl的方式,跳转到LifeCirclePage页面

ps2:pushUrl方式:即,IndexPage页面是隐藏,不是销毁

  首先创建页面组件,执行aboutToAppear函数 —— 隐藏IndexPage页面——展示LifeCirclePage页面

  (3)LifeCirclePage页面子组件的显示、隐藏:(if/else控制时)

  显示:执行 aboutToAppear 函数 —— 隐藏:执行 aboutToDisappear 函数

  (4)LifeCirclePage页面子组件的新增、删除:(foreach控制时)

  新增1个:执行 aboutToAppear 函数 —— 删除1个:执行 aboutToDisappear 函数

  新增3个:执行 aboutToAppear 函数3次 —— 删除第一个:执行 aboutToDisappear 函数3次,执行 aboutToAppear 函数2次(因为foreach循环渲染有一个特性:当数组中的元素发生变更时,会检查数组中的元素那些发生了变更,对发生了变更的元素进行重新渲染,重新渲染就会把之前的所有元素进行销毁,再重新渲染)

  (5)返回IndexPage页面(通过系统提供的返回按钮返回)

  触发onBackPress函数(只有系统提供的返回按钮才会触发)—— 执行onPageHide函数(隐藏LifeCirclePage页面)—— 执行onPageShow函数(展示IndexPage页面,同时,栈顶页面销毁)—— LifeCirclePage页面执行aboutToDisappear 函数 —— 子组件执行aboutToDisappear 函数(LifeCirclePage页面销毁,里面的子组件也会被销毁)

  (6)跳转到TestPage1页面

5、UIAbility 的启动模式

配置路径:entry\src\main\module.json5

(1)singleton 启动模式
  • 默认启动模式

  • 每一个UIAbility只存在唯一实例;

  • 任务列表中,只会存在一个相同的UIAbility;

(2)standard 启动模式(官网叫:multiton模式)
  • 每次启动UIAbility都会创建一个新的实例;

  • 任务列表中,可能存在一个或多个相同的UIAbility;

ps:

经过实际测试 standard 和 multiton 非常接近,但有不一样的地方:

  • 相同点:每次启动UIAbility都会创建一个新的实例;

  • 不同点:创建新实例,standard模式的旧实例也会并存;multiton模式的旧实例会被移除;

(3)specified 启动模式
  • 每个UIAbility实例可以设置Key标示;

  • 启动UIAbility时,需要指定Key,存在key相同实例直接被拉起,不存在则创建新实例;

  • 在一个Ability中调用另一个Ability的步骤:

        第一步:当前UIAbility方法拉起目标UIAbility

        1.1 获取上下文

    context = getContext(this) as common.UIAbilityContext

        1.2 指定要跳转到的UIAbility的信息

    let want = {
        deviceId:'', // deviceId为空:表示本设备
        bundleName:'com.example.myapplication',
        abilityName:'entry', // moduleName 非必填
        Parameters: {
          // getInstanceKey:自定义方法,生成目标UIAbility实例的key
          instanceKey: this.getInstanceKey()
        }
    }

        1.3 尝试拉起目标UIAbility实例

    this.context.startAbility(want)

        第二步:在AbilityStage的生命周期回调中为目标UIAbility实例生成key

    export default class MyAbilityStage extends AbilityStage{
        onAcceptWant(want: Want): string {
            // 判断当前要拉取的是否是DocumentAbility
            if(want.abilityName === 'DocumentAbility'){
                // 根据参数中的instanceKey参数拼接生成一个key值并返回
                return `DocAbility_${want.parameters.instanceKey}`
            }
            return '';
        }
    }

        第三步:在module.json5配置文件中,通过srcEntry参数指定AbilityStage路径

    {
        "module": {
            "name": "entry",
            "type": "entry",
            "srcEntry": "./ets/myabIlitystage/MyAbIlityStage.ts",
            ...
        }
    }

(七)网络连接

1、Http 请求数据

  (1)导入http模块
import http from '@ohos.net.http'
  (2)发送请求,处理响应
// 1. 创建一个http的请求对象,不可复用
let httpRequest = http.createHttp()
// 2. 发起网络请求
httpRequest.request(
    // 请求url路径
    'http://localhost:3000/users',
    // 请求选项 HttpRequestOptions
    {
        method:http.RequestMethod.GET,
        extraData: {'param1':'value1'} // k1=v1 & k2=v2
    }
)
// Promise:存放未来会完成的结果
// 3. 处理响应结果
.then((resp:http.HttpResponse) => {
    if(resp.responseCode === 200){
        // 请求成功
    }
})
.catch((err: Error) => {
    // 请求失败
})

2、第三方库 axios

(1)下载和安装ohpm(鸿蒙第三方库的包管理工具)
   步骤

        第一步:下载ohpm工具包DevEco Studio-HarmonyOS SDK下载和升级-华为开发者联盟

        第二步:解压工具包,执行初始化命令

        将压缩包放在自己习惯的安装路径下 —— 解压到当前文件夹 —— 进入文件夹的“ohpm/bin”目录 —— cmd,执行初始化命令“init.bat” —— 验证是否执行成功“ohpm -v”,出现版本号就是成功


// windows 环境
init.bat

// Linux 或 Mac 环境
./init.sh

        第三步:将ohpm配置到环境变量

// windows环境,直接在我的电脑配置即可
此电脑 > 属性 > 高级系统设置 > 高级 > 环境变量中:
1. 新建:名称“ OHPM_HOME ”、路径“ 解压文件夹打开到ohpm这一级的对应路径 ”
2. 双击Path —— 新建:添加路径“ %OHPM_HOME%\bin ”

// Linux 或 Mac 环境,其中 OHPM 的路径请替换为 ohpm 的安装路径
export OHPM_HOME=/home/xx/Downloads/ohpm  //OHPM_HOME=ohpm的安装路径
export PATH=${OHPM_HOME}/bin:${PATH}
更多详细说明

  官方文档:ohpm使用指导-命令行工具-DevEco Studio使用指南-工具

(2)下载和安装axios
步骤

        第一步:下载axios

// 进入项目目录,输入命令
ohpm install @ohos/axios
    第二步:开放网络权限
// 在模块的module.json5文件中配置网络权限
{
    "module": {
        "requestPermissions": [
                        {
                "name": "ohos.permission.INTERNET",
            }
        ]
    }
}

更多详细说明

        官方地址:https://ohpm.openharmony.cn/#/cn/home

 

(3)使用axios

导入axios

import axios from '@ohos/axios'

发送请求并处理响应

// 请求方式,不同方式使用不同方法
axios.get(
    'url', // 请求路径
    {
        params: {'param1':'value1'}, // 请求选项
        data: {'param1':'value1'}
    }
)
  // 响应结果
  .then(response => {
    if(response.staus === 200){
        console.log('查询成功')
    }else{
        console.log('查询失败')
    }
    
  })
  .catch(error => {
      console.log('查询失败',JSON.stringify(error))
  })

(八)数据持久化

1、用户首选项

作用:为应用提供 Key-Value 键值型的数据处理能力,支持应用持久化轻量级数据。

2、关系型数据库

1、导入首选项模块

import dataPreference from '@ohos.data.preferences'

2、获取首选项实例,读取指定文件

dataPreference.getPreferences(this.context,'MyAppPreferences')
    .then(preferences => {
        // 获取成功
    })
    .catch(reason=> {
        // 获取失败
    })

3、数据操作

// 1.写入数据,如果已经存在,则会覆盖,可利用.has()判断是否存在
preferences.put('key',val)
    .then(() => preferences.flush()) // 刷到磁盘
    .catch(reason => {}) // 处理异常

// 2.删除数据
preferences.delete('key')
    .then(() => {})
    .catch(reason => {})
    
// 3.查询数据
preferences.get('key','defaultValue')
    .then(value => console.log('查询成功'))
    .catch(reason => console.log('查询失败'))

ps:

key为string类型,要求非空且长度不超过80字节;

value可以是string、number、boolean及以上类型数组,大小不超过8192字节;

(九)通知

1、基础通知

2、进度条通知

3、通知行为意图

三、实战案例