鸿蒙-3. 状态管理

自定义构建函数

1. 构建函数-@Builder

ArkUI还提供了一种更轻量的UI元素复用机制 @Builder,可以将重复使用的UI元素抽象成一个方法,在 build 方法里调用。

  • 组件内定义
  • 全局定义

1)组件内定义

1@Builder MyBuilderFunction() {}
1this.MyBuilderFunction()

2)全局定义

1@Builder function MyGlobalBuilderFunction() {}
1MyGlobalBuilderFunction()

📕📕📕 练习案例→商品详情-更多按钮

  • 待实现页面
1@Entry
2@Component
3struct Index {
4  build() {
5    Column() {
6      GridRow({ columns: 2, gutter: 15 }) {
7        GridCol({ span: 2 }) {
8          Column() {
9            Row() {
10              Text('评价(2000+)')
11                .layoutWeight(1)
12                .fontWeight(600)
13              // TODO
14            }
15            .padding(10)
16
17            Row()
18              .height(100)
19          }
20          .borderRadius(12)
21          .backgroundColor('#fff')
22        }
23
24        GridCol() {
25          Column() {
26            Row() {
27              Text('推荐')
28                .layoutWeight(1)
29                .fontWeight(600)
30              // TODO
31            }
32            .padding(10)
33
34            Row()
35              .height(100)
36          }
37          .borderRadius(12)
38          .backgroundColor('#fff')
39        }
40
41        GridCol() {
42          Column() {
43            Row() {
44              Text('体验')
45                .layoutWeight(1)
46                .fontWeight(600)
47              // TODO
48            }
49            .padding(10)
50
51            Row()
52              .height(100)
53          }
54          .borderRadius(12)
55          .backgroundColor('#fff')
56        }
57      }
58    }
59    .height('100%')
60    .padding(15)
61    .backgroundColor('#f5f5f5')
62  }
63}
  • 使用 @Builder 提取UI结构
1@Entry
2@Component
3struct Index {
4
5  @Builder
6  MoreBuilder () {
7    Row() {
8      Text('查看更多')
9        .fontSize(14)
10        .fontColor('#666666')
11      Image($r('app.media.ic_public_arrow_right'))
12        .width(16)
13        .fillColor('#666666')
14    }
15  }
16
17  build() {
18    Column() {
19      GridRow({ columns: 2, gutter: 15 }) {
20        GridCol({ span: 2 }) {
21          Column() {
22            Row() {
23              Text('评价(2000+)')
24                .layoutWeight(1)
25                .fontWeight(600)
26              this.MoreBuilder()
27            }
28            .padding(10)
29
30            Row()
31              .height(100)
32          }
33          .borderRadius(12)
34          .backgroundColor('#fff')
35        }
36
37        GridCol() {
38          Column() {
39            Row() {
40              Text('推荐')
41                .layoutWeight(1)
42                .fontWeight(600)
43              this.MoreBuilder()
44            }
45            .padding(10)
46
47            Row()
48              .height(100)
49          }
50          .borderRadius(12)
51          .backgroundColor('#fff')
52        }
53
54        GridCol() {
55          Column() {
56            Row() {
57              Text('体验')
58                .layoutWeight(1)
59                .fontWeight(600)
60              this.MoreBuilder()
61            }
62            .padding(10)
63
64            Row()
65              .height(100)
66          }
67          .borderRadius(12)
68          .backgroundColor('#fff')
69        }
70      }
71    }
72    .height('100%')
73    .padding(15)
74    .backgroundColor('#f5f5f5')
75  }
76}
TIP

小结:

  • 遇到非遍历情况下,一个组件分散着相同的UI结构,可以使用 @Builder 更轻量

其他:

  • GridRow GridCol 栅格布局

2. 构建函数-传参传递

1)按值传递(场景:构建不同的UI)

1@Builder MyBuilderFunction( title: string ) {}
1this.MyBuilderFunction('Title')

需求:不同板块查看更多文案不一样

  • 评价 好评率 98%

  • 推荐 查看全部

  • 体验 4 条测评

1@Builder
2  MoreBuilder (title: string) {
3    Row() {
4      Text(title)
5        .fontSize(14)
6        .fontColor('#666666')
7      Image($r('app.media.ic_public_arrow_right'))
8        .width(16)
9        .fillColor('#666666')
10    }
11  }
1this.MoreBuilder('好评率 98%')
2this.MoreBuilder('查看全部')
3this.MoreBuilder('4 条测评')

2)引用传递(场景:当传递的数据更新,需要更新UI)

需求:

  • 点击按钮后模拟加载好评率数据

1@Entry
2@Component
3struct Index {
4  @State
5  rate: number = 0
6
7  @Builder
8  MoreBuilder(params: { title: string }) {
9    Row() {
10      Text(params.title)
11        .fontSize(14)
12        .fontColor('#666666')
13      Image($r('app.media.ic_public_arrow_right'))
14        .width(16)
15        .fillColor('#666666')
16    }
17  }
18
19  build() {
20    Column() {
21      Button('获取数据')
22        .margin({ bottom: 15 })
23        .onClick(() => {
24          this.rate = 99
25        })
26      GridRow({ columns: 2, gutter: 15 }) {
27        GridCol({ span: 2 }) {
28          Column() {
29            Row() {
30              Text('评价(2000+)')
31                .layoutWeight(1)
32                .fontWeight(600)
33              this.MoreBuilder({ title: `好评率 ${this.rate} %` })
34            }
35            .padding(10)
36
37            Row()
38              .height(100)
39          }
40          .borderRadius(12)
41          .backgroundColor('#fff')
42        }
43
44        GridCol() {
45          Column() {
46            Row() {
47              Text('推荐')
48                .layoutWeight(1)
49                .fontWeight(600)
50              this.MoreBuilder({ title: '查看全部' })
51            }
52            .padding(10)
53
54            Row()
55              .height(100)
56          }
57          .borderRadius(12)
58          .backgroundColor('#fff')
59        }
60
61        GridCol() {
62          Column() {
63            Row() {
64              Text('体验')
65                .layoutWeight(1)
66                .fontWeight(600)
67              this.MoreBuilder({ title: '4 条测评' })
68            }
69            .padding(10)
70
71            Row()
72              .height(100)
73          }
74          .borderRadius(12)
75          .backgroundColor('#fff')
76        }
77      }
78    }
79    .height('100%')
80    .padding(15)
81    .backgroundColor('#f5f5f5')
82  }
83}
TIP
  • 使用 @Builder 复用逻辑的时候,支持传参可以更灵活的渲染UI
  • 参数可以使用状态数据,不过建议通过对象的方式传入 @Builder

3. 构建函数-@BuilderParam 传递UI

@BuilderParam 该装饰器用于声明任意UI描述的一个元素,类似 slot 占位符

前置知识

组件属性初始化:

  • 定义组件声明属性 title: string
  • 使用组件初始化属性 Comp({ title: string })
  • 尾随闭包初始化组件

    • 组件内有且仅有一个使用 @BuilderParam 装饰的属性
  • 参数初始化组件

    • 组件内有多个使用 @BuilderParam 装饰器属性

1)尾随闭包初始化组件(默认插槽)

需求:

  • 标题文字和更多文案通过属性传入

  • 内容结构需要传入

1@Component
2struct PanelComp {
3  title: string
4  more: string
5  @BuilderParam
6  panelContent: () => void = this.DefaultPanelContent
7
8  // 备用 Builder
9  @Builder
10  DefaultPanelContent () {
11    Text('默认内容')
12  }
13
14  build() {
15    Column() {
16      Row() {
17        Text(this.title)
18          .layoutWeight(1)
19          .fontWeight(600)
20        Row() {
21          Text(this.more)
22            .fontSize(14)
23            .fontColor('#666666')
24          Image($r('app.media.ic_public_arrow_right'))
25            .width(16)
26            .fillColor('#666666')
27        }
28      }
29      .padding(10)
30
31      Row() {
32        this.panelContent()
33      }
34      .height(100)
35    }
36    .borderRadius(12)
37    .backgroundColor('#fff')
38  }
39}
40
41@Entry
42@Component
43struct Index {
44  build() {
45    Column() {
46      GridRow({ columns: 2, gutter: 15 }) {
47        GridCol({ span: 2 }) {
48          PanelComp({ title: '评价(2000+)', more: '好评率98%' })
49        }
50
51        GridCol() {
52          PanelComp({ title: '推荐', more: '查看全部' }){
53            Text('推荐内容')
54          }
55        }
56
57        GridCol() {
58          PanelComp({ title: '体验', more: '4 条测评' }){
59            Text('体验内容')
60          }
61        }
62      }
63    }
64    .height('100%')
65    .padding(15)
66    .backgroundColor('#f5f5f5')
67  }
68}

2)参数初始化组件(具名插槽)

需求:需要传入内容结构和底部结构

1@Component
2struct PanelComp {
3  title: string
4  more: string
5  @BuilderParam
6  panelContent: () => void
7  @BuilderParam
8  panelFooter: () => void
9
10  build() {
11    Column() {
12      Row() {
13        Text(this.title)
14          .layoutWeight(1)
15          .fontWeight(600)
16        Row() {
17          Text(this.more)
18            .fontSize(14)
19            .fontColor('#666666')
20          Image($r('app.media.ic_public_arrow_right'))
21            .width(16)
22            .fillColor('#666666')
23        }
24      }
25      .padding(10)
26
27      Row() {
28        this.panelContent()
29      }
30      .height(100)
31      Row() {
32        this.panelFooter()
33      }
34      .height(50)
35    }
36    .borderRadius(12)
37    .backgroundColor('#fff')
38  }
39}
40
41@Entry
42@Component
43struct Index {
44  @Builder
45  ContentBuilderA() {
46    Text('评价内容')
47  }
48  @Builder
49  FooterBuilderA() {
50    Text('评价底部')
51  }
52
53  build() {
54    Column() {
55      GridRow({ columns: 2, gutter: 15 }) {
56        GridCol({ span: 2 }) {
57          PanelComp({
58            title: '评价(2000+)',
59            more: '好评率98%',
60            panelFooter: this.FooterBuilderA,
61            panelContent: this.ContentBuilderA
62          })
63        }
64
65        // GridCol() {
66        //   PanelComp({ title: '推荐', more: '查看全部' }){
67        //     Text('推荐内容')
68        //   }
69        // }
70        //
71        // GridCol() {
72        //   PanelComp({ title: '体验', more: '4 条测评' }){
73        //     Text('体验内容')
74        //   }
75        // }
76      }
77    }
78    .height('100%')
79    .padding(15)
80    .backgroundColor('#f5f5f5')
81  }
82}
TIP
  • 当子组件使用一个 @BuilderParam 的时候,使用组件的时候在尾随 {} 插入UI结构
  • 当子组件使用多个 @BuilderParam 的时候,使用组件的时候 Comp({ xxx: this.builderFn }) 传入
  • 子组件本身可以提供一个默认的 @Builder 函数作为 @BuilderParam 备用函数,当做备用内容使用

4. 构建函数-系统组件自定义UI

在一些系统组件中,根据配置无法达到预期UI,可以使用 @Builder 构建函数自定义UI,前提该组件支持自定义。

需求:自定义 Tabs 组件的 TabBar UI结构

1class ToolBarItem {
2  defaultIcon: string | Resource
3  activeIcon: string | Resource
4  label: string
5}
6
7@Entry
8@Component
9struct Index {
10  @State
11  activeIndex: number = 0
12  toolBars: ToolBarItem[] = [
13    { defaultIcon: $r('app.media.home'), activeIcon: $r('app.media.home_select'), label: '首页' },
14    { defaultIcon: $r('app.media.project'), activeIcon: $r('app.media.project_select'), label: '项目' },
15    { defaultIcon: $r('app.media.interview'), activeIcon: $r('app.media.interview_select'), label: '面经' },
16    { defaultIcon: $r('app.media.mine'), activeIcon: $r('app.media.mine_select'), label: '我的' }
17  ]
18
19  @Builder
20  TabBarBuilder(item: ToolBarItem, index: number) {
21    Column() {
22      Image(this.activeIndex === index ? item.activeIcon : item.defaultIcon)
23        .width(24)
24      Text(item.label)
25        .fontSize(12)
26        .margin({ top: 4 })
27        .lineHeight(12)
28        .fontColor(this.activeIndex === index ? '#000' : '#aaa')
29    }
30  }
31
32  build() {
33    Tabs({
34      index: this.activeIndex
35    }) {
36      ForEach(this.toolBars, (item: ToolBarItem, index: number) => {
37        TabContent() {
38          Text(index.toString())
39        }
40        .tabBar(this.TabBarBuilder(item, index))
41      })
42    }
43    .barPosition(BarPosition.End)
44    .onChange(index => this.activeIndex = index)
45  }
46}

组件状态共享

1. 状态共享-父子单向

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

1@Entry
2@Component
3struct Index {
4
5  @State
6  money: number = 0
7
8  build() {
9    Column({ space: 20 }){
10      Text('父组件:' + this.money)
11        .onClick(() => {
12          this.money ++
13        })
14      Child({ money: this.money })
15    }
16    .width('100%')
17    .height('100%')
18    .alignItems(HorizontalAlign.Center)
19    .justifyContent(FlexAlign.Center)
20  }
21}
22
23@Component
24struct Child {
25
26  @Prop
27  money: number
28
29  build() {
30    Text('子组件:' + this.money)
31      .onClick(() => {
32        this.money ++
33      })
34  }
35}
TIP
  • 支持类型 string、number、boolean、enum 类型
  • 子组件可修改 Prop 数据值,但不同步到父组件,父组件更新后覆盖子组件 Prop 数据
  • 子组件可以初始化默认值,注意:目前编译器会提示错误,请忽略,下个版本将修复

2. 状态共享-父子双向

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

1)简单类型 string、number、boolean、enum

1@Entry
2@Component
3struct Index {
4
5  @State
6  money: number = 0
7
8  build() {
9    Column({ space: 20 }){
10      Text('父组件:' + this.money)
11        .onClick(() => {
12          this.money ++
13        })
14      Child({ money: $money })
15    }
16    .width('100%')
17    .height('100%')
18    .alignItems(HorizontalAlign.Center)
19    .justifyContent(FlexAlign.Center)
20  }
21}
22
23@Component
24struct Child {
25
26  @Link
27  money: number
28
29  build() {
30    Text('子组件:' + this.money)
31      .onClick(() => {
32        this.money ++
33      })
34  }
35}

2)复杂类型 Object、class

1class Person {
2  name: string
3  age: number
4}
5
6@Entry
7@Component
8struct Index {
9
10  @State
11  person: Person = { name: 'jack', age: 18 }
12
13  build() {
14    Column({ space: 20 }){
15      Text('父组件:' + `${this.person.name} 今年 ${ this.person.age }`)
16        .onClick(() => {
17          this.person.age ++
18        })
19      Child({ person: $person })
20    }
21    .width('100%')
22    .height('100%')
23    .alignItems(HorizontalAlign.Center)
24    .justifyContent(FlexAlign.Center)
25  }
26}
27
28@Component
29struct Child {
30
31  @Link
32  person: Person
33
34  build() {
35    Text('子组件:' +  `${this.person.name} 今年 ${ this.person.age }`)
36      .onClick(() => {
37        this.person.age ++
38      })
39  }
40}
TIP
  • 父组件传值的时候需要 this. 改成 $,子组件 @Link 修饰数据

3. 状态共享-后代组件

@Provide@Consume,应用于与后代组件的双向数据同步,应用于状态数据在多个层级之间传递的场景。

1)通过相同的变量名绑定

1@Entry
2@Component
3struct Index {
4  @Provide
5  money: number = 0
6
7  build() {
8    Column({ space: 20 }) {
9      Text('父组件:' + this.money)
10        .onClick(() => {
11          this.money++
12        })
13      Parent()
14    }
15    .width('100%')
16    .height('100%')
17    .justifyContent(FlexAlign.Center)
18  }
19}
20
21@Component
22struct Parent {
23  @Consume
24  money: number
25
26  build() {
27    Column({ space: 20 }) {
28      Text('父组件:' + this.money)
29        .onClick(() => {
30          this.money++
31        })
32      Child()
33    }
34  }
35}
36
37@Component
38struct Child {
39  @Consume
40  money: number
41
42  build() {
43    Text('子组件:' + this.money)
44      .onClick(() => {
45        this.money++
46      })
47  }
48}
TIP
  • Object、class、string、number、boolean、enum 类型均支持
  • 通过相同的变量别名绑定 @Provide('key')@Consume('key') key需要保持一致

4. 状态共享-状态监听器

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

  • @State、@Prop、@Link 等装饰器在 @Watch 装饰之前

1import promptAction from '@ohos.promptAction'
2
3@Component
4struct Child {
5  @Prop
6  @Watch('onActiveIndex')
7  activeIndex: number
8
9  onActiveIndex() {
10    promptAction.showToast({ message: '监听变化' })
11  }
12
13  build() {
14    Column() {
15      Text('Child' + this.activeIndex)
16    }
17  }
18}
19
20@Entry
21@Component
22struct Index {
23  @State activeIndex: number = 0
24
25  onChange (index: number) {
26    this.activeIndex = index
27    promptAction.showToast({ message: '点击' })
28  }
29
30  build() {
31    Navigation() {
32      Child({ activeIndex: this.activeIndex })
33    }.toolBar({
34      items: [
35        { value: '首页', action: () => this.onChange(0) },
36        { value: '我的', action: () => this.onChange(1) },
37      ]
38    })
39  }
40}
TIP
  • 在第一次初始化的时候,@Watch装饰的方法不会被调用

之前我们通过 赋值的方式 修改嵌套对象或对象数组这类复杂数据来更新UI,会影响对象对应所有UI更新; 通过 @Observed @ObjectLink 可以优化这个问题;

使用步骤:

  • class 数据模拟需要定义通过构造函数,使用 @Observed 修饰这个类

  • 初始化数据:需要通过初始化构造函数的方式添加

  • 通过 @ObjectLink 关联对象,可以直接修改被关联对象来更新UI

需求:改造下知乎评论案例

1)定义构造函数和使用@Observed 修饰符,以及初始化数据

models/index.ets
1@Observed
2export class ReplyItem {
3  id: number
4  avatar: string | Resource
5  author: string
6  content: string
7  time: string
8  area: string
9  likeNum: number
10  likeFlag?: boolean
11
12  constructor(item: ReplyItem) {
13    for (const key in item) {
14      this[key] = item[key]
15    }
16  }
17}
18
19export const replyList: ReplyItem[] = [
20  new ReplyItem({
21    id: 1,
22    avatar: 'https://picx.zhimg.com/027729d02bdf060e24973c3726fea9da_l.jpg?source=06d4cd63',
23    author: '偏执狂-妄想家',
24    content: '更何况还分到一个摩洛哥[惊喜]',
25    time: '11-30',
26    area: '海南',
27    likeNum: 34
28  }),
29  new ReplyItem({
30    id: 2,
31    avatar: 'https://pic1.zhimg.com/v2-5a3f5190369ae59c12bee33abfe0c5cc_xl.jpg?source=32738c0c',
32    author: 'William',
33    content: '当年希腊可是把1:0发挥到极致了',
34    time: '11-29',
35    area: '北京',
36    likeNum: 58
37  }),
38  new ReplyItem({
39    id: 3,
40    avatar: 'https://picx.zhimg.com/v2-e6f4605c16e4378572a96dad7eaaf2b0_l.jpg?source=06d4cd63',
41    author: 'Andy Garcia',
42    content: '欧洲杯其实16队球队打正赛已经差不多,24队打正赛意味着正赛阶段在小组赛一样有弱队。',
43    time: '11-28',
44    area: '上海',
45    likeNum: 10
46  }),
47  new ReplyItem({
48    id: 4,
49    avatar: 'https://picx.zhimg.com/v2-53e7cf84228e26f419d924c2bf8d5d70_l.jpg?source=06d4cd63',
50    author: '正宗好鱼头',
51    content: '确实眼红啊,亚洲就没这种球队,让中国队刷',
52    time: '11-27',
53    area: '香港',
54    likeNum: 139
55  }),
56  new ReplyItem({
57    id: 5,
58    avatar: 'https://pic1.zhimg.com/v2-eeddfaae049df2a407ff37540894c8ce_l.jpg?source=06d4cd63',
59    author: '柱子哥',
60    content: '我是支持扩大的,亚洲杯欧洲杯扩到32队,世界杯扩到64队才是好的,世界上有超过200支队伍,欧洲区55支队伍,亚洲区47支队伍,即使如此也就六成出现率',
61    time: '11-27',
62    area: '旧金山',
63    likeNum: 29
64  }),
65  new ReplyItem({
66    id: 6,
67    avatar: 'https://picx.zhimg.com/v2-fab3da929232ae911e92bf8137d11f3a_l.jpg?source=06d4cd63',
68    author: '飞轩逸',
69    content: '禁止欧洲杯扩军之前,应该先禁止世界杯扩军,或者至少把亚洲名额一半给欧洲。',
70    time: '11-26',
71    area: '里约',
72    likeNum: 100
73  })
74]

2)嵌套的对象,或者数组中的对象,传入子组件,组件使用 @ObjectLink 修饰符获取数据

pages/Index.ets
1import promptAction from '@ohos.promptAction'
2import { ReplyItem, replyList } from '../models'
3
4
5@Entry
6@Component
7struct Index {
8  @State
9  replyList: ReplyItem[] = replyList
10  @State
11  content: string = ''
12
13  onReply() {
14    const reply: ReplyItem = new ReplyItem({
15      id: Math.random(),
16      content: this.content,
17      author: 'Zhousg',
18      avatar: $r('app.media.avatar'),
19      time: '12-01',
20      likeNum: 0,
21      area: '北京'
22    })
23    this.replyList.unshift(reply)
24    this.content = ''
25    promptAction.showToast({ message: '回复成功' })
26  }
27
28  build() {
29    Stack() {
30      Scroll() {
31        Column() {
32          NavComp()
33          CommentComp()
34          // space
35          Divider()
36            .strokeWidth(8)
37            .color('#f5f5f5')
38          // reply
39          Column() {
40            Text('回复 7')
41              .width('100%')
42              .margin({ bottom: 15 })
43              .fontWeight(500)
44            ForEach(this.replyList, (item: ReplyItem) => {
45              ReplyComp({ item })
46            })
47          }
48          .padding({ left: 15, right: 15, top: 15 })
49        }
50      }
51      .padding({ bottom: 50 })
52
53      Row() {
54        TextInput({ placeholder: '回复~', text: this.content })
55          .placeholderColor('#c3c4c5')
56          .layoutWeight(1)
57          .onChange((value) => {
58            this.content = value
59          })
60        Text('发布')
61          .fontSize(14)
62          .fontColor('#09f')
63          .margin({ left: 15 })
64          .onClick(() => {
65            this.onReply()
66          })
67      }
68      .width('100%')
69      .height(50)
70      .padding({ left: 15, right: 15 })
71      .position({ y: '100%' })
72      .translate({ y: -50 })
73      .backgroundColor('#fff')
74      .border({ width: { top: 0.5 }, color: '#e4e4e4' })
75    }
76
77  }
78}
79
80@Component
81struct ReplyComp {
82  @ObjectLink
83  item: ReplyItem
84
85  onLike() {
86    if (this.item.likeFlag) {
87      this.item.likeNum--
88      this.item.likeFlag = false
89      promptAction.showToast({ message: '取消点赞' })
90    } else {
91      this.item.likeNum++
92      this.item.likeFlag = true
93      promptAction.showToast({ message: '点赞成功' })
94    }
95  }
96
97  build() {
98    Row() {
99      Image(this.item.avatar)
100        .width(32)
101        .height(32)
102        .borderRadius(16)
103      Column() {
104        Text(this.item.author)
105          .fontSize(15)
106          .fontWeight(FontWeight.Bold)
107          .margin({ bottom: 5 })
108        Text(this.item.content)
109          .margin({ bottom: 5 })
110          .fontColor('#565656')
111          .lineHeight(20)
112        Row() {
113          Text(`${this.item.time}•IP 属地${this.item.area}`)
114            .layoutWeight(1)
115            .fontSize(14)
116            .fontColor('#c3c4c5')
117          Row() {
118            Image($r('app.media.heart'))
119              .width(14)
120              .height(14)
121              .fillColor(this.item.likeFlag ? '#ff6600' : '#c3c4c5')
122              .margin({ right: 4 })
123            Text(this.item.likeNum.toString())
124              .fontSize(14)
125              .fontColor(this.item.likeFlag ? '#ff6600' : '#c3c4c5')
126          }
127          .onClick(() => {
128            this.onLike()
129          })
130        }
131      }
132      .layoutWeight(1)
133      .padding({ left: 10 })
134      .alignItems(HorizontalAlign.Start)
135    }
136    .width('100%')
137    .padding({ bottom: 15 })
138    .alignItems(VerticalAlign.Top)
139  }
140}
141
142@Component
143struct NavComp {
144  build() {
145    // nav
146    Row() {
147      Row() {
148        Image($r('app.media.ic_public_arrow_left'))
149          .width(12)
150          .height(12)
151          .fillColor('#848484')
152      }
153      .width(24)
154      .height(24)
155      .borderRadius(12)
156      .backgroundColor('#f5f5f5')
157      .justifyContent(FlexAlign.Center)
158      .margin({ left: 13 })
159
160      Text('评论回复')
161        .padding({ right: 50 })
162        .textAlign(TextAlign.Center)
163        .fontSize(18)
164        .layoutWeight(1)
165    }
166    .height(50)
167
168  }
169}
170
171@Component
172struct CommentComp {
173  build() {
174    // comment
175    Row() {
176      Image('https://picx.zhimg.com/1754b6bd9_xl.jpg?source=c885d018')
177        .width(32)
178        .height(32)
179        .borderRadius(16)
180      Column() {
181        Text('欧洲足球锦标赛')
182          .fontSize(15)
183          .fontWeight(FontWeight.Bold)
184          .margin({ bottom: 5 })
185        Text('14-0!欧洲杯超级惨案+刷爆纪录!姆巴佩帽子戏法,法国7连胜,怎么评价这场比赛?')
186          .margin({ bottom: 5 })
187          .fontColor('#565656')
188          .lineHeight(20)
189        Row() {
190          Text('10-21•IP 属地辽宁')
191            .layoutWeight(1)
192            .fontSize(14)
193            .fontColor('#c3c4c5')
194          Row() {
195            Image($r('app.media.heart'))
196              .width(14)
197              .height(14)
198              .fillColor('#c3c4c5')
199              .margin({ right: 4 })
200            Text('100')
201              .fontSize(14)
202              .fontColor('#c3c4c5')
203          }
204        }
205      }
206      .layoutWeight(1)
207      .padding({ left: 10 })
208      .alignItems(HorizontalAlign.Start)
209    }
210    .width('100%')
211    .padding({ left: 15, right: 15, bottom: 15 })
212    .alignItems(VerticalAlign.Top)
213  }
214}
注意:
  • 对象需要通过构造函数初始化
  • 需要嵌套组件,因为需要使用 @ObjectLink

应用状态

TIP

关于应用状态相关的内容需要使用模拟器或真机调试

1. UIAbility内状态-LocalStorage

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

1)页面内共享

  • 创建 LocalStorage 实例:const storage = new LocalStorage({ key: value })
  • 单向 @LocalStorageProp('user') 组件内可变
  • 双向 @LocalStorageLink('user') 全局均可变
1class User {
2  name?: string
3  age?: number
4}
5const storage = new LocalStorage({
6  user: { name: 'jack', age: 18 }
7})
8
9@Entry(storage)
10@Component
11struct Index {
12  @LocalStorageProp('user')
13  user: User = {}
14
15  build() {
16    Column({ space: 15 }){
17      Text('Index:')
18      Text(this.user.name + this.user.age)
19      Divider()
20      ChildA()
21      Divider()
22      ChildB()
23    }
24    .width('100%')
25    .height('100%')
26    .justifyContent(FlexAlign.Center)
27  }
28}
29
30@Component
31struct ChildA {
32  @LocalStorageProp('user')
33  user: User = {}
34
35  build() {
36    Column({ space: 15 }){
37      Text('ChildA:')
38      Text(this.user.name + this.user.age)
39        .onClick(()=>{
40          this.user.age ++
41        })
42    }
43  }
44}
45
46@Component
47struct ChildB {
48  @LocalStorageLink('user')
49  user: User = {}
50
51  build() {
52    Column({ space: 15 }){
53      Text('ChildB:')
54      Text(this.user.name + this.user.age)
55        .onClick(()=>{
56          this.user.age ++
57        })
58    }
59  }
60}

2)页面间共享

  • UIAbility 创建 LocalStorage 通过 loadContent 提供给加载的窗口

  • 在页面使用 const storage = LocalStorage.GetShared() 得到实例,通过 @Entry(storage) 传入页面

entryAbility/EntryAbility.ts
1+  storage = new LocalStorage({
2+    user: { name: 'jack', age: 18 }
3+  })
4
5  onWindowStageCreate(windowStage: window.WindowStage) {
6    // Main window is created, set main page for this ability
7    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
8
9+    windowStage.loadContent('pages/Index', this.storage , (err, data) => {
10      if (err.code) {
11        hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
12        return;
13      }
14      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
15    });
16  }
models/index.ets
1export class User {
2  name?: string
3  age?: number
4}
pages/Index.ets
1import { User } from '../models'
2const storage = LocalStorage.GetShared()
3
4@Entry(storage)
5@Component
6struct Index {
7  @LocalStorageProp('user')
8  user: User = {}
9
10  build() {
11    Column({ space: 15 }) {
12      Text('Index:')
13      Text(this.user.name + this.user.age)
14        .onClick(()=>{
15          this.user.age ++
16        })
17      Navigator({ target: 'pages/OtherPage' }){
18        Text('Go Other Page')
19      }
20    }
21    .width('100%')
22    .height('100%')
23    .justifyContent(FlexAlign.Center)
24  }
25}
pages/OtherPage.ets
1import { User } from '../models'
2const storage = LocalStorage.GetShared()
3
4@Entry(storage)
5@Component
6struct OtherPage {
7  @LocalStorageLink('user')
8  user: User = {}
9
10  build() {
11    Column({ space: 15 }) {
12      Text('OtherPage:')
13      Text(this.user.name + this.user.age)
14        .onClick(()=>{
15          this.user.age ++
16        })
17    }
18    .width('100%')
19    .height('100%')
20    .justifyContent(FlexAlign.Center)
21  }
22}
TIP
  • 页面间共享需要要模拟器测试
  • 应用逻辑中使用参考 链接

2. 应用状态-AppStorage

AppStorage 是应用全局的UI状态存储,是和应用的进程绑定的,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储。

  • 如果是初始化使用 AppStorage.SetOrCreate(key,value)
  • 单向 @StorageProp('user') 组件内可变
  • 双向 @StorageLink('user') 全局均可变

1)通过UI装饰器使用

1import { User } from '../models'
2
3AppStorage.SetOrCreate<User>('user', { name: 'jack', age: 18 })
4
5@Entry
6@Component
7struct Index {
8  @StorageProp('user')
9  // 可忽略,编辑器类型错误
10  user: User = {}
11
12  build() {
13    Column({ space: 15 }) {
14      Text('Index:')
15      Text(this.user.name + this.user.age)
16        .onClick(() => {
17          this.user.age++
18        })
19      Divider()
20      ChildA()
21    }
22    .width('100%')
23    .height('100%')
24    .justifyContent(FlexAlign.Center)
25  }
26}
27
28@Component
29struct ChildA {
30  @StorageLink('user')
31  user: User = {}
32
33  build() {
34    Column({ space: 15 }){
35      Text('ChildA:')
36      Text(this.user.name + this.user.age)
37        .onClick(()=>{
38          this.user.age ++
39        })
40    }
41  }
42}

2)通过逻辑使用

  • AppStorage.Get<ValueType>(key) 获取数据
  • AppStorage.Set<ValueType>(key,value) 覆盖数据
  • const link: SubscribedAbstractProperty<ValueType> = AppStorage.Link(key) 覆盖数据
    • link.set(value) 修改
    • link.get() 获取
1import promptAction from '@ohos.promptAction'
2import { User } from '../models'
3
4AppStorage.SetOrCreate<User>('user', { name: 'jack', age: 18 })
5
6@Entry
7@Component
8struct Index {
9  @StorageLink('user')
10  user: User = {}
11
12  build() {
13    Column({ space: 15 }) {
14      Text('Index:')
15      Text(this.user.name + this.user.age)
16        .onClick(() => {
17          this.user.age++
18        })
19      Divider()
20      Text('Get()')
21        .onClick(() => {
22          // 仅获取
23          const user = AppStorage.Get<User>('user')
24          promptAction.showToast({
25            message: JSON.stringify(user)
26          })
27        })
28      Text('Set()')
29        .onClick(() => {
30          // 直接设置
31          AppStorage.Set<User>('user', {
32            name: 'tom',
33            age: 100
34          })
35          // 观察页面更新没
36        })
37      Text('Link()')
38        .onClick(() => {
39          // 获取user的prop
40          const user: SubscribedAbstractProperty<User> = AppStorage.Link('user')
41          user.set({
42            name: user.get().name,
43            // 获取后修改
44            age: user.get().age + 1
45          })
46        })
47    }
48    .width('100%')
49    .height('100%')
50    .justifyContent(FlexAlign.Center)
51  }
52}

3. 状态持久化-PersistentStorage

PersistentStorage 将选定的 AppStorage 属性保留在设备磁盘上。

DETAILS

UI和业务逻辑不直接访问 PersistentStorage 中的属性,所有属性访问都是对 AppStorage 的访问,AppStorage 中的更改会自动同步到 PersistentStorage

WARNING
  • 支持:number, string, boolean, enum 等简单类型;
  • 如果:要支持对象类型,可以转换成json字符串
  • 持久化变量最好是小于2kb的数据,如果开发者需要存储大量的数据,建议使用数据库api。

1)简单数据类型的持久化,和获取和修改

1import { User } from '../models'
2
3PersistentStorage.PersistProp('count', 100)
4
5@Entry
6@Component
7struct Index {
8  @StorageLink('count')
9  count: number = 0
10
11  build() {
12    Column({ space: 15 }) {
13      Text(this.count.toString())
14        .onClick(() => {
15          this.count++
16        })
17    }
18    .width('100%')
19    .height('100%')
20    .justifyContent(FlexAlign.Center)
21  }
22}

2)复杂数据类型的持久化,和获取和修改

1import promptAction from '@ohos.promptAction'
2import { User } from '../models'
3
4PersistentStorage.PersistProp('userJson', `{ "name": "jack", "age": 18 }`)
5
6@Entry
7@Component
8struct Index {
9  @StorageProp('userJson')
10  @Watch('onUpdateUser')
11  userJson: string = '{}'
12  @State
13  user: User = JSON.parse(this.userJson)
14
15  onUpdateUser() {
16    this.user = JSON.parse(this.userJson)
17  }
18
19  build() {
20    Column({ space: 15 }) {
21      Text('Index:')
22      Text(this.user.name + this.user.age)
23        .onClick(() => {
24          this.user.age++
25          // 修改
26          AppStorage.Set('userJson', JSON.stringify(this.user))
27        })
28      Divider()
29      Text('Get()')
30        .onClick(() => {
31          // 获取
32          const user = AppStorage.Get<string>('userJson')
33          promptAction.showToast({ message: user })
34        })
35    }
36    .width('100%')
37    .height('100%')
38    .justifyContent(FlexAlign.Center)
39  }
40}
TIP
  • 测试:需要在真机或模拟器调试

4. 设备环境-Environment

开发者如果需要应用程序运行的设备的环境参数,以此来作出不同的场景判断,比如多语言,暗黑模式等,需要用到 Environment 设备环境查询。

  • Environment 的所有属性都是不可变的(即应用不可写入),所有的属性都是简单类型。
1import i18n from '@ohos.i18n';
2// 获取系统语言
3const lang = i18n.getSystemLanguage()
4// 设置环境状态
5Environment.EnvProp('lang', lang);
6
7@Entry
8@Component
9struct Index{
10  @StorageProp('lang')
11  lang: string = ''
12
13  build() {
14    Column() {
15      Text(this.lang)
16        .onClick(()=>{
17          // 不能修改
18          // Environment.EnvProp('lang', 'en');
19        })
20    }
21    .width('100%')
22    .height('100%')
23    .justifyContent(FlexAlign.Center)
24  }
25}

网络管理

1. 应用权限

ATM (AccessTokenManager) 是HarmonyOS上基于AccessToken构建的统一的应用权限管理能力。

应用权限保护的对象可以分为数据和功能:

  • 数据包含了个人数据(如照片、通讯录、日历、位置等)、设备数据(如设备标识、相机、麦克风等)、应用数据。

  • 功能则包括了设备功能(如打电话、发短信、联网等)、应用功能(如弹出悬浮框、创建快捷方式等)等。

根据授权方式的不同,权限类型可分为system_grant(系统授权)和user_grant(用户授权)。

  • 配置文件权限声明

  • 向用户申请授权

例如:访问网络需要联网权限

module.json5
1{
2  "module" : {
3    // ...
4    "requestPermissions":[
5      {
6        "name" : "ohos.permission.INTERNET"
7      }
8    ]
9  }
10}
TIP

2. HTTP请求

请求服务器需要使用 IP 地址,或者域名。不可使用 localhost127.0.0.1

1)启动 json-server 服务,npm i json-server -g 全局安装。

  • 新建 data.json 文件在任意目录,比如 server 文件夹
1{
2  "takeaway": [
3    {
4      "tag": "318569657",
5      "name": "一人套餐",
6      "foods": [
7        {
8          "id": 8078956697,
9          "name": "烤羊肉串(10串)",
10          "like_ratio_desc": "好评度100%",
11          "month_saled": 40,
12          "unit": "10串",
13          "food_tag_list": [
14            "点评网友推荐"
15          ],
16          "price": 90,
17          "picture": "https://zqran.gitee.io/images/waimai/8078956697.jpg",
18          "description": "",
19          "tag": "318569657",
20          "count": 1
21        },
22        {
23          "id": 7384994864,
24          "name": "腊味煲仔饭",
25          "like_ratio_desc": "好评度81%",
26          "month_saled": 100,
27          "unit": "1人份",
28          "food_tag_list": [],
29          "price": 39,
30          "picture": "https://zqran.gitee.io/images/waimai/7384994864.jpg",
31          "description": "",
32          "tag": "318569657",
33          "count": 1
34        },
35        {
36          "id": 2305772036,
37          "name": "鸡腿胡萝卜焖饭",
38          "like_ratio_desc": "好评度91%",
39          "month_saled": 300,
40          "unit": "1人份",
41          "food_tag_list": [],
42          "price": 34.32,
43          "picture": "https://zqran.gitee.io/images/waimai/2305772036.jpg",
44          "description": "主料:大米、鸡腿、菜心、胡萝卜",
45          "tag": "318569657",
46          "count": 1
47        },
48        {
49          "id": 2233861812,
50          "name": "小份酸汤莜面鱼鱼+肉夹馍套餐",
51          "like_ratio_desc": "好评度73%",
52          "month_saled": 600,
53          "unit": "1人份",
54          "food_tag_list": [
55            "“口味好,包装很好~点赞”"
56          ],
57          "price": 34.32,
58          "picture": "https://zqran.gitee.io/images/waimai/2233861812.jpg",
59          "description": "酸汤莜面鱼鱼,主料:酸汤、莜面 肉夹馍,主料:白皮饼、猪肉",
60          "tag": "318569657",
61          "count": 1
62        }
63      ]
64    },
65    {
66      "tag": "82022594",
67      "name": "特色烧烤",
68      "foods": [
69        {
70          "id": 3823780596,
71          "name": "藤椒鸡肉串",
72          "like_ratio_desc": "",
73          "month_saled": 200,
74          "unit": "10串",
75          "food_tag_list": [
76            "点评网友推荐"
77          ],
78          "price": 6,
79          "picture": "https://zqran.gitee.io/images/waimai/3823780596.jpg",
80          "description": "1串。藤椒味,主料:鸡肉",
81          "tag": "82022594",
82          "count": 1
83        },
84        {
85          "id": 6592009498,
86          "name": "烤羊排",
87          "like_ratio_desc": "",
88          "month_saled": 50,
89          "unit": "1人份",
90          "food_tag_list": [],
91          "price": 169,
92          "picture": "https://zqran.gitee.io/images/waimai/6592009498.jpg",
93          "description": "6-8个月草原羔羊肋排,烤到皮脆肉香",
94          "tag": "82022594",
95          "count": 1
96        }
97      ]
98    },
99    {
100      "tag": "98147100",
101      "name": "杂粮主食",
102      "foods": [
103        {
104          "id": 4056954171,
105          "name": "五常稻花香米饭",
106          "like_ratio_desc": "",
107          "month_saled": 1000,
108          "unit": "约300克",
109          "food_tag_list": [],
110          "price": 5,
111          "picture": "https://zqran.gitee.io/images/waimai/4056954171.jpg",
112          "description": "浓浓的稻米清香,软糯Q弹有嚼劲",
113          "tag": "98147100",
114          "count": 1
115        },
116        {
117          "id": 740430262,
118          "name": "小米发糕(3个)",
119          "like_ratio_desc": "好评度100%",
120          "month_saled": 100,
121          "unit": "3块",
122          "food_tag_list": [],
123          "price": 13,
124          "picture": "https://zqran.gitee.io/images/waimai/740430262.jpg",
125          "description": "柔软蓬松,葡萄干和蔓越莓酸甜适口",
126          "tag": "98147100",
127          "count": 1
128        },
129        {
130          "id": 7466390504,
131          "name": "沙枣玉米窝头(3个)",
132          "like_ratio_desc": "好评度100%",
133          "month_saled": 100,
134          "unit": "3个",
135          "food_tag_list": [],
136          "price": 13,
137          "picture": "https://zqran.gitee.io/images/waimai/7466390504.jpg",
138          "description": "",
139          "tag": "98147100",
140          "count": 1
141        }
142      ]
143    }
144  ]
145}
  • 进入 server 文件夹,按照下面命令启动接口服务器,查看本机IP ipconfig | ifconfig
1json-server data.json --host 192.168.0.1

2)使用 @ohos.net.http 模块发请求

1import http from '@ohos.net.http'
2
3const req = http.createHttp()
4req.request('https://zhoushugang.gitee.io/fe2022/takeaway.json')
5  .then(res => {
6    console.log('MEITUAN', res.result.toString().replace(/\n/g, '').substr(0, 250))  // === 3  注意:字符长度大于1024字节,控制台不可见。
7    const data = JSON.parse(res.result as string)
8    console.log('MEITUAN', data.length)
9  })
10  .catch(err => {
11    console.error('MEITUAN', err.message)
12  })