鸿蒙
学习Stage模型(FA模型会逐步淘汰)
应用结构
项目目录
Module
一个鸿蒙应用包含多个Module (每个Module可以单独编译为.hap后缀的文件,所有.hap文件整体称为Bundle,可以打包为一个.app后缀的文件用于上架)
Module分为3种类型:
Ability(HAP)
entry类型: 应用的主模块(项目可以包含一个或多个Entry模块,如果同一类型的设备包含多个Entry模块,需要配置distroFilter分发规则,使得应用市场在做应用的云端分发时,对该设备类型下不同规格的设备进行精确分发)
feature类型:类似于分包,甚至配置应用市场的分发策略实现按设备下发分包(按照设备一次性加载好,不是类似微信触发后再去下载)
创建:需要选择模块类型
entry类型的设备类型可以是1个或多个 (Phone、2in1、Tablet),假如存在一个Phone+Tablet类型的entry了,再次创建只能选剩下的2in1了,即确定设备上只能有一个entry入口
feature类型可选择的设备类型只能是entry覆盖的类型
Shared Library(HSP)
需要选择设备类型
Static Library Module(HAR)
静态共享包(Harmony Archive,HAR),包含代码、C++库、资源和配置文件。通过HAR可以实现多个模块或多个工程共享ArkUI组件、资源等相关代码。HAR不同于HAP,不能独立安装运行在设备上,只能作为应用模块的依赖项被引用
目录右键 -> New Module,可以看到下面的菜单。处理框中的两个,其他都是Ability
多入口
在entry中可以定义多个ability,然后在entry模块的module.json中配置module.abilities字段
该字段是个数组类型,可以配置多个入口
用户安装后在桌面会展示多个应用图标入口
Stage模型
Window:当前窗口实例,窗口管理器管理的基本单元
WindowStage:窗口管理器。管理各个基本窗口单元
抓包
请参考如下流程:
当前网络模块已支持适配Charles工具抓包,配置方式如下:
一、导出证书,点击 Help--->SSL Proxying--->Save Charles Root Certificate
(1)导入证书到手机执行命令参考如下:
hdc file send charles.pem(pc上证书路径) /storage/media/100/local/files/Download(工程机指定路径)
(2)连接工程机后执行命令启动证书安装界面
hdc shell aa start -a MainAbility -b com.ohos.certmanager
(3) 选择从存储设备安装,选择指定pem证书
二、安装Charles证书到系统可信目录,操作步骤:
点击 Help--->SSL Proxying--->Install Charles Root Certificate--->安装证书--->选择证书存储路径为:受信任的根证书颁发机构
三、设置代理操作步骤:
1)点击 Proxy--->SSL Proxy Settings--->在Include添加 *:* 和 *:443
2)点击 Proxy--->Proxy Settings--->勾选Enable transparent HTTP proxying
四、Wifi代理设置:
将手机与PC同一局域网下连接,手机连接WiFi时,点击代理设置为手动,修改设置代理IP,端口为Charles监听的端口,默认为8888,可在上一步Proxy Settings中查看和修改
五、应用抓取http包: App开发时,HTTP请求HttpRequestOptions参数设置,可参考文档
1)设置usingProxy为true,表示使用HTTP代理(该字段默认为false 不使用代理)
2)设置caPath(可根据环境使用设置,默认使用系统预设CA)
http参数设置可参考:https://docs.openharmony.cn/pages/v4.0/zh-cn/application-dev/reference/apis/js-apis-http.md/#httprequestoptions%E6%94%AF%E6%8C%81%E8%B7%A8%E5%B9%B3%E5%8F%B0
charles工具配置可参考博客:https://juejin.cn/post/6844904182588112904
类型标记
类型描述 需 使用 interface、class
interface yyy =Record<string,string>
@Component
export default struct xxxx {
@Status obj:yyy = {} //必须初始化
@Prop 不必初始化,但是需要从外部传入
}
如果用明确类型
interface yyy {
name:string
}
@Component
export default struct xxxx {
@Status obj:yyy = {
name:''
} //必须初始化,且初始值必须明确字段
@Prop 不必初始化,但是需要从外部传入
}
组件
Row、Column
默认布局方向:
Row主轴水平方向,默认子元素 水平靠左,竖直居中
Column主轴竖直方向,默认子元素 竖直靠上,水平居中
【也可以说两者,主轴方向靠Start、纵轴居中】
Flex
Flex默认水平方向为主轴,可通过direction参数改变
默认布局方向:
- 主轴靠Start
- 纵轴靠Start
记得一般居中布局要设置下
Flex({ direction: FlexDirection.xxx }) {
// 子组件
}
Scroll
- 默认子组件竖直、水平方向都居中,一般要使用align可以靠顶部、左侧开始
- 必须指定scrollable属性,设置滚动方向,否则不可滚动
Scroll() {
Column(){
}
}
.scrollable(ScrollDirection.Vertical) // ScrollDirection.Horizontal
.align(Alignment.Top)
// 无论滚动方向。Alignment轴是固定的
// 竖直方向,Alignment.Top
// 水平方向,Alignment.Start
align设置子元素与父元素对齐的方式(不受滚动方向的影响)
Grid
Grid通过columnsTemplate指定列数、列的宽度,通过rowsTemplate指定行数、行的高度(如果宽度使用fr比例的话,不能确定Grid的宽高了,这时候需要参考Grid的width、height,如果还没有则认为不能确定)
注意:Grid最坑的一点是不能确定宽或高,宽或高就会继承父级(不是由内容撑起来)
Grid布局的三种情况:
仅仅指定列(最常见)
- 高继承父级
主轴为竖直
仅仅指定行
- 宽继承父级
主轴为水平
指定列、行
需要判断是否能确定宽或高,不能才会就会继承父级
Grid只展示固定行列数的元素,多余元素不展示,且Grid不可滚动
不指定列、行
宽高继承父级
主轴方向需要layoutDirection指定,主轴方向元素minCount、maxCount决定是否换行
Grid() {
GridItem() {
}
}
.columnsTemplate('1fr 1fr')
.columnsGap('14lpx')
.rowsGap('16lpx')
.maxCount(2)
目前发现了一个Grid撑起来高度的方案,下面绿框中的是Gird,成功撑起来下拉框
Column() {
Grid() {
ForEach(this.data, (item: SpecificGroupColorItem) => {
GridItem() {
Row() {
ColorItem({ color: item.colorRgb, isActive: false })
Text(item.colorName)
.fontSize('28lpx')
.fontColor(this.activeColor.colorId === item.colorId ? '#2567ff' : '#0f1d37')
.fontWeight(this.activeColor.colorId === item.colorId ? FontWeight.Medium : FontWeight.Regular)
.margin({ left: '8lpx' })
}
.width('100%')
.height('80lpx')
.backgroundColor(this.activeColor.colorId === item.colorId ? '#f2f6ff' : '#f5f7fb')
.borderWidth('2lpx')
.borderColor(this.activeColor.colorId === item.colorId ? '#d6e3ff' : Color.Transparent)
.borderRadius('8lpx')
.padding({ top: '24lpx', bottom: '24lpx', left: '24lpx', right: '24lpx' })
.onClick(() => {
this?.onChange?.({
colorId: item.colorId as number,
colorName: item.colorName as string
})
})
}
})
}
.columnsTemplate('1fr 1fr')
.columnsGap('14lpx')
.rowsGap('16lpx')
// 重点
.maxCount(2)
}
// 重点 ,加margin也行
.padding({ top: '24lpx', bottom: '32lpx', left: '32lpx', right: '32lpx' })
Toggle组件
hoverEffect可以去掉切换开关时背景的遮罩
Toggle({ type: ToggleType.Switch, isOn: false })
.margin({ left: '8lpx' })
.selectedColor('#12C2CE')
.switchPointColor('#FFFFFF')
.onChange((isOn: boolean) => {
console.info('Component status:' + isOn)
})
.hoverEffect(HoverEffect.None)
Blank
空白填充组件,在容器主轴方向上,空白填充组件具有自动填充容器空余部分的能力。仅当父组件为Row/Column/Flex时生效
TabContent组件
高度=屏幕高度 - Tab高度
layoutWeight属性
通用属性,仅在Row/Column/Flex布局中生效。
设置占据权重
Divider
设置颜色用color,不要用backgroundColor,使用backgroundColor颜色会有问题
Divider().vertical(true).color('#eeeeee').strokeWidth('2lpx').height('66lpx')
盒子模型
border
占据宽高范围,radius可裁剪盒子形状
Row()
.width(100)
.height(100)
.backgroundColor(Color.Yellow)
.border({
width: 20,
color: Color.Red,
radius: 50
})
outline
不占据宽高范围
盒子被裁剪,outline仍然是正方形,如果需要裁剪outline请使用它的radius属性
Row()
.width(100)
.height(100)
.backgroundColor(Color.Yellow)
.outline({
width: 20,
color: Color.Red,
})
.border({
color: Color.Red,
radius: 50
})
定位偏移
属性
position({x:number,y:number}) // 以父组件左上角为基础
offset({x:number,y:number}) // 在当前位置的基础上,进行偏移
zIndex(value:number) // 设置层级 ,组件Stack、RelativeContainer都是按顺序叠加层级,但是zIndex可以改变
组件
Stack
只能设置向一个方向对齐
子组件按顺序,层级从低到高
Stack不设置宽高默认为所有子元素中最大的宽、最大的高
offset设置偏移量,子组件可以偏移出Stack的范围
@Entry
@Component
struct Index {
build() {
// 布局只能设置一个对齐方向,这里是设置了所有子组件都向下边框对齐
Stack({ alignContent: Alignment.Bottom }) {
// 层级低
Row().width('100lpx').height('100lpx').backgroundColor(Color.Red)
// 层级高
Row().width('50lpx').height('50lpx').backgroundColor(Color.Blue)
// 层级高+1
Row().width('20lpx').height('20lpx').backgroundColor(Color.Yellow)
}.backgroundColor(Color.Black).margin({ left: '200lpx' })
}
}
虽然Stack只能指定一个方向对齐,但是我们可以通过嵌套Stack实现多个方向对齐
@Entry
@Component
export default struct Index {
build() {
Stack({ alignContent: Alignment.Bottom }) {
Stack({ alignContent: Alignment.TopEnd }) {
Row()
.width('97lpx')
.height('97lpx')
.backgroundColor('#eeeeee')
Image($r('sys.media.ohos_ic_public_close'))
.width('36lpx')
.height('36lpx')
.offset({ x: '10lpx', y: '-10lpx' })
}
Text('你好你好你好你好你好').fontSize('20lpx')
}
}
}
RelativeContainer
RelativeContainer可以实现多个方向对齐,比Stack功能更强大
但是其缺点更明显:必须设置宽高,否则就是父级宽高。内部内容不能撑起宽高
目前是使用嵌套的Stack实现了多个方向对齐(关闭按钮右上角对齐,文案下边中心对齐)
注意:
- 每个元素必须设置id,RelativeContainer的id默认为
__container__
- alignRules可设置相对谁布局
- 内部子组件按顺序,层级从低到高
@Entry
@Component
export default struct Index {
build() {
RelativeContainer() {
// 文本
Text('标题') {
}
.alignRules({
// 垂直方向 当前组件位置:{ id: 相对的组件id , align:相对的组件位置}
center: { anchor: "__container__", align: VerticalAlign.Center },
// 水平方向
middle: { anchor: "__container__", align: HorizontalAlign.Center }
})
.id("a")
// 红色方块
Row()
.width('48lpx')
.height('48lpx')
.backgroundColor(Color.Red)
.alignRules({
top: { anchor: "a", align: VerticalAlign.Bottom },
left: { anchor: "a", align: HorizontalAlign.Start }
})
.id("b")
// 麦克风图片
Image($r('sys.media.ohos_ic_public_voice'))
.width('48lpx')
.height('48lpx')
.alignRules({
top: { anchor: "a", align: VerticalAlign.Bottom },
left: { anchor: "a", align: HorizontalAlign.Start }
})
.id("c")
.offset({ x: '300lpx' })
}
.width('500lpx')
.height('500lpx')
.backgroundColor(Color.Yellow)
}
}
事件机制
手势没有冒泡的概念
事件:
父子均绑定事件,如果子响应,父级就不会响应。如果自己没有绑定,父级响应
可以用hitTestBehavior(HitTestMode.Block)
,阻止父级响应
状态管理最佳实践
官方目前还不稳定,从@track、@observed到现在的@ObservedV2还在变动
待稳定后补充这部分
组件状态管理
无状态修饰符
变量可以显示在页面上,但是变量变更不会更新页面
必须赋初始值
从目前的官方例子来看,定义的变量如果没有状态修饰符,都用private修饰
父级组件是否传入这个字段可选
// 必须赋值
private isShowTitle: boolean = true
// 可以不赋值,使用时必须判断是否存在
private isShowTitle?: boolean
箭头函数
// 防止this指向变化。如果将方法传入子组件,必须使用这种方式声明函数
private navToBrandListTop = () => {
// xxxx
}
@Status
@Status修饰的变量,必须赋初值,不允许使用any、undefined和null
变量类型
简单类型,直接改就能触发刷新页面。tabsIndex变量使用@Status修饰,onchange方法修改这个变量,页面才会更新(tab会切换)
ts@Entry @Component struct Home { // 初始化tabsBar的index值为0 @State tabsIndex: number = 0; build() { Tabs() { ForEach(HOME_TABS, (item: FirstLevelCategory, index: number) => { TabContent() { xxxx } .tabBar(this.CustomTabBar(item, index)) }) } .onChange((index: number) => { this.tabsIndex = index; }) } @Builder CustomTabBar(item: FirstLevelCategory, index: number) { Column() { Image(this.tabsIndex === index ? item.iconSelected : item.icon) .width(24) .height(24) .margin({ bottom: 4 }) Text(item.tabBarName) .fontSize(10) .fontFamily('HarmonyHeiTi-Medium') .fontColor(this.tabsIndex === index ? $r('app.color.tab_bar_select') : $r('app.color.tab_bar_unselect')) } .width('100%') .id(`tabBar${index}`) .padding({ top: 6, bottom: 6 }) .alignItems(HorizontalAlign.Center) } }
数组类型,必须通过数组方法修改才能触发更新,例如splice、push。如果数组元素为对象,例如 let arr=[{count:1},{count:2}],如果修改第一个元素的count,必须整个元素赋值 arr[0]={count:2},不能 arr[0].count=2
对象类型,修改第一层数据才可以触发更新,但是深层次必须整体赋值到第一层才能更新
tsclass FirstLevelClass { public a: string; public child:SecondLevelClass constructor(a:string,child:SecondLevelClass) { this.a = a; this.child=child; } } class SecondLevelClass { public b: string; constructor(b:string) { this.b=b } } // 父组件 @Component export struct Parent{ @State instance:FirstLevelClass=new FirstLevelClass('a',new SecondLevelClass('c')) build(){ Column(){ Text('父级:') TextInput({text:this.instance.a}).onChange((value:string)=>{ this.instance.a=value }) Child({ instance: this.instance }) } } } // 子组件 @Component export struct Child{ @Link instance:FirstLevelClass build(){ Column(){ Text('子级:') TextInput({text:this.instance.a}).onChange((value:string)=>{ this.instance.a=value }) } } }
@Status与@Prop
@Prop(父 -> 子 )修饰的变量不必赋初始值
即使父组件不使用circleColor渲染页面,也必须使用 @Status 修饰
@Status circleColor='red'
// 单向同步子组件 组件内部 @Prop circleColor:string
ChildCompProp({ circleColor: this.circleColor })
是否可以多级传递。父级State修饰,子、孙子都使用Prop(子级不必再使用@Status修饰,就可以被孙子级别识别到)
父级变动会引起子级、孙子级一起变动
tsPersistentStorage.persistProp('PropA', 47) @Entry @Component struct Parent { @State a: number = 1; build() { Column() { Button(`父级:${this.a}`).onClick(() => { this.a++ }) Child({ childA: this.a }) }.height('100%').alignItems(HorizontalAlign.Center) } } @Component struct Child { @Prop childA: number build() { Column() { Button(`子级 :${this.childA}`) Grandson({ GrandsonA: this.childA }) } .width('100%') .justifyContent(FlexAlign.Center) } } @Component struct Grandson { @Prop GrandsonA: number build() { Column({ space: 20 }) { Button(`孙子: ${this.GrandsonA}`) } .width('100%') .justifyContent(FlexAlign.Center) } }
孙子级别使用Link
tsPersistentStorage.persistProp('PropA', 47) @Entry @Component struct Parent { @State a: number = 1; build() { Column() { Button(`父级:${this.a}`).onClick(() => { this.a++ }) Child({ childA: this.a }) }.height('100%').alignItems(HorizontalAlign.Center) } } @Component struct Child { @Prop childA: number build() { Column() { Button(`子级 :${this.childA}`) Grandson({ GrandsonA: this.childA }) } .width('100%') .justifyContent(FlexAlign.Center) } } @Component struct Grandson { @Link GrandsonA: number build() { Column({ space: 20 }) { Button(`孙子: ${this.GrandsonA}`).onClick(() => { // 可以修改子级数据,无法修改父级数据 this.GrandsonA++ }) } .width('100%') .justifyContent(FlexAlign.Center) } }
@Status与@Link
@Link(父 <->子)修饰的变量不必赋初始值,但是他们只能接受父组件的 @Status 修饰的变量
@Status circleColor='red'
// 双向同步子组件 组件内部 @Link circleColor:string
ChildCompLink({ circleColor: this.circleColor }) //还有一种写法是 circleColor:$circleColor
对象修改一层数据,也可以用。如果是嵌套的对象,需要整个赋值子对象,才能触发
class FirstLevelClass {
public a: string;
public child:SecondLevelClass
constructor(a:string,child:SecondLevelClass) {
this.a = a;
this.child=child;
}
}
class SecondLevelClass {
public b: string;
constructor(b:string) {
this.b=b
}
}
// 父组件
@Component
export struct Parent{
@State instance:FirstLevelClass=new FirstLevelClass('a',new SecondLevelClass('b'))
build(){
Column(){
Text('父级:')
TextInput({text:this.instance.a}).onChange((value:string)=>{
this.instance.a=value
})
Child({ instance: this.instance })
}
}
}
// 子组件
@Component
export struct Child{
@Link instance:FirstLevelClass
build(){
Column(){
Text('子级:')
TextInput({text:this.instance.a}).onChange((value:string)=>{
this.instance.a=value
})
}
}
}
@Status与@ObjectLink、@observe
@ObjectLink与@Link共功能一样,都是父子同步
@ObjectLink修饰的变量,其类型必须是@observe修饰的,可以不初始化值
当父组件修饰的是数组,但是传入子组件的不是这个数组,而是其中的元素。子组件想要改父级的大数组需要用@ObjectLink
@Status arr=[{count:1},{count:2}]
// 双向同步子组件 组件内部 @ObjectLink circleColor:xxx
ChildCompLink({ circleColor: this.arr[0] })
还有一种情况 嵌套对象,但是只将其子对象传入组件,使用@ObjectLink能同步父级与子级的数据
但是有一个问题:虽然父级的子对象数据更新了,但是由于是深层对象,父级页面没法触发更新
class Student {
public name: string
public score: Score
constructor(name: string, score: Score) {
this.name = name
this.score = score
}
}
@Observed
class Score {
public math: number
constructor(math: number) {
this.math = math
}
}
@Entry
@Component
struct Parent {
@State stu: Student = new Student('张三', new Score(50))
build() {
Column() {
Text('父级')
Column() {
Text(`数学:${this.stu.score.math}`)
Button(`数学 +1`).onClick(() => {
this.stu.score.math += 1 // 不使用@ObjectLink、@observe,这里操作不能触发子级更新
})
}
Child({ score: this.stu.score })
}.height('100%').alignItems(HorizontalAlign.Center).borderWidth('10lpx')
}
}
@Component
struct Child {
@ObjectLink score: Score
build() {
Column() {
Text('子级')
Column() {
Text(`数学:${this.score.math}`)
Button(`数学+1`).onClick(() => {
this.score.math += 1
})
}
}
.width('100%')
.justifyContent(FlexAlign.Center).borderWidth('10lpx')
}
}
内存存储
页面级存储
运行时存吃在内存中,并不是持久化的,在应用退出再次启动后会丢失
@LocalStorageProp(key) 单项、@LocalStorageLink(key)双向
因为,不确定初始化时 LocalStorage是否存在某个属性key,所以一定要初始化。如果LocalStorage没有对应key的话,将使用本地默认值初始化
页面与其组件共享
ts// 创建新实例并使用给定对象初始化 let para:Record<string,number> = { 'PropA': 47 }; let storage: LocalStorage = new LocalStorage(para); // 页面都是@Entry修饰的,可以传入一个LocalStorage实例。这样页面、和页面中的组件都可以通过@LocalStorageProp(key)、@LocalStorageLink(key)来访问到这个实例 @Entry(storage) @Component struct CompA { // storLink1与LocalStorage中的'PropA'属性建立双向绑定 @LocalStorageLink('PropA') storLink1: number = 1; build() { Column() { Child() } } } @Component struct Child { // storLink2与LocalStorage中的'PropA'属性建立双向绑定 @LocalStorageLink('PropA') storLink2: number = 1; build() { Button(`Child from LocalStorage ${this.storLink2}`) // 更改将同步至LocalStorage中的'PropA'以及Parent.storLink1 .onClick(() => this.storLink2 += 1) } }
一个Ability中多个页面共享一个LocalStorage
ts// EntryAbility.ts import UIAbility from '@ohos.app.ability.UIAbility'; import window from '@ohos.window'; export default class EntryAbility extends UIAbility { private storage: LocalStorage = new LocalStorage(); onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { // 设置当前内容字体为18 this.storage.setOrCreate<number>('contentFontSize', FONT_SIZE); } onWindowStageCreate(windowStage: window.WindowStage) { // loadContent第二个参数可以注入LocalStorage windowStage.loadContent('pages/Index', this.storage); } }
这个Ability持有的页面,都可以通过getShared函数访问到实例。再通过@Entry将LocalStorage实例注入到页面中
ts// 通过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) } } }
应用级存储
运行时存在内存中,并不是持久化的,在应用退出再次启动后会丢失
但是可以借助下一节的【PersistentStorage】实现持久化
@StorageProp(单向)、@StorageLink(双向)
因为,不确定初始化时 AppStorage是否存在某个属性key,所以一定要初始化
用法基本相同,区别:不用@Entry()注入,全局多个Ability都可以共享
AppStorage.setOrCreate('PropA', 47);
@Entry
@Component
struct CompA {
@StorageLink('PropA') storLink: number = 1;
build() {
Column({ space: 20 }) {
Text(`From AppStorage ${this.storLink}`)
.onClick(() => this.storLink += 1)
}
}
}
疑问:
未创建设置 AppStorage ,直接使用@StorageLink(key)是否会报错
不会报错,会自动创建AppStorage
持久化存储
在应用退出再次启动后,依然能保存选定的结果,是应用开发中十分常见的现象
PersistentStorage的作用是持久化指定的AppStorage属性,
PersistentStorage不能直接和页面通讯,页面需要通过AppStorage访问到PersistentStorage持久化的数据
PersistentStorage不支持存储嵌套对象(对象数组,对象的属性是对象等),这种数据数据变化后,无法回到存储中
持久化是一个同步操作,大量数据持久化会阻塞UI,所以大量数据持久化建议使用数据库api
// 指定 持久化AppStorage中的PropA字段
// 如果PersistentStorage不存在PropA字段,则创建AppStorage并设置其PropA为47
// 如果PersistentStorage存在PropA字段,则创建AppStorage设置PropA为持久化存储的值
PersistentStorage.persistProp('PropA', 47)
@Entry
@Component
struct Index {
// 这里的storLink修改,就直接修改AppStorage中PropA的值,进一步修改PersistentStorage存在PropA字段的值
@StorageLink('PropA') storLink: number = 1;
build() {
Column({ space: 20 }) {
Text(`AppStorage 的PropA ${this.storLink}`)
.onClick(() => this.storLink += 1)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
首选项
Preferences会将该数据缓存在内存中,当用户读取时能够快速从内存中快速获取数据,当需要持久化时可以使用flush接口将内存中的数据写入持久化文件中
Preferences不适合存放过多的数据,适用的场景一般为应用保存用户的个性化设置(字体大小,是否开启夜间模式)等
页面数据通讯
UIContext
很多api的使用和具体UI实例的执行上下文相关的,在调用时,会追溯调用链跟踪到UI的上下文,来确定具体的UI实例。但是,若在非UI页面中或者一些异步回调中调用这类api,可能无法跟踪到当前UI的上下文,导致接口执行失败
这里分3中情况讨论:
ability中
WindowStage/Window可以通过loadContent接口加载页面并创建UI的实例,并将页面内容渲染到关联的窗口中,所以UI实例和窗口是一一关联的。api会自动获取上下文,例如router跳转
获取UI实例:windowClass.getUIContext
jsimport UIAbility from '@ohos.app.ability.UIAbility'; import window from '@ohos.window'; import { BusinessError } from '@ohos.base'; import { UIContext } from '@ohos.arkui.UIContext'; export default class EntryAbility extends UIAbility { onWindowStageCreate(windowStage: window.WindowStage) { // 为主窗口加载对应的目标页面。 windowStage.loadContent("pages/page2", (err: BusinessError) => { let errCode: number = err.code; if (errCode) { console.error('Failed to load the content. Cause:' + JSON.stringify(err)); return; } console.info('Succeeded in loading the content.'); // 获取应用主窗口。 let windowClass: window.Window | undefined = undefined; windowStage.getMainWindow((err: BusinessError, data) => { let errCode: number = err.code; if (errCode) { console.error('Failed to obtain the main window. Cause: ' + JSON.stringify(err)); return; } windowClass = data; console.info('Succeeded in obtaining the main window. Data: ' + JSON.stringify(data)); // 获取UIContext实例。 let uiContext: UIContext | null = null; uiContext = windowClass.getUIContext(); }) }); } };
页面中
api会自动获取上下文,例如router跳转
获取UI实例getContext(this)
ts@Component export default struct xxx { bundleName: string = (getContext(this) as common.UIAbilityContext).applicationInfo.name }
封装ets工具函数中
// 这种建议函数通过入参的形式传入UI实例,这样函数既可以在页面使用,也可以在ability使用
目前看社区很多工具函数,都是这样做的
获取UI实例后可使用runScopedTask方法,在回调函数内部是明确的UI上下文
// cityLocationUtils.ets
uiContext?.runScopedTask(() => {
// 持久化存储用户选择的城市信息
PersistentStorage.persistProp('userSelectedCity', '')
// 持久化存储 GPS 定位的城市信息
PersistentStorage.persistProp('gpsLocationCity', '')
const userCityLocal = AppStorage.get<string>('userCity')
if (userCityLocal) {
this.userCityLocal = JSON.parse(userCityLocal)
}
})
})
t
文件
文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/app-file-overview-0000001820880057
窗口概念
Stage模型中:
WindowStage:窗口管理器,管理各个Window
Window:当前窗口实例
窗口分为:
- 主窗口、子窗口
- 窗口
主窗口
核心是通过WindowStage控制窗口
窗口
流程
- 获取窗口实例(window对象)
- 创建窗口(createWindow)
- 获取当前应用内最上层的子窗口,若无应用子窗口,则返回应用主窗口 (getLastWindow)
- 查找name所对应的窗口(findWindow,注:每个窗口创建时都有唯一的name)
- 指定窗口位置(moveWindowTo)
- 指定窗口大小(resize)
- 窗口加载内容(setUIContent)
- 窗口显示(showWindow)
例子:本例子是创建实例
注意,创建时指定不同windowType的区别:
TYPE_DIALOG 模态窗,只在当前应用内显示
TYPE_FLOAT 全局悬浮窗 ,应用切换到后台,仍然可以在屏幕中显示。需要申请ohos.permission.SYSTEM_FLOAT_WINDOW权限,而且仅在2in1设备使用,需要ACL跨级别申请。官方推荐使用画中画替代
注意,使用不同窗口加载同一页面会发现,窗口内容彼此独立,互不影响
import window from '@ohos.window';
import { BusinessError } from '@ohos.base';
@Entry
@Component
struct test {
destroyWin(){
this.win.destroyWindow((err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to destroy the window. Cause: ' + JSON.stringify(err));
return;
}
console.info('Succeeded in destroying the window.');
});
}
async createSubWin() {
const win = await window.createWindow({
name: '1',
windowType: window.WindowType.TYPE_SYSTEM_ALERT,
ctx: getContext(this)
})
win.moveWindowTo(500, 800, (err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to move the window. Cause:' + JSON.stringify(err));
return;
}
console.info('Succeeded in moving the window.');
});
win.resize(500, 700, (err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to change the window size. Cause:' + JSON.stringify(err));
return;
}
console.info('Succeeded in changing the window size.');
});
// 3.为子窗口加载对应的目标页面。
win.setUIContent("pages/Index2", (err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to load the content. Cause:' + JSON.stringify(err));
return;
}
console.info('Succeeded in loading the content.');
// 3.显示子窗口。
(win as window.Window).showWindow((err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to show the window. Cause: ' + JSON.stringify(err));
return;
}
console.info('Succeeded in showing the window.');
});
});
}
build() {
Button('创建').onClick(() => {
this.createWin()
})
Button('销毁').onClick(() => {
this.destroyWin()
})
}
}
图像(待补充)
图像分为 三种类型 : uri、Resource、PixelMap
大纲:
如何转换(https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-image-0000001821001457)
@ohos.multimedia.image image.createImageSource() // 入参 uri、fd文件描述符、ArrayBuffer、resourceManager.RawFileDescriptor 。 返回ImageSource类型 ImageSource.createPixelMap() // ImageSource转换为PixelMap类型
如何下载、缓存
如何应用变换(矩阵变换)
文件(待补充)
关于Builder函数
基础用法
@Builder
function navBuilder() {
// 不用build函数,直接写布局
Column(){
}
}
Builder函数内部可以是自定义组件
@Builder
function navBuilder() {
// import进来的自定义组件
NavBar()
}
Builder支持全局、局部(写在struct中)两种作用域
@Builder
function navBuilder() {
}
@Component
struct xxx{
@Builder
navBuilder() {
}
}
Builder参数
无参数(上面例子就是无参数的)
有参数
默认参数是值传递,无论入参是基本类型还是引用类型,Builder组件不会更新
@Builder
function navBuilder(title: string) {// 这里形参是拷贝了一份副本
Text(title)
}
@Entry
struct Index {
@State title: string = '标题1'
build() {
Column() {
// 使用builder,传入参数
navBuilder(this.title)
// 这里不会改变Builder内的数据
Button('修改标题').onClick(() => {
this.title = '标题2'
})
}
}
}
使用引用传参
这个比较绕,所以解释下。builder入参必须是 { key: 这里才是数据 } ,改变数据才会更新页面
这个数据是简单类型、对象都会更新页面
interface BuilderData {
data: Info
}
interface Info {
title: string
}
@Builder
function navBuilder(builderData: BuilderData) { //这个builderData是 {key:数据}
Text(builderData.data.title)
}
@Entry
struct Index {
@State data: Info = { title: '标题1' }
build() {
Column() {
// 使用builder,传入参数,参数形式必须是 {key: 数据}
navBuilder({ data: this.data })
// 修改数据
Button('修改标题').onClick(() => {
this.data.title = '标题2'
})
}
}
}
使用Builder
作为组件
@Builder
navBuilder(title:string) {
//xxx
}
@component
struct xx{
build(){
this.navBuilder()
}
}
作为参数(插槽用法)
子组件使用@BuilderParam定义的参数,父级可以将Builder传入子组件
子组件数据变更,Builder更新,这需要Builder使用引用传递
关于Builder中的this指向子组件(比如,在Builder中的元素的onClick回调中使用this)
如何访问父级this?
答:使用bind绑定父级的this
Child({ customOverBuilderParam: this.overBuilder.bind(this) })
例子:
class Info {
label: string = ''
}
interface BuilderData {
info: Info
}
@Entry
@Component
struct Parent {
label: string = 'Parent'
@Builder
overBuilder($$: BuilderData) {
Text($$.info.label)
}
build() {
Column() {
Child({ customOverBuilderParam: this.overBuilder })
}
}
}
@Component
struct Child {
@State info: Info = { label: '初始值' }
// 有参数类型
@BuilderParam customOverBuilderParam: ($$: BuilderData) => void;
@Builder
customBuilder() {
}
build() {
Column() {
Button('修改内部的值').onClick(() => {
this.info.label = '测试2'
})
// 使用引用传递 {key:数据}
this.customOverBuilderParam({ info: this.info })
}
}
}
Emitter
import emitter from '@ohos.events.emitter';
struct Parent{
build(){
Button('按钮')
.onClick(()=>{
// 事件名
let innerEvent: emitter.InnerEvent = { eventId: 'event1' }
// 数据
let eventData: emitter.EventData = {
data: {
"colorTag": 1
}
}
emitter.emit(innerEvent, eventData)
})
}
}
struct Child{
private index: number = 0;
aboutToAppear() {
//定义事件ID
let innerEvent: emitter.InnerEvent = { eventId:'event1'}
emitter.on(innerEvent, data => {
this.onTapIndexChange(data)
})
}
}
经验
初始值零值
对于简单类型,初始类型零值可以设置为null,最好是一个有意义的值
对枚举类型,可以设置None为零值
tsenum xx{ None Type1, Type2 }
最重要的是对于对象,如果设置为null,极有出现空指针,导致闪退现象
对应对象建议使用class,
tsclass Person { name: string = '' //设置零值 constructor(name?: string) { this.name = name|'' //如果是可选属性,必须在这里设置零值 } }
对象key必须明确
比如,一个组件接收入参,点击组件返回参数
但是,返参字段与入参保持一致,不同入参会导致返参结果不确定,这需要通过if一个个取判断
if(backParam.a){
// 存在a,就xxx
}
if(backParam.b){
// 存在b,就xxx
}
组件入参或者函数入参,如果是对象。一定要保证对象的key是固定的,否则会很难写
比如,一个下拉框组件,传入了对象结构如下。复用组件时可能传入的是动态的配置,在组件每部不确定传入的对象的key到底可能是什么(arkts中不能for循环key,通过索引的方式取出对象的value)
{
dataType:{xxx}
weekType:{xxx}
}
{
yearType:{xxx}
}
{
dayType:{xxx}
}
建议可变数据,传入数组,组件内可以forEach读取
[
{type:dataType,value:xxx}
{type:weekType,value:xxx}
]
[
{type:yearType,value:xxx}
]
[
{type:dayType,value:xxx}
]
关于滚动事件
List、Scroll组件onScroll、onScrollFrameBegin区别
手指上滑,页面标签折叠;下滑展开
必须用onScrollFrameBegin,如果用onScroll下滑到底会向上回弹下,onScroll也会触发
.onScroll((scrollOffset, scrollState) => {
// 滚动触发,注意 滚动的回弹也会触发
})
.onScrollFrameBegin((offset: number) => {
// 手指滑动触发
return { offsetRemain: offset }
})
过渡
transition过渡 :https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-enter-exit-transition-0000001820879809
过渡需要加上animation指定动画(过渡时间,动画曲线),否则过渡会一闪而过
xxx.transition(TransitionEffect.translate({ y: 1000 }).animation({ curve: curves.springMotion(0.6, 0.8) }))
if控制组件显隐,产生过渡
过渡出现消失是对称的,我们指定是出现的过渡效果
if (this.isPresent) {
Column() {
// xxx
}.transition(TransitionEffect.translate({ x: '580lpx' }).animation({ duration: 300 }))
// transition 指定过渡的方式,比如平移、旋转、放大
// animation 指定动画的时长、动画的运动函数
}
也可以自定义不对称的过渡效果,使用asymmetric函数,第一个参数是出现的过渡、第二个参数是消失的过渡
if (this.isPresent) {
Column() {
// xxx
}.transition(
TransitionEffect.asymmetric(TransitionEffect.move(TransitionEdge.START), TransitionEffect.move(TransitionEdge.END))
).animation({ duration: 300 })
}
动画
事件触发,动画跟变量绑定
xxx.onClick(()=>{
animateTo({ curve: curves.springMotion(),duration: 500 }, () => {
// 所有绑定isPresent的元素都会发生动画
this.isPresent = false;
})
})
动画和组件绑定
模态转场
页面切换、半弹层弹出
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-modal-transition-0000001774120166
获取布局信息
xxx().id('id名')
注意:
- 组件和获取布局代码可以不在一个est文件中
- 单位是px
import componentUtils from '@ohos.arkui.componentUtils'
import inspector from '@ohos.arkui.inspector'
struct xxx{
listener: inspector.ComponentObserver = inspector.createComponentObserver('组件id')
aboutToAppear(): void {
this.listener.on('draw', () => {
this.spaceHeight = componentUtils.getRectangleById('RankCarItem').size.height
console.log('高度', String(componentUtils.getRectangleById('RankCarItem').size.height))
})
}
}
手势处理
需求:二级城市选择弹层,点击弹层中的城市则选中,右滑弹层则关闭
@Component
export default struct CitySecondLevelSelectList {
onSelectSecondLevelCity?: (cityInfo: CityInfo) => void
onClose?: () => void
build() {
Scroll() {
Column() {
if (this.data !== null) {
ForEach(this.data, (cityName: string) => {
Column() {
Row() {
Text(cityName)
}
.width('100%')
.height('102lpx')
Divider().width('540lpx').strokeWidth('1lpx').color('#eeeeee')
}
.width('100%')
.padding({ left: '40lpx', right: '40lpx' })
// 绑定点击手势(TapGesture),点击选中城市
.gesture(TapGesture().onAction(() => {
this?.onSelectSecondLevelCity?.(cityName)
}))
})
}
}
.width('100%')
}
.width('100%')
.height('100%')
.backgroundColor(Color.White)
.align(Alignment.Top)
.scrollBar(BarState.Off)
.scrollable(ScrollDirection.Vertical)
// 绑定滑动手势(SwipeGesture),点击选中城市
.priorityGesture(SwipeGesture({ direction: SwipeDirection.Horizontal }).onAction(() => {
this?.onClose?.()
}))
}
}
使用priorityGesture,priorityGesture优先级大于gesture,响应了priorityGesture的事件,gesture就不会响应了
parallelGesture,parallelGesture、gesture手势都会触发。在这个例子里,会出现滑动关闭时,同时选中城市的Bug
编译模式
可以区分是否release、debug模式
import BuildProfile from 'BuildProfile';
@State message: string = BuildProfile.BUILD_MODE_NAME
样式封装
开发时,经常对Text组件设置多个样式,每个Text组件都要写很繁琐
可以将多个样式封装成一个
注意:只支持全局作用域,即不能写在Struct里
// 支持传参
@Extend(Text) function newTextStyle (fontWeight: number,
textAttribute: number, fontSize: Resource, fontColor: Resource) {
.fontWeight(fontWeight)
.letterSpacing(textAttribute)
.fontSize(fontSize)
.fontColor(fontColor)
}
使用
Text('测试')
.height(CommonConstants.LAUNCHER_TEXT_INTRODUCE_HEIGHT)
.newTextStyle(xxx,xxx,xxx,xxx)
对于某组件有选中、未选中两种样式。但是需要对多个属性进行判断
Text('测试')
.backgroundColor(this.active?Color.Red,Color.Black)
.fontSize(this.active?'30lpx','10lpx')
可以使用动态属性设置
class modifier implements AttributeModifier<TextAttribute> {
applyNormalAttribute(instance: ButtonAttribute): void {
if (this.isSelect) {
instance.backgroundColor(Color.Red)
instance.fontSize('30lpx')
} else {
instance.backgroundColor(Color.Black)
instance.fontSize('10lpx')
}
}
}
@Entry
@Component
struct xx{
@State modifier: MyButtonModifier = new MyButtonModifier()
@State isSelect:boolean=false
build(){
Text('测试')
.onClick(()=>{
isSelect=!this.isSelect
})
.attributeModifier(this.modifier)
}
}
Scroll与List的偏移量
需求:
当元素个数小于等于6时,均分空间
大于6时,可以滚动
注意点:
// 每帧开始的时候触发
.onScrollFrameBegin((offset: number, state: ScrollState) => {
if (state === ScrollState.Scroll) {
// 查询Row这个组件相对于其父级Scroll滚动的偏移量
const offsetX = componentUtils.getRectangleById('RowContainer').localOffset.x
this.scrollLeftOffset = -(px2lpx(offsetX) / px2lpx(this.overFlowWidth)) * 16
}
return { offsetRemain: offset }
})
// 1、为什么不用onScroll?
// 实际测试发现onScroll并不是按帧触发,offsetX查询并不及时,即可能出现Scroll滚动到起始位置时onScroll并没触发,offsetX最后的值不是0
// 2、为什么不用 offset 属性,totalOffset=totalOffset + offset累计求和计算?
// 实际测试发现Scroll滚动到起始位置时可能totalOffset最后的值不是0,而是逼近0的小数,例如:0.0000xxxxx
代码:
import componentUtils from '@ohos.arkui.componentUtils'
import inspector from '@ohos.arkui.inspector'
@Preview
@Component
export default struct CarStyleJellyBean {
@State List: string[] = ['', '', '', '', '', '', '', '',]
@State scrollLeftOffset: number = 0
@State isScroll: boolean = false
overFlowWidth: number = 0
RowContainerListener: inspector.ComponentObserver = inspector.createComponentObserver('RowContainer')
ScrollContainerListener: inspector.ComponentObserver = inspector.createComponentObserver('ScrollContainer')
aboutToAppear(): void {
this.ScrollContainerListener.on('draw', () => {
let scrollContainerWidth = componentUtils.getRectangleById('ScrollContainer').size.width
this.RowContainerListener.on('draw', () => {
this.overFlowWidth = componentUtils.getRectangleById('RowContainer').size.width - scrollContainerWidth
})
})
}
build() {
Column() {
Scroll() {
Row({ space: this.List.length > 6 ? '25lpx' : '' }) {
ForEach(this.List, () => {
Column() {
Image($r('app.media.app_icon'))
.width('48lpx')
.height('48lpx')
Text('车主点评')
.fontSize('22lpx')
.fontColor('#0f1d37')
.fontWeight(FontWeight.Regular)
.margin({ top: '8lpx' })
}
})
}
.RowScrollStyle(this.List.length > 6)
.id('RowContainer')
}
.id('ScrollContainer')
.width('100%')
.backgroundColor(Color.White)
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.align(Alignment.Start)
.onScrollFrameBegin((offset: number, state: ScrollState) => {
if (state === ScrollState.Scroll) {
const offsetX = componentUtils.getRectangleById('RowContainer').localOffset.x
this.scrollLeftOffset = -(px2lpx(offsetX) / px2lpx(this.overFlowWidth)) * 16
}
return { offsetRemain: offset }
})
Row() {
Row()
.width('18lpx')
.height('6lpx')
.borderRadius('3lpx')
.backgroundColor('#0f1d37')
.position({ x: `${this.scrollLeftOffset}lpx` })
}
.width('34lpx')
.height('6lpx')
.borderRadius('3lpx')
.backgroundColor('#e0e0e0')
.margin({ top: '12lpx' })
}
.width('100%')
.padding({ top: '12lpx', bottom: '12lpx' })
}
}
@Extend(Row)
function RowScrollStyle(isScroll: boolean) {
.width(isScroll ? '' : '100%')
.justifyContent(isScroll ? FlexAlign.Start : FlexAlign.SpaceBetween)
}
popup
注意:
一定要取消监听,遇到过反复创建崩溃的问题
// 通过句柄向对应的查询条件取消注册回调,由开发者自行决定在何时调用。
// this.listener.off('layout', OffFuncLayout)
// this.listener.off('draw', OffFuncDraw)
import inspector from '@ohos.arkui.inspector'
import componentUtils from '@ohos.arkui.componentUtils'
@State vis: boolean = false
@State pageMainContentHeight: number = 0
listener: inspector.ComponentObserver = inspector.createComponentObserver('pageMainContent')
aboutToAppear(): void {
this.listener.on('draw', () => {
this.pageMainContentHeight = componentUtils.getRectangleById('pageMainContent').size.height // 单位是px
})
}
@Builder
popupBuilder() {
Scroll() {
Column() {
//xxx
}
}
.width('100%')
.height(`${this.pageMainContentHeight}px`)
.backgroundColor(Color.White)
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.align(Alignment.Top)
}
xxx.bindPopup(this.vis, {
builder: this.popupBuilder,
placement: Placement.Bottom,
enableArrow: false,
popupColor: Color.Transparent,
mask: true,
radius: 0,
shadow: {
radius: 0,
},
backgroundBlurStyle: BlurStyle.NONE,
autoCancel: true,
// 设置为false(默认值),底部导航条会有避让。
// true会完全遮住,但是textInput、Search使用键盘输入数据时,会自动关闭
// showInSubWindow: true,
onStateChange: (e) => {
console.log(`onSearchAssociateVis 弹窗回调${e.isVisible}`)
this.searchAssociateVis = e.isVisible
}
})
pageMainContent高度需要算对,如果算多了,会以屏幕顶部为起点
让人误以为Placement.Bottom失效了
Canvas
绘制一个圆球,背景渐变
@Entry
@Component
struct Index {
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
// 设置尺寸
private canvasWidth = px2vp(lpx2px(44))
private canvasHeight = px2vp(lpx2px(44))
private lineWidth = px2vp(lpx2px(2))
build() {
Canvas(this.context)
.width(this.canvasWidth)
.height(this.canvasHeight)
.onReady(() => {
// 设置渐变
let grad = this.context.createLinearGradient(this.canvasWidth / 2, 0, this.canvasWidth / 2, this.canvasHeight)
grad.addColorStop(0.5, 'red')
grad.addColorStop(0.5, 'blue')
this.context.fillStyle = grad
// 绘制轮廓
this.context.arc(this.canvasWidth / 2, this.canvasWidth / 2, this.canvasWidth / 2 - this.lineWidth, 0, 6.28)
// 填充轮廓内部
this.context.fill()
// 轮廓设置
this.context.strokeStyle = '#cccccc'
this.context.lineWidth = this.lineWidth
this.context.stroke()
})
}
}
页面布局区域
关于UUID
官方回复:
您好,udid需要系统权限 三方应用不支持获取
为更好的保障用户隐私安全,Next版本不再提供UUID,建议伙伴根据使用场景,考虑使用AAID或OAID替代
AAID 应用匿名标识符
不会触发弹窗提示
应用卸载重装、调用删除AAID接口、清理应用数据、恢复出厂设置都会丢失
但是可以配合Asset存储,确保卸载重装后获取的id是同一个。在Asset Store Kit(关键资产存储服务)保存业务关键数据,并设置IS_PERSISTENT为true(在应用卸载时是否需要保留关键资产,需要权限: ohos.permission.STORE_PERSISTENT_DATA),应用卸载重装后仍然可以查询到之前保存的数据(注意:在查询asset时,如果从来没保存过某个资产,首次查询会抛出24000008(数据库操作失败)异常,因此在查询时除了判断取出的数组长度是否为0,还要专门处理这个异常码)
OAID广告标识服务
会触发弹窗提示
仅恢复出厂设置会丢失,注意卸载不会丢失
第二种方案会触发弹窗提示 "是否允许 OAID 访问应用跟踪",用户体验不太好。所以更推荐第一中方案
突发奇想
vconsole工具
形式
- 子窗口设计一个 vconsole
- 项目中增加一个ability,build-profile时要排除在打包产物外
是否能使用 hcd 实时获取日志,在vconsole中作格式化处理
疑问
popup 内容设置 100% 到底继承哪里
Tab子组件100%高度是哪里
数据更新疑惑
ObservedArray修饰的的数组。子元素都是用@Observe修饰的,这个子元素传入子组件的属性,必须用@ObjectLink修饰。但是我发现如果用builder传入参数就不用任何修饰符
imageModelList: ObservedArray<ImageViewTouch.Model> = []
子组件入参是@Link类型,如果 @State list:xx=[] ,forEach遍历list的子元素,如果传入子组件会提示常规属不能作为@Link属性的入参
查阅文档:@State, @StorageLink和@Link 才能作为入参。可是这几个装饰器只能在页面用
但是@Observed可以脱离页面用,但是用这种方式装饰数组,子组件的入参就必须是@ObjectLink(也是只能在页面用)
@Observed
export class ObservedArray<T> extends Array<T> {
constructor(args?: T[]) {
if (args instanceof Array) {
super(...args)
} else {
super()
}
}
}
为啥我的向普通数组中推入数据,页面就能更新?但是写的demo就不行
UI更新装饰器
仅修饰页面变量(状态变量)
以@State为例子:
@State a 如果接受的是其他状态变量传过来的,其他状态变量改变a也会更新UI
@State a = x 如果接受的是普通变量,只会初始化a的值,更新变量x不会更新UI。必须直接操作a才会更新,this.a=2
@State a = 对象 如果接受的是值,比如可操作的对象,直接修改的这也是不会更新的(@Track可以提供更新能力)
@State (接受所有状态变量+普通变量) @Prop (接受所有状态变量+普通变量) @Link (仅接受状态变量@State、@StorageLink、@Link修饰) @ObjectLink (仅接受 @Observed ,注意:其他装饰器接受普通变量数据都只会初始化,修改变量并不会更新UI。唯独@ObjectLink可以接受对象数据,且提供了修改数据更新UI的能力) 其他(@Provide、@Consume、@StorageLink、@StorageProp、@LocalStorageLink和@LocalStorageProp)
仅修饰非页面(修饰对象数据)
@Track
仅能用于修饰Class的非静态属性
@Observed
仅能用于修饰Class
@Observed export class ObservedArray<T> extends Array<T> { constructor(args?: T[]) { if (args instanceof Array) { super(...args) } else { super() } } }
UI更新
页面变量装饰器,修改会更新页面(深层次对象,只有修改第一次才会更新页面)
@Track
@Track修饰的Class属性,实例化后的对象可直接在UI中使用
UI更新方式:
方式1:修改状态修饰符的变量(见【1】,本质是修改了状态变量的第一层,所以可以获得更新,状态变量修改后是同步修改对象的,下面例子可以印证)
jsexport class BaseInfoModel { subTitle: string = 'subTitle初始值' getSubTitle() { return this.subTitle } } @Entry @Component export default struct Parent { @State model: BaseInfoModel = new BaseInfoModel() build() { Column() { Text(this.model.subTitle) Button('修改subTitle').onClick(() => { this.model.subTitle = 'subTitle更新' }) Button('获取model对象内部数据').onClick(() => { console.log(this.model.getSubTitle()) //1、 确实对象内部修改了 }) Child({ model: this.model }) } } } @Component struct Child { @Link model: BaseInfoModel build() { Column() { Text(this.model.subTitle) // 对象修改了,这里也会更新 } } }
方式2:修改@Track修饰的属性(见【2】,本质是@Track提供等能力,可以让直接修改值达到触发更新的目的。注意@Track没有深层监测能力,如果是Class的属性类型是数组、对象则推荐直接赋值属性达到更新的目的。对于数组push子元素、对象的一层属性修改也是不更新的)
非@Track修饰的Class属性,直接在UI中使用会直接报错
tsexport class BaseInfoModel { @Track title: string = '初始值' // 类内部修改也会触发更新 setTitle(val: string) { this.title = val } } @Entry @Component export default struct Index { // 这里必须用@State修饰 @State model: BaseInfoModel = new BaseInfoModel() build() { Column() { Text(this.model.title) Button('直接修改model.title').onClick(() => { this.model.title = '内容1' //【1】 页面中修改会触发更新 }) Button('model内部修改title').onClick(() => { this.model.setTitle('内容2') //【2】 通过类方法,修改对象属性也会触发更新 }) } } }
注意1:下面这种直接取出model值的方式,pageTitle只初始化为title的值,但是改title是不会更新UI的
js@State model: BaseInfoModel = new BaseInfoModel() @State pageTitle: string = this.model.title
注意2:如果需要监听model数据的变化(例如:城市数据变化了,页面除了要展示数据,还要弹出提示窗口)
js// 方式1: @watch监听 @State @Watch('onChangeModel') model: BaseInfoModel = new BaseInfoModel() onChangeModel() { console.log(this.model.title) } // 方式2:如果cityInfo的变更明确只会在model内的方法中赋值(不会再页面修改),可以在model中定义notify方法,赋值时调用notify方法,通知页面 (不推荐,页面上直接修改变量无法监听到) @State model: BaseInfoModel = new BaseInfoModel() aboutToAppear(): void { this.model.notify(()=>{ // 收到通知 }) }
目前发现对象传入子组件,需要子组件使用@Link修饰(使用@Prop实际上是拷贝了对象)
Child(model:this.model) Child中需要用@Link model
@Observed
与页面状态修饰符@ObjectLink强关联,当Observed修饰的对象第一层变化时,触发@ObjectLink修饰的页面变量的UI更新
正常情况下@State a1:string=a,修改数据源是无效的,但是@Observed可以让数据源的修改生效
ts@Observed class ClassA { a: number=1 } // 父级 @State classa =new ClassA() // 子组件用@ObjectLink接受到了父级传@Observed修饰的对象,修改对象的一级属性才行 // @ObjectLink不能本地初始化,必须从父级传入 @ObjectLink a: ClassA
@Track 与 @Observed对比
相同点:
两者都没有深层次监听的能力,但是@Observed可直接修饰Class,所以可以用ObservedArray的方式让数组的对象元素得到第一层的监听能力
不同点:
// 这个Class实例化的对象,第一层全部属性只要变动就会全部触发更新 // 页面必须需要使用@ObjectLink接收,例如 @ObjectLink xx = new xx() @Observed Class xx{ a:string, b:string, c:string } // 这个Class实例化的对象,仅仅@Track修饰的属性变化才会触发页面更新且,仅触发绑定该变量位置的UI更新。 例如:改a数据,页面上仅a处更新UI,b处不更新。改c页面不更新 // 页面可以用所以状态变量接收,例如 @Status model = new xx() // @Status c1 =model.c ,数据model.c更新页面是不会更新的 Class xx{ @Track a:string, @Track b:对象类型[], // 目前发现无论是直接赋值this.b=xxx,还是push元素都是更新的 c:string } // 两者都不用呢?尝试复现宇航的逻辑 Class xx{ b:string, c:string[] d:ObserveArray<对象类型> getDataB(){ b=xxx } getDataC(){ c=xxx } getDataD(){ for 推入数据 } } private model = new xx() // 只会初始化数据,但是没有更新UI的能力 @State b1 = this.model.b // 取变量赋值给状态变量 @State c1 = this.model.c // 取变量赋值给状态变量 @State d1 = this.model.d // d是数组,这里相对于把一个 ObserveArray<对象类型> 赋值给d1 aboutToAppear(){ this.model.getDataB() // 不更新 this.model.getDataC() // 不更新 this.model.getDataD() // 更新 } ForEach(this.d1, (item: 数组子元素对象的类型) => { this.builder(item) // <?疑问点>为啥用builder,是不是因为item实际上是Observe修饰的,子组件只能用@ObjectLink接收 }) // 也就是页面之所以更新和model完全没关系,重点是因为@State d才更新 // 相当于还是页面持有数据并更新。model只是作为数据源传入页面,这种方式本质上更像是总线的思想,即直接不持有页面数据 // 使用@Track的方式,才是pinia的思想,能直接提供响应式能力
问题1:加上数组属性,基础类型属性就会报错没有@Track修饰
interface BaseInfo {
title: string
}
export class BaseInfoModel {
// 基础类型
subTitle: string = 'subTitle初始值'
// 对象数组《这里去掉》
// @Track baseInfoList: BaseInfo[] = []
// 类内部修改也会触发更新
setTitle(val: string) {
this.subTitle = val
}
}
@Entry
@Component
export default struct Index {
// 这里必须用@State修饰
@State model: BaseInfoModel = new BaseInfoModel()
build() {
Column() {
Text(this.model.subTitle)
Button('直接修改model.title').onClick(() => {
this.model.subTitle = '内容1' //【1】 页面中修改会触发更新
})
Button('model内部修改title').onClick(() => {
this.model.setTitle('内容2') //【2】 通过类方法,修改对象属性也会触发更新
})
}
}
}