鸿蒙-那些年我们踩过的坑-下


书接上回,在上一篇文章中介绍了 ForEach循环渲染和自绘制输入框遇到的坑,这里聊一下 字面量对象和类对象 以及 自定义 Dialog 的坑。

先从简单的Dialog 开始,这里没有很深入的讲解,只是一些注意点以及官方推荐用法

CustomDialogController

先说结论:在使用CustomDialogCustomDialogController做自定义弹窗时,只能作为被@Component修饰的自定义组件的成员变量,甚至可以写在组件的点击事件中,但不能写到单纯的方法中。因为它需要 UIContext 上下文

示例

正常情况:

@Entry
@Component
struct DialogControllerPage {
  @State message: string = 'Hello World';
  dialogID: number = 0
  dialogController: CustomDialogController | null = new CustomDialogController({
    builder: CustomDialogExample({
      cancel: () => {
      },
      confirm: () => {
      },
    }),
  })
  build() {
    Column() {
      Text('在 Click 事件中定义').margin(10)
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
        .onClick((_) => {
          let dialogController: CustomDialogController | null = new CustomDialogController({
            builder: CustomDialogExample({
              cancel: () => {
              },
              confirm: () => {
              },
            }),
          })
          dialogController.open()
        })
      
      //在自定义组件中定义
      CustomDialogView()

      Text('在页面中定义').margin(10)
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
        .onClick((_) => {
          this.dialogController?.open()
        })
    }
  }
}
@Component
struct CustomDialogView{
  dialogController: CustomDialogController | null = new CustomDialogController({
    builder: CustomDialogExample({
      cancel: () => {
      },
      confirm: () => {
      },
    }),
  })
  build() {
    Text('在自定义组件中定义').margin(10)
      .fontSize(30)
      .fontWeight(FontWeight.Bold)
      .onClick((_) => {
        this.dialogController?.open()
      })
  }
}

上面的这三种情况都是可以正常弹出弹窗的,但当我们把CustomDialogController写在普通方法中时

export function showDialog() {
  let dialogController: CustomDialogController | null = new CustomDialogController({
    builder: CustomDialogExample({
      cancel: () => {
      },
      confirm: () => {
      },
    }),
  })
  dialogController.open()
}

这里会报一个错误,应用会崩溃,报错信息挺长的,这里截取了一部分

Pid:25224
Uid:20020185
Process name:com.huangyuanlove.arkui_demo
Process life time:47s
Reason:Signal:SIGSEGV(SEGV_MAPERR)@0x00000000000008b0 probably caused by NULL pointer dereference
Fault thread info:
Tid:25224, Name:love.arkui_demo
#00 pc 00000000029cfd70 /system/lib64/platformsdk/libace_compatible.z.so(OHOS::Ace::Framework::JSCustomDialogController::JsOpenDialog(OHOS::Ace::Framework::JsiCallbackInfo const&)+8)(1a64ce74d582cc151101042697df670d)
#01 pc 00000000009a8cb0 /system/lib64/platformsdk/libace_compatible.z.so(panda::Localpanda::JSValueRef OHOS::Ace::Framework::JsiClassOHOS::Ace::Framework::JSCustomDialogController::InternalJSMemberFunctionCallbackOHOS::Ace::Framework::JSCustomDialogController(panda::JsiRuntimeCallInfo*)+2148)(1a64ce74d582cc151101042697df670d)
#02 pc 00000000004dc50c /system/lib64/platformsdk/libark_jsruntime.so(panda::Callback::RegisterCallback(panda::ecmascript::EcmaRuntimeCallInfo*)+456)(3499a0e0c3b8b8dc50b1a4589295965e)

我想这可能就是为啥需要在@CustomDialog修饰的 struct 中声明一个CustomDialogController变量的原因。

官方推荐方案

在官方文档中有一个 不依赖UI组件的全局自定义弹窗 (推荐)。虽然说是不依赖UI组件,但实际上还是使用的UIContext这个上下文获取到promptAction,调用promptAction.openCustomDialog方法来实现的弹窗。
吐槽归吐槽,先看下用法,看完了再评价也不迟。
这里有两种方案,一种是传入ComponentContent对象,这个方案在 不依赖UI组件的全局自定义弹窗 (推荐)这里有详细介绍
另外一种方案是传入 promptAction.CustomDialogOptions,这种方案是在@ohos.promptAction (弹窗) API 参考中介绍的。

传入ComponentContent对象

创建ComponentContent对象需要一个UIContext对象,一个wrapBuilder以及wrapBuilder中需要的参数对象。

  • UIContext对象可以在页面中通过this.getUIContext()获取。
  • wrapBuilder需要一个全局被@Build修饰的方法。
function  glaobleConfirmOrCancelDialogBuilder1(dialogData: DialogData) {
  Column() {
    //这里写弹窗中的布局
  }
}

然后我们可以在某个组件的点击事件中展示弹窗

.onClick((_) => {
  let dialogData: DialogData = new DialogData()
  dialogData.title = '推荐方案 一'
  dialogData.message = '使用  promptAction.openCustomDialog'


  let uiContext = this.getUIContext();
  let promptAction = uiContext.getPromptAction();

  let contentNode = new ComponentContent(uiContext, wrapBuilder(glaobleConfirmOrCancelDialogBuilder1), dialogData);
  dialogData.onCancel = () => {
    promptAction.closeCustomDialog(contentNode)

  }
  dialogData.onConfirm = () => {
    promptAction.closeCustomDialog(contentNode)
  }
  try {
    promptAction.openCustomDialog(contentNode);
  } catch (error) {
    let message = (error as BusinessError).message;
    let code = (error as BusinessError).code;
    console.error(`OpenCustomDialog args error code is ${code}, message is ${message}`);
  };
})

当然,在调用openCustomDialog还有第二个可选参数promptAction.BaseDialogOptions,相应的介绍在这里

传入CustomDialogOptions
.onClick((_) => {
  let dialogData: DialogData1 = new DialogData1()
  dialogData.title = '推荐方案二'
  dialogData.message = '使用  promptAction.openCustomDialog'
  dialogData.onCancel = () => {
    promptAction.closeCustomDialog(this.dialogID)
  }
  dialogData.onConfirm = () => {
    promptAction.closeCustomDialog(this.dialogID)
  }
  this.getUIContext().getPromptAction().openCustomDialog({
    builder: () => {
      this.confirmOrCancelDialogBuilder1(dialogData)
    },

  }).then((dialogID: number) => {
    this.dialogID = dialogID
  })
})

这里展示弹窗的时候会返回一个dialogID,我们在关闭弹窗的时候需要传入这个id。

字面量对象与类对象

对应的英文是plain (literal) objects,class (constructor) objects,但是在不知道该怎么优雅的翻译,就先这么叫吧。
在 ArkTS 中,创建的每个字面量对象都必须有对应的类型,比如

let tmpUser = {
  name:"123"
}

直接这么写会报错,提示:Object literal must correspond to some explicitly declared class or interface (arkts-no-untyped-obj-literals)
也就是说我们必须要先定义一个class 或者 interface,但是这里需要注意一下,我们直接使用字面量语法创建对应的class对象时,要求该class对象中不能声明方法:

interface UserInterface {
  name: string;
}
class UserWithOutMethod{
  name:string =''
}

class UserWithMethod{
  name:string =''
  getInfo(){
    hilog.error(0x01,'UserWithMethod','getInfo')
  }
}


let userInterface: UserInterface = {
  name: "123"
}
let userWithOutMethod: UserWithOutMethod = {
  name: "123"
}

let userWithMethod: UserWithMethod = {
  name: "123",
}

userInterfaceuserWithOutMethod都是正常的,但userWithMethod会报错,提示Property 'getInfo' is missing in type '{ name: string; }' but required in type 'UserWithMethod'

字面量语法创建含有方法的对象错误信息

即使我们把这个方法补上,也是会提示错误:Object literal must correspond to some explicitly declared class or interface
字面量语法创建含有方法的对象错误信息

不过话又说回来,为啥要用字面量的语法创建类对象嘞?用new关键字它不香么?

let userWithMethod = new UserWithMethod()
小坑

不过对于上面包含方法的类,也有其他方案,比如通过as关键字强转

let userStr =  `{"name":"123"}`
let userWithMethodJSON = JSON.parse(userStr) as  UserWithMethod
hilog.error(0x01,'UseASPage',userWithMethodJSON.name)

这样的话,我们是可以获取到对象的name属性,也能正常使用,
但是,不能调用这个对象的getInfo()方法,会崩溃,报错提示Error message:is not callable.
这个也挺好理解:

使用JSON.parse(userStr) as UserWithMethod这种方式得到的对象实际上是字面量对象,这个对象中并没有getInfo()方法,它的原型链上也没有这个方法,所以就会报错。

为啥 IDE 不给提示嘞?那就不知道了
当然,我们也有方法将字面量对象转为类对象,使得我们可以调用其方法:使用"class-transformer": "^0.5.1" 这个三方库,github 地址(https://github.com/typestack/class-transformer)[https://github.com/typestack/class-transformer],但要注意的是,这个库不是一个标准的ohpm库,虽然它可以在 ArkTS 里面使用。

import { plainToClass } from 'class-transformer';
let userStr = `{"name":"123"}`
let userWithMethodJSON = JSON.parse(userStr) as UserWithMethod
let tmp = plainToClass(UserWithMethod, userWithMethodJSON)
tmp.getInfo()

这样就正常了。

另外一个坑

还记得上一篇中提到的状态管理装饰器 @Observed装饰器和@ObjectLink装饰器:嵌套类对象属性变化么?
这里还有一个小坑,使用as强转或者使用plainToClass方法创建的对象的属性发生变化时,是无法被@ObjectLink装饰器观察到的。
举个例子,我们有一个嵌套类,使用@Observed装饰

@Observed
class FirstLevel {
  time: number = 0
  secondLevel: SecondLevel = new SecondLevel()
}

@Observed
class SecondLevel {
  name: string = ''
  age: number = 0
}

再定义几个赋值的方法

  @State firstLevel?:FirstLevel = undefined
   initWithNew() {
    this.firstLevel = new FirstLevel()
    this.firstLevel.time = systemDateTime.getTime()
    let secondLevel:SecondLevel = new SecondLevel()
    secondLevel.name = 'new SecondLevel'
    secondLevel.age = Math.floor(Math.random() * 100)
    this.firstLevel.secondLevel = secondLevel
  }

  initWithAs() {

    let secondLevel:SecondLevel = {
      name: 'as SecondLevel',
      age: Math.floor(Math.random() * 100)
    }
    this.firstLevel = {
      time:systemDateTime.getTime(),
      secondLevel:secondLevel
    }
  }
  initWithPlainToText(){
    let str = `{"time":${systemDateTime.getTime()},"secondLevel":{"name":"PlainToText${Math.floor(Math.random() * 100)}","age":${Math.floor(Math.random() * 100)}}}`
    let tmp:FirstLevel = JSON.parse(str) as FirstLevel
    this.firstLevel = plainToClass(FirstLevel,tmp)
  }

两个用于展示数据的自定义组件


@Component
struct  ShowFistLevel{
  @Watch('onFirstLevelChange') @ObjectLink firstLevel:FirstLevel
  onFirstLevelChange(){
    hilog.error(0x01, 'UseASPage', 'onFirstLevelChange')
  }
  build() {
    Column(){
      Text(this.firstLevel.time.toString())
      ShowSecondLevel({secondLevel:this.firstLevel.secondLevel})
    }.margin(15)
    .backgroundColor("#e7e7e7e7")
  }
}

@Component
struct  ShowSecondLevel{
  @Watch('onSecondLevelChange') @ObjectLink secondLevel:SecondLevel
  onSecondLevelChange(){
    hilog.error(0x01, 'UseASPage', 'onSecondLevelChange')
  }
  build() {
    Column(){
      Text(this.secondLevel.name)
      Text(this.secondLevel.age.toString())
    }.margin(15)
    .backgroundColor("#e7e7e7e7")
  }
}

这里需要注意的是,渲染嵌套类的组件需要和类对象的层级相同,不然也不会刷新。
比如这里FirstLevel类中有SecondLevel类型属性,就需要写成上面这样:拆成两个组件,在ShowFistLevel组件中引用ShowSecondLevel,而不能这样写

@Component
struct  ShowFistLevel{
  @Watch('onFirstLevelChange') @ObjectLink firstLevel:FirstLevel
  onFirstLevelChange(){
    hilog.error(0x01, 'UseASPage', 'onFirstLevelChange')
  }
  build() {
    Column(){
      Text(this.firstLevel.time.toString())
      //这里
      Text(this.firstLevel.secondLevel.name)
      Text(this.firstLevel.secondLevel.age.toString())
    }.margin(15)
    .backgroundColor("#e7e7e7e7")
  }
}

这样合并成一个组件后,其中的nameage属性发生变化时,并不能刷新页面

然后我们写个页面测试一下



build() {
  Column() {
    Row() {
      Button('使用New').margin(10).onClick((_) => {
        this.initWithNew()
      })
      Button('使用PlainToClass').margin(10).onClick((_) => {
        this.initWithPlainToText()
      })

      Button('使用As').margin(10).onClick((_) => {
        this.initWithAs()
      })
    }

    Row() {
      Button('修改time属性').margin(10).onClick((_) => {
        if(this.firstLevel){
          this.firstLevel.time = systemDateTime.getTime()
        }
      })

      Button('修改 name、age 属性').margin(10).onClick((_) => {
        if(this.firstLevel) {
          this.firstLevel.secondLevel.name = '新名字 ' + Math.floor(Math.random() * 10)
          this.firstLevel.secondLevel.age = Math.floor(Math.random() * 100)
        }
      })
    }
    if(this.firstLevel){
      ShowFistLevel({firstLevel:this.firstLevel})
    }

  }
  .height('100%')
  .width('100%')
}

点击使用New后,再点击修改属性,可以看到页面刷新了
这时候点击使用PlainToClass后,页面也刷新了,但这时候点击修改time属性,页面会刷新,但点击修改 name、age 属性,页面是没有刷新的。但我们多次点击使用PlainToClass时,页面是可以刷新的。
点击使用使用As后,页面也刷新了,,但这时候点击修改time属性,页面会刷新,但点击修改 name、age 属性,页面是没有刷新的。但我们多次点击使用As时,页面是可以刷新的。

也就是说我们使用PlainToClassas 这两种方式创建出来的对象,会使得@Observed装饰器和@ObjectLink装饰器失效。这是开发过程中需要注意的。

总结

  1. 使用CustomDialogController做弹窗展示时,需要在组件中创建CustomDialogController对象,至少在 api12 上是这样的。
  2. 不想使用CustomDialogController的话,可以使用promptAction.openCustomDialog做弹窗展示,当时,它是依赖UIContext这个上下文。注意不要和Context弄混了
  3. 注意字面量对象和类对象。使用as将字面量对象转为类对象时,无法使用类本身的方法,可以使用class-transformer中的plainToClass创建类对象,这样可以调用对象的方法
    刷新的。

也就是说我们使用PlainToClassas 这两种方式创建出来的对象,会使得@Observed装饰器和@ObjectLink装饰器失效。这是开发过程中需要注意的。

总结

  1. 使用CustomDialogController做弹窗展示时,需要在组件中创建CustomDialogController对象,至少在 api12 上是这样的。
  2. 不想使用CustomDialogController的话,可以使用promptAction.openCustomDialog做弹窗展示,当时,它是依赖UIContext这个上下文。注意不要和Context弄混了
  3. 注意字面量对象和类对象。使用as将字面量对象转为类对象时,无法使用类本身的方法,可以使用class-transformer中的plainToClass创建类对象,这样可以调用对象的方法
  4. 使用PlainToClassas 这两种方式创建出来的对象,会使得@Observed装饰器和@ObjectLink装饰器失效。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值