HarmoneyOS

259 阅读17分钟

HarmonyOs

安装编译器

华为鸿蒙开发者官网可以下载编译器和查看学习路径。

软件安装可以参考开发者文档

image-20231212222051161

点击IDE下载,选择对应的版本就OK了,安装软件也是一步一步的nextimage-20231212222256580

安装成功之后,打开编译器需要分步骤安装一些东西。

第一步是选择node环境和鸿蒙包管理。值得注意的是截止2023年12月12日node版本要保持在14-16的版本,14开头和16开头的都可以。

如果本地安装了node选择Local就可以,如果没有安装node还是建议安装或者是选择Install

鸿蒙包管理Ohpm 我们选择Install

image-20231212222433198

安装好了之后点击 next。

第二步SDK 安装

选择HarmonyOS SDK安装位置

image-20231212222817506

安装好了之后点击next。点击Accecpt

image-20231212222934303

安装完成之后进入确认页,点击next就完成了编译器的安装。

ArKTS

ArkTS和TS和JS有什么关系

TS是JS的一个超集,而ArkTS是JS的一个超集。

image-20231212223515532

声明式UI就是需要什么就声明什么。ArkTS中集成了ArkUI。

build(){
  Button("点击了")
    .onClick(()=>{
      
    })
}

这样就是实现了一个按钮,这个按钮是有样式的,样式从哪里来呢?就是ArkUI中集成就Button这个按钮。

可以这样去理解。 ArkUI中定义了很多个组件,我们只需要直接调用这个组件就可以了。全部的属性和方法都变成了组件的一种属性。

比如下面的一个文本,设置文本的一些属性可以链式调用。

Text("我是文本")
  .fontSize(30)
  .fontWeight(FontWeigth.Blob)

创建第一个Hello world

创建第一个项目,按照下面的创建第一个程序。

image-20231212224435490

image-20231212224645296

这样就创建出了第一个程序。展开enrty文件夹page/Index.ets就是我们的页面了。展开之后可以在编译器最右边看见Preview按钮,点击就可以看见预览界面。

image-20231212224903778

image-20231212225011368

@Entry @Component @State struct

@Entry 标记当前组件为入口组件,也就是可以被预览的界面。

@Component 组件的定义标记,自定义组件的时候需要加上这个装饰器。

@State 定义状态的关键装饰器,可以理解为react的useState和vue的双向绑定,用这个装饰器修饰的变量,被修改之后,UI会跟着刷新。

struct 用来定义一个复用的组件

@Entry
@Component
struct Index {
  @State message: string = 'Hello World'
​
  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
    }
    .height('100%')
  }
}

事件

事件太简单了,比如按钮的点击事件,其实就是按钮的一个属性。

Button('点击')
  .onClick(()=>{
  //...
})

build(){}

每一个组件都需要有一个build函数,这个函数中书写的就是UI组件。

build函数中只能有一个根组件

ForEach

循环控制

@Component
struct forEachCom{
  @State arr:Array<string> = ['A','B','C','D']
  build(){
    ForEach(this.arr,()=>{},item=>item)
  }
}

ForEach(this.arr,()=>{},item=>item) 分成三个参数,第一个参数是需要循环的数组,第二个参数是一个回调函数,第三个参数是标记独一唯一的。

我们循环可以配合List组件使用,List组件呢需要搭配ListItem,并且只能有ListItem

@Component
struct forEachCom{
  @State arr:Array<string> = ['A','B','C','D']
  build(){
    List(){
      ForEach(this.arr,(item)=>{
        ListItem(){
          Text(item)
        }
      },item=>item)
    }
  }
}

值得注意的是ListItem中只能有一个父组件

@Builder @Styles

@Entry
@Component
struct Index {
  @State message: string = 'Hello World'
  @Builder Item(){  // 自定义构建函数
    Text('我是公共逻辑')
  }
  @Styles styleCom(){
    .margin({top:10})
  }
  build() {
    Row() {
      this.Item() // 使用自定义构建函数
    }
    .height('100%')
    .styleCom()
  }
}

@Builder用来绑定一个函数,自定义构建函数,我们将一个公共的逻辑抽取出来之后,用@Builder绑定然后直接调用这个函数就可以了。@Builder所装饰的函数遵循build()函数语法规则,开发者可以将重复使用的UI元素抽象成一个方法,在build方法里调用。

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

@Styles 抽取共同的样式,避免重复写。

@Styles可以定义在组件内或全局,在全局定义时需在方法名前面添加function关键字,组件内定义时则不需要添加function关键字。

全局的自定义构建函数

@Builder function MyGlobalBuilderFunction(){ ... }
  • 全局的自定义构建函数可以被整个应用获取,不允许使用this和bind方法。
  • 如果不涉及组件状态变化,建议使用全局的自定义构建方法。

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

当开发者创建了自定义组件,并想对该组件添加特定功能时,例如在自定义组件中添加一个点击跳转操作。若直接在组件内嵌入事件方法,将会导致所有引入该自定义组件的地方均增加了该功能。为解决此问题,ArkUI引入了@BuilderParam装饰器,@BuilderParam用来装饰指向@Builder方法的变量,开发者可在初始化自定义组件时对此属性进行赋值,为自定义组件增加特定的功能。该装饰器用于声明任意UI描述的一个元素,类似slot占位符。

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

@Builder function GlobalBuilder0() {}
​
@Component
struct Child {
  @Builder doNothingBuilder() {};
  @BuilderParam aBuilder0: () => void = this.doNothingBuilder;
  @BuilderParam aBuilder1: () => void = GlobalBuilder0;
  build(){}
}
​

@Extend装饰器:定义扩展组件样式

可以使用@Styles用于样式的扩展,在@Styles的基础上,我们提供了@Extend,用于扩展原生组件样式。

@Extend(UIComponentName) function functionName { ... }

使用规则

  • 和@Styles不同,@Extend仅支持在全局定义,不支持在组件内部定义。
  • 和@Styles不同,@Extend支持封装指定的组件的私有属性和私有事件,以及预定义相同组件的@Extend的方法。
  • 和@Styles不同,@Extend装饰的方法支持参数,开发者可以在调用时传递参数,调用遵循TS方法传值调用。
  • @Extend装饰的方法的参数可以为function,作为Event事件的句柄。
  • @Extend的参数可以为状态变量,当状态变量改变时,UI可以正常的被刷新渲染。

自定义组件

在ArkUI中,UI显示的内容均为组件,由框架直接提供的称为系统组件,由开发者定义的称为自定义组件。在进行 UI 界面开发时,通常不是简单的将系统组件进行组合使用,而是需要考虑代码可复用性、业务逻辑与UI分离,后续版本演进等因素。因此,将UI和部分业务逻辑封装成自定义组件是不可或缺的能力。

自定义组件一般用@Component装饰器来装饰。

@Component
struct MyCom{
  @State msg:string = 'hello world'
  build(){
    // UI
    Text(this.msg) // 内置UI 也就是arkUI提供的组件
  }
}

自定义组件具有以下特点:

  • 可组合:允许开发者组合使用系统组件、及其属性和方法。
  • 可重用:自定义组件可以被其他组件重用,并作为不同的实例在不同的父组件或容器中使用。
  • 数据驱动UI更新:通过状态变量的改变,来驱动UI的刷新。

stateStyles:多态样式

@Styles和@Extend仅仅应用于静态页面的样式复用,stateStyles可以依据组件的内部状态的不同,快速设置不同样式。这就是我们本章要介绍的内容stateStyles(又称为:多态样式)。

stateStyles是属性方法,可以根据UI内部状态来设置样式,类似于css伪类,但语法不同。ArkUI提供以下四种状态:

  • focused:获焦态。
  • normal:正常态。
  • pressed:按压态。
  • disabled:不可用态。
  • selected10+:选中态。
@Entry
@Component
struct StateStylesSample {
  build() {
    Column() {
      Button('Click me')
        .stateStyles({
          focused: {
            .backgroundColor(Color.Pink)
          },
          pressed: {
            .backgroundColor(Color.Black)
          },
          normal: {
            .backgroundColor(Color.Yellow)
          }
        })
    }.margin('30%')
  }
}
​

@Styles和stateStyles联合使用

@Entry
@Component
struct MyComponent {
  @Styles normalStyle() {
    .backgroundColor(Color.Gray)
  }
​
  @Styles pressedStyle() {
    .backgroundColor(Color.Red)
  }
​
  build() {
    Column() {
      Text('Text1')
        .fontSize(50)
        .fontColor(Color.White)
        .stateStyles({
          normal: this.normalStyle,
          pressed: this.pressedStyle,
        })
    }
  }
}
​

在stateStyles里可以使用常规变量和状态变量

页面和自定义组件生命周期

在开始之前,我们先明确自定义组件和页面的关系:

  • 自定义组件:@Component装饰的UI单元,可以组合多个系统组件实现UI的复用,可以调用组件的生命周期。
  • 页面:即应用的UI页面。可以由一个或者多个自定义组件组成,@Entry装饰的自定义组件为页面的入口组件,即页面的根节点,一个页面有且仅能有一个@Entry。只有被@Entry装饰的组件才可以调用页面的生命周期。

页面生命周期,即被@Entry装饰的组件生命周期,提供以下生命周期接口:

  • onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景,仅@Entry装饰的自定义组件生效。
  • onPageHide:页面每次隐藏时触发一次,包括路由过程、应用进入前后台等场景,仅@Entry装饰的自定义组件生效。
  • onBackPress:当用户点击返回按钮时触发,仅@Entry装饰的自定义组件生效。

组件生命周期,即一般用@Component装饰的自定义组件的生命周期,提供以下生命周期接口:

  • aboutToAppear:组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其build()函数之前执行。
  • aboutToDisappear:在自定义组件析构销毁之前执行。不允许在aboutToDisappear函数中改变状态变量,特别是@Link变量的修改可能会导致应用程序行为不稳定。

生命周期流程如下图所示,下图展示的是被@Entry装饰的组件(首页)生命周期。

image-20231213092823156

// Index.ets
import router from '@ohos.router';
​
@Entry
@Component
struct MyComponent {
  @State showChild: boolean = true;
​
  // 只有被@Entry装饰的组件才可以调用页面的生命周期
  onPageShow() {
    console.info('Index onPageShow');
  }
  // 只有被@Entry装饰的组件才可以调用页面的生命周期
  onPageHide() {
    console.info('Index onPageHide');
  }
​
  // 只有被@Entry装饰的组件才可以调用页面的生命周期
  onBackPress() {
    console.info('Index onBackPress');
  }
​
  // 组件生命周期
  aboutToAppear() {
    console.info('MyComponent aboutToAppear');
  }
​
  // 组件生命周期
  aboutToDisappear() {
    console.info('MyComponent aboutToDisappear');
  }
​
  build() {
    Column() {
      // this.showChild为true,创建Child子组件,执行Child aboutToAppear
      if (this.showChild) {
        Child()
      }
      // this.showChild为false,删除Child子组件,执行Child aboutToDisappear
      Button('create or delete Child').onClick(() => {
        this.showChild = false;
      })
      // push到Page2页面,执行onPageHide
      Button('push to next page')
        .onClick(() => {
          router.pushUrl({ url: 'pages/Page2' });
        })
    }
​
  }
}
​
@Component
struct Child {
  @State title: string = 'Hello World';
  // 组件生命周期
  aboutToDisappear() {
    console.info('[lifeCycle] Child aboutToDisappear')
  }
  // 组件生命周期
  aboutToAppear() {
    console.info('[lifeCycle] Child aboutToAppear')
  }
​
  build() {
    Text(this.title).fontSize(50).onClick(() => {
      this.title = 'Hello ArkUI';
    })
  }
}

以上示例中,Index页面包含两个自定义组件,一个是被@Entry装饰的MyComponent,也是页面的入口组件,即页面的根节点;一个是Child,是MyComponent的子组件。只有@Entry装饰的节点才可以使页面级别的生命周期方法生效,所以MyComponent中声明了当前Index页面的页面生命周期函数。MyComponent和其子组件Child也同时也声明了组件的生命周期函数。

  • 应用冷启动的初始化流程为:MyComponent aboutToAppear --> MyComponent build --> Child aboutToAppear --> Child build --> Child build执行完毕 --> MyComponent build执行完毕 --> Index onPageShow。
  • 点击“delete Child”,if绑定的this.showChild变成false,删除Child组件,会执行Child aboutToDisappear方法。
  • 点击“push to next page”,调用router.pushUrl接口,跳转到另外一个页面,当前Index页面隐藏,执行页面生命周期Index onPageHide。此处调用的是router.pushUrl接口,Index页面被隐藏,并没有销毁,所以只调用onPageHide。跳转到新页面后,执行初始化新页面的生命周期的流程。
  • 如果调用的是router.replaceUrl,则当前Index页面被销毁,执行的生命周期流程将变为:Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear。上文已经提到,组件的销毁是从组件树上直接摘下子树,所以先调用父组件的aboutToDisappear,再调用子组件的aboutToDisappear,然后执行初始化新页面的生命周期流程。
  • 点击返回按钮,触发页面生命周期Index onBackPress,且触发返回一个页面后会导致当前Index页面被销毁。
  • 最小化应用或者应用进入后台,触发Index onPageHide。当前Index页面没有被销毁,所以并不会执行组件的aboutToDisappear。应用回到前台,执行Index onPageShow。
  • 退出应用,执行Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear。

状态管理

组件内的状态@State

@State装饰的变量,或称为状态变量,一旦变量拥有了状态属性,就和自定义组件的渲染绑定起来。当状态改变时,UI会发生对应的渲染改变。

在状态变量相关装饰器中,@State是最基础的,使变量拥有状态属性的装饰器,它也是大部分状态变量的数据源。

@State变量装饰器说明
装饰器参数
同步类型不与父组件中任何类型的变量同步。
允许装饰的变量类型Object、class、string、number、boolean、enum类型,以及这些类型的数组。 支持Date类型。 支持类型的场景请参考观察变化。 类型必须被指定。 不支持any,不支持简单类型和复杂类型的联合类型,不允许使用undefined和null。 说明: 不支持Length、ResourceStr、ResourceColor类型,Length、ResourceStr、ResourceColor为简单类型和复杂类型的联合类型。
被装饰变量的初始值必须本地初始化。

父子单向传递 @Prop

@Prop装饰的变量可以和父组件建立单向的同步关系。@Prop装饰的变量是可变的,但是变化不会同步回其父组件。

其实就是想vue中的父子传值。很相似了。

@Prop装饰的变量和父组件建立单向的同步关系:

  • @Prop变量允许在本地修改,但修改后的变化不会同步回父组件。

  • 当数据源更改时,@Prop装饰的变量都会更新,并且会覆盖本地所有更改。因此,数值的同步是父组件到子组件(所属组件),子组件数值的变化不会同步到父组件。

    不用初始化。

    父组件

    @State a:string = 'hello'
    ​
    Child({a: this.a})
    

    Child组件

    @Prop a:string
    
    @Prop变量装饰器说明
    装饰器参数
    同步类型单向同步:对父组件状态变量值的修改,将同步给子组件@Prop装饰的变量,子组件@Prop变量的修改不会同步到父组件的状态变量上。嵌套类型的场景请参考观察变化
    允许装饰的变量类型Object、class、string、number、boolean、enum类型,以及这些类型的数组。 不支持any,不支持简单类型和复杂类型的联合类型,不允许使用undefined和null。 支持Date类型。 支持类型的场景请参考观察变化。 必须指定类型。 说明 : 不支持Length、ResourceStr、ResourceColor类型,Length,ResourceStr、ResourceColor为简单类型和复杂类型的联合类型。 在父组件中,传递给@Prop装饰的值不能为undefined或者null,反例如下所示。 CompA ({ aProp: undefined }) CompA ({ aProp: null }) @Prop和数据源类型需要相同,有以下三种情况: - @Prop装饰的变量和@State以及其他装饰器同步时双方的类型必须相同,示例请参考父组件@State到子组件@Prop简单数据类型同步。 - @Prop装饰的变量和@State以及其他装饰器装饰的数组的项同步时 ,@Prop的类型需要和@State装饰的数组的数组项相同,比如@Prop : T和@State : Array,示例请参考父组件@State数组中的项到子组件@Prop简单数据类型同步; - 当父组件状态变量为Object或者class时,@Prop装饰的变量和父组件状态变量的属性类型相同,示例请参考从父组件中的@State类对象属性到@Prop简单类型的同步
    嵌套传递层数在组件复用场景,建议@Prop深度嵌套数据不要超过5层,嵌套太多会导致深拷贝占用的空间过大以及GarbageCollection(垃圾回收),引起性能问题,此时更建议使用@ObjectLink。如果子组件的数据不想同步回父组件,建议采用@Reusable中的aboutToReuse,实现父组件向子组件传递数据,具体用例请参考组件复用场景
    被装饰变量的初始值允许本地初始化。

父子双向传递 @Link

子组件中被@Link装饰的变量与其父组件中对应的数据源建立双向数据绑定。

在传递值的时候用$符号。

父组件

@State a:string = 'hello'
​
Child({a: $a})

Child组件

@Link a:string

@Link装饰器不能在@Entry装饰的自定义组件中使用。

@Link变量装饰器说明
装饰器参数
同步类型双向同步。 父组件中@State, @StorageLink和@Link 和子组件@Link可以建立双向数据同步,反之亦然。
允许装饰的变量类型Object、class、string、number、boolean、enum类型,以及这些类型的数组。 支持Date类型。支持类型的场景请参考观察变化。 类型必须被指定,且和双向绑定状态变量的类型相同。 不支持any,不支持简单类型和复杂类型的联合类型,不允许使用undefined和null。 说明: 不支持Length、ResourceStr、ResourceColor类型,Length、ResourceStr、ResourceColor为简单类型和复杂类型的联合类型。
被装饰变量的初始值无,禁止本地初始化。

跨层级双向传递 @Provide @Consume

@Provide和@Consume,应用于与后代组件的双向数据同步,应用于状态数据在多个层级之间传递的场景。不同于上文提到的父子组件之间通过命名参数机制传递,@Provide和@Consume摆脱参数传递机制的束缚,实现跨层级传递。

其中@Provide装饰的变量是在祖先组件中,可以理解为被“提供”给后代的状态变量。@Consume装饰的变量是在后代组件中,去“消费(绑定)”祖先组件提供的变量。

@Provide/@Consume装饰的状态变量有以下特性:

  • @Provide装饰的状态变量自动对其所有后代组件可用,即该变量被“provide”给他的后代组件。由此可见,@Provide的方便之处在于,开发者不需要多次在组件之间传递变量。
  • 后代通过使用@Consume去获取@Provide提供的变量,建立在@Provide和@Consume之间的双向数据同步,与@State/@Link不同的是,前者可以在多层级的父子组件之间传递。
  • @Provide和@Consume可以通过相同的变量名或者相同的变量别名绑定,建议类型相同,否则会发生类型隐式转换,从而导致应用行为异常。
// 通过相同的变量名绑定
@Provide a: number = 0;
@Consume a: number;
​
// 通过相同的变量别名绑定
@Provide('a') b: number = 0;
@Consume('a') c: number;

@State的规则同样适用于@Provide,差异为@Provide还作为多层后代的同步源。

@Provide变量装饰器说明
装饰器参数别名:常量字符串,可选。 如果指定了别名,则通过别名来绑定变量;如果未指定别名,则通过变量名绑定变量。
同步类型双向同步。 从@Provide变量到所有@Consume变量以及相反的方向的数据同步。双向同步的操作与@State和@Link的组合相同。
允许装饰的变量类型Object、class、string、number、boolean、enum类型,以及这些类型的数组。 支持Date类型。 支持类型的场景请参考观察变化。 不支持any,不支持简单类型和复杂类型的联合类型,不允许使用undefined和null。 必须指定类型。@Provide变量的@Consume变量的类型必须相同。 说明: 不支持Length、ResourceStr、ResourceColor类型,Length、ResourceStr、ResourceColor为简单类型和复杂类型的联合类型。
被装饰变量的初始值必须指定。
@Consume变量装饰器说明
装饰器参数别名:常量字符串,可选。 如果提供了别名,则必须有@Provide的变量和其有相同的别名才可以匹配成功;否则,则需要变量名相同才能匹配成功。
同步类型双向:从@Provide变量(具体请参见@Provide)到所有@Consume变量,以及相反的方向。双向同步操作与@State和@Link的组合相同。
允许装饰的变量类型Object、class、string、number、boolean、enum类型,以及这些类型的数组。 支持Date类型。 支持类型的场景请参考观察变化。 不支持any,不允许使用undefined和null。 必须指定类型。@Provide变量的@Consume变量的类型必须相同。 说明: @Consume装饰的变量,在其父节点或者祖先节点上,必须有对应的属性和别名的@Provide装饰的变量。
被装饰变量的初始值无,禁止本地初始化。
  • 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
  • 当装饰的数据类型为class或者Object的时候,可以观察到赋值和属性赋值的变化(属性为Object.keys(observedObject)返回的所有属性)。
  • 当装饰的对象是array的时候,可以观察到数组的添加、删除、更新数组单元。
  • 当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。

嵌套类对象属性变化 @Oberved @ObejectLink

上文所述的装饰器仅能观察到第一层的变化,但是在实际应用开发中,应用会根据开发需要,封装自己的数据模型。对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这就引出了@Observed/@ObjectLink装饰器。

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

  • 被@Observed装饰的类,可以被观察到属性的变化;
  • 子组件中@ObjectLink装饰器装饰的状态变量用于接收@Observed装饰的类的实例,和父组件中对应的状态变量建立双向数据绑定。这个实例可以是数组中的被@Observed装饰的项,或者是class object中的属性,这个属性同样也需要被@Observed装饰。
  • 单独使用@Observed是没有任何作用的,需要搭配@ObjectLink或者@Prop使用。
@Observed
export class TaskInfoType {
  finshedValue: number
  totalValue: number
}
@Observed
export class TaskType{
  id:number
  taskName: string
  isFinshed: boolean
  constructor(id:number,taskName:string,isFinshed:boolean) {
    this.isFinshed = isFinshed
    this.id = id
    this.taskName = taskName
  }
}

在页面中定义一个对象数组。

@State tasks: Array<TaskType> = []

在子页面中用@Link接收这个数组

@Link tasks: Array<TaskType>

在子页面的子页面通过@ObjectLink

@ObjectLink task: TaskType

在这个页面修改task数组中的数据也会同步的去修改刷新页面。

如果我们需要再子组件中调用父组件的方法怎么办?

1.在父组件定义调用的方法

 // 定义要在父类执行的方法
  onTaskChage(){
    this.taskInfo.finshedValue = this.tasks.filter((task:TaskType)=>{
      return task.isFinshed
    }).length
  }

2.在子组件中定义空方法

  // 子类调用父类的方法,先在子类定义一个空方法
  onTaskChange: ()=>void

3.在父组件中调用的地方传递

 // 在这里将父类的方法传递给子类
 Child({task:task,onTaskChange:this.onTaskChage.bind(this)}) // 这里就相当于把父组件定义的方法传递给子组件的参数

4.子组件调用父组件的方法

//   执行修改的方法
this.onTaskChange()

管理应用拥有的状态

章节中介绍的装饰器仅能在页面内,即一个组件树上共享状态变量。如果开发者要实现应用级的,或者多个页面的状态数据共享,就需要用到应用级别的状态管理的概念。ArkTS根据不同特性,提供了多种应用状态管理的能力:

  • LocalStorage:页面级UI状态存储,通常用于UIAbility内、页面间的状态共享。
  • AppStorage:特殊的单例LocalStorage对象,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储;
  • PersistentStorage:持久化存储UI状态,通常和AppStorage配合使用,选择AppStorage存储的数据写入磁盘,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同;
  • Environment:应用程序运行的设备的环境参数,环境参数会同步到AppStorage中,可以和AppStorage搭配使用。

LocalStorage:页面级UI状态存储

LocalStorage是页面级的UI状态存储,通过@Entry装饰器接收的参数可以在页面内共享同一个LocalStorage实例。LocalStorage也可以在UIAbility内,页面间共享状态。

本文仅介绍LocalStorage使用场景和相关的装饰器:@LocalStorageProp和@LocalStorageLink。

LocalStorage是ArkTS为构建页面级别状态变量提供存储的内存内“数据库”。

如果要建立LocalStorage和自定义组件的联系,需要使用@LocalStorageProp和@LocalStorageLink装饰器。使用 @LocalStorageProp(key)/@LocalStorageLink(key) 装饰组件内的变量,key标识了LocalStorage的属性。当自定义组件初始化的时候, @LocalStorageProp(key)/@LocalStorageLink(key)装饰的变量会通过给定的key,绑定LocalStorage对应的属性,完成初始化。

@LocalStorageProp(key)是和LocalStorage中key对应的属性建立单向数据同步,我们允许本地改变的发生,但是对于@LocalStorageProp,本地的修改永远不会同步回LocalStorage中,相反,如果LocalStorage给定key的属性发生改变,改变会被同步给@LocalStorageProp,并覆盖掉本地的修改。

如果我们需要将自定义组件的状态变量的更新同步回LocalStorage,就需要用到@LocalStorageLink。

@LocalStorageLink(key)是和LocalStorage中key对应的属性建立双向数据同步

  1. 本地修改发生,该修改会被写回LocalStorage中;
  2. LocalStorage中的修改发生后,该修改会被同步到所有绑定LocalStorage对应key的属性上,包括单向(@LocalStorageProp和通过prop创建的单向绑定变量)、双向(@LocalStorageLink和通过link创建的双向绑定变量)变量。

从UI内部使用LocalStorage

@LocalStorageLink绑定LocalStorage对给定的属性,建立双向数据同步。

父组件中

import { StudyChild } from './StudyChild'
// 创建一个LocalStore
let para:Record<string,string> = { 'PropA': 'AAA' };
const store: LocalStorage = new LocalStorage(para)
@Entry(store)
@Component
struct Study {
  @State message: string = 'Hello World'
  // PropA 是para中的数据 其实就相当于是一个状态仓库
  @LocalStorageLink('PropA') link1: string = '张三'
  // @LocalStorageProp('PropA') link1: string = '张三'
​
  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
        Text(this.link1)
          .fontSize(30)
        Button("change").onClick(()=>{
          this.link1 = '父组件改变的link'
        })
​
        StudyChild()
      }
      .width('100%')
    }
    .height('100%')
  }
}

在子组件中

@Component
export struct StudyChild{
  @LocalStorageLink('PropA') link2: string = 'lisi'
  // @LocalStorageProp('PropA') link2: string = 'lisi'
​
  build(){
    Column(){
      Text(this.link2)
      Button('子组件的改变')
        .onClick(()=>{
          this.link2 = '子组件改变的'
        })
    }
    .height('100%')
  }
}
​

父子组件都给值的时候,以父组件的为准,父组件将link1值修改之后,子组件的link2的值会同步变化,在子组件中修改link2的值,父组件的link1的值也会相应的变化。

而LocalStorageProp的修改是单向的,父组件修改了值之后再父组件生效,子组件修改了值只会在子组件生效。

将LocalStorage实例从UIAbility共享到一个或多个视图

上面的实例中,LocalStorage的实例仅仅在一个@Entry装饰的组件和其所属的子组件(一个页面)中共享,如果希望其在多个视图中共享,可以在所属UIAbility中创建LocalStorage实例,并调用windowStage.loadContent

// EntryAbility.ts
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';
let para:Record<string,number> = { 'PropA': 47 };
let localStorage: LocalStorage = new LocalStorage(para);
export default class EntryAbility extends UIAbility {
storage: LocalStorage = localStorageonWindowStageCreate(windowStage: window.WindowStage) {
windowStage.loadContent('pages/Index', this.storage);
}
}
​
// 通过getShared接口获取stage共享的LocalStorage实例
let storage = LocalStorage.getShared()
​
@Entry(storage)
@Component
struct CompA {
  // can access LocalStorage instance using 
  // @LocalStorageLink/Prop decorated variables
  @LocalStorageLink('PropA') varA: number = 1;
​
  build() {
    Column() {
      Text(`${this.varA}`).fontSize(50)
    }
  }
}
​

@Watch装饰器:状态变量更改通知

@Watch应用于对状态变量的监听。如果开发者需要关注某个状态变量的值是否改变,可以使用@Watch为状态变量设置回调函数。

@Watch用于监听状态变量的变化,当状态变量变化时,@Watch的回调方法将被调用。@Watch在ArkUI框架内部判断数值有无更新使用的是严格相等(===),遵循严格相等规范。当在严格相等为false的情况下,就会触发@Watch的回调。

  1. 当观察到状态变量的变化(包括双向绑定的AppStorage和LocalStorage中对应的key发生的变化)的时候,对应的@Watch的回调方法将被触发;
  2. @Watch方法在自定义组件的属性变更之后同步执行;
  3. 如果在@Watch的方法里改变了其他的状态变量,也会引起状态变更和@Watch的执行;
  4. 在第一次初始化的时候,@Watch装饰的方法不会被调用,即认为初始化不是状态变量的改变。只有在后续状态改变时,才会调用@Watch回调方法。
@Component
struct TotalView {
  @Prop @Watch('onCountUpdated') count: number = 0;
  @State total: number = 0;
  // @Watch 回调
  onCountUpdated(propName: string): void {
    this.total += this.count;
  }
​
  build() {
    Text(`Total: ${this.total}`)
  }
}
​
@Entry
@Component
struct CountModifier {
  @State count: number = 0;
​
  build() {
    Column() {
      Button('add to basket')
        .onClick(() => {
          this.count++
        })
      TotalView({ count: this.count })
    }
  }
}
​

$$语法:内置组件双向同步

$$ 运算符为系统内置组件提供TS变量的引用,使得TS变量和系统内置组件的内部状态保持同步。

内部状态具体指什么取决于组件。例如,TextInput组件的text参数。其实就类似于双向绑定

  • 当前 $$ 支持基础类型变量,以及@State、@Link和@Prop装饰的变量。
  • 当前 $$ 支持的组件
组件支持的参数/属性起始API版本
Checkboxselect10
CheckboxGroupselectAll10
DatePickerselected10
TimePickerselected10
MenuItemselected10
Panelmode10
Radiochecked10
Ratingrating10
Searchvalue10
SideBarContainershowSideBar10
Slidervalue10
Stepperindex10
Swiperindex10
Tabsindex10
TextAreatext10
TextInputtext10
TextPickerselected、value10
ToggleisOn10
AlphabetIndexerselected10
Selectselected、value10
BindSheetisShow10
BindContentCoverisShow10
Refreshrefreshing8
  • $$ 绑定的变量变化时,会触发UI的同步刷新。
// xxx.ets
@Entry
@Component
struct TextInputExample {
  @State text: string = ''
  controller: TextInputController = new TextInputController()
​
  build() {
    Column({ space: 20 }) {
      Text(this.text)
      TextInput({ text: $$this.text, placeholder: 'input your word...', controller: this.controller })
        .placeholderColor(Color.Grey)
        .placeholderFont({ size: 14, weight: 400 })
        .caretColor(Color.Blue)
        .width(300)
    }.width('100%').height('100%').justifyContent(FlexAlign.Center)
  }
}
​

路由跳转

@ohos.router本模块提供通过不同的url访问不同的页面,包括跳转到应用内的指定页面、同应用内的某个页面替换当前页面、返回上一页面或指定的页面等。

路由的基本使用

导入模块

import router from '@ohos.router'

路由跳转使用,跳转到应用内的指定页面。pushUrl会被历史记录栈记录,便于前进后退。目标页面不会替换当前页,而是压入页面栈。这样可以保留当前页的状态,并且可以通过返回键或者调用router.back()方法返回到当前页。

Button('跳转页面')
  .onClick(()=>{
    router.pushUrl({
      url:'pages/Index',
      params:{
        name:'张三',
        age:'12'
      }
    })
  })

replaceUrl会销毁和替换页面,目标页面会替换当前页,并销毁当前页。这样可以释放当前页的资源,并且无法返回到当前页。

router.replaceUrl(
  {    url: 'pages/detail', 
       params: new routerParams('message')  
  }
)

pushNamedRoute(options: NamedRouterOptions): Promise

跳转到指定的命名路由页面。

 router.pushNamedRoute({
    name: 'myPage',
    params: new routerParams('message' ,[123,456,789])
  })

getParams(): Object

获取发起跳转的页面往当前页传入的参数。

路由的两种模式

同时,Router模块提供了两种实例模式,分别是Standard和Single。这两种模式决定了目标url是否会对应多个实例。

  • Standard:多实例模式,也是默认情况下的跳转模式。目标页面会被添加到页面栈顶,无论栈中是否存在相同url的页面。

  • Single:单实例模式。如果目标页面的url已经存在于页面栈中,则会将离栈顶最近的同url页面移动到栈顶,该页面成为新建页。如果目标页面的url在页面栈中不存在同url页面,则按照默认的多实例模式进行跳转。

  • 场景一:有一个主页(Home)和一个详情页(Detail),希望从主页点击一个商品,跳转到详情页。同时,需要保留主页在页面栈中,以便返回时恢复状态。这种场景下,可以使用pushUrl()方法,并且使用Standard实例模式(或者省略)。

    import router from '@ohos.router';
    // 在Home页面中
    function onJumpClick(): void {
      router.pushUrl({
        url: 'pages/Detail' // 目标url
      }, router.RouterMode.Standard, (err) => {
        if (err) {
          console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
          return;
        }
        console.info('Invoke pushUrl succeeded.');
      });
    }
    ​
    

路由返回

页面返回

//返回上一个页面
router.back();
​
// 返回指定页面
router.back({
  url: 'pages/Home'
});
​
// 返回到指定页面,并传递自定义参数信息。
import router from '@ohos.router';
router.back({
  url: 'pages/Home',
  params: {
    info: '来自Home页'
  }
});

页面返回前增加一个询问框

在开发应用时,为了避免用户误操作或者丢失数据,有时候需要在用户从一个页面返回到另一个页面之前,弹出一个询问框,让用户确认是否要执行这个操作。

本文将从系统默认询问框自定义询问框两个方面来介绍如何实现页面返回前增加一个询问框的功能。

系统默认的询问框为了实现这个功能,可以使用页面路由Router模块提供的两个方法:router.showAlertBeforeBackPage()router.back()来实现这个功能。如果想要在目标界面开启页面返回询问框,需要在调用router.back()方法之前,通过调用router.showAlertBeforeBackPage()方法设置返回询问框的信息

router.showAlertBeforeBackPage({
  message: '您还没有完成支付,确定要返回吗?' // 设置询问框的内容
});
router.back()

其中,router.showAlertBeforeBackPage()方法接收一个对象作为参数,该对象包含以下属性:

message:string类型,表示询问框的内容。 如果调用成功,则会在目标界面开启页面返回询问框;如果调用失败,则会抛出异常,并通过err.code和err.message获取错误码和错误信息。

当用户点击“返回”按钮时,会弹出确认对话框,询问用户是否确认返回。选择“取消”将停留在当前页目标页面;选择“确认”将触发router.back()方法,并根据参数决定如何执行跳转。

自定义询问框

自定义询问框的方式,可以使用弹窗或者自定义弹窗实现。这样可以让应用界面与系统默认询问框有所区别,提高应用的用户体验度。

import router from '@ohos.router';
import promptAction from '@ohos.promptAction';
import { BusinessError } from '@ohos.base';
​
function onBackClick() {
  // 弹出自定义的询问框
  promptAction.showDialog({
    message: '您还没有完成支付,确定要返回吗?',
    buttons: [
      {
        text: '取消',
        color: '#FF0000'
      },
      {
        text: '确认',
        color: '#0099FF'
      }
    ]
  }).then((result:promptAction.ShowDialogSuccessResponse) => {
    if (result.index === 0) {
      // 用户点击了“取消”按钮
      console.info('User canceled the operation.');
    } else if (result.index === 1) {
      // 用户点击了“确认”按钮
      console.info('User confirmed the operation.');
      // 调用router.back()方法,返回上一个页面
      router.back();
    }
  }).catch((err:Error) => {
    let message = (err as BusinessError).message
    let code = (err as BusinessError).code
    console.error(`Invoke showDialog failed, code is ${code}, message is ${message}`);
  })
}
​

命名路由

在开发中为了跳转到共享包中的页面(即共享包中路由跳转),可以使用router.pushNamedRoute()来实现。

@Entry({ routeName : 'myPage' })
@Component
export struct MyComponent {
}
​

配置成功后需要在跳转的页面中引入命名路由的页面:

const moudel = import('ets/pages/Index') // 引入共享包中的命名路由页面

网络请求

HTTP数据请求

应用通过HTTP发起一个数据请求,支持常见的GET、POST、OPTIONS、HEAD、PUT、DELETE、TRACE、CONNECT方法。

HTTP数据请求功能主要由http模块提供。

使用该功能需要申请ohos.permission.INTERNET权限。

权限申请请参考访问控制(权限)开发指导

也就是在module.json5配置文件中声明权限。

"module"{
  //...
  "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ],
}

request接口开发步骤:

  1. 从@ohos.net.http.d.ts中导入http命名空间。
  2. 调用createHttp()方法,创建一个HttpRequest对象。
  3. 调用该对象的on()方法,订阅http响应头事件,此接口会比request请求先返回。可以根据业务需要订阅此消息。
  4. 调用该对象的request()方法,传入http请求的url地址和可选参数,发起网络请求。
  5. 按照实际业务需要,解析返回结果。
  6. 调用该对象的off()方法,取消订阅http响应头事件。
  7. 当该请求使用完毕时,调用destroy()方法主动销毁。
// 引入包名
import http from '@ohos.net.http';
import { BusinessError } from '@ohos.base';
​
// 每一个httpRequest对应一个HTTP请求任务,不可复用
let httpRequest = http.createHttp();
// 用于订阅HTTP响应头,此接口会比request请求先返回。可以根据业务需要订阅此消息
// 从API 8开始,使用on('headersReceive', Callback)替代on('headerReceive', AsyncCallback)。 8+
httpRequest.on('headersReceive', (header) => {
  console.info('header: ' + JSON.stringify(header));
});
httpRequest.request(
  // 填写HTTP请求的URL地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
  "EXAMPLE_URL",
  {
    method: http.RequestMethod.POST, // 可选,默认为http.RequestMethod.GET
    // 开发者根据自身业务需要添加header字段
    header: [{
      'Content-Type': 'application/json'
    }],
    // 当使用POST请求时此字段用于传递内容
    extraData: "data to send",
    expectDataType: http.HttpDataType.STRING, // 可选,指定返回数据的类型
    usingCache: true, // 可选,默认为true
    priority: 1, // 可选,默认为1
    connectTimeout: 60000, // 可选,默认为60000ms
    readTimeout: 60000, // 可选,默认为60000ms
    usingProtocol: http.HttpProtocol.HTTP1_1, // 可选,协议类型默认值由系统自动指定
    usingProxy: false, //可选,默认不使用网络代理,自API 10开始支持该属性
  }, (err: BusinessError, data: http.HttpResponse) => {
    if (!err) {
      // data.result为HTTP响应内容,可根据业务需要进行解析
      console.info('Result:' + JSON.stringify(data.result));
      console.info('code:' + JSON.stringify(data.responseCode));
      // data.header为HTTP响应头,可根据业务需要进行解析
      console.info('header:' + JSON.stringify(data.header));
      console.info('cookies:' + JSON.stringify(data.cookies)); // 8+
      // 当该请求使用完毕时,调用destroy方法主动销毁
      httpRequest.destroy();
    } else {
      console.error('error:' + JSON.stringify(err));
      // 取消订阅HTTP响应头事件
      httpRequest.off('headersReceive');
      // 当该请求使用完毕时,调用destroy方法主动销毁
      httpRequest.destroy();
    }
  }
);
​