鸿蒙-3. 状态管理

鸿蒙(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)
语言DartTypeScript 超集(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 更轻量

其他:

  • GridRow GridCol 栅格布局

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装饰的方法不会被调用

之前我们通过 赋值的方式 修改嵌套对象或对象数组这类复杂数据来更新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 文件夹,按照下面命令启动接口服务器,查看本机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  })

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值