diff --git a/zh-cn/application-dev/napi/xcomponent-guidelines.md b/zh-cn/application-dev/napi/xcomponent-guidelines.md index 1709f7d25e1b37d3fedfaa91653b15af0957e7b0..4d0a38fa23cab9ec92e8c67b04f40469d33fe1b4 100644 --- a/zh-cn/application-dev/napi/xcomponent-guidelines.md +++ b/zh-cn/application-dev/napi/xcomponent-guidelines.md @@ -81,7 +81,7 @@ XComponent({ id: 'xcomponentId1', type: 'surface', libraryname: 'nativerender' } // ... ``` -2. **Napi模块注册**,具体使用请参考[Native API在应用工程中的使用指导](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/napi/napi-guidelines.md)。 +2. **Napi模块注册**,具体使用请参考[Native API在应用工程中的使用指导](napi-guidelines.md)。 ```c++ // 在napi_init.cpp文件中,Init方法注册接口函数,从而将封装的C++方法传递出来,供JS侧调用 diff --git a/zh-cn/application-dev/quick-start/arkts-mvvm.md b/zh-cn/application-dev/quick-start/arkts-mvvm.md index 6185ed8d6574e2083605b4d2a4171ca768918106..0db5a4028746911fcae2eb2e07dffaafcd9ce086 100644 --- a/zh-cn/application-dev/quick-start/arkts-mvvm.md +++ b/zh-cn/application-dev/quick-start/arkts-mvvm.md @@ -1,1334 +1,1334 @@ -# MVVM模式 - - -应用通过状态去渲染更新UI是程序设计中相对复杂,但又十分重要的,往往决定了应用程序的性能。程序的状态数据通常包含了数组、对象,或者是嵌套对象组合而成。在这些情况下,ArkUI采取MVVM = Model + View + ViewModel模式,其中状态管理模块起到的就是ViewModel的作用,将数据与视图绑定在一起,更新数据的时候直接更新视图。 - - -- Model层:存储数据和相关逻辑的模型。它表示组件或其他相关业务逻辑之间传输的数据。Model是对原始数据的进一步处理。 - -- View层:在ArkUI中通常是\@Components修饰组件渲染的UI。 - -- ViewModel层:在ArkUI中,ViewModel是存储在自定义组件的状态变量、LocalStorage和AppStorage中的数据。 - - 自定义组件通过执行其build()方法或者\@Builder装饰的方法来渲染UI,即ViewModel可以渲染View。 - - View可以通过相应event handler来改变ViewModel,即事件驱动ViewModel的改变,另外ViewModel提供了\@Watch回调方法用于监听状态数据的改变。 - - 在ViewModel被改变时,需要同步回Model层,这样才能保证ViewModel和Model的一致性,即应用自身数据的一致性。 - - ViewModel结构设计应始终为了适配自定义组件的构建和更新,这也是将Model和ViewModel分开的原因。 - - -目前很多关于UI构造和更新的问题,都是由于ViewModel的设计并没有很好的支持自定义组件的渲染,或者试图去让自定义组件强行适配Model层,而中间没有用ViewModel来进行分离。例如,一个应用程序直接将SQL数据库中的数据读入内存,这种数据模型不能很好的直接适配自定义组件的渲染,所以在应用程序开发中需要适配ViewModel层。 - - -![zh-cn_image_0000001653986573](figures/zh-cn_image_0000001653986573.png) - - -根据上面涉及SQL数据库的示例,应用程序应设计为: - - -- Model:针对数据库高效操作的数据模型。 - -- ViewModel:针对ArkUI状态管理功能进行高效的UI更新的视图模型。 - -- 部署 converters/adapters: converters/adapters作用于Model和ViewModel的相互转换。 - - converters/adapters可以转换最初从数据库读取的Model,来创建并初始化ViewModel。 - - 在应用的使用场景中,UI会通过event handler改变ViewModel,此时converters/adapters需要将ViewModel的更新数据同步回Model。 - - -虽然与强制将UI拟合到SQL数据库模式(MV模式)相比,MVVM的设计比较复杂,但应用程序开发人员可以通过ViewModel层的隔离,来简化UI的设计和实现,以此来收获更好的UI性能。 - - -## ViewModel的数据源 - - -ViewModel通常包含多个顶层数据源。\@State和\@Provide装饰的变量以及LocalStorage和AppStorage都是顶层数据源,其余装饰器都是与数据源做同步的数据。装饰器的选择取决于状态需要在自定义组件之间的共享范围。共享范围从小到大的排序是: - - -- \@State:组件级别的共享,通过命名参数机制传递,例如:CompA: ({ aProp: this.aProp }),表示传递层级(共享范围)是父子之间的传递。 - -- \@Provide:组件级别的共享,可以通过key和\@Consume绑定,因此不用参数传递,实现多层级的数据共享,共享范围大于\@State。 - -- LocalStorage:页面级别的共享,可以通过\@Entry在当前组件树上共享LocalStorage实例。 - -- AppStorage:应用全局的UI状态存储,和应用进程绑定,在整个应用内的状态数据的共享。 - - -### \@State装饰的变量与一个或多个子组件共享状态数据 - - -\@State可以初始化多种状态变量,\@Prop、\@Link和\@ObjectLink可以和其建立单向或双向同步,详情见[@State使用规范](arkts-state.md)。 - - -1. 使用Parent根节点中\@State装饰的testNum作为ViewModel数据项。将testNum传递给其子组件LinkChild和Sibling。 - - ```ts - // xxx.ets - @Entry - @Component - struct Parent { - @State @Watch("testNumChange1") testNum: number = 1; - - testNumChange1(propName: string): void { - console.log(`Parent: testNumChange value ${this.testNum}`) - } - - build() { - Column() { - LinkChild({ testNum: $testNum }) - Sibling({ testNum: $testNum }) - } - } - } - ``` - -2. LinkChild和Sibling中用\@Link和父组件的数据源建立双向同步。其中LinkChild中创建了LinkLinkChild和PropLinkChild。 - - ```ts - @Component - struct Sibling { - @Link @Watch("testNumChange") testNum: number; - - testNumChange(propName: string): void { - console.log(`Sibling: testNumChange value ${this.testNum}`); - } - - build() { - Text(`Sibling: ${this.testNum}`) - } - } - - @Component - struct LinkChild { - @Link @Watch("testNumChange") testNum: number; - - testNumChange(propName: string): void { - console.log(`LinkChild: testNumChange value ${this.testNum}`); - } - - build() { - Column() { - Button('incr testNum') - .onClick(() => { - console.log(`LinkChild: before value change value ${this.testNum}`); - this.testNum = this.testNum + 1 - console.log(`LinkChild: after value change value ${this.testNum}`); - }) - Text(`LinkChild: ${this.testNum}`) - LinkLinkChild({ testNumGrand: $testNum }) - PropLinkChild({ testNumGrand: this.testNum }) - } - .height(200).width(200) - } - } - ``` - -3. LinkLinkChild和PropLinkChild声明如下,PropLinkChild中的\@Prop和其父组件建立单向同步关系。 - - ```ts - @Component - struct LinkLinkChild { - @Link @Watch("testNumChange") testNumGrand: number; - - testNumChange(propName: string): void { - console.log(`LinkLinkChild: testNumGrand value ${this.testNumGrand}`); - } - - build() { - Text(`LinkLinkChild: ${this.testNumGrand}`) - } - } - - - @Component - struct PropLinkChild { - @Prop @Watch("testNumChange") testNumGrand: number; - - testNumChange(propName: string): void { - console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); - } - - build() { - Text(`PropLinkChild: ${this.testNumGrand}`) - .height(70) - .backgroundColor(Color.Red) - .onClick(() => { - this.testNumGrand += 1; - }) - } - } - ``` - - ![zh-cn_image_0000001638250945](figures/zh-cn_image_0000001638250945.png) - - 当LinkChild中的\@Link testNum更改时。 - - 1. 更改首先同步到其父组件Parent,然后更改从Parent同步到Siling。 - - 2. LinkChild中的\@Link testNum更改也同步给子组件LinkLinkChild和PropLinkChild。 - - \@State装饰器与\@Provide、LocalStorage、AppStorage的区别: - - - \@State如果想要将更改传递给孙子节点,需要先将更改传递给子组件,再从子节点传递给孙子节点。 - - 共享只能通过构造函数的参数传递,即命名参数机制CompA: ({ aProp: this.aProp })。 - - 完整的代码示例如下: - - - ```ts - @Component - struct LinkLinkChild { - @Link @Watch("testNumChange") testNumGrand: number; - - testNumChange(propName: string): void { - console.log(`LinkLinkChild: testNumGrand value ${this.testNumGrand}`); - } - - build() { - Text(`LinkLinkChild: ${this.testNumGrand}`) - } - } - - - @Component - struct PropLinkChild { - @Prop @Watch("testNumChange") testNumGrand: number; - - testNumChange(propName: string): void { - console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); - } - - build() { - Text(`PropLinkChild: ${this.testNumGrand}`) - .height(70) - .backgroundColor(Color.Red) - .onClick(() => { - this.testNumGrand += 1; - }) - } - } - - - @Component - struct Sibling { - @Link @Watch("testNumChange") testNum: number; - - testNumChange(propName: string): void { - console.log(`Sibling: testNumChange value ${this.testNum}`); - } - - build() { - Text(`Sibling: ${this.testNum}`) - } - } - - @Component - struct LinkChild { - @Link @Watch("testNumChange") testNum: number; - - testNumChange(propName: string): void { - console.log(`LinkChild: testNumChange value ${this.testNum}`); - } - - build() { - Column() { - Button('incr testNum') - .onClick(() => { - console.log(`LinkChild: before value change value ${this.testNum}`); - this.testNum = this.testNum + 1 - console.log(`LinkChild: after value change value ${this.testNum}`); - }) - Text(`LinkChild: ${this.testNum}`) - LinkLinkChild({ testNumGrand: $testNum }) - PropLinkChild({ testNumGrand: this.testNum }) - } - .height(200).width(200) - } - } - - - @Entry - @Component - struct Parent { - @State @Watch("testNumChange1") testNum: number = 1; - - testNumChange1(propName: string): void { - console.log(`Parent: testNumChange value ${this.testNum}`) - } - - build() { - Column() { - LinkChild({ testNum: $testNum }) - Sibling({ testNum: $testNum }) - } - } - } - ``` - - -### \@Provide装饰的变量与任何后代组件共享状态数据 - -\@Provide装饰的变量可以与任何后代组件共享状态数据,其后代组件使用\@Consume创建双向同步,详情见[@Provide和@Consume](arkts-provide-and-consume.md)。 - -因此,\@Provide-\@Consume模式比使用\@State-\@Link-\@Link从父组件将更改传递到孙子组件更方便。\@Provide-\@Consume适合在单个页面UI组件树中共享状态数据。 - -使用\@Provide-\@Consume模式时,\@Consume和其祖先组件中的\@Provide通过绑定相同的key连接,而不是在组件的构造函数中通过参数来进行传递。 - -以下示例通过\@Provide-\@Consume模式,将更改从父组件传递到孙子组件。 - - -```ts -@Component -struct LinkLinkChild { - @Consume @Watch("testNumChange") testNum: number; - - testNumChange(propName: string): void { - console.log(`LinkLinkChild: testNum value ${this.testNum}`); - } - - build() { - Text(`LinkLinkChild: ${this.testNum}`) - } -} - -@Component -struct PropLinkChild { - @Prop @Watch("testNumChange") testNumGrand: number; - - testNumChange(propName: string): void { - console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); - } - - build() { - Text(`PropLinkChild: ${this.testNumGrand}`) - .height(70) - .backgroundColor(Color.Red) - .onClick(() => { - this.testNumGrand += 1; - }) - } -} - -@Component -struct Sibling { - @Consume @Watch("testNumChange") testNum: number; - - testNumChange(propName: string): void { - console.log(`Sibling: testNumChange value ${this.testNum}`); - } - - build() { - Text(`Sibling: ${this.testNum}`) - } -} - -@Component -struct LinkChild { - @Consume @Watch("testNumChange") testNum: number; - - testNumChange(propName: string): void { - console.log(`LinkChild: testNumChange value ${this.testNum}`); - } - - build() { - Column() { - Button('incr testNum') - .onClick(() => { - console.log(`LinkChild: before value change value ${this.testNum}`); - this.testNum = this.testNum + 1 - console.log(`LinkChild: after value change value ${this.testNum}`); - }) - Text(`LinkChild: ${this.testNum}`) - LinkLinkChild({ /* empty */ }) - PropLinkChild({ testNumGrand: this.testNum }) - } - .height(200).width(200) - } -} - -@Entry -@Component -struct Parent { - @Provide @Watch("testNumChange1") testNum: number = 1; - - testNumChange1(propName: string): void { - console.log(`Parent: testNumChange value ${this.testNum}`) - } - - build() { - Column() { - LinkChild({ /* empty */ }) - Sibling({ /* empty */ }) - } - } -} -``` - - -### 给LocalStorage实例中对应的属性建立双向或单向同步 - -通过\@LocalStorageLink和\@LocalStorageProp,给LocalStorage实例中的属性建立双向或单向同步。可以将LocalStorage实例视为\@State变量的Map,使用详情参考LocalStorage。 - -LocalStorage对象可以在ArkUI应用程序的几个页面上共享。因此,使用\@LocalStorageLink、\@LocalStorageProp和LocalStorage可以在应用程序的多个页面上共享状态。 - -以下示例中: - -1. 创建一个LocalStorage实例,并通过\@Entry(storage)将其注入根节点。 - -2. 在Parent组件中初始化\@LocalStorageLink("testNum")变量时,将在LocalStorage实例中创建testNum属性,并设置指定的初始值为1,即\@LocalStorageLink("testNum") testNum: number = 1。 - -3. 在其子组件中,都使用\@LocalStorageLink或\@LocalStorageProp绑定同一个属性名key来传递数据。 - -LocalStorage可以被认为是\@State变量的Map,属性名作为Map中的key。 - -\@LocalStorageLink和LocalStorage中对应的属性的同步行为,和\@State和\@Link一致,都为双向数据同步。 - -以下为组件的状态更新图: - -![zh-cn_image_0000001588450934](figures/zh-cn_image_0000001588450934.png) - - -```ts -@Component -struct LinkLinkChild { - @LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1; - - testNumChange(propName: string): void { - console.log(`LinkLinkChild: testNum value ${this.testNum}`); - } - - build() { - Text(`LinkLinkChild: ${this.testNum}`) - } -} - -@Component -struct PropLinkChild { - @LocalStorageProp("testNum") @Watch("testNumChange") testNumGrand: number = 1; - - testNumChange(propName: string): void { - console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); - } - - build() { - Text(`PropLinkChild: ${this.testNumGrand}`) - .height(70) - .backgroundColor(Color.Red) - .onClick(() => { - this.testNumGrand += 1; - }) - } -} - -@Component -struct Sibling { - @LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1; - - testNumChange(propName: string): void { - console.log(`Sibling: testNumChange value ${this.testNum}`); - } - - build() { - Text(`Sibling: ${this.testNum}`) - } -} - -@Component -struct LinkChild { - @LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1; - - testNumChange(propName: string): void { - console.log(`LinkChild: testNumChange value ${this.testNum}`); - } - - build() { - Column() { - Button('incr testNum') - .onClick(() => { - console.log(`LinkChild: before value change value ${this.testNum}`); - this.testNum = this.testNum + 1 - console.log(`LinkChild: after value change value ${this.testNum}`); - }) - Text(`LinkChild: ${this.testNum}`) - LinkLinkChild({ /* empty */ }) - PropLinkChild({ /* empty */ }) - } - .height(200).width(200) - } -} - -// create LocalStorage object to hold the data -const storage = new LocalStorage(); -@Entry(storage) -@Component -struct Parent { - @LocalStorageLink("testNum") @Watch("testNumChange1") testNum: number = 1; - - testNumChange1(propName: string): void { - console.log(`Parent: testNumChange value ${this.testNum}`) - } - - build() { - Column() { - LinkChild({ /* empty */ }) - Sibling({ /* empty */ }) - } - } -} -``` - - -### 给AppStorage中对应的属性建立双向或单向同步 - -AppStorage是LocalStorage的单例对象,ArkUI在应用程序启动时创建该对象,在页面中使用\@StorageLink和\@StorageProp为多个页面之间共享数据,具体使用方法和LocalStorage类似。 - -也可以使用PersistentStorage将AppStorage中的特定属性持久化到本地磁盘的文件中,再次启动的时候\@StorageLink和\@StorageProp会恢复上次应用退出的数据。详情请参考[PersistentStorage文档](arkts-persiststorage.md)。 - -示例如下: - - -```ts -@Component -struct LinkLinkChild { - @StorageLink("testNum") @Watch("testNumChange") testNum: number = 1; - - testNumChange(propName: string): void { - console.log(`LinkLinkChild: testNum value ${this.testNum}`); - } - - build() { - Text(`LinkLinkChild: ${this.testNum}`) - } -} - -@Component -struct PropLinkChild { - @StorageProp("testNum") @Watch("testNumChange") testNumGrand: number = 1; - - testNumChange(propName: string): void { - console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); - } - - build() { - Text(`PropLinkChild: ${this.testNumGrand}`) - .height(70) - .backgroundColor(Color.Red) - .onClick(() => { - this.testNumGrand += 1; - }) - } -} - -@Component -struct Sibling { - @StorageLink("testNum") @Watch("testNumChange") testNum: number = 1; - - testNumChange(propName: string): void { - console.log(`Sibling: testNumChange value ${this.testNum}`); - } - - build() { - Text(`Sibling: ${this.testNum}`) - } -} - -@Component -struct LinkChild { - @StorageLink("testNum") @Watch("testNumChange") testNum: number = 1; - - testNumChange(propName: string): void { - console.log(`LinkChild: testNumChange value ${this.testNum}`); - } - - build() { - Column() { - Button('incr testNum') - .onClick(() => { - console.log(`LinkChild: before value change value ${this.testNum}`); - this.testNum = this.testNum + 1 - console.log(`LinkChild: after value change value ${this.testNum}`); - }) - Text(`LinkChild: ${this.testNum}`) - LinkLinkChild({ /* empty */ - }) - PropLinkChild({ /* empty */ - }) - } - .height(200).width(200) - } -} - - -@Entry -@Component -struct Parent { - @StorageLink("testNum") @Watch("testNumChange1") testNum: number = 1; - - testNumChange1(propName: string): void { - console.log(`Parent: testNumChange value ${this.testNum}`) - } - - build() { - Column() { - LinkChild({ /* empty */ - }) - Sibling({ /* empty */ - }) - } - } -} -``` - - -## ViewModel的嵌套场景 - - -大多数情况下,ViewModel数据项都是复杂类型的,例如,对象数组、嵌套对象或者这些类型的组合。对于嵌套场景,可以使用\@Observed搭配\@Prop或者\@ObjectLink来观察变化。 - - -### \@Prop和\@ObjectLink嵌套数据结构 - -推荐设计单独的\@Component来渲染每一个数组或对象。此时,对象数组或嵌套对象(属性是对象的对象称为嵌套对象)需要两个\@Component,一个\@Component呈现外部数组/对象,另一个\@Component呈现嵌套在数组/对象内的类对象。 \@Prop、\@Link、\@ObjectLink修饰的变量只能观察到第一层的变化。 - -- 对于类: - - 可以观察到赋值的变化:this.obj=new ClassObj(...) - - 可以观察到对象属性的更改:this.obj.a=new ClassA(...) - - 不能观察更深层级的属性更改:this.obj.a.b = 47 - -- 对于数组: - - 可以观察到数组的整体赋值:this.arr=[...] - - 可以观察到数据项的删除、插入和替换:this.arr[1] = new ClassA(); this.arr.pop(); this.arr.push(new ClassA(...)))、this.arr.sort(...) - - 不能观察更深层级的数组变化:this.arr[1].b = 47 - -如果要观察嵌套类的内部对象的变化,可以使用\@ObjectLink或\@Prop。优先考虑\@ObjectLink,其通过嵌套对象内部属性的引用初始化自身。\@Prop会对嵌套在内部的对象的深度拷贝来进行初始化,以实现单向同步。在性能上\@Prop的深度拷贝比\@ObjectLink的引用拷贝慢很多。 - -\@ObjectLink或\@Prop可以用来存储嵌套内部的类对象,该类必须用\@Observed类装饰器装饰,否则类的属性改变并不会触发更新UI并不会刷新。\@Observed为其装饰的类实现自定义构造函数,此构造函数创建了一个类的实例,并使用ES6代理包装(由ArkUI框架实现),拦截修饰class属性的所有“get”和“set”。“set”观察属性值,当发生赋值操作时,通知ArkUI框架更新。“get”收集哪些UI组件依赖该状态变量,实现最小化UI更新。 - -如果嵌套场景中,嵌套数据内部是数组或者class时,需根据以下场景使用\@Observed类装饰器。 - -- 如果嵌套数据内部是class,直接被\@Observed装饰。 - -- 如果嵌套数据内部是数组,可以通过以下方式来观察数组变化。 - - ```ts - @Observed class ObservedArray extends Array { - constructor(args: any[]) { - super(...args); - } - /* otherwise empty */ - } - ``` - - ViewModel为外层class。 - - - ```ts - class Outer { - innerArrayProp : ObservedArray; - ... - } - ``` - - -### 嵌套数据结构中\@Prop和\@ObjectLink之的区别 - -以下示例中: - -- 父组件ViewB渲染\@State arrA:Array<ClassA>。\@State可以观察新数组的分配、数组项插入、删除和替换。 - -- 子组件ViewA渲染每一个ClassA的对象。 - -- 类装饰器\@Observed ClassA与\@ObjectLink a: ClassA。 - - - 可以观察嵌套在Array内的ClassA对象的变化。 - - - 不使用\@Observed时: - ViewB中的this.arrA[Math.floor(this.arrA.length/2)].c=10将不会被观察到,相应的ViewA组件也不会更新。 - - 对于数组中的第一个和第二个数组项,每个数组项都初始化了两个ViewA的对象,渲染了同一个ViewA实例。在一个ViewA中的属性赋值this.a.c += 1;时不会引发另外一个使用同一个ClassA初始化的ViewA的渲染更新。 - -![zh-cn_image_0000001588610894](figures/zh-cn_image_0000001588610894.png) - - -```ts -let NextID: number = 1; - -// 类装饰器@Observed装饰ClassA -@Observed -class ClassA { - public id: number; - public c: number; - - constructor(c: number) { - this.id = NextID++; - this.c = c; - } -} - -@Component -struct ViewA { - @ObjectLink a: ClassA; - label: string = "ViewA1"; - - build() { - Row() { - Button(`ViewA [${this.label}] this.a.c= ${this.a.c} +1`) - .onClick(() => { - // 改变对象属性 - this.a.c += 1; - }) - } - } -} - -@Entry -@Component -struct ViewB { - @State arrA: ClassA[] = [new ClassA(0), new ClassA(0)]; - - build() { - Column() { - ForEach(this.arrA, - (item) => { - ViewA({ label: `#${item.id}`, a: item }) - }, - (item) => item.id.toString() - ) - - Divider().height(10) - - if (this.arrA.length) { - ViewA({ label: `ViewA this.arrA[first]`, a: this.arrA[0] }) - ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1] }) - } - - Divider().height(10) - - Button(`ViewB: reset array`) - .onClick(() => { - // 替换整个数组,会被@State this.arrA观察到 - this.arrA = [new ClassA(0), new ClassA(0)]; - }) - Button(`array push`) - .onClick(() => { - // 数组中插入数据,会被@State this.arrA观察到 - this.arrA.push(new ClassA(0)) - }) - Button(`array shift`) - .onClick(() => { - // 数组中移除数据,会被@State this.arrA观察到 - this.arrA.shift() - }) - Button(`ViewB: chg item property in middle`) - .onClick(() => { - // 替换数组中的某个元素,会被@State this.arrA观察到 - this.arrA[Math.floor(this.arrA.length / 2)] = new ClassA(11); - }) - Button(`ViewB: chg item property in middle`) - .onClick(() => { - // 改变数组中某个元素的属性c,会被ViewA中的@ObjectLink观察到 - this.arrA[Math.floor(this.arrA.length / 2)].c = 10; - }) - } - } -} -``` - -在ViewA中,将\@ObjectLink替换为\@Prop。 - - -```ts -@Component -struct ViewA { - - @Prop a: ClassA; - label : string = "ViewA1"; - - build() { - Row() { - Button(`ViewA [${this.label}] this.a.c= ${this.a.c} +1`) - .onClick(() => { - // change object property - this.a.c += 1; - }) - } - } -} -``` - -与用\@ObjectLink修饰不同,用\@ObjectLink修饰时,点击数组的第一个或第二个元素,后面两个ViewA会发生同步的变化。 - -\@Prop是单向数据同步,ViewA内的Button只会触发Button自身的刷新,不会传播到其他的ViewA实例中。在ViewA中的ClassA只是一个副本,并不是其父组件中\@State arrA : Array<ClassA>中的对象,也不是其他ViewA的ClassA,这使得数组的元素和ViewA中的元素表面是传入的同一个对象,实际上在UI上渲染使用的是两个互不相干的对象。 - -需要注意\@Prop和\@ObjectLink还有一个区别:\@ObjectLink装饰的变量是仅可读的,不能被赋值;\@Prop装饰的变量可以被赋值。 - -- \@ObjectLink实现双向同步,因为它是通过数据源的引用初始化的。 - -- \@Prop是单向同步,需要深拷贝数据源。 - -- 对于\@Prop赋值新的对象,就是简单地将本地的值覆写,但是对于实现双向数据同步的\@ObjectLink,覆写新的对象相当于要更新数据源中的数组项或者class的属性,这个对于 TypeScript/JavaScript是不能实现的。 - - -## MVVM应用示例 - - -以下示例深入探讨了嵌套ViewModel的应用程序设计,特别是自定义组件如何渲染一个嵌套的Object,该场景在实际的应用开发中十分常见。 - - -开发一个电话簿应用,实现功能如下: - - -- 显示联系人和本机("Me")电话号码 。 - -- 选中联系人时,进入可编辑态”Edit“,可以更新该联系人详细信息,包括电话号码,住址。 - -- 在更新联系人信息时,只有在单击保存“Save Changes”之后,才会保存更改。 - -- 可以点击删除联系人”Delete Contact“,可以在联系人列表删除该联系人。 - - -ViewModel需要包括: - - -- AddressBook(class) - - me (本机): 存储一个Person类。 - - contacts(本机联系人):存储一个Person类数组。 - - -AddressBook类声明如下: - - - -```ts -export class AddressBook { - me: Person; - contacts: ObservedArray; -​ - constructor(me: Person, contacts: Person[]) { - this.me = me; - this.contacts = new ObservedArray(contacts); - } -} -``` - - -- Person (class) - - name : string - - address : Address - - phones: ObservedArray<string> - - Address (class) - - street : string - - zip : number - - city : string - - -Address类声明如下: - - - -```ts -@Observed -export class Address { - street: string; - zip: number; - city: string; - - constructor(street: string, - zip: number, - city: string) { - this.street = street; - this.zip = zip; - this.city = city; - } -} -``` - - -Person类声明如下: - - - -```ts -@Observed -export class Person { - id_: string; - name: string; - address: Address; - phones: ObservedArray; - - constructor(name: string, - street: string, - zip: number, - city: string, - phones: string[]) { - this.id_ = `${nextId}`; - nextId++; - this.name = name; - this.address = new Address(street, zip, city); - this.phones = new ObservedArray(phones); - } -} -``` - - -需要注意的是,因为phones是嵌套属性,如果要观察到phones的变化,需要extends array,并用\@Observed修饰它。ObservedArray类的声明如下。 - - - -```ts -@Observed -export class ObservedArray extends Array { - constructor(args?: any[]) { - console.log(`ObservedArray: ${JSON.stringify(args)} `) - if (Array.isArray(args)) { - super(...args); - } else { - super(args) - } - } -} -``` - - -- selected : 对Person的引用。 - - -更新流程如下: - - -1. 在根节点PageEntry中初始化所有的数据,将me和contacts和其子组件AddressBookView建立双向数据同步,selectedPerson默认为me,需要注意,selectedPerson并不是PageEntry数据源中的数据,而是数据源中,对某一个Person的引用。 - PageEntry和AddressBookView声明如下: - - - ```ts - @Component - struct AddressBookView { - - @ObjectLink me : Person; - @ObjectLink contacts : ObservedArray; - @State selectedPerson: Person = undefined; - - aboutToAppear() { - this.selectedPerson = this.me; - } - - build() { - Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start}) { - Text("Me:") - PersonView({person: this.me, phones: this.me.phones, selectedPerson: this.$selectedPerson}) - - Divider().height(8) - - ForEach(this.contacts, - contact => { - PersonView({person: contact, phones: contact.phones, selectedPerson: this.$selectedPerson}) - }, - contact => contact.id_ - ) - - Divider().height(8) - - Text("Edit:") - PersonEditView({ selectedPerson: this.$selectedPerson, name: this.selectedPerson.name, address: this.selectedPerson.address, phones: this.selectedPerson.phones }) - } - .borderStyle(BorderStyle.Solid).borderWidth(5).borderColor(0xAFEEEE).borderRadius(5) - } - } - - @Entry - @Component - struct PageEntry { - @Provide addrBook: AddressBook = new AddressBook( - new Person("Gigi", "Itamerenkatu 9", 180, "Helsinki", ["+358441234567", "+35891234567", "+49621234567889"]), - [ - new Person("Oly", "Itamerenkatu 9", 180, "Helsinki", ["+358449876543", "+3589456789"]), - new Person("Sam", "Itamerenkatu 9", 180, "Helsinki", ["+358509876543", "+358910101010"]), - new Person("Vivi", "Itamerenkatu 9", 180, "Helsinki", ["+358400908070", "+35894445555"]), - ]); - - build() { - Column() { - AddressBookView({ me: this.addrBook.me, contacts: this.addrBook.contacts, selectedPerson: this.addrBook.me }) - } - } - } - ``` - -2. PersonView,即电话簿中联系人姓名和首选电话的View,当用户选中,即高亮当前Person,需要同步回其父组件AddressBookView的selectedPerson,所以需要通过\@Link建立双向同步。 - PersonView声明如下: - - - ```ts - // 显示联系人姓名和首选电话 - // 为了更新电话号码,这里需要@ObjectLink person和@ObjectLink phones, - // 显示首选号码不能使用this.person.phones[0],因为@ObjectLink person只代理了Person的属性,数组内部的变化观察不到 - // 触发onClick事件更新selectedPerson - @Component - struct PersonView { - - @ObjectLink person : Person; - @ObjectLink phones : ObservedArray; - - @Link selectedPerson : Person; - - build() { - Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { - Text(this.person.name) - if (this.phones.length) { - Text(this.phones[0]) - } - } - .height(55) - .backgroundColor(this.selectedPerson.name == this.person.name ? "#ffa0a0" : "#ffffff") - .onClick(() => { - this.selectedPerson = this.person; - }) - } - } - ``` - -3. 选中的Person会在PersonEditView中显示详细信息,对于PersonEditView的数据同步分为以下三种方式: - - - 在Edit状态通过Input.onChange回调事件接受用户的键盘输入时,在点击“Save Changes”之前,这个修改是不希望同步会数据源的,但又希望刷新在当前的PersonEditView中,所以\@Prop深拷贝当前Person的详细信息; - - - PersonEditView通过\@Link seletedPerson: Person和AddressBookView的``selectedPerson建立双向同步,当用户点击“Save Changes”的时候,\@Prop的修改将被赋值给\@Link seletedPerson: Person,这就意味这,数据将被同步回数据源。 - - - PersonEditView中通过\@Consume addrBook: AddressBook和根节点PageEntry建立跨组件层级的直接的双向同步关系,当用户在PersonEditView界面删除某一个联系人时,会直接同步回PageEntry,PageEntry的更新会通知AddressBookView刷新contracts的列表页。 PersonEditView声明如下: - - ```ts - // 渲染Person的详细信息 - // @Prop装饰的变量从父组件AddressBookView深拷贝数据,将变化保留在本地, TextInput的变化只会在本地副本上进行修改。 - // 点击 "Save Changes" 会将所有数据的复制通过@Prop到@Link, 同步到其他组件 - @Component - struct PersonEditView { - - @Consume addrBook : AddressBook; - - /* 指向父组件selectedPerson的引用 */ - @Link selectedPerson: Person; - - /*在本地副本上编辑,直到点击保存*/ - @Prop name: string; - @Prop address : Address; - @Prop phones : ObservedArray; - - selectedPersonIndex() : number { - return this.addrBook.contacts.findIndex((person) => person.id_ == this.selectedPerson.id_); - } - - build() { - Column() { - TextInput({ text: this.name}) - .onChange((value) => { - this.name = value; - }) - TextInput({text: this.address.street}) - .onChange((value) => { - this.address.street = value; - }) - - TextInput({text: this.address.city}) - .onChange((value) => { - this.address.city = value; - }) - - TextInput({text: this.address.zip.toString()}) - .onChange((value) => { - const result = parseInt(value); - this.address.zip= isNaN(result) ? 0 : result; - }) - - if(this.phones.length>0) { - ForEach(this.phones, - (phone, index) => { - TextInput({text: phone}) - .width(150) - .onChange((value) => { - console.log(`${index}. ${value} value has changed`) - this.phones[index] = value; - }) - }, - (phone, index) => `${index}-${phone}` - ) - } - - Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { - Text("Save Changes") - .onClick(() => { - // 将本地副本更新的值赋值给指向父组件selectedPerson的引用 - // 避免创建新对象,在现有属性上进行修改 - this.selectedPerson.name = this.name; - this.selectedPerson.address.street = this.address.street - this.selectedPerson.address.city = this.address.city - this.selectedPerson.address.zip = this.address.zip - this.phones.forEach((phone : string, index : number) => { this.selectedPerson.phones[index] = phone } ); - }) - if (this.selectedPersonIndex()!=-1) { - Text("Delete Contact") - .onClick(() => { - let index = this.selectedPersonIndex(); - console.log(`delete contact at index ${index}`); - - // 删除当前联系人 - this.addrBook.contacts.splice(index, 1); - - // 删除当前selectedPerson,选中态前移一位 - index = (index < this.addrBook.contacts.length) ? index : index-1; - - // 如果contract被删除完,则设置me为选中态 - this.selectedPerson = (index>=0) ? this.addrBook.contacts[index] : this.addrBook.me; - }) - } - } - - } - } - } - ``` - - 其中在关于\@ObjectLink和\@Link的区别要注意以下几点: - - 1. 在AddressBookView中实现和父组件PageView的双向同步,需要用\@ObjectLink me : Person和\@ObjectLink contacts : ObservedArray<Person>,而不能用\@Link,原因如下: - - \@Link需要和其数据源类型完全相同,且仅能观察到第一层的变化; - - \@ObjectLink可以被数据源的属性初始化,且代理了\@Observed装饰类的属性,可以观察到被装饰类属性的变化。 - 2. 当 联系人姓名 (Person.name) 或者首选电话号码 (Person.phones[0]) 发生更新时,PersonView也需要同步刷新,其中Person.phones[0]属于第二层的更新,如果使用\@Link将无法观察到,而且\@Link需要和其数据源类型完全相同。所以在PersonView中也需要使用\@ObjectLink,即\@ObjectLink person : Person和\@ObjectLink phones : ObservedArray<string>。 - - ![zh-cn_image_0000001605293914](figures/zh-cn_image_0000001605293914.png) - - 在这个例子中,我们可以大概了解到如何构建ViewModel,在应用的根节点中,ViewModel的数据可能是可以巨大的嵌套数据,但是在ViewModel和View的适配和渲染中,我们尽可能将ViewModel的数据项和View相适配,这样的话在针对每一层的View,都是一个相对“扁平”的数据,仅观察当前层就可以了。 - - 在应用实际开发中,也许我们无法避免去构建一个十分庞大的Model,但是我们可以在UI树状结构中合理地去拆分数据,使得ViewModel和View更好的适配,从而搭配最小化更新来实现高性能开发。 - - 完整应用代码如下: - - - ```ts - // ViewModel classes - let nextId = 0; - - @Observed - export class ObservedArray extends Array { - constructor(args?: any[]) { - console.log(`ObservedArray: ${JSON.stringify(args)} `) - if (Array.isArray(args)) { - super(...args); - } else { - super(args) - } - } - } - - @Observed - export class Address { - street: string; - zip: number; - city: string; - - constructor(street: string, - zip: number, - city: string) { - this.street = street; - this.zip = zip; - this.city = city; - } - } - - @Observed - export class Person { - id_: string; - name: string; - address: Address; - phones: ObservedArray; - - constructor(name: string, - street: string, - zip: number, - city: string, - phones: string[]) { - this.id_ = `${nextId}`; - nextId++; - this.name = name; - this.address = new Address(street, zip, city); - this.phones = new ObservedArray(phones); - } - } - - - export class AddressBook { - me: Person; - contacts: ObservedArray; - - constructor(me: Person, contacts: Person[]) { - this.me = me; - this.contacts = new ObservedArray(contacts); - } - } - - //渲染出Person对象的名称和手机Observed数组中的第一个号码 - //为了更新电话号码,这里需要@ObjectLink person和@ObjectLink phones, - //不能使用this.person.phones,内部数组的更改不会被观察到。 - // 在AddressBookView、PersonEditView中的onClick更新selectedPerson - @Component - struct PersonView { - @ObjectLink person: Person; - @ObjectLink phones: ObservedArray; - @Link selectedPerson: Person; - - build() { - Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { - Text(this.person.name) - if (this.phones.length) { - Text(this.phones[0]) - } - } - .height(55) - .backgroundColor(this.selectedPerson.name == this.person.name ? "#ffa0a0" : "#ffffff") - .onClick(() => { - this.selectedPerson = this.person; - }) - } - } - - // 渲染Person的详细信息 - // @Prop装饰的变量从父组件AddressBookView深拷贝数据,将变化保留在本地, TextInput的变化只会在本地副本上进行修改。 - // 点击 "Save Changes" 会将所有数据的复制通过@Prop到@Link, 同步到其他组件 - @Component - struct PersonEditView { - @Consume addrBook: AddressBook; - - /* 指向父组件selectedPerson的引用 */ - @Link selectedPerson: Person; - - /*在本地副本上编辑,直到点击保存*/ - @Prop name: string; - @Prop address: Address; - @Prop phones: ObservedArray; - - selectedPersonIndex(): number { - return this.addrBook.contacts.findIndex((person) => person.id_ == this.selectedPerson.id_); - } - - build() { - Column() { - TextInput({ text: this.name }) - .onChange((value) => { - this.name = value; - }) - TextInput({ text: this.address.street }) - .onChange((value) => { - this.address.street = value; - }) - - TextInput({ text: this.address.city }) - .onChange((value) => { - this.address.city = value; - }) - - TextInput({ text: this.address.zip.toString() }) - .onChange((value) => { - const result = parseInt(value); - this.address.zip = isNaN(result) ? 0 : result; - }) - - if (this.phones.length > 0) { - ForEach(this.phones, - (phone, index) => { - TextInput({ text: phone }) - .width(150) - .onChange((value) => { - console.log(`${index}. ${value} value has changed`) - this.phones[index] = value; - }) - }, - (phone, index) => `${index}-${phone}` - ) - } - - Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { - Text("Save Changes") - .onClick(() => { - // 将本地副本更新的值赋值给指向父组件selectedPerson的引用 - // 避免创建新对象,在现有属性上进行修改 - this.selectedPerson.name = this.name; - this.selectedPerson.address.street = this.address.street - this.selectedPerson.address.city = this.address.city - this.selectedPerson.address.zip = this.address.zip - this.phones.forEach((phone: string, index: number) => { - this.selectedPerson.phones[index] = phone - }); - }) - if (this.selectedPersonIndex() != -1) { - Text("Delete Contact") - .onClick(() => { - let index = this.selectedPersonIndex(); - console.log(`delete contact at index ${index}`); - - // 删除当前联系人 - this.addrBook.contacts.splice(index, 1); - - // 删除当前selectedPerson,选中态前移一位 - index = (index < this.addrBook.contacts.length) ? index : index - 1; - - // 如果contract被删除完,则设置me为选中态 - this.selectedPerson = (index >= 0) ? this.addrBook.contacts[index] : this.addrBook.me; - }) - } - } - - } - } - } - - @Component - struct AddressBookView { - @ObjectLink me: Person; - @ObjectLink contacts: ObservedArray; - @State selectedPerson: Person = undefined; - - aboutToAppear() { - this.selectedPerson = this.me; - } - - build() { - Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start }) { - Text("Me:") - PersonView({ person: this.me, phones: this.me.phones, selectedPerson: this.$selectedPerson }) - - Divider().height(8) - - ForEach(this.contacts, - contact => { - PersonView({ person: contact, phones: contact.phones, selectedPerson: this.$selectedPerson }) - }, - contact => contact.id_ - ) - - Divider().height(8) - - Text("Edit:") - PersonEditView({ - selectedPerson: this.$selectedPerson, - name: this.selectedPerson.name, - address: this.selectedPerson.address, - phones: this.selectedPerson.phones - }) - } - .borderStyle(BorderStyle.Solid).borderWidth(5).borderColor(0xAFEEEE).borderRadius(5) - } - } - - @Entry - @Component - struct PageEntry { - @Provide addrBook: AddressBook = new AddressBook( - new Person("Gigi", "Itamerenkatu 9", 180, "Helsinki", ["+358441234567", "+35891234567", "+49621234567889"]), - [ - new Person("Oly", "Itamerenkatu 9", 180, "Helsinki", ["+358449876543", "+3589456789"]), - new Person("Sam", "Itamerenkatu 9", 180, "Helsinki", ["+358509876543", "+358910101010"]), - new Person("Vivi", "Itamerenkatu 9", 180, "Helsinki", ["+358400908070", "+35894445555"]), - ]); - - build() { - Column() { - AddressBookView({ me: this.addrBook.me, contacts: this.addrBook.contacts, selectedPerson: this.addrBook.me }) - } - } - } - ``` \ No newline at end of file +# MVVM模式 + + +应用通过状态去渲染更新UI是程序设计中相对复杂,但又十分重要的,往往决定了应用程序的性能。程序的状态数据通常包含了数组、对象,或者是嵌套对象组合而成。在这些情况下,ArkUI采取MVVM = Model + View + ViewModel模式,其中状态管理模块起到的就是ViewModel的作用,将数据与视图绑定在一起,更新数据的时候直接更新视图。 + + +- Model层:存储数据和相关逻辑的模型。它表示组件或其他相关业务逻辑之间传输的数据。Model是对原始数据的进一步处理。 + +- View层:在ArkUI中通常是\@Components修饰组件渲染的UI。 + +- ViewModel层:在ArkUI中,ViewModel是存储在自定义组件的状态变量、LocalStorage和AppStorage中的数据。 + - 自定义组件通过执行其build()方法或者\@Builder装饰的方法来渲染UI,即ViewModel可以渲染View。 + - View可以通过相应event handler来改变ViewModel,即事件驱动ViewModel的改变,另外ViewModel提供了\@Watch回调方法用于监听状态数据的改变。 + - 在ViewModel被改变时,需要同步回Model层,这样才能保证ViewModel和Model的一致性,即应用自身数据的一致性。 + - ViewModel结构设计应始终为了适配自定义组件的构建和更新,这也是将Model和ViewModel分开的原因。 + + +目前很多关于UI构造和更新的问题,都是由于ViewModel的设计并没有很好的支持自定义组件的渲染,或者试图去让自定义组件强行适配Model层,而中间没有用ViewModel来进行分离。例如,一个应用程序直接将SQL数据库中的数据读入内存,这种数据模型不能很好的直接适配自定义组件的渲染,所以在应用程序开发中需要适配ViewModel层。 + + +![zh-cn_image_0000001653986573](figures/zh-cn_image_0000001653986573.png) + + +根据上面涉及SQL数据库的示例,应用程序应设计为: + + +- Model:针对数据库高效操作的数据模型。 + +- ViewModel:针对ArkUI状态管理功能进行高效的UI更新的视图模型。 + +- 部署 converters/adapters: converters/adapters作用于Model和ViewModel的相互转换。 + - converters/adapters可以转换最初从数据库读取的Model,来创建并初始化ViewModel。 + - 在应用的使用场景中,UI会通过event handler改变ViewModel,此时converters/adapters需要将ViewModel的更新数据同步回Model。 + + +虽然与强制将UI拟合到SQL数据库模式(MV模式)相比,MVVM的设计比较复杂,但应用程序开发人员可以通过ViewModel层的隔离,来简化UI的设计和实现,以此来收获更好的UI性能。 + + +## ViewModel的数据源 + + +ViewModel通常包含多个顶层数据源。\@State和\@Provide装饰的变量以及LocalStorage和AppStorage都是顶层数据源,其余装饰器都是与数据源做同步的数据。装饰器的选择取决于状态需要在自定义组件之间的共享范围。共享范围从小到大的排序是: + + +- \@State:组件级别的共享,通过命名参数机制传递,例如:CompA: ({ aProp: this.aProp }),表示传递层级(共享范围)是父子之间的传递。 + +- \@Provide:组件级别的共享,可以通过key和\@Consume绑定,因此不用参数传递,实现多层级的数据共享,共享范围大于\@State。 + +- LocalStorage:页面级别的共享,可以通过\@Entry在当前组件树上共享LocalStorage实例。 + +- AppStorage:应用全局的UI状态存储,和应用进程绑定,在整个应用内的状态数据的共享。 + + +### \@State装饰的变量与一个或多个子组件共享状态数据 + + +\@State可以初始化多种状态变量,\@Prop、\@Link和\@ObjectLink可以和其建立单向或双向同步,详情见[@State使用规范](arkts-state.md)。 + + +1. 使用Parent根节点中\@State装饰的testNum作为ViewModel数据项。将testNum传递给其子组件LinkChild和Sibling。 + + ```ts + // xxx.ets + @Entry + @Component + struct Parent { + @State @Watch("testNumChange1") testNum: number = 1; + + testNumChange1(propName: string): void { + console.log(`Parent: testNumChange value ${this.testNum}`) + } + + build() { + Column() { + LinkChild({ testNum: $testNum }) + Sibling({ testNum: $testNum }) + } + } + } + ``` + +2. LinkChild和Sibling中用\@Link和父组件的数据源建立双向同步。其中LinkChild中创建了LinkLinkChild和PropLinkChild。 + + ```ts + @Component + struct Sibling { + @Link @Watch("testNumChange") testNum: number; + + testNumChange(propName: string): void { + console.log(`Sibling: testNumChange value ${this.testNum}`); + } + + build() { + Text(`Sibling: ${this.testNum}`) + } + } + + @Component + struct LinkChild { + @Link @Watch("testNumChange") testNum: number; + + testNumChange(propName: string): void { + console.log(`LinkChild: testNumChange value ${this.testNum}`); + } + + build() { + Column() { + Button('incr testNum') + .onClick(() => { + console.log(`LinkChild: before value change value ${this.testNum}`); + this.testNum = this.testNum + 1 + console.log(`LinkChild: after value change value ${this.testNum}`); + }) + Text(`LinkChild: ${this.testNum}`) + LinkLinkChild({ testNumGrand: $testNum }) + PropLinkChild({ testNumGrand: this.testNum }) + } + .height(200).width(200) + } + } + ``` + +3. LinkLinkChild和PropLinkChild声明如下,PropLinkChild中的\@Prop和其父组件建立单向同步关系。 + + ```ts + @Component + struct LinkLinkChild { + @Link @Watch("testNumChange") testNumGrand: number; + + testNumChange(propName: string): void { + console.log(`LinkLinkChild: testNumGrand value ${this.testNumGrand}`); + } + + build() { + Text(`LinkLinkChild: ${this.testNumGrand}`) + } + } + + + @Component + struct PropLinkChild { + @Prop @Watch("testNumChange") testNumGrand: number; + + testNumChange(propName: string): void { + console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); + } + + build() { + Text(`PropLinkChild: ${this.testNumGrand}`) + .height(70) + .backgroundColor(Color.Red) + .onClick(() => { + this.testNumGrand += 1; + }) + } + } + ``` + + ![zh-cn_image_0000001638250945](figures/zh-cn_image_0000001638250945.png) + + 当LinkChild中的\@Link testNum更改时。 + + 1. 更改首先同步到其父组件Parent,然后更改从Parent同步到Siling。 + + 2. LinkChild中的\@Link testNum更改也同步给子组件LinkLinkChild和PropLinkChild。 + + \@State装饰器与\@Provide、LocalStorage、AppStorage的区别: + + - \@State如果想要将更改传递给孙子节点,需要先将更改传递给子组件,再从子节点传递给孙子节点。 + - 共享只能通过构造函数的参数传递,即命名参数机制CompA: ({ aProp: this.aProp })。 + + 完整的代码示例如下: + + + ```ts + @Component + struct LinkLinkChild { + @Link @Watch("testNumChange") testNumGrand: number; + + testNumChange(propName: string): void { + console.log(`LinkLinkChild: testNumGrand value ${this.testNumGrand}`); + } + + build() { + Text(`LinkLinkChild: ${this.testNumGrand}`) + } + } + + + @Component + struct PropLinkChild { + @Prop @Watch("testNumChange") testNumGrand: number; + + testNumChange(propName: string): void { + console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); + } + + build() { + Text(`PropLinkChild: ${this.testNumGrand}`) + .height(70) + .backgroundColor(Color.Red) + .onClick(() => { + this.testNumGrand += 1; + }) + } + } + + + @Component + struct Sibling { + @Link @Watch("testNumChange") testNum: number; + + testNumChange(propName: string): void { + console.log(`Sibling: testNumChange value ${this.testNum}`); + } + + build() { + Text(`Sibling: ${this.testNum}`) + } + } + + @Component + struct LinkChild { + @Link @Watch("testNumChange") testNum: number; + + testNumChange(propName: string): void { + console.log(`LinkChild: testNumChange value ${this.testNum}`); + } + + build() { + Column() { + Button('incr testNum') + .onClick(() => { + console.log(`LinkChild: before value change value ${this.testNum}`); + this.testNum = this.testNum + 1 + console.log(`LinkChild: after value change value ${this.testNum}`); + }) + Text(`LinkChild: ${this.testNum}`) + LinkLinkChild({ testNumGrand: $testNum }) + PropLinkChild({ testNumGrand: this.testNum }) + } + .height(200).width(200) + } + } + + + @Entry + @Component + struct Parent { + @State @Watch("testNumChange1") testNum: number = 1; + + testNumChange1(propName: string): void { + console.log(`Parent: testNumChange value ${this.testNum}`) + } + + build() { + Column() { + LinkChild({ testNum: $testNum }) + Sibling({ testNum: $testNum }) + } + } + } + ``` + + +### \@Provide装饰的变量与任何后代组件共享状态数据 + +\@Provide装饰的变量可以与任何后代组件共享状态数据,其后代组件使用\@Consume创建双向同步,详情见[@Provide和@Consume](arkts-provide-and-consume.md)。 + +因此,\@Provide-\@Consume模式比使用\@State-\@Link-\@Link从父组件将更改传递到孙子组件更方便。\@Provide-\@Consume适合在单个页面UI组件树中共享状态数据。 + +使用\@Provide-\@Consume模式时,\@Consume和其祖先组件中的\@Provide通过绑定相同的key连接,而不是在组件的构造函数中通过参数来进行传递。 + +以下示例通过\@Provide-\@Consume模式,将更改从父组件传递到孙子组件。 + + +```ts +@Component +struct LinkLinkChild { + @Consume @Watch("testNumChange") testNum: number; + + testNumChange(propName: string): void { + console.log(`LinkLinkChild: testNum value ${this.testNum}`); + } + + build() { + Text(`LinkLinkChild: ${this.testNum}`) + } +} + +@Component +struct PropLinkChild { + @Prop @Watch("testNumChange") testNumGrand: number; + + testNumChange(propName: string): void { + console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); + } + + build() { + Text(`PropLinkChild: ${this.testNumGrand}`) + .height(70) + .backgroundColor(Color.Red) + .onClick(() => { + this.testNumGrand += 1; + }) + } +} + +@Component +struct Sibling { + @Consume @Watch("testNumChange") testNum: number; + + testNumChange(propName: string): void { + console.log(`Sibling: testNumChange value ${this.testNum}`); + } + + build() { + Text(`Sibling: ${this.testNum}`) + } +} + +@Component +struct LinkChild { + @Consume @Watch("testNumChange") testNum: number; + + testNumChange(propName: string): void { + console.log(`LinkChild: testNumChange value ${this.testNum}`); + } + + build() { + Column() { + Button('incr testNum') + .onClick(() => { + console.log(`LinkChild: before value change value ${this.testNum}`); + this.testNum = this.testNum + 1 + console.log(`LinkChild: after value change value ${this.testNum}`); + }) + Text(`LinkChild: ${this.testNum}`) + LinkLinkChild({ /* empty */ }) + PropLinkChild({ testNumGrand: this.testNum }) + } + .height(200).width(200) + } +} + +@Entry +@Component +struct Parent { + @Provide @Watch("testNumChange1") testNum: number = 1; + + testNumChange1(propName: string): void { + console.log(`Parent: testNumChange value ${this.testNum}`) + } + + build() { + Column() { + LinkChild({ /* empty */ }) + Sibling({ /* empty */ }) + } + } +} +``` + + +### 给LocalStorage实例中对应的属性建立双向或单向同步 + +通过\@LocalStorageLink和\@LocalStorageProp,给LocalStorage实例中的属性建立双向或单向同步。可以将LocalStorage实例视为\@State变量的Map,使用详情参考LocalStorage。 + +LocalStorage对象可以在ArkUI应用程序的几个页面上共享。因此,使用\@LocalStorageLink、\@LocalStorageProp和LocalStorage可以在应用程序的多个页面上共享状态。 + +以下示例中: + +1. 创建一个LocalStorage实例,并通过\@Entry(storage)将其注入根节点。 + +2. 在Parent组件中初始化\@LocalStorageLink("testNum")变量时,将在LocalStorage实例中创建testNum属性,并设置指定的初始值为1,即\@LocalStorageLink("testNum") testNum: number = 1。 + +3. 在其子组件中,都使用\@LocalStorageLink或\@LocalStorageProp绑定同一个属性名key来传递数据。 + +LocalStorage可以被认为是\@State变量的Map,属性名作为Map中的key。 + +\@LocalStorageLink和LocalStorage中对应的属性的同步行为,和\@State和\@Link一致,都为双向数据同步。 + +以下为组件的状态更新图: + +![zh-cn_image_0000001588450934](figures/zh-cn_image_0000001588450934.png) + + +```ts +@Component +struct LinkLinkChild { + @LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1; + + testNumChange(propName: string): void { + console.log(`LinkLinkChild: testNum value ${this.testNum}`); + } + + build() { + Text(`LinkLinkChild: ${this.testNum}`) + } +} + +@Component +struct PropLinkChild { + @LocalStorageProp("testNum") @Watch("testNumChange") testNumGrand: number = 1; + + testNumChange(propName: string): void { + console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); + } + + build() { + Text(`PropLinkChild: ${this.testNumGrand}`) + .height(70) + .backgroundColor(Color.Red) + .onClick(() => { + this.testNumGrand += 1; + }) + } +} + +@Component +struct Sibling { + @LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1; + + testNumChange(propName: string): void { + console.log(`Sibling: testNumChange value ${this.testNum}`); + } + + build() { + Text(`Sibling: ${this.testNum}`) + } +} + +@Component +struct LinkChild { + @LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1; + + testNumChange(propName: string): void { + console.log(`LinkChild: testNumChange value ${this.testNum}`); + } + + build() { + Column() { + Button('incr testNum') + .onClick(() => { + console.log(`LinkChild: before value change value ${this.testNum}`); + this.testNum = this.testNum + 1 + console.log(`LinkChild: after value change value ${this.testNum}`); + }) + Text(`LinkChild: ${this.testNum}`) + LinkLinkChild({ /* empty */ }) + PropLinkChild({ /* empty */ }) + } + .height(200).width(200) + } +} + +// create LocalStorage object to hold the data +const storage = new LocalStorage(); +@Entry(storage) +@Component +struct Parent { + @LocalStorageLink("testNum") @Watch("testNumChange1") testNum: number = 1; + + testNumChange1(propName: string): void { + console.log(`Parent: testNumChange value ${this.testNum}`) + } + + build() { + Column() { + LinkChild({ /* empty */ }) + Sibling({ /* empty */ }) + } + } +} +``` + + +### 给AppStorage中对应的属性建立双向或单向同步 + +AppStorage是LocalStorage的单例对象,ArkUI在应用程序启动时创建该对象,在页面中使用\@StorageLink和\@StorageProp为多个页面之间共享数据,具体使用方法和LocalStorage类似。 + +也可以使用PersistentStorage将AppStorage中的特定属性持久化到本地磁盘的文件中,再次启动的时候\@StorageLink和\@StorageProp会恢复上次应用退出的数据。详情请参考[PersistentStorage文档](arkts-persiststorage.md)。 + +示例如下: + + +```ts +@Component +struct LinkLinkChild { + @StorageLink("testNum") @Watch("testNumChange") testNum: number = 1; + + testNumChange(propName: string): void { + console.log(`LinkLinkChild: testNum value ${this.testNum}`); + } + + build() { + Text(`LinkLinkChild: ${this.testNum}`) + } +} + +@Component +struct PropLinkChild { + @StorageProp("testNum") @Watch("testNumChange") testNumGrand: number = 1; + + testNumChange(propName: string): void { + console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); + } + + build() { + Text(`PropLinkChild: ${this.testNumGrand}`) + .height(70) + .backgroundColor(Color.Red) + .onClick(() => { + this.testNumGrand += 1; + }) + } +} + +@Component +struct Sibling { + @StorageLink("testNum") @Watch("testNumChange") testNum: number = 1; + + testNumChange(propName: string): void { + console.log(`Sibling: testNumChange value ${this.testNum}`); + } + + build() { + Text(`Sibling: ${this.testNum}`) + } +} + +@Component +struct LinkChild { + @StorageLink("testNum") @Watch("testNumChange") testNum: number = 1; + + testNumChange(propName: string): void { + console.log(`LinkChild: testNumChange value ${this.testNum}`); + } + + build() { + Column() { + Button('incr testNum') + .onClick(() => { + console.log(`LinkChild: before value change value ${this.testNum}`); + this.testNum = this.testNum + 1 + console.log(`LinkChild: after value change value ${this.testNum}`); + }) + Text(`LinkChild: ${this.testNum}`) + LinkLinkChild({ /* empty */ + }) + PropLinkChild({ /* empty */ + }) + } + .height(200).width(200) + } +} + + +@Entry +@Component +struct Parent { + @StorageLink("testNum") @Watch("testNumChange1") testNum: number = 1; + + testNumChange1(propName: string): void { + console.log(`Parent: testNumChange value ${this.testNum}`) + } + + build() { + Column() { + LinkChild({ /* empty */ + }) + Sibling({ /* empty */ + }) + } + } +} +``` + + +## ViewModel的嵌套场景 + + +大多数情况下,ViewModel数据项都是复杂类型的,例如,对象数组、嵌套对象或者这些类型的组合。对于嵌套场景,可以使用\@Observed搭配\@Prop或者\@ObjectLink来观察变化。 + + +### \@Prop和\@ObjectLink嵌套数据结构 + +推荐设计单独的\@Component来渲染每一个数组或对象。此时,对象数组或嵌套对象(属性是对象的对象称为嵌套对象)需要两个\@Component,一个\@Component呈现外部数组/对象,另一个\@Component呈现嵌套在数组/对象内的类对象。 \@Prop、\@Link、\@ObjectLink修饰的变量只能观察到第一层的变化。 + +- 对于类: + - 可以观察到赋值的变化:this.obj=new ClassObj(...) + - 可以观察到对象属性的更改:this.obj.a=new ClassA(...) + - 不能观察更深层级的属性更改:this.obj.a.b = 47 + +- 对于数组: + - 可以观察到数组的整体赋值:this.arr=[...] + - 可以观察到数据项的删除、插入和替换:this.arr[1] = new ClassA(); this.arr.pop(); this.arr.push(new ClassA(...)))、this.arr.sort(...) + - 不能观察更深层级的数组变化:this.arr[1].b = 47 + +如果要观察嵌套类的内部对象的变化,可以使用\@ObjectLink或\@Prop。优先考虑\@ObjectLink,其通过嵌套对象内部属性的引用初始化自身。\@Prop会对嵌套在内部的对象的深度拷贝来进行初始化,以实现单向同步。在性能上\@Prop的深度拷贝比\@ObjectLink的引用拷贝慢很多。 + +\@ObjectLink或\@Prop可以用来存储嵌套内部的类对象,该类必须用\@Observed类装饰器装饰,否则类的属性改变并不会触发更新UI并不会刷新。\@Observed为其装饰的类实现自定义构造函数,此构造函数创建了一个类的实例,并使用ES6代理包装(由ArkUI框架实现),拦截修饰class属性的所有“get”和“set”。“set”观察属性值,当发生赋值操作时,通知ArkUI框架更新。“get”收集哪些UI组件依赖该状态变量,实现最小化UI更新。 + +如果嵌套场景中,嵌套数据内部是数组或者class时,需根据以下场景使用\@Observed类装饰器。 + +- 如果嵌套数据内部是class,直接被\@Observed装饰。 + +- 如果嵌套数据内部是数组,可以通过以下方式来观察数组变化。 + + ```ts + @Observed class ObservedArray extends Array { + constructor(args: any[]) { + super(...args); + } + /* otherwise empty */ + } + ``` + + ViewModel为外层class。 + + + ```ts + class Outer { + innerArrayProp : ObservedArray; + ... + } + ``` + + +### 嵌套数据结构中\@Prop和\@ObjectLink之的区别 + +以下示例中: + +- 父组件ViewB渲染\@State arrA:Array<ClassA>。\@State可以观察新数组的分配、数组项插入、删除和替换。 + +- 子组件ViewA渲染每一个ClassA的对象。 + +- 类装饰器\@Observed ClassA与\@ObjectLink a: ClassA。 + + - 可以观察嵌套在Array内的ClassA对象的变化。 + + - 不使用\@Observed时: + ViewB中的this.arrA[Math.floor(this.arrA.length/2)].c=10将不会被观察到,相应的ViewA组件也不会更新。 + + 对于数组中的第一个和第二个数组项,每个数组项都初始化了两个ViewA的对象,渲染了同一个ViewA实例。在一个ViewA中的属性赋值this.a.c += 1;时不会引发另外一个使用同一个ClassA初始化的ViewA的渲染更新。 + +![zh-cn_image_0000001588610894](figures/zh-cn_image_0000001588610894.png) + + +```ts +let NextID: number = 1; + +// 类装饰器@Observed装饰ClassA +@Observed +class ClassA { + public id: number; + public c: number; + + constructor(c: number) { + this.id = NextID++; + this.c = c; + } +} + +@Component +struct ViewA { + @ObjectLink a: ClassA; + label: string = "ViewA1"; + + build() { + Row() { + Button(`ViewA [${this.label}] this.a.c= ${this.a.c} +1`) + .onClick(() => { + // 改变对象属性 + this.a.c += 1; + }) + } + } +} + +@Entry +@Component +struct ViewB { + @State arrA: ClassA[] = [new ClassA(0), new ClassA(0)]; + + build() { + Column() { + ForEach(this.arrA, + (item) => { + ViewA({ label: `#${item.id}`, a: item }) + }, + (item) => item.id.toString() + ) + + Divider().height(10) + + if (this.arrA.length) { + ViewA({ label: `ViewA this.arrA[first]`, a: this.arrA[0] }) + ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1] }) + } + + Divider().height(10) + + Button(`ViewB: reset array`) + .onClick(() => { + // 替换整个数组,会被@State this.arrA观察到 + this.arrA = [new ClassA(0), new ClassA(0)]; + }) + Button(`array push`) + .onClick(() => { + // 数组中插入数据,会被@State this.arrA观察到 + this.arrA.push(new ClassA(0)) + }) + Button(`array shift`) + .onClick(() => { + // 数组中移除数据,会被@State this.arrA观察到 + this.arrA.shift() + }) + Button(`ViewB: chg item property in middle`) + .onClick(() => { + // 替换数组中的某个元素,会被@State this.arrA观察到 + this.arrA[Math.floor(this.arrA.length / 2)] = new ClassA(11); + }) + Button(`ViewB: chg item property in middle`) + .onClick(() => { + // 改变数组中某个元素的属性c,会被ViewA中的@ObjectLink观察到 + this.arrA[Math.floor(this.arrA.length / 2)].c = 10; + }) + } + } +} +``` + +在ViewA中,将\@ObjectLink替换为\@Prop。 + + +```ts +@Component +struct ViewA { + + @Prop a: ClassA; + label : string = "ViewA1"; + + build() { + Row() { + Button(`ViewA [${this.label}] this.a.c= ${this.a.c} +1`) + .onClick(() => { + // change object property + this.a.c += 1; + }) + } + } +} +``` + +与用\@ObjectLink修饰不同,用\@ObjectLink修饰时,点击数组的第一个或第二个元素,后面两个ViewA会发生同步的变化。 + +\@Prop是单向数据同步,ViewA内的Button只会触发Button自身的刷新,不会传播到其他的ViewA实例中。在ViewA中的ClassA只是一个副本,并不是其父组件中\@State arrA : Array<ClassA>中的对象,也不是其他ViewA的ClassA,这使得数组的元素和ViewA中的元素表面是传入的同一个对象,实际上在UI上渲染使用的是两个互不相干的对象。 + +需要注意\@Prop和\@ObjectLink还有一个区别:\@ObjectLink装饰的变量是仅可读的,不能被赋值;\@Prop装饰的变量可以被赋值。 + +- \@ObjectLink实现双向同步,因为它是通过数据源的引用初始化的。 + +- \@Prop是单向同步,需要深拷贝数据源。 + +- 对于\@Prop赋值新的对象,就是简单地将本地的值覆写,但是对于实现双向数据同步的\@ObjectLink,覆写新的对象相当于要更新数据源中的数组项或者class的属性,这个对于 TypeScript/JavaScript是不能实现的。 + + +## MVVM应用示例 + + +以下示例深入探讨了嵌套ViewModel的应用程序设计,特别是自定义组件如何渲染一个嵌套的Object,该场景在实际的应用开发中十分常见。 + + +开发一个电话簿应用,实现功能如下: + + +- 显示联系人和设备("Me")电话号码 。 + +- 选中联系人时,进入可编辑态”Edit“,可以更新该联系人详细信息,包括电话号码,住址。 + +- 在更新联系人信息时,只有在单击保存“Save Changes”之后,才会保存更改。 + +- 可以点击删除联系人”Delete Contact“,可以在联系人列表删除该联系人。 + + +ViewModel需要包括: + + +- AddressBook(class) + - me (设备): 存储一个Person类。 + - contacts(设备联系人):存储一个Person类数组。 + + +AddressBook类声明如下: + + + +```ts +export class AddressBook { + me: Person; + contacts: ObservedArray; + + constructor(me: Person, contacts: Person[]) { + this.me = me; + this.contacts = new ObservedArray(contacts); + } +} +``` + + +- Person (class) + - name : string + - address : Address + - phones: ObservedArray<string> + - Address (class) + - street : string + - zip : number + - city : string + + +Address类声明如下: + + + +```ts +@Observed +export class Address { + street: string; + zip: number; + city: string; + + constructor(street: string, + zip: number, + city: string) { + this.street = street; + this.zip = zip; + this.city = city; + } +} +``` + + +Person类声明如下: + + + +```ts +@Observed +export class Person { + id_: string; + name: string; + address: Address; + phones: ObservedArray; + + constructor(name: string, + street: string, + zip: number, + city: string, + phones: string[]) { + this.id_ = `${nextId}`; + nextId++; + this.name = name; + this.address = new Address(street, zip, city); + this.phones = new ObservedArray(phones); + } +} +``` + + +需要注意的是,因为phones是嵌套属性,如果要观察到phones的变化,需要extends array,并用\@Observed修饰它。ObservedArray类的声明如下。 + + + +```ts +@Observed +export class ObservedArray extends Array { + constructor(args?: any[]) { + console.log(`ObservedArray: ${JSON.stringify(args)} `) + if (Array.isArray(args)) { + super(...args); + } else { + super(args) + } + } +} +``` + + +- selected : 对Person的引用。 + + +更新流程如下: + + +1. 在根节点PageEntry中初始化所有的数据,将me和contacts和其子组件AddressBookView建立双向数据同步,selectedPerson默认为me,需要注意,selectedPerson并不是PageEntry数据源中的数据,而是数据源中,对某一个Person的引用。 + PageEntry和AddressBookView声明如下: + + + ```ts + @Component + struct AddressBookView { + + @ObjectLink me : Person; + @ObjectLink contacts : ObservedArray; + @State selectedPerson: Person = undefined; + + aboutToAppear() { + this.selectedPerson = this.me; + } + + build() { + Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start}) { + Text("Me:") + PersonView({person: this.me, phones: this.me.phones, selectedPerson: this.$selectedPerson}) + + Divider().height(8) + + ForEach(this.contacts, + contact => { + PersonView({person: contact, phones: contact.phones, selectedPerson: this.$selectedPerson}) + }, + contact => contact.id_ + ) + + Divider().height(8) + + Text("Edit:") + PersonEditView({ selectedPerson: this.$selectedPerson, name: this.selectedPerson.name, address: this.selectedPerson.address, phones: this.selectedPerson.phones }) + } + .borderStyle(BorderStyle.Solid).borderWidth(5).borderColor(0xAFEEEE).borderRadius(5) + } + } + + @Entry + @Component + struct PageEntry { + @Provide addrBook: AddressBook = new AddressBook( + new Person("Gigi", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********", "18*********"]), + [ + new Person("Oly", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]), + new Person("Sam", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]), + new Person("Vivi", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]), + ]); + + build() { + Column() { + AddressBookView({ me: this.addrBook.me, contacts: this.addrBook.contacts, selectedPerson: this.addrBook.me }) + } + } + } + ``` + +2. PersonView,即电话簿中联系人姓名和首选电话的View,当用户选中,即高亮当前Person,需要同步回其父组件AddressBookView的selectedPerson,所以需要通过\@Link建立双向同步。 + PersonView声明如下: + + + ```ts + // 显示联系人姓名和首选电话 + // 为了更新电话号码,这里需要@ObjectLink person和@ObjectLink phones, + // 显示首选号码不能使用this.person.phones[0],因为@ObjectLink person只代理了Person的属性,数组内部的变化观察不到 + // 触发onClick事件更新selectedPerson + @Component + struct PersonView { + + @ObjectLink person : Person; + @ObjectLink phones : ObservedArray; + + @Link selectedPerson : Person; + + build() { + Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { + Text(this.person.name) + if (this.phones.length) { + Text(this.phones[0]) + } + } + .height(55) + .backgroundColor(this.selectedPerson.name == this.person.name ? "#ffa0a0" : "#ffffff") + .onClick(() => { + this.selectedPerson = this.person; + }) + } + } + ``` + +3. 选中的Person会在PersonEditView中显示详细信息,对于PersonEditView的数据同步分为以下三种方式: + + - 在Edit状态通过Input.onChange回调事件接受用户的键盘输入时,在点击“Save Changes”之前,这个修改是不希望同步会数据源的,但又希望刷新在当前的PersonEditView中,所以\@Prop深拷贝当前Person的详细信息; + + - PersonEditView通过\@Link seletedPerson: Person和AddressBookView的``selectedPerson建立双向同步,当用户点击“Save Changes”的时候,\@Prop的修改将被赋值给\@Link seletedPerson: Person,这就意味这,数据将被同步回数据源。 + + - PersonEditView中通过\@Consume addrBook: AddressBook和根节点PageEntry建立跨组件层级的直接的双向同步关系,当用户在PersonEditView界面删除某一个联系人时,会直接同步回PageEntry,PageEntry的更新会通知AddressBookView刷新contracts的列表页。 PersonEditView声明如下: + + ```ts + // 渲染Person的详细信息 + // @Prop装饰的变量从父组件AddressBookView深拷贝数据,将变化保留在本地, TextInput的变化只会在本地副本上进行修改。 + // 点击 "Save Changes" 会将所有数据的复制通过@Prop到@Link, 同步到其他组件 + @Component + struct PersonEditView { + + @Consume addrBook : AddressBook; + + /* 指向父组件selectedPerson的引用 */ + @Link selectedPerson: Person; + + /*在本地副本上编辑,直到点击保存*/ + @Prop name: string; + @Prop address : Address; + @Prop phones : ObservedArray; + + selectedPersonIndex() : number { + return this.addrBook.contacts.findIndex((person) => person.id_ == this.selectedPerson.id_); + } + + build() { + Column() { + TextInput({ text: this.name}) + .onChange((value) => { + this.name = value; + }) + TextInput({text: this.address.street}) + .onChange((value) => { + this.address.street = value; + }) + + TextInput({text: this.address.city}) + .onChange((value) => { + this.address.city = value; + }) + + TextInput({text: this.address.zip.toString()}) + .onChange((value) => { + const result = parseInt(value); + this.address.zip= isNaN(result) ? 0 : result; + }) + + if(this.phones.length>0) { + ForEach(this.phones, + (phone, index) => { + TextInput({text: phone}) + .width(150) + .onChange((value) => { + console.log(`${index}. ${value} value has changed`) + this.phones[index] = value; + }) + }, + (phone, index) => `${index}-${phone}` + ) + } + + Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { + Text("Save Changes") + .onClick(() => { + // 将本地副本更新的值赋值给指向父组件selectedPerson的引用 + // 避免创建新对象,在现有属性上进行修改 + this.selectedPerson.name = this.name; + this.selectedPerson.address.street = this.address.street + this.selectedPerson.address.city = this.address.city + this.selectedPerson.address.zip = this.address.zip + this.phones.forEach((phone : string, index : number) => { this.selectedPerson.phones[index] = phone } ); + }) + if (this.selectedPersonIndex()!=-1) { + Text("Delete Contact") + .onClick(() => { + let index = this.selectedPersonIndex(); + console.log(`delete contact at index ${index}`); + + // 删除当前联系人 + this.addrBook.contacts.splice(index, 1); + + // 删除当前selectedPerson,选中态前移一位 + index = (index < this.addrBook.contacts.length) ? index : index-1; + + // 如果contract被删除完,则设置me为选中态 + this.selectedPerson = (index>=0) ? this.addrBook.contacts[index] : this.addrBook.me; + }) + } + } + + } + } + } + ``` + + 其中在关于\@ObjectLink和\@Link的区别要注意以下几点: + + 1. 在AddressBookView中实现和父组件PageView的双向同步,需要用\@ObjectLink me : Person和\@ObjectLink contacts : ObservedArray<Person>,而不能用\@Link,原因如下: + - \@Link需要和其数据源类型完全相同,且仅能观察到第一层的变化; + - \@ObjectLink可以被数据源的属性初始化,且代理了\@Observed装饰类的属性,可以观察到被装饰类属性的变化。 + 2. 当 联系人姓名 (Person.name) 或者首选电话号码 (Person.phones[0]) 发生更新时,PersonView也需要同步刷新,其中Person.phones[0]属于第二层的更新,如果使用\@Link将无法观察到,而且\@Link需要和其数据源类型完全相同。所以在PersonView中也需要使用\@ObjectLink,即\@ObjectLink person : Person和\@ObjectLink phones : ObservedArray<string>。 + + ![zh-cn_image_0000001605293914](figures/zh-cn_image_0000001605293914.png) + + 在这个例子中,我们可以大概了解到如何构建ViewModel,在应用的根节点中,ViewModel的数据可能是可以巨大的嵌套数据,但是在ViewModel和View的适配和渲染中,我们尽可能将ViewModel的数据项和View相适配,这样的话在针对每一层的View,都是一个相对“扁平”的数据,仅观察当前层就可以了。 + + 在应用实际开发中,也许我们无法避免去构建一个十分庞大的Model,但是我们可以在UI树状结构中合理地去拆分数据,使得ViewModel和View更好的适配,从而搭配最小化更新来实现高性能开发。 + + 完整应用代码如下: + + +```ts + + // ViewModel classes + let nextId = 0; + + @Observed + export class ObservedArray extends Array { + constructor(args?: any[]) { + console.log(`ObservedArray: ${JSON.stringify(args)} `) + if (Array.isArray(args)) { + super(...args); + } else { + super(args) + } + } + } + + @Observed + export class Address { + street: string; + zip: number; + city: string; + + constructor(street: string, + zip: number, + city: string) { + this.street = street; + this.zip = zip; + this.city = city; + } + } + + @Observed + export class Person { + id_: string; + name: string; + address: Address; + phones: ObservedArray; + + constructor(name: string, + street: string, + zip: number, + city: string, + phones: string[]) { + this.id_ = `${nextId}`; + nextId++; + this.name = name; + this.address = new Address(street, zip, city); + this.phones = new ObservedArray(phones); + } + } + + export class AddressBook { + me: Person; + contacts: ObservedArray; + + constructor(me: Person, contacts: Person[]) { + this.me = me; + this.contacts = new ObservedArray(contacts); + } + } + + //渲染出Person对象的名称和Observed数组中的第一个号码 + //为了更新电话号码,这里需要@ObjectLink person和@ObjectLink phones, + //不能使用this.person.phones,内部数组的更改不会被观察到。 + // 在AddressBookView、PersonEditView中的onClick更新selectedPerson + @Component + struct PersonView { + @ObjectLink person: Person; + @ObjectLink phones: ObservedArray; + @Link selectedPerson: Person; + + build() { + Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { + Text(this.person.name) + if (this.phones.length) { + Text(this.phones[0]) + } + } + .height(55) + .backgroundColor(this.selectedPerson.name == this.person.name ? "#ffa0a0" : "#ffffff") + .onClick(() => { + this.selectedPerson = this.person; + }) + } + } + + // 渲染Person的详细信息 + // @Prop装饰的变量从父组件AddressBookView深拷贝数据,将变化保留在本地, TextInput的变化只会在本地副本上进行修改。 + // 点击 "Save Changes" 会将所有数据的复制通过@Prop到@Link, 同步到其他组件 + @Component + struct PersonEditView { + @Consume addrBook: AddressBook; + + /* 指向父组件selectedPerson的引用 */ + @Link selectedPerson: Person; + + /*在本地副本上编辑,直到点击保存*/ + @Prop name: string; + @Prop address: Address; + @Prop phones: ObservedArray; + + selectedPersonIndex(): number { + return this.addrBook.contacts.findIndex((person) => person.id_ == this.selectedPerson.id_); + } + + build() { + Column() { + TextInput({ text: this.name }) + .onChange((value) => { + this.name = value; + }) + TextInput({ text: this.address.street }) + .onChange((value) => { + this.address.street = value; + }) + + TextInput({ text: this.address.city }) + .onChange((value) => { + this.address.city = value; + }) + + TextInput({ text: this.address.zip.toString() }) + .onChange((value) => { + const result = parseInt(value); + this.address.zip = isNaN(result) ? 0 : result; + }) + + if (this.phones.length > 0) { + ForEach(this.phones, + (phone, index) => { + TextInput({ text: phone }) + .width(150) + .onChange((value) => { + console.log(`${index}. ${value} value has changed`) + this.phones[index] = value; + }) + }, + (phone, index) => `${index}-${phone}` + ) + } + + Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { + Text("Save Changes") + .onClick(() => { + // 将本地副本更新的值赋值给指向父组件selectedPerson的引用 + // 避免创建新对象,在现有属性上进行修改 + this.selectedPerson.name = this.name; + this.selectedPerson.address.street = this.address.street + this.selectedPerson.address.city = this.address.city + this.selectedPerson.address.zip = this.address.zip + this.phones.forEach((phone: string, index: number) => { + this.selectedPerson.phones[index] = phone + }); + }) + if (this.selectedPersonIndex() != -1) { + Text("Delete Contact") + .onClick(() => { + let index = this.selectedPersonIndex(); + console.log(`delete contact at index ${index}`); + + // 删除当前联系人 + this.addrBook.contacts.splice(index, 1); + + // 删除当前selectedPerson,选中态前移一位 + index = (index < this.addrBook.contacts.length) ? index : index - 1; + + // 如果contract被删除完,则设置me为选中态 + this.selectedPerson = (index >= 0) ? this.addrBook.contacts[index] : this.addrBook.me; + }) + } + } + + } + } + } + + @Component + struct AddressBookView { + @ObjectLink me: Person; + @ObjectLink contacts: ObservedArray; + @State selectedPerson: Person = undefined; + + aboutToAppear() { + this.selectedPerson = this.me; + } + + build() { + Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start }) { + Text("Me:") + PersonView({ person: this.me, phones: this.me.phones, selectedPerson: this.$selectedPerson }) + + Divider().height(8) + + ForEach(this.contacts, + contact => { + PersonView({ person: contact, phones: contact.phones, selectedPerson: this.$selectedPerson }) + }, + contact => contact.id_ + ) + + Divider().height(8) + + Text("Edit:") + PersonEditView({ + selectedPerson: this.$selectedPerson, + name: this.selectedPerson.name, + address: this.selectedPerson.address, + phones: this.selectedPerson.phones + }) + } + .borderStyle(BorderStyle.Solid).borderWidth(5).borderColor(0xAFEEEE).borderRadius(5) + } + } + + @Entry + @Component + struct PageEntry { + @Provide addrBook: AddressBook = new AddressBook( + new Person("Gigi", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********", "18*********"]), + [ + new Person("Oly", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]), + new Person("Sam", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]), + new Person("Vivi", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]), + ]); + + build() { + Column() { + AddressBookView({ me: this.addrBook.me, contacts: this.addrBook.contacts, selectedPerson: this.addrBook.me }) + } + } + } +``` diff --git a/zh-cn/application-dev/quick-start/typescript-to-arkts-migration-guide.md b/zh-cn/application-dev/quick-start/typescript-to-arkts-migration-guide.md index 083638d33287c0464b22dac0ee85808b92a95ef8..0d4553fa5cd806134a393ca02af5e895a864e760 100644 --- a/zh-cn/application-dev/quick-start/typescript-to-arkts-migration-guide.md +++ b/zh-cn/application-dev/quick-start/typescript-to-arkts-migration-guide.md @@ -651,7 +651,7 @@ interface Identity { interface Contact { email: string - phone: string + phoneNumber: string } type Employee = Identity & Contact @@ -667,7 +667,7 @@ interface Identity { interface Contact { email: string - phone: string + phoneNumber: string } interface Employee extends Identity, Contact {} @@ -3174,8 +3174,8 @@ class Person { const person: Person = { name: "John", age: 30, - email: "john@example.com", - phone: 1234567890, + email: "***@example.com", + phoneNumber: 18*********, } ``` @@ -3186,18 +3186,18 @@ class Person { name: string age: number email: string - phone: number + phoneNumber: number - constructor(name: string, age: number, email: string, phone: number) { + constructor(name: string, age: number, email: string, phoneNumber: number) { this.name = name this.age = age this.email = email - this.phone = phone + this.phoneNumber = phoneNumber } } function main(): void { - const person: Person = new Person("John", 30, "john@example.com", 1234567890) + const person: Person = new Person("John", 30, "***@example.com", 18*********) } ``` diff --git a/zh-cn/application-dev/reference/apis/Readme-CN.md b/zh-cn/application-dev/reference/apis/Readme-CN.md index d46d5298c1114d61c1032612394e8cee862c0c5d..2f4215d257914d24bab4ba9be16939c4b6c01c9a 100644 --- a/zh-cn/application-dev/reference/apis/Readme-CN.md +++ b/zh-cn/application-dev/reference/apis/Readme-CN.md @@ -16,6 +16,8 @@ - [@ohos.app.ability.ServiceExtensionAbility (ServiceExtensionAbility)](js-apis-app-ability-serviceExtensionAbility.md) - [@ohos.app.ability.StartOptions (StartOptions)](js-apis-app-ability-startOptions.md) - [@ohos.app.ability.UIAbility (UIAbility)](js-apis-app-ability-uiAbility.md) + - [@ohos.app.ability.UIExtensionAbility (带界面扩展能力基类)](js-apis-app-ability-uiExtensionAbility.md) + - [@ohos.app.ability.UIExtensionContentSession (带界面扩展能力界面操作类)](js-apis-app-ability-uiExtensionContentSession.md) - [@ohos.app.form.FormExtensionAbility (FormExtensionAbility)](js-apis-app-form-formExtensionAbility.md) - [@ohos.application.DataShareExtensionAbility (数据共享扩展能力)](js-apis-application-dataShareExtensionAbility.md) - [@ohos.application.StaticSubscriberExtensionAbility (StaticSubscriberExtensionAbility)](js-apis-application-staticSubscriberExtensionAbility.md) @@ -106,6 +108,7 @@ - [ProcessInformation](js-apis-inner-application-processInformation.md) - [ServiceExtensionContext](js-apis-inner-application-serviceExtensionContext.md) - [UIAbilityContext](js-apis-inner-application-uiAbilityContext.md) + - [UIExtensionContext](js-apis-inner-application-uiExtensionContext.md) - [shellCmdResult](js-apis-inner-application-shellCmdResult.md) - [WindowExtensionContext](js-apis-inner-application-windowExtensionContext.md) - wantAgent @@ -116,7 +119,7 @@ - continuation - [continuationExtraParams](js-apis-continuation-continuationExtraParams.md) - [continuationResult](js-apis-continuation-continuationResult.md) - + - 公共事件与通知 - [系统公共事件定义](commonEventManager-definitions.md) - [@ohos.commonEventManager (公共事件模块)(推荐)](js-apis-commonEventManager.md) @@ -198,22 +201,21 @@ - UI界面 - [@ohos.animator (动画)](js-apis-animator.md) - [@ohos.arkui.componentSnapshot (组件截图)](js-apis-arkui-componentSnapshot.md) + - [@ohos.arkui.componentUtils (componentUtils)](js-apis-arkui-componentUtils.md) - [@ohos.arkui.dragController (DragController)](js-apis-arkui-dragController.md) - [@ohos.arkui.drawableDescriptor (DrawableDescriptor)](js-apis-arkui-drawableDescriptor.md) - [@ohos.arkui.inspector (布局回调)](js-apis-arkui-inspector.md) + - [ @ohos.arkui.performanceMonitor (性能监测)](js-apis-arkui-performancemonitor.md) - [@ohos.arkui.UIContext (UIContext)](js-apis-arkui-UIContext.md) - - [@ohos.arkui.componentUtils (componentUtils)](js-apis-arkui-componentUtils.md) - [@ohos.curves (插值计算)](js-apis-curve.md) - [@ohos.font (注册自定义字体)](js-apis-font.md) - [@ohos.matrix4 (矩阵变换)](js-apis-matrix4.md) + - [@ohos.measure (文本计算)](js-apis-measure.md) - [@ohos.mediaquery (媒体查询)](js-apis-mediaquery.md) - [@ohos.pluginComponent (PluginComponentManager)](js-apis-plugincomponent.md) - [@ohos.promptAction (弹窗)](js-apis-promptAction.md) - [@ohos.router (页面路由)](js-apis-router.md) - - [@ohos.measure (文本计算)](js-apis-measure.md) - - [@ohos.uiAppearance (用户界面外观)](js-apis-uiappearance.md) - - [ @ohos.arkui.performanceMonitor (性能监测)](js-apis-arkui-performancemonitor.md) - + - [@ohos.uiAppearance (用户界面外观)](js-apis-uiappearance.md) - 图形图像 - [@ohos.animation.windowAnimationManager (窗口动画管理)](js-apis-windowAnimationManager.md) - [@ohos.application.WindowExtensionAbility (窗口扩展能力)](js-apis-application-windowExtensionAbility.md) @@ -321,16 +323,6 @@ - [@ohos.request (上传下载)](js-apis-request.md) - 通信与连接 - - [@ohos.bluetooth.a2dp(蓝牙a2dp模块)(推荐)](js-apis-bluetooth-a2dp.md) - - [@ohos.bluetooth.access(蓝牙access模块)(推荐)](js-apis-bluetooth-access.md) - - [@ohos.bluetooth.baseProfile(蓝牙baseProfile模块)(推荐)](js-apis-bluetooth-baseProfile.md) - - [@ohos.bluetooth.ble(蓝牙ble模块)(推荐)](js-apis-bluetooth-ble.md) - - [@ohos.bluetooth.connection(蓝牙connection模块)(推荐)](js-apis-bluetooth-connection.md) - - [@ohos.bluetooth.constant(蓝牙constant模块)(推荐)](js-apis-bluetooth-constant.md) - - [@ohos.bluetooth.hfp(蓝牙hfp模块)(推荐)](js-apis-bluetooth-hfp.md) - - [@ohos.bluetooth.hid(蓝牙hid模块)(推荐)](js-apis-bluetooth-hid.md) - - [@ohos.bluetooth.pan(蓝牙pan模块)(推荐)](js-apis-bluetooth-pan.md) - - [@ohos.bluetooth.socket(蓝牙socket模块)(推荐)](js-apis-bluetooth-socket.md) - [@ohos.bluetooth (蓝牙)(待停用)](js-apis-bluetooth.md) - [@ohos.bluetoothManager (蓝牙)(待停用)](js-apis-bluetoothManager.md) - [@ohos.connectedTag (有源标签)](js-apis-connectedTag.md) @@ -346,7 +338,7 @@ - tag - [nfctech (标准NFC-Tag Nfc 技术)](js-apis-nfctech.md) - [tagSession (标准NFC-Tag TagSession)](js-apis-tagSession.md) - + - 系统基础能力 - [@ohos.accessibility (辅助功能)](js-apis-accessibility.md) - [@ohos.accessibility.config (系统辅助功能配置)](js-apis-accessibility-config.md) diff --git a/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-image.md b/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-image.md index 4e4158658ff7940b443f618a3bb944c8f048a791..f90c522177b557bcaeee14467214b40e8739cb27 100644 --- a/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-image.md +++ b/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-image.md @@ -52,7 +52,7 @@ Image组件加载图片失败或图片尺寸为0时,图片组件大小自动 | syncLoad8+ | boolean | 设置是否同步加载图片,默认是异步加载。同步加载时阻塞UI线程,不会显示占位图。
默认值:false
从API version 9开始,该接口支持在ArkTS卡片中使用。
**说明:**
建议加载尺寸较小的本地图片时将syncLoad设为true,因为耗时较短,在主线程上执行即可。 | | copyOption9+ | [CopyOptions](ts-appendix-enums.md#copyoptions9) | 设置图片是否可复制。
当copyOption设置为非CopyOptions.None时,支持使用长按、鼠标右击、快捷组合键'CTRL+C'等方式进行复制。
默认值:CopyOptions.None
从API version 9开始,该接口支持在ArkTS卡片中使用。
**说明:**
svg图片不支持复制。 | | colorFilter9+ | [ColorFilter](ts-types.md#colorfilter9) | 给图像设置颜色滤镜效果,入参为一个的4x5的RGBA转换矩阵。
矩阵第一行表示R(红色)的向量值,第二行表示G(绿色)的向量值,第三行表示B(蓝色)的向量值,第四行表示A(透明度)的向量值,4行分别代表不同的RGBA的向量值。
RGBA值分别是0和1之间的浮点数字,当矩阵对角线值为1时,保持图片原有色彩。
**计算规则:**
如果输入的滤镜矩阵为:
![image-matrix-1](figures/image-matrix-1.jpg)
像素点为[R, G, B, A]
则过滤后的颜色为 [R’, G’, B’, A’]
![image-matrix-2](figures/image-matrix-2.jpg)
从API version 9开始,该接口支持在ArkTS卡片中使用。 | -| draggable(deprecated) | boolean | 设置组件默认拖拽效果,设置为true时,组件可拖拽。
不能和[onDragStart](ts-universal-events-drag-drop.md)事件同时使用。
默认值:false
**说明:**
从 API version 9 开始支持,从 API version 10 开始废弃,建议使用通用属性[draggable](ts-universal-events-drag-drop.md)替代。 | +| draggable(deprecated) | boolean | 设置组件默认拖拽效果,设置为true时,组件可拖拽。
不能和[onDragStart](ts-universal-attributes-drag-drop.md)事件同时使用。
默认值:false
**说明:**
从 API version 9 开始支持,从 API version 10 开始废弃,建议使用通用属性[draggable](ts-universal-events-drag-drop.md)替代。 | > **说明:** > diff --git a/zh-cn/application-dev/reference/arkui-ts/ts-container-listitem.md b/zh-cn/application-dev/reference/arkui-ts/ts-container-listitem.md index 5ef2d3622ec875996e9fd224339b8a27d346608e..797c0baf2b36070ef961555cb38f071ba5a15ddb 100644 --- a/zh-cn/application-dev/reference/arkui-ts/ts-container-listitem.md +++ b/zh-cn/application-dev/reference/arkui-ts/ts-container-listitem.md @@ -100,6 +100,8 @@ List垂直布局,ListItem向右滑动,item左边的长距离滑动删除选 ## 示例 +### 示例1 + ```ts // xxx.ets @Entry @@ -126,6 +128,7 @@ struct ListItemExample { ![zh-cn_image_0000001219864159](figures/zh-cn_image_0000001219864159.gif) +### 示例2 ```ts // xxx.ets @@ -192,7 +195,8 @@ struct ListItemExample2 { ``` ![deleteListItem](figures/deleteListItem.gif) -## 示例3 +### 示例3 + ```ts // xxx.ets @Entry diff --git a/zh-cn/application-dev/ui/arkui-overview.md b/zh-cn/application-dev/ui/arkui-overview.md index c638196d60e87bba137c8e9bf6aa9923c707d6af..e1b1a3dc2500f6316adc895aaa558a2505e2bcf3 100644 --- a/zh-cn/application-dev/ui/arkui-overview.md +++ b/zh-cn/application-dev/ui/arkui-overview.md @@ -13,7 +13,7 @@ ## 两种开发范式 -针对不用的应用场景及技术背景,方舟开发框架提供了两种开发范式,分别是[基于ArkTS的声明式开发范式](arkts-ui-development-overview.md)(简称“声明式开发范式”)和[兼容JS的类Web开发范式](../ui/ui-js-overview.md)(简称“类Web开发范式”)。 +针对不同的应用场景及技术背景,方舟开发框架提供了两种开发范式,分别是[基于ArkTS的声明式开发范式](arkts-ui-development-overview.md)(简称“声明式开发范式”)和[兼容JS的类Web开发范式](../ui/ui-js-overview.md)(简称“类Web开发范式”)。 - **声明式开发范式**:采用基于TypeScript声明式UI语法扩展而来的[ArkTS语言](../quick-start/arkts-get-started.md),从组件、动画和状态管理三个维度提供UI绘制能力。 diff --git a/zh-cn/application-dev/website.md b/zh-cn/application-dev/website.md index 54248dd2d00cf3d6b471e06870ef0719afdfa6af..a0500deabf30e4489a1d5cd7cf1e38ac97672f45 100644 --- a/zh-cn/application-dev/website.md +++ b/zh-cn/application-dev/website.md @@ -76,11 +76,14 @@ - [其他状态管理概述](quick-start/arkts-other-state-mgmt-functions-overview.md) - [\@Watch装饰器:状态变量更改通知](quick-start/arkts-watch.md) - [$$语法:内置组件双向同步](quick-start/arkts-two-way-sync.md) + - [MVVM模式](quick-start/arkts-mvvm.md) + - [状态管理优秀实践](quick-start/arkts-state-management-best-practices.md) - 渲染控制 - [渲染控制概述](quick-start/arkts-rendering-control-overview.md) - [if/else:条件渲染](quick-start/arkts-rendering-control-ifelse.md) - [ForEach:循环渲染](quick-start/arkts-rendering-control-foreach.md) - [LazyForEach:数据懒加载](quick-start/arkts-rendering-control-lazyforeach.md) + - [渲染控制优秀实践](quick-start/arkts-rendering-control-best-practices.md) - 开发 - 应用模型 - 应用模型概述 @@ -479,6 +482,7 @@ - [开发音频通话功能](media/audio-call-development.md) - [视频播放](media/video-playback.md) - [视频录制](media/video-recording.md) + - [屏幕录制](media/avscreen-capture.md) - 音视频编解码 - [获取支持的编解码能力](media/obtain-supported-codecs.md) - [音频编码](media/audio-encoding.md) @@ -782,6 +786,8 @@ - [@ohos.app.ability.ServiceExtensionAbility (ServiceExtensionAbility)](reference/apis/js-apis-app-ability-serviceExtensionAbility.md) - [@ohos.app.ability.StartOptions (StartOptions)](reference/apis/js-apis-app-ability-startOptions.md) - [@ohos.app.ability.UIAbility (UIAbility)](reference/apis/js-apis-app-ability-uiAbility.md) + - [@ohos.app.ability.UIExtensionAbility (带界面扩展能力基类)](reference/apis/js-apis-app-ability-uiExtensionAbility.md) + - [@ohos.app.ability.UIExtensionContentSession (带界面扩展能力界面操作类)](reference/apis/js-apis-app-ability-uiExtensionContentSession.md) - [@ohos.app.form.FormExtensionAbility (FormExtensionAbility)](reference/apis/js-apis-app-form-formExtensionAbility.md) - [@ohos.application.DataShareExtensionAbility (数据共享扩展能力)](reference/apis/js-apis-application-dataShareExtensionAbility.md) - [@ohos.application.StaticSubscriberExtensionAbility (StaticSubscriberExtensionAbility)](reference/apis/js-apis-application-staticSubscriberExtensionAbility.md) @@ -872,6 +878,7 @@ - [ProcessInformation](reference/apis/js-apis-inner-application-processInformation.md) - [ServiceExtensionContext](reference/apis/js-apis-inner-application-serviceExtensionContext.md) - [UIAbilityContext](reference/apis/js-apis-inner-application-uiAbilityContext.md) + - [UIExtensionContext](reference/apis/js-apis-inner-application-uiExtensionContext.md) - [shellCmdResult](reference/apis/js-apis-inner-application-shellCmdResult.md) - [WindowExtensionContext](reference/apis/js-apis-inner-application-windowExtensionContext.md) - wantAgent @@ -961,21 +968,21 @@ - UI界面 - [@ohos.animator (动画)](reference/apis/js-apis-animator.md) - [@ohos.arkui.componentSnapshot (组件截图)](reference/apis/js-apis-arkui-componentSnapshot.md) + - [@ohos.arkui.componentUtils (componentUtils)](reference/apis/js-apis-arkui-componentUtils.md) - [@ohos.arkui.dragController (DragController)](reference/apis/js-apis-arkui-dragController.md) - [@ohos.arkui.drawableDescriptor (DrawableDescriptor)](reference/apis/js-apis-arkui-drawableDescriptor.md) - [@ohos.arkui.inspector (布局回调)](reference/apis/js-apis-arkui-inspector.md) + - [@ohos.arkui.performanceMonitor (性能监测)](reference/apis/js-apis-arkui-performancemonitor.md) - [@ohos.arkui.UIContext (UIContext)](reference/apis/js-apis-arkui-UIContext.md) - - [@ohos.arkui.componentUtils (componentUtils)](reference/apis/js-apis-arkui-componentUtils.md) - [@ohos.curves (插值计算)](reference/apis/js-apis-curve.md) - [@ohos.font (注册自定义字体)](reference/apis/js-apis-font.md) - [@ohos.matrix4 (矩阵变换)](reference/apis/js-apis-matrix4.md) + - [@ohos.measure (文本计算)](reference/apis/js-apis-measure.md) - [@ohos.mediaquery (媒体查询)](reference/apis/js-apis-mediaquery.md) - [@ohos.pluginComponent (PluginComponentManager)](reference/apis/js-apis-plugincomponent.md) - [@ohos.promptAction (弹窗)](reference/apis/js-apis-promptAction.md) - [@ohos.router (页面路由)](reference/apis/js-apis-router.md) - - [@ohos.measure (文本计算)](reference/apis/js-apis-measure.md) - [@ohos.uiAppearance (用户界面外观)](reference/apis/js-apis-uiappearance.md) - - [ @ohos.arkui.performanceMonitor (性能监测)](reference/apis/js-apis-arkui-performancemonitor.md) - 图形图像 - [@ohos.animation.windowAnimationManager (窗口动画管理)](reference/apis/js-apis-windowAnimationManager.md) - [@ohos.application.WindowExtensionAbility (窗口扩展能力)](reference/apis/js-apis-application-windowExtensionAbility.md) @@ -1073,16 +1080,6 @@ - [@ohos.net.webSocket (WebSocket连接)](reference/apis/js-apis-webSocket.md) - [@ohos.request (上传下载)](reference/apis/js-apis-request.md) - 通信与连接 - - [@ohos.bluetooth.a2dp(蓝牙a2dp模块)(推荐)](reference/apis/js-apis-bluetooth-a2dp.md) - - [@ohos.bluetooth.access(蓝牙access模块)(推荐)](reference/apis/js-apis-bluetooth-access.md) - - [@ohos.bluetooth.baseProfile(蓝牙baseProfile模块)(推荐)](reference/apis/js-apis-bluetooth-baseProfile.md) - - [@ohos.bluetooth.ble(蓝牙ble模块)(推荐)](reference/apis/js-apis-bluetooth-ble.md) - - [@ohos.bluetooth.connection(蓝牙connection模块)(推荐)](reference/apis/js-apis-bluetooth-connection.md) - - [@ohos.bluetooth.constant(蓝牙constant模块)(推荐)](reference/apis/js-apis-bluetooth-constant.md) - - [@ohos.bluetooth.hfp(蓝牙hfp模块)(推荐)](reference/apis/js-apis-bluetooth-hfp.md) - - [@ohos.bluetooth.hid(蓝牙hid模块)(推荐)](reference/apis/js-apis-bluetooth-hid.md) - - [@ohos.bluetooth.pan(蓝牙pan模块)(推荐)](reference/apis/js-apis-bluetooth-pan.md) - - [@ohos.bluetooth.socket(蓝牙socket模块)(推荐)](reference/apis/js-apis-bluetooth-socket.md) - [@ohos.bluetooth (蓝牙)(待停用)](reference/apis/js-apis-bluetooth.md) - [@ohos.bluetoothManager (蓝牙)(待停用)](reference/apis/js-apis-bluetoothManager.md) - [@ohos.connectedTag (有源标签)](reference/apis/js-apis-connectedTag.md) @@ -1418,6 +1415,7 @@ - [Swiper](reference/arkui-ts/ts-container-swiper.md) - [Tabs](reference/arkui-ts/ts-container-tabs.md) - [TabContent](reference/arkui-ts/ts-container-tabcontent.md) + - [UIExtensionComponent](reference/arkui-ts/ts-container-ui-extension-component.md) - [WaterFlow](reference/arkui-ts/ts-container-waterflow.md) - 媒体组件 - [Video](reference/arkui-ts/ts-media-components-video.md)