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}
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}
小结:
@Builder
更轻量其他:
GridRow
GridCol
栅格布局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}
@Builder
复用逻辑的时候,支持传参可以更灵活的渲染UI状态数据
,不过建议通过对象的方式传入 @Builder
@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}
@BuilderParam
的时候,使用组件的时候在尾随 {}
插入UI结构@BuilderParam
的时候,使用组件的时候 Comp({ xxx: this.builderFn })
传入@Builder
函数作为 @BuilderParam
备用函数,当做备用内容使用在一些系统组件中,根据配置无法达到预期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}
@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}
string、number、boolean、enum
类型Prop
数据值,但不同步到父组件,父组件更新后覆盖子组件 Prop
数据子组件中被@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}
this.
改成 $
,子组件 @Link
修饰数据
@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}
Object、class、string、number、boolean、enum
类型均支持@Provide('key')
和 @Consume('key')
key需要保持一致如果开发者需要关注某个状态变量的值是否改变,可以使用
@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}
之前我们通过 赋值的方式 修改嵌套对象或对象数组这类复杂数据来更新UI,会影响对象对应所有UI更新; 通过
@Observed
与@ObjectLink
可以优化这个问题;
使用步骤:
类 class
数据模拟需要定义通过构造函数,使用 @Observed
修饰这个类
初始化数据:需要通过初始化构造函数的方式添加
通过 @ObjectLink
关联对象,可以直接修改被关联对象来更新UI
需求:改造下知乎评论案例
1)定义构造函数和使用@Observed
修饰符,以及初始化数据
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
修饰符获取数据
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
关于应用状态相关的内容需要使用模拟器或真机调试
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)
传入页面
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 }
1export class User {
2 name?: string
3 age?: number
4}
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}
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}
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}
PersistentStorage
将选定的AppStorage
属性保留在设备磁盘上。
UI和业务逻辑不直接访问 PersistentStorage
中的属性,所有属性访问都是对 AppStorage
的访问,AppStorage
中的更改会自动同步到 PersistentStorage
。
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}
开发者如果需要应用程序运行的设备的环境参数,以此来作出不同的场景判断,比如多语言,暗黑模式等,需要用到
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}
ATM (AccessTokenManager) 是HarmonyOS上基于AccessToken构建的统一的应用权限管理能力。
应用权限保护的对象可以分为数据和功能:
数据包含了个人数据(如照片、通讯录、日历、位置等)、设备数据(如设备标识、相机、麦克风等)、应用数据。
功能则包括了设备功能(如打电话、发短信、联网等)、应用功能(如弹出悬浮框、创建快捷方式等)等。
根据授权方式的不同,权限类型可分为system_grant(系统授权)和user_grant(用户授权)。
配置文件权限声明
向用户申请授权
例如:访问网络需要联网权限
1{
2 "module" : {
3 // ...
4 "requestPermissions":[
5 {
6 "name" : "ohos.permission.INTERNET"
7 }
8 ]
9 }
10}
请求服务器需要使用 IP 地址,或者域名。不可使用
localhost
或127.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 })