鸿蒙(HarmonyOS)和 Flutter 异同,都提供了状态管理的能力。尽管它们目标类似(构建高性能 UI 应用),但底层技术栈、编程语言和生态系统有很大不同,因此它们的状态管理机制在实现方式和设计理念上既有相似之处,也存在明显差异。下面我们从几个方面进行对比:
🌟 一、共同点(相似之处)
1. 响应式编程理念
-
Flutter 和 鸿蒙的ArkTS(ArkUI) 都采用响应式 UI 编程方式。
-
状态变化后,界面会根据状态自动重建或刷新,这类似于 React/Flutter 的“数据驱动 UI”。
2. 数据驱动 UI
-
都强调“状态即 UI”,你管理好状态,界面会自动更新。
-
通过绑定机制(例如
@State)或状态容器(如 Provider、Riverpod / @Observed)来控制视图更新。
3. 组件化开发
-
Flutter 的 Widget 和 鸿蒙的组件(Component)类似,都是构建 UI 的基础单元。
-
状态绑定基本上是针对这些组件生效。
🔍 二、差异点(核心差异)
| 方面 | Flutter | 鸿蒙(ArkUI with ArkTS) |
|---|---|---|
| 语言 | Dart | TypeScript 超集(ArkTS) |
| UI 构建方式 | 声明式 Widget 树 | 声明式组件树(类似 React) |
| 状态变量声明 | 使用 StatefulWidget + setState(),或 Riverpod、Provider 等 | 使用装饰器如 @State, @Prop, @Link, @Observed 等 |
| 状态更新方式 | setState() 或依赖注入框架 | 修改变量即可触发更新,如 this.count++ |
| 框架内置状态管理机制 | StatefulWidget 是基础,但需手动调用 setState();支持第三方如 Bloc、Provider | 原生支持响应式变量 + 装饰器绑定(零样板代码) |
| 跨组件状态传递 | 依赖状态提升、InheritedWidget、状态管理库 | 使用 @Prop, @Link, @Provide 等,天然支持父子通信 |
| 第三方生态支持 | 非常丰富(如 Riverpod, Bloc, MobX, GetX) | 生态尚在发展中,主要依赖官方提供的机制 |
| 开发体验 | 热重载 + 丰富工具支持 | DevEco Studio 支持 ArkTS 热重载,但 IDE 体验稍弱 |
案例介绍
自定义构建函数#
1. 构建函数-@Builder#
ArkUI还提供了一种更轻量的UI元素复用机制
@Builder,可以将重复使用的UI元素抽象成一个方法,在build方法里调用。
- 组件内定义
- 全局定义
1)组件内定义
@Builder MyBuilderFunction() {}
this.MyBuilderFunction()
2)全局定义
@Builder function MyGlobalBuilderFunction() {}
MyGlobalBuilderFunction()
📕📕📕 练习案例→商品详情-更多按钮

- 待实现页面
@Entry
@Component
struct Index {
build() {
Column() {
GridRow({ columns: 2, gutter: 15 }) {
GridCol({ span: 2 }) {
Column() {
Row() {
Text('评价(2000+)')
.layoutWeight(1)
.fontWeight(600)
// TODO
}
.padding(10)
Row()
.height(100)
}
.borderRadius(12)
.backgroundColor('#fff')
}
GridCol() {
Column() {
Row() {
Text('推荐')
.layoutWeight(1)
.fontWeight(600)
// TODO
}
.padding(10)
Row()
.height(100)
}
.borderRadius(12)
.backgroundColor('#fff')
}
GridCol() {
Column() {
Row() {
Text('体验')
.layoutWeight(1)
.fontWeight(600)
// TODO
}
.padding(10)
Row()
.height(100)
}
.borderRadius(12)
.backgroundColor('#fff')
}
}
}
.height('100%')
.padding(15)
.backgroundColor('#f5f5f5')
}
}
- 使用 @Builder 提取UI结构
@Entry
@Component
struct Index {
@Builder
MoreBuilder () {
Row() {
Text('查看更多')
.fontSize(14)
.fontColor('#666666')
Image($r('app.media.ic_public_arrow_right'))
.width(16)
.fillColor('#666666')
}
}
build() {
Column() {
GridRow({ columns: 2, gutter: 15 }) {
GridCol({ span: 2 }) {
Column() {
Row() {
Text('评价(2000+)')
.layoutWeight(1)
.fontWeight(600)
this.MoreBuilder()
}
.padding(10)
Row()
.height(100)
}
.borderRadius(12)
.backgroundColor('#fff')
}
GridCol() {
Column() {
Row() {
Text('推荐')
.layoutWeight(1)
.fontWeight(600)
this.MoreBuilder()
}
.padding(10)
Row()
.height(100)
}
.borderRadius(12)
.backgroundColor('#fff')
}
GridCol() {
Column() {
Row() {
Text('体验')
.layoutWeight(1)
.fontWeight(600)
this.MoreBuilder()
}
.padding(10)
Row()
.height(100)
}
.borderRadius(12)
.backgroundColor('#fff')
}
}
}
.height('100%')
.padding(15)
.backgroundColor('#f5f5f5')
}
}
TIP
小结:
- 遇到非遍历情况下,一个组件分散着相同的UI结构,可以使用
@Builder更轻量
其他:
GridRowGridCol栅格布局
2. 构建函数-传参传递#
1)按值传递(场景:构建不同的UI)
@Builder MyBuilderFunction( title: string ) {}
this.MyBuilderFunction('Title')
需求:不同板块查看更多文案不一样
-
评价 好评率 98%
-
推荐 查看全部
-
体验 4 条测评

@Builder
MoreBuilder(title: string) {
Row() {
Text(title)
.fontSize(14)
.fontColor('#666666')
Image($r('app.media.ic_public_arrow_right'))
.width(16)
.fillColor('#666666')
}
}
this.MoreBuilder('好评率 98%')
this.MoreBuilder('查看全部')
this.MoreBuilder('4 条测评')
2)引用传递(场景:当传递的数据更新,需要更新UI)
需求:
-
点击按钮后模拟加载好评率数据

@Entry
@Component
struct Index {
@State
rate: number = 0
@Builder
MoreBuilder(params: { title: string }) {
Row() {
Text(params.title)
.fontSize(14)
.fontColor('#666666')
Image($r('app.media.ic_public_arrow_right'))
.width(16)
.fillColor('#666666')
}
}
build() {
Column() {
Button('获取数据')
.margin({ bottom: 15 })
.onClick(() => {
this.rate = 99
})
GridRow({ columns: 2, gutter: 15 }) {
GridCol({ span: 2 }) {
Column() {
Row() {
Text('评价(2000+)')
.layoutWeight(1)
.fontWeight(600)
this.MoreBuilder({ title: `好评率 ${this.rate} %` })
}
.padding(10)
Row()
.height(100)
}
.borderRadius(12)
.backgroundColor('#fff')
}
GridCol() {
Column() {
Row() {
Text('推荐')
.layoutWeight(1)
.fontWeight(600)
this.MoreBuilder({ title: '查看全部' })
}
.padding(10)
Row()
.height(100)
}
.borderRadius(12)
.backgroundColor('#fff')
}
GridCol() {
Column() {
Row() {
Text('体验')
.layoutWeight(1)
.fontWeight(600)
this.MoreBuilder({ title: '4 条测评' })
}
.padding(10)
Row()
.height(100)
}
.borderRadius(12)
.backgroundColor('#fff')
}
}
}
.height('100%')
.padding(15)
.backgroundColor('#f5f5f5')
}
}
TIP
- 使用
@Builder复用逻辑的时候,支持传参可以更灵活的渲染UI - 参数可以使用
状态数据,不过建议通过对象的方式传入@Builder
3. 构建函数-@BuilderParam 传递UI#
@BuilderParam该装饰器用于声明任意UI描述的一个元素,类似 slot 占位符
前置知识
组件属性初始化:
- 定义组件声明属性
title: string - 使用组件初始化属性
Comp({ title: string })
-
尾随闭包初始化组件
- 组件内有且仅有一个使用
@BuilderParam装饰的属性
- 组件内有且仅有一个使用
-
参数初始化组件
-
组件内有多个使用
@BuilderParam装饰器属性
-
1)尾随闭包初始化组件(默认插槽)
需求:
-
标题文字和更多文案通过属性传入
-
内容结构需要传入

@Component
struct PanelComp {
title: string
more: string
@BuilderParam
panelContent: () => void = this.DefaultPanelContent
// 备用 Builder
@Builder
DefaultPanelContent() {
Text('默认内容')
}
build() {
Column() {
Row() {
Text(this.title)
.layoutWeight(1)
.fontWeight(600)
Row() {
Text(this.more)
.fontSize(14)
.fontColor('#666666')
Image($r('app.media.ic_public_arrow_right'))
.width(16)
.fillColor('#666666')
}
}
.padding(10)
Row() {
this.panelContent()
}
.height(100)
}
.borderRadius(12)
.backgroundColor('#fff')
}
}
@Entry
@Component
struct Index {
build() {
Column() {
GridRow({ columns: 2, gutter: 15 }) {
GridCol({ span: 2 }) {
PanelComp({ title: '评价(2000+)', more: '好评率98%' })
}
GridCol() {
PanelComp({ title: '推荐', more: '查看全部' }) {
Text('推荐内容')
}
}
GridCol() {
PanelComp({ title: '体验', more: '4 条测评' }) {
Text('体验内容')
}
}
}
}
.height('100%')
.padding(15)
.backgroundColor('#f5f5f5')
}
}
2)参数初始化组件(具名插槽)
需求:需要传入内容结构和底部结构

@Component
struct PanelComp {
title: string
more: string
@BuilderParam
panelContent: () => void
@BuilderParam
panelFooter: () => void
build() {
Column() {
Row() {
Text(this.title)
.layoutWeight(1)
.fontWeight(600)
Row() {
Text(this.more)
.fontSize(14)
.fontColor('#666666')
Image($r('app.media.ic_public_arrow_right'))
.width(16)
.fillColor('#666666')
}
}
.padding(10)
Row() {
this.panelContent()
}
.height(100)
Row() {
this.panelFooter()
}
.height(50)
}
.borderRadius(12)
.backgroundColor('#fff')
}
}
@Entry
@Component
struct Index {
@Builder
ContentBuilderA() {
Text('评价内容')
}
@Builder
FooterBuilderA() {
Text('评价底部')
}
build() {
Column() {
GridRow({ columns: 2, gutter: 15 }) {
GridCol({ span: 2 }) {
PanelComp({
title: '评价(2000+)',
more: '好评率98%',
panelFooter: this.FooterBuilderA,
panelContent: this.ContentBuilderA
})
}
// GridCol() {
// PanelComp({ title: '推荐', more: '查看全部' }){
// Text('推荐内容')
// }
// }
// GridCol() {
// PanelComp({ title: '体验', more: '4 条测评' }){
// Text('体验内容')
// }
// }
}
}
.height('100%')
.padding(15)
.backgroundColor('#f5f5f5')
}
}
TIP
- 当子组件使用一个
@BuilderParam的时候,使用组件的时候在尾随{}插入UI结构 - 当子组件使用多个
@BuilderParam的时候,使用组件的时候Comp({ xxx: this.builderFn })传入 - 子组件本身可以提供一个默认的
@Builder函数作为@BuilderParam备用函数,当做备用内容使用
4. 构建函数-系统组件自定义UI#
在一些系统组件中,根据配置无法达到预期UI,可以使用 @Builder 构建函数自定义UI,前提该组件支持自定义。

需求:自定义 Tabs 组件的 TabBar UI结构
class ToolBarItem {
defaultIcon: string | Resource
activeIcon: string | Resource
label: string
}
@Entry
@Component
struct Index {
@State
activeIndex: number = 0
toolBars: ToolBarItem[] = [
{ defaultIcon: $r('app.media.home'), activeIcon: $r('app.media.home_select'), label: '首页' },
{ defaultIcon: $r('app.media.project'), activeIcon: $r('app.media.project_select'), label: '项目' },
{ defaultIcon: $r('app.media.interview'), activeIcon: $r('app.media.interview_select'), label: '面经' },
{ defaultIcon: $r('app.media.mine'), activeIcon: $r('app.media.mine_select'), label: '我的' }
]
@Builder
TabBarBuilder(item: ToolBarItem, index: number) {
Column() {
Image(this.activeIndex === index ? item.activeIcon : item.defaultIcon)
.width(24)
Text(item.label)
.fontSize(12)
.margin({ top: 4 })
.lineHeight(12)
.fontColor(this.activeIndex === index ? '#000' : '#aaa')
}
}
build() {
Tabs({
index: this.activeIndex
}) {
ForEach(this.toolBars, (item: ToolBarItem, index: number) => {
TabContent() {
Text(index.toString())
}
.tabBar(this.TabBarBuilder(item, index))
})
}
.barPosition(BarPosition.End)
.onChange(index => this.activeIndex = index)
}
}
组件状态共享#
1. 状态共享-父子单向#
@Prop装饰的变量可以和父组件建立单向的同步关系。@Prop装饰的变量是可变的,但是变化不会同步回其父组件。
@Entry
@Component
struct Index {
@State
money: number = 0
build() {
Column({ space: 20 }) {
Text('父组件:' + this.money)
.onClick(() => {
this.money++
})
Child({ money: this.money })
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
}
}
@Component
struct Child {
@Prop
money: number
build() {
Text('子组件:' + this.money)
.onClick(() => {
this.money++
})
}
}
TIP
- 支持类型
string、number、boolean、enum类型 - 子组件可修改
Prop数据值,但不同步到父组件,父组件更新后覆盖子组件Prop数据 - 子组件可以初始化默认值,注意:目前编译器会提示错误,请忽略,下个版本将修复
2. 状态共享-父子双向#
子组件中被@Link装饰的变量与其父组件中对应的数据源建立双向数据绑定。
1)简单类型 string、number、boolean、enum
@Entry
@Component
struct Index {
@State
money: number = 0
build() {
Column({ space: 20 }) {
Text('父组件:' + this.money)
.onClick(() => {
this.money++
})
Child({ money: $money })
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
}
}
@Component
struct Child {
@Link
money: number
build() {
Text('子组件:' + this.money)
.onClick(() => {
this.money++
})
}
}
2)复杂类型 Object、class
class Person {
name: string
age: number
}
@Entry
@Component
struct Index {
@State
person: Person = { name: 'jack', age: 18 }
build() {
Column({ space: 20 }){
Text('父组件:' + `${this.person.name} 今年 ${ this.person.age } 岁`)
.onClick(() => {
this.person.age ++
})
Child({ person: $person })
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
}
}
@Component
struct Child {
@Link
person: Person
build() {
Text('子组件:' + `${this.person.name} 今年 ${ this.person.age } 岁`)
.onClick(() => {
this.person.age ++
})
}
}
TIP
- 父组件传值的时候需要
this.改成$,子组件@Link修饰数据
3. 状态共享-后代组件#
@Provide和@Consume,应用于与后代组件的双向数据同步,应用于状态数据在多个层级之间传递的场景。
1)通过相同的变量名绑定
@Entry
@Component
struct Index {
@Provide
money: number = 0
build() {
Column({ space: 20 }) {
Text('父组件:' + this.money)
.onClick(() => {
this.money++
})
Parent()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
@Component
struct Parent {
@Consume
money: number
build() {
Column({ space: 20 }) {
Text('父组件:' + this.money)
.onClick(() => {
this.money++
})
Child()
}
}
}
@Component
struct Child {
@Consume
money: number
build() {
Text('子组件:' + this.money)
.onClick(() => {
this.money++
})
}
}
TIP
Object、class、string、number、boolean、enum类型均支持- 通过相同的变量别名绑定
@Provide('key')和@Consume('key')key需要保持一致
4. 状态共享-状态监听器#
如果开发者需要关注某个状态变量的值是否改变,可以使用
@Watch为状态变量设置回调函数。
-
@State、@Prop、@Link等装饰器在@Watch装饰之前
import promptAction from '@ohos.promptAction'
@Component
struct Child {
@Prop
@Watch('onActiveIndex')
activeIndex: number
onActiveIndex() {
promptAction.showToast({ message: '监听变化' })
}
build() {
Column() {
Text('Child' + this.activeIndex)
}
}
}
@Entry
@Component
struct Index {
@State activeIndex: number = 0
onChange(index: number) {
this.activeIndex = index
promptAction.showToast({ message: '点击' })
}
build() {
Navigation() {
Child({ activeIndex: this.activeIndex })
}.toolBar({
items: [
{ value: '首页', action: () => this.onChange(0) },
{ value: '我的', action: () => this.onChange(1) },
]
})
}
}
TIP
- 在第一次初始化的时候,@Watch装饰的方法不会被调用
5. @Observed与@ObjectLink#
之前我们通过 赋值的方式 修改嵌套对象或对象数组这类复杂数据来更新UI,会影响对象对应所有UI更新; 通过
@Observed与@ObjectLink可以优化这个问题;
使用步骤:
-
类
class数据模拟需要定义通过构造函数,使用@Observed修饰这个类 -
初始化数据:需要通过初始化构造函数的方式添加
-
通过
@ObjectLink关联对象,可以直接修改被关联对象来更新UI
需求:改造下知乎评论案例
1)定义构造函数和使用@Observed 修饰符,以及初始化数据
models/index.ets
@Observed
export class ReplyItem {
id: number
avatar: string | Resource
author: string
content: string
time: string
area: string
likeNum: number
likeFlag?: boolean
constructor(item: ReplyItem) {
for (const key in item) {
this[key] = item[key]
}
}
}
export const replyList: ReplyItem[] = [
new ReplyItem({
id: 1,
avatar: 'https://picx.zhimg.com/027729d02bdf060e24973c3726fea9da_l.jpg?source=06d4cd63',
author: '偏执狂-妄想家',
content: '更何况还分到一个摩洛哥[惊喜]',
time: '11-30',
area: '海南',
likeNum: 34
}),
new ReplyItem({
id: 2,
avatar: 'https://pic1.zhimg.com/v2-5a3f5190369ae59c12bee33abfe0c5cc_xl.jpg?source=32738c0c',
author: 'William',
content: '当年希腊可是把1:0发挥到极致了',
time: '11-29',
area: '北京',
likeNum: 58
}),
new ReplyItem({
id: 3,
avatar: 'https://picx.zhimg.com/v2-e6f4605c16e4378572a96dad7eaaf2b0_l.jpg?source=06d4cd63',
author: 'Andy Garcia',
content: '欧洲杯其实16队球队打正赛已经差不多,24队打正赛意味着正赛阶段在小组赛一样有弱队。',
time: '11-28',
area: '上海',
likeNum: 10
}),
new ReplyItem({
id: 4,
avatar: 'https://picx.zhimg.com/v2-53e7cf84228e26f419d924c2bf8d5d70_l.jpg?source=06d4cd63',
author: '正宗好鱼头',
content: '确实眼红啊,亚洲就没这种球队,让中国队刷',
time: '11-27',
area: '香港',
likeNum: 139
}),
new ReplyItem({
id: 5,
avatar: 'https://pic1.zhimg.com/v2-eeddfaae049df2a407ff37540894c8ce_l.jpg?source=06d4cd63',
author: '柱子哥',
content: '我是支持扩大的,亚洲杯欧洲杯扩到32队,世界杯扩到64队才是好的,世界上有超过200支队伍,欧洲区55支队伍,亚洲区47支队伍,即使如此也就六成出现率',
time: '11-27',
area: '旧金山',
likeNum: 29
}),
new ReplyItem({
id: 6,
avatar: 'https://picx.zhimg.com/v2-fab3da929232ae911e92bf8137d11f3a_l.jpg?source=06d4cd63',
author: '飞轩逸',
content: '禁止欧洲杯扩军之前,应该先禁止世界杯扩军,或者至少把亚洲名额一半给欧洲。',
time: '11-26',
area: '里约',
likeNum: 100
})
]
2)嵌套的对象,或者数组中的对象,传入子组件,组件使用 @ObjectLink 修饰符获取数据
pages/Index.ets
import promptAction from '@ohos.promptAction'
import { ReplyItem, replyList } from '../models'
@Entry
@Component
struct Index {
@State
replyList: ReplyItem[] = replyList
@State
content: string = ''
onReply() {
const reply: ReplyItem = new ReplyItem({
id: Math.random(),
content: this.content,
author: 'Zhousg',
avatar: $r('app.media.avatar'),
time: '12-01',
likeNum: 0,
area: '北京'
})
this.replyList.unshift(reply)
this.content = ''
promptAction.showToast({ message: '回复成功' })
}
build() {
Stack() {
Scroll() {
Column() {
NavComp()
CommentComp()
// space
Divider()
.strokeWidth(8)
.color('#f5f5f5')
// reply
Column() {
Text('回复 7')
.width('100%')
.margin({ bottom: 15 })
.fontWeight(500)
ForEach(this.replyList, (item: ReplyItem) => {
ReplyComp({ item })
})
}
.padding({ left: 15, right: 15, top: 15 })
}
}
.padding({ bottom: 50 })
Row() {
TextInput({ placeholder: '回复~', text: this.content })
.placeholderColor('#c3c4c5')
.layoutWeight(1)
.onChange((value) => {
this.content = value
})
Text('发布')
.fontSize(14)
.fontColor('#09f')
.margin({ left: 15 })
.onClick(() => {
this.onReply()
})
}
.width('100%')
.height(50)
.padding({ left: 15, right: 15 })
.position({ y: '100%' })
.translate({ y: -50 })
.backgroundColor('#fff')
.border({ width: { top: 0.5 }, color: '#e4e4e4' })
}
}
}
@Component
struct ReplyComp {
@ObjectLink
item: ReplyItem
onLike() {
if (this.item.likeFlag) {
this.item.likeNum--
this.item.likeFlag = false
promptAction.showToast({ message: '取消点赞' })
} else {
this.item.likeNum++
this.item.likeFlag = true
promptAction.showToast({ message: '点赞成功' })
}
}
build() {
Row() {
Image(this.item.avatar)
.width(32)
.height(32)
.borderRadius(16)
Column() {
Text(this.item.author)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 5 })
Text(this.item.content)
.margin({ bottom: 5 })
.fontColor('#565656')
.lineHeight(20)
Row() {
Text(`${this.item.time}•IP 属地${this.item.area}`)
.layoutWeight(1)
.fontSize(14)
.fontColor('#c3c4c5')
Row() {
Image($r('app.media.heart'))
.width(14)
.height(14)
.fillColor(this.item.likeFlag ? '#ff6600' : '#c3c4c5')
.margin({ right: 4 })
Text(this.item.likeNum.toString())
.fontSize(14)
.fontColor(this.item.likeFlag ? '#ff6600' : '#c3c4c5')
}
.onClick(() => {
this.onLike()
})
}
}
.layoutWeight(1)
.padding({ left: 10 })
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding({ bottom: 15 })
.alignItems(VerticalAlign.Top)
}
}
@Component
struct NavComp {
build() {
// nav
Row() {
Row() {
Image($r('app.media.ic_public_arrow_left'))
.width(12)
.height(12)
.fillColor('#848484')
}
.width(24)
.height(24)
.borderRadius(12)
.backgroundColor('#f5f5f5')
.justifyContent(FlexAlign.Center)
.margin({ left: 13 })
Text('评论回复')
.padding({ right: 50 })
.textAlign(TextAlign.Center)
.fontSize(18)
.layoutWeight(1)
}
.height(50)
}
}
@Component
struct CommentComp {
build() {
// comment
Row() {
Image('https://picx.zhimg.com/1754b6bd9_xl.jpg?source=c885d018')
.width(32)
.height(32)
.borderRadius(16)
Column() {
Text('欧洲足球锦标赛')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 5 })
Text('14-0!欧洲杯超级惨案+刷爆纪录!姆巴佩帽子戏法,法国7连胜,怎么评价这场比赛?')
.margin({ bottom: 5 })
.fontColor('#565656')
.lineHeight(20)
Row() {
Text('10-21•IP 属地辽宁')
.layoutWeight(1)
.fontSize(14)
.fontColor('#c3c4c5')
Row() {
Image($r('app.media.heart'))
.width(14)
.height(14)
.fillColor('#c3c4c5')
.margin({ right: 4 })
Text('100')
.fontSize(14)
.fontColor('#c3c4c5')
}
}
}
.layoutWeight(1)
.padding({ left: 10 })
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding({ left: 15, right: 15, bottom: 15 })
.alignItems(VerticalAlign.Top)
}
}
注意:
- 对象需要通过构造函数初始化
- 需要嵌套组件,因为需要使用
@ObjectLink
应用状态#
TIP
关于应用状态相关的内容需要使用模拟器或真机调试
1. UIAbility内状态-LocalStorage#
LocalStorage是页面级的UI状态存储,通过@Entry装饰器接收的参数可以在页面内共享同一个LocalStorage实例。LocalStorage也可以在UIAbility内,页面间共享状态。
1)页面内共享
- 创建
LocalStorage实例:const storage = new LocalStorage({ key: value }) - 单向
@LocalStorageProp('user')组件内可变 - 双向
@LocalStorageLink('user')全局均可变
import promptAction from '@ohos.promptAction'
import { ReplyItem, replyList } from '../models'
@Entry
@Component
struct Index {
@State
replyList: ReplyItem[] = replyList
@State
content: string = ''
onReply() {
const reply: ReplyItem = new ReplyItem({
id: Math.random(),
content: this.content,
author: 'Zhousg',
avatar: $r('app.media.avatar'),
time: '12-01',
likeNum: 0,
area: '北京'
})
this.replyList.unshift(reply)
this.content = ''
promptAction.showToast({ message: '回复成功' })
}
build() {
Stack() {
Scroll() {
Column() {
NavComp()
CommentComp()
// space
Divider()
.strokeWidth(8)
.color('#f5f5f5')
// reply
Column() {
Text('回复 7')
.width('100%')
.margin({ bottom: 15 })
.fontWeight(500)
ForEach(this.replyList, (item: ReplyItem) => {
ReplyComp({ item })
})
}
.padding({ left: 15, right: 15, top: 15 })
}
}
.padding({ bottom: 50 })
Row() {
TextInput({ placeholder: '回复~', text: this.content })
.placeholderColor('#c3c4c5')
.layoutWeight(1)
.onChange((value) => {
this.content = value
})
Text('发布')
.fontSize(14)
.fontColor('#09f')
.margin({ left: 15 })
.onClick(() => {
this.onReply()
})
}
.width('100%')
.height(50)
.padding({ left: 15, right: 15 })
.position({ y: '100%' })
.translate({ y: -50 })
.backgroundColor('#fff')
.border({ width: { top: 0.5 }, color: '#e4e4e4' })
}
}
}
@Component
struct ReplyComp {
@ObjectLink
item: ReplyItem
onLike() {
if (this.item.likeFlag) {
this.item.likeNum--
this.item.likeFlag = false
promptAction.showToast({ message: '取消点赞' })
} else {
this.item.likeNum++
this.item.likeFlag = true
promptAction.showToast({ message: '点赞成功' })
}
}
build() {
Row() {
Image(this.item.avatar)
.width(32)
.height(32)
.borderRadius(16)
Column() {
Text(this.item.author)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 5 })
Text(this.item.content)
.margin({ bottom: 5 })
.fontColor('#565656')
.lineHeight(20)
Row() {
Text(`${this.item.time}•IP 属地${this.item.area}`)
.layoutWeight(1)
.fontSize(14)
.fontColor('#c3c4c5')
Row() {
Image($r('app.media.heart'))
.width(14)
.height(14)
.fillColor(this.item.likeFlag ? '#ff6600' : '#c3c4c5')
.margin({ right: 4 })
Text(this.item.likeNum.toString())
.fontSize(14)
.fontColor(this.item.likeFlag ? '#ff6600' : '#c3c4c5')
}
.onClick(() => {
this.onLike()
})
}
}
.layoutWeight(1)
.padding({ left: 10 })
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding({ bottom: 15 })
.alignItems(VerticalAlign.Top)
}
}
@Component
struct NavComp {
build() {
// nav
Row() {
Row() {
Image($r('app.media.ic_public_arrow_left'))
.width(12)
.height(12)
.fillColor('#848484')
}
.width(24)
.height(24)
.borderRadius(12)
.backgroundColor('#f5f5f5')
.justifyContent(FlexAlign.Center)
.margin({ left: 13 })
Text('评论回复')
.padding({ right: 50 })
.textAlign(TextAlign.Center)
.fontSize(18)
.layoutWeight(1)
}
.height(50)
}
}
@Component
struct CommentComp {
build() {
// comment
Row() {
Image('https://picx.zhimg.com/1754b6bd9_xl.jpg?source=c885d018')
.width(32)
.height(32)
.borderRadius(16)
Column() {
Text('欧洲足球锦标赛')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 5 })
Text('14-0!欧洲杯超级惨案+刷爆纪录!姆巴佩帽子戏法,法国7连胜,怎么评价这场比赛?')
.margin({ bottom: 5 })
.fontColor('#565656')
.lineHeight(20)
Row() {
Text('10-21•IP 属地辽宁')
.layoutWeight(1)
.fontSize(14)
.fontColor('#c3c4c5')
Row() {
Image($r('app.media.heart'))
.width(14)
.height(14)
.fillColor('#c3c4c5')
.margin({ right: 4 })
Text('100')
.fontSize(14)
.fontColor('#c3c4c5')
}
}
}
.layoutWeight(1)
.padding({ left: 10 })
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding({ left: 15, right: 15, bottom: 15 })
.alignItems(VerticalAlign.Top)
}
}
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
export class User {
name?: string
age?: number
}
pages/Index.ets
import { User } from '../models'
const storage = LocalStorage.GetShared()
@Entry(storage)
@Component
struct Index {
@LocalStorageProp('user')
user: User = {}
build() {
Column({ space: 15 }) {
Text('Index:')
Text(this.user.name + this.user.age)
.onClick(()=>{
this.user.age ++
})
Navigator({ target: 'pages/OtherPage' }){
Text('Go Other Page')
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
pages/OtherPage.ets
import { User } from '../models'
const storage = LocalStorage.GetShared()
@Entry(storage)
@Component
struct OtherPage {
@LocalStorageLink('user')
user: User = {}
build() {
Column({ space: 15 }) {
Text('OtherPage:')
Text(this.user.name + this.user.age)
.onClick(()=>{
this.user.age ++
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
TIP
- 页面间共享需要要模拟器测试
- 应用逻辑中使用参考 链接
2. 应用状态-AppStorage#
AppStorage是应用全局的UI状态存储,是和应用的进程绑定的,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储。
- 如果是初始化使用
AppStorage.SetOrCreate(key,value) - 单向
@StorageProp('user')组件内可变 - 双向
@StorageLink('user')全局均可变
1)通过UI装饰器使用
import { User } from '../models'
AppStorage.SetOrCreate<User>('user', { name: 'jack', age: 18 })
@Entry
@Component
struct Index {
@StorageProp('user')
user: User = {}
build() {
Column({ space: 15 }) {
Text('Index:')
Text(this.user.name + this.user.age)
.onClick(() => {
this.user.age++
})
Divider()
ChildA()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
@Component
struct ChildA {
@StorageLink('user')
user: User = {}
build() {
Column({ space: 15 }){
Text('ChildA:')
Text(this.user.name + this.user.age)
.onClick(()=>{
this.user.age ++
})
}
}
}
2)通过逻辑使用
AppStorage.Get<ValueType>(key)获取数据AppStorage.Set<ValueType>(key,value)覆盖数据const link: SubscribedAbstractProperty<ValueType> = AppStorage.Link(key)覆盖数据link.set(value)修改link.get()获取
import promptAction from '@ohos.promptAction'
import { User } from '../models'
AppStorage.SetOrCreate<User>('user', { name: 'jack', age: 18 })
@Entry
@Component
struct Index {
@StorageLink('user')
user: User = {}
build() {
Column({ space: 15 }) {
Text('Index:')
Text(this.user.name + this.user.age)
.onClick(() => {
this.user.age++
})
Divider()
Text('Get()')
.onClick(() => {
const user = AppStorage.Get<User>('user')
promptAction.showToast({
message: JSON.stringify(user)
})
})
Text('Set()')
.onClick(() => {
AppStorage.Set<User>('user', {
name: 'tom',
age: 100
})
})
Text('Link()')
.onClick(() => {
const user: SubscribedAbstractProperty<User> = AppStorage.Link('user')
user.set({
name: user.get().name,
age: user.get().age + 1
})
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
3. 状态持久化-PersistentStorage#
PersistentStorage将选定的AppStorage属性保留在设备磁盘上。
DETAILS
UI和业务逻辑不直接访问 PersistentStorage 中的属性,所有属性访问都是对 AppStorage 的访问,AppStorage 中的更改会自动同步到 PersistentStorage。
WARNING
- 支持:number, string, boolean, enum 等简单类型;
- 如果:要支持对象类型,可以转换成json字符串
- 持久化变量最好是小于2kb的数据,如果开发者需要存储大量的数据,建议使用数据库api。
1)简单数据类型的持久化,和获取和修改
import { User } from '../models'
PersistentStorage.PersistProp('count', 100)
@Entry
@Component
struct Index {
@StorageLink('count')
count: number = 0
build() {
Column({ space: 15 }) {
Text(this.count.toString())
.onClick(() => {
this.count++
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
2)复杂数据类型的持久化,和获取和修改
import promptAction from '@ohos.promptAction'
import { User } from '../models'
PersistentStorage.PersistProp('userJson', `{ "name": "jack", "age": 18 }`)
@Entry
@Component
struct Index {
@StorageProp('userJson')
@Watch('onUpdateUser')
userJson: string = '{}'
@State
user: User = JSON.parse(this.userJson)
onUpdateUser() {
this.user = JSON.parse(this.userJson)
}
build() {
Column({ space: 15 }) {
Text('Index:')
Text(this.user.name + this.user.age)
.onClick(() => {
this.user.age++
// 修改
AppStorage.Set('userJson', JSON.stringify(this.user))
})
Divider()
Text('Get()')
.onClick(() => {
// 获取
const user = AppStorage.Get<string>('userJson')
promptAction.showToast({ message: user })
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
TIP
- 测试:需要在真机或模拟器调试
4. 设备环境-Environment#
开发者如果需要应用程序运行的设备的环境参数,以此来作出不同的场景判断,比如多语言,暗黑模式等,需要用到
Environment设备环境查询。
Environment的所有属性都是不可变的(即应用不可写入),所有的属性都是简单类型。
import i18n from '@ohos.i18n';
// 获取系统语言
const lang = i18n.getSystemLanguage()
// 设置环境状态
Environment.EnvProp('lang', lang);
@Entry
@Component
struct Index{
@StorageProp('lang')
lang: string = ''
build() {
Column() {
Text(this.lang)
.onClick(()=>{
// 不能修改
// Environment.EnvProp('lang', 'en');
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
网络管理#
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 地址,或者域名。不可使用
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文件夹,按照下面命令启动接口服务器,查看本机IPipconfig | 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 })
723

被折叠的 条评论
为什么被折叠?



