书接上回,在上一篇文章中介绍了 ForEach循环渲染和自绘制输入框遇到的坑,这里聊一下 字面量对象和类对象 以及 自定义 Dialog 的坑。
先从简单的Dialog 开始,这里没有很深入的讲解,只是一些注意点以及官方推荐用法
CustomDialogController 先说结论:在使用CustomDialog
和CustomDialogController
做自定义弹窗时,只能作为被@Component
修饰的自定义组件的成员变量,甚至可以写在组件的点击事件中,但不能写到单纯的方法中。因为它需要 UIContext 上下文
示例 正常情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 @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
写在普通方法中时
1 2 3 4 5 6 7 8 9 10 11 12 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修饰的方法。
1 2 3 4 5 function glaobleConfirmOrCancelDialogBuilder1 (dialogData: DialogData ) { Column () { } }
然后我们可以在某个组件的点击事件中展示弹窗
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 .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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 .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 中,创建的每个字面量对象都必须有对应的类型,比如
1 2 3 let tmpUser = { name :"123" }
直接这么写会报错,提示:Object literal must correspond to some explicitly declared class or interface (arkts-no-untyped-obj-literals) 也就是说我们必须要先定义一个class
或者 interface
,但是这里需要注意一下,我们直接使用字面量语法创建对应的class
对象时,要求该class
对象中不能声明方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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" , }
userInterface
和 userWithOutMethod
都是正常的,但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
关键字它不香么?
1 let userWithMethod = new UserWithMethod ()
小坑 不过对于上面包含方法的类,也有其他方案,比如通过as
关键字强转
1 2 3 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 里面使用。
1 2 3 4 5 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
装饰
1 2 3 4 5 6 7 8 9 10 11 @Observed class FirstLevel { time : number = 0 secondLevel : SecondLevel = new SecondLevel () }@Observed class SecondLevel { name : string = '' age : number = 0 }
再定义几个赋值的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @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) }
两个用于展示数据的自定义组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @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
,而不能这样写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @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" ) } }
这样合并成一个组件后,其中的name
和age
属性发生变化时,并不能刷新页面
然后我们写个页面测试一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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
时,页面是可以刷新的。
也就是说我们使用PlainToClass
和as
这两种方式创建出来的对象,会使得@Observed装饰器和@ObjectLink装饰器
失效。这是开发过程中需要注意的。
总结
使用CustomDialogController
做弹窗展示时,需要在组件中创建CustomDialogController
对象,至少在 api12 上是这样的。
不想使用CustomDialogController
的话,可以使用promptAction.openCustomDialog
做弹窗展示,当时,它是依赖UIContext
这个上下文。注意不要和Context
弄混了
注意字面量对象和类对象。使用as
将字面量对象转为类对象时,无法使用类本身的方法,可以使用class-transformer中的plainToClass 创建类对象,这样可以调用对象的方法
使用PlainToClass
和as
这两种方式创建出来的对象,会使得@Observed装饰器和@ObjectLink装饰器
失效。