HarmonyOS Next V2 状态管理实战
介绍
以下案例适合刚开始手鸿蒙开发的小伙伴,有大量的最新逻辑锻炼、鸿蒙核心语法、使用最新鸿蒙的@Local、@Computed 等装饰器来完成。
另外,考虑在学习知识的知识时候,优先关注核心功能,所以提供的布局都会适当简化,但是能保证把核心功能展示出来。
每一个案例会点出终点和核心知识,让学习者可以练习完毕,可以得到什么。
学习的路线
- 先看效果
- 复现效果
- 如果有对代码产生的疑问,可以在评论区内直接提出,有疑问,必回复
- 如果能帮助到你,就很好了。😄
点击高亮
- 练习基本的线性布局
- 练习基本的数组使用
- 练习列表渲染语法 ForEach
- 练习布局中的状态切换 三元表达式
- 掌握通用的点击高亮

| @Entry |
| @ComponentV2 |
| struct Index { |
| @Local |
| list: string[] = ["小明", "小红", "小黑", "小黄"] |
| |
| @Local |
| select: number = 1 |
| |
| build() { |
| Column() { |
| |
| |
| ForEach(this.list, (item: string, index: number) => { |
| Button(item + " " + (this.select == index)) |
| .backgroundColor(this.select == index ? "#ffcd43" : "#007dfe") |
| .onClick(() => { |
| this.select = index |
| }) |
| |
| }) |
| } |
| .width("100%") |
| .height("100%") |
| .padding({ top: 100 }) |
| |
| } |
| } |
待办列表
- 新手上手新的编程语言的必做案例 crud - 增删该查
- 练习 V2 装饰器、@Local、@Computed、事件等
- 打通 状态 -> UI 、 UI-> 状态 的一些交互

| @ObservedV2 |
| class Task { |
| @Trace task: string = "" |
| @Trace isFinished: boolean = false |
| } |
| |
| |
| @Entry |
| @ComponentV2 |
| struct Index { |
| |
| @Local |
| list: Task[] = [ |
| |
| |
| ] |
| |
| @Local |
| inpValue: string = "" |
| |
| |
| |
| @Computed |
| get statistics() { |
| let undoneNum = this.list.filter(v =>!v.isFinished).length |
| let doneNum = this.list.length - undoneNum |
| return [undoneNum, doneNum] |
| } |
| |
| |
| onClear = () => { |
| |
| this.list = this.list.filter((v =>!v.isFinished)) |
| } |
| |
| onDelete = (index: number) => { |
| |
| this.list.splice(index, 1) |
| |
| } |
| |
| build() { |
| Column() { |
| Row() { |
| Button("清理已完成") |
| .onClick(this.onClear) |
| } |
| |
| Row() { |
| Text(`未完成的数量 ${this.statistics[0]}`) |
| Text(`完成的数量 ${this.statistics[1]}`) |
| } |
| .width("80%") |
| .justifyContent(FlexAlign.SpaceAround) |
| |
| Row() { |
| TextInput() |
| .width(200) |
| .onChange(value => { |
| |
| this.inpValue = value |
| }) |
| Button("确认") |
| .onClick(() => { |
| |
| |
| const isExit = this.list.some(element => element.task == this.inpValue) |
| if (!isExit) { |
| const p = new Task() |
| p.task = this.inpValue |
| this.list.push(p) |
| } |
| }) |
| } |
| |
| ForEach(this.list, (item: Task, index: number) => { |
| Row() { |
| Text(item.task) |
| .fontColor(item.isFinished ? "#666" : "#000") |
| .decoration({ |
| type: item.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None |
| }) |
| .fontStyle(item.isFinished ? FontStyle.Italic : FontStyle.Normal) |
| .onClick(() => { |
| this.onDelete(index) |
| }) |
| |
| Button(item.isFinished ? "继续" : "完成") |
| .backgroundColor(item.isFinished ? "#ffa601" : "#007dfe") |
| .onClick(() => { |
| |
| this.list[index].isFinished = !this.list[index].isFinished |
| |
| }) |
| } |
| }) |
| |
| } |
| .width("100%") |
| .height("100%") |
| .padding({ top: 100 }) |
| |
| } |
| } |
| |
B 站显示更多
- 练习 Flex 布局的换行
- 练习 Scroll 布局的水平滚动
- 练习绝对定位-水平居中
- 练习条件渲染

| @Entry |
| @ComponentV2 |
| struct Index { |
| @Local |
| list: string[] = |
| ["首页", "动画", "番剧", "国创", "音乐", "舞蹈", "游戏", "知识", "科技", "运动", "汽车", "生活", "美食", "动物圈", |
| "鬼畜", "时尚", "娱乐", "影视", "纪录片", "电影", "电视剧", "直播", "课堂"] |
| |
| @Local |
| isShowMore: boolean = false |
| |
| build() { |
| Column() { |
| Row({ space: 5 }) { |
| Scroll() { |
| |
| Flex({ wrap: this.isShowMore ? FlexWrap.Wrap : FlexWrap.NoWrap }) { |
| ForEach(this.list, (item: string) => { |
| Text(item) |
| .margin(10) |
| }) |
| } |
| } |
| |
| .scrollable(ScrollDirection.Horizontal) |
| .layoutWeight(1) |
| |
| .padding({ |
| bottom: this.isShowMore ? 30 : 0 |
| }) |
| |
| if (this.isShowMore) { |
| Image($r("app.media.app_icon")) |
| .width(20) |
| .position({ |
| left: "50%", |
| bottom: 0 |
| }) |
| .translate({ |
| x: -10 |
| }) |
| .onClick(() => { |
| this.isShowMore = !this.isShowMore |
| }) |
| } else { |
| Image($r("app.media.app_icon")) |
| .width(20) |
| .onClick(() => { |
| this.isShowMore = !this.isShowMore |
| }) |
| } |
| |
| } |
| .width("100%") |
| .backgroundColor(Color.Red) |
| |
| } |
| .width("100%") |
| .height("100%") |
| |
| } |
| } |
仿考研日程
- 练习如何根据需求来拆分数据
- 简单的渲染

| |
| interface SubContent { |
| subTitle: string |
| subContent: string |
| } |
| |
| |
| interface OneContent { |
| title: string |
| content: SubContent[] |
| } |
| |
| |
| @Entry |
| @ComponentV2 |
| struct Index { |
| @Local |
| list: OneContent[] = [ |
| { |
| title: "统考", |
| content: [ |
| { |
| subTitle: "国家线", |
| subContent: "2024。。。。" |
| }, { |
| subTitle: "考研复试流程图", |
| subContent: "" |
| } |
| ] |
| }, |
| { |
| title: "统考22", |
| content: [ |
| { |
| subTitle: "国家线22", |
| subContent: "2024。。。。22" |
| }, { |
| subTitle: "考研复试流程图22", |
| subContent: "" |
| } |
| ] |
| } |
| ] |
| |
| @Local |
| current: number = 0 |
| |
| build() { |
| |
| Column() { |
| |
| Row({ space: 10 }) { |
| ForEach(this.list, (item: OneContent, index: number) => { |
| Text(item.title) |
| .fontColor(this.current == index ? "#0094ff" : "#000") |
| .onClick(() => { |
| this.current = index |
| }) |
| }) |
| } |
| |
| |
| Column() { |
| ForEach(this.list[this.current].content, (item: SubContent) => { |
| Column() { |
| Text(item.subTitle) |
| Text(item.subContent) |
| } |
| }) |
| } |
| |
| } |
| .width("100%") |
| .height("100%") |
| .backgroundColor("#eee") |
| |
| } |
| } |
仿 vantUI -倒计时
- 练习定时器
- 练习一点关于时间处理的逻辑功能

| @Entry |
| @ComponentV2 |
| struct Index { |
| @Local |
| str: string = "" |
| |
| time: number = 5 * 60 * 60 * 1000 |
| tid: number = -1 |
| |
| build() { |
| |
| Column() { |
| Button("开始倒计时") |
| .onClick(() => { |
| |
| this.tid = setInterval(() => { |
| this.time -= 10 |
| |
| |
| const hour = Math.floor(this.time / 1000 / 60 / 60) |
| const minute = Math.floor(this.time / 1000 / 60 % 60) |
| const seconde = Math.floor(this.time / 1000 % 60) |
| const milliSeconde = this.time % 1000 |
| this.str = `${hour}:${minute}:${seconde}.${milliSeconde}` |
| |
| |
| }, 10) |
| }) |
| |
| Button("暂停") |
| .onClick(() => { |
| clearInterval(this.tid) |
| }) |
| Text(this.str) |
| .fontSize(30) |
| } |
| } |
| } |
仿掘金抽奖
- 练习 flex 布局-换行
- 练习随机数
- 练习数组+随机数实现随机获取元素

| @Entry |
| @ComponentV2 |
| struct Index { |
| @Local |
| list: string[] = [ |
| "4090", |
| "4399", |
| "大彩电", |
| "iphone16", |
| "meta70", |
| "Mac", |
| "小牛电动车", |
| "迪拜7日游", |
| "北京房子一套" |
| ] |
| @Local |
| current: number = 0 |
| |
| |
| setHighline(index: number) { |
| this.current = index |
| } |
| |
| build() { |
| Column() { |
| Flex({ |
| wrap: FlexWrap.Wrap |
| }) { |
| ForEach(this.list, (item: string, index: number) => { |
| Text(item) |
| .width("33.33%") |
| .padding({ |
| top: 20, bottom: 20 |
| }) |
| .border({ |
| width: 1 |
| }) |
| .backgroundColor(this.current == index ? "#e37815" : "#fff") |
| }) |
| } |
| |
| Button("开始抽啦") |
| .onClick(() => { |
| |
| |
| let tid = setInterval(() => { |
| |
| const index = Math.floor(Math.random() * this.list.length) |
| |
| this.setHighline(index) |
| |
| }, 10) |
| |
| |
| setTimeout(() => { |
| clearInterval(tid) |
| }, 5000) |
| }) |
| } |
| |
| } |
| } |
仿掘金抽奖 - 不重复抽奖
- 加强逻辑处理,如何实现不重复抽奖
- 练习一些数组的方法
- 练习使用 @Computed

| @Entry |
| @ComponentV2 |
| struct Index { |
| @Local |
| list: string[] = [ |
| "4090", |
| "4399", |
| "大彩电", |
| "iphone16", |
| "meta70", |
| "Mac", |
| "小牛电动车", |
| "迪拜7日游", |
| "北京房子一套" |
| ] |
| @Local |
| selectedList: number[] = [] |
| @Local |
| current: number = 0 |
| |
| |
| @Computed |
| get newList() { |
| const newList: number[] = [] |
| for (let index = 0; index < this.list.length; index++) { |
| let item = this.selectedList.find(v => v === index) |
| |
| if (!item) { |
| newList.push(index) |
| } |
| } |
| return newList |
| } |
| |
| |
| setHighline(index: number) { |
| this.current = index |
| } |
| |
| build() { |
| Column() { |
| Flex({ |
| wrap: FlexWrap.Wrap |
| }) { |
| ForEach(this.list, (item: string, index: number) => { |
| Text(item) |
| .width("33.33%") |
| .padding({ |
| top: 20, bottom: 20 |
| }) |
| .border({ |
| width: 1 |
| }) |
| .backgroundColor( |
| this.selectedList.includes(index) ? "#e37815" : |
| (this.current == index ? "#e37815" : "#fff")) |
| }) |
| } |
| |
| Button("开始抽啦") |
| .onClick(() => { |
| |
| |
| |
| let tid = setInterval(() => { |
| |
| |
| const index = Math.floor(Math.random() * this.newList.length) |
| |
| |
| this.setHighline(this.newList[index]) |
| |
| }, 10) |
| |
| |
| setTimeout(() => { |
| clearInterval(tid) |
| this.selectedList.push(this.current) |
| }, 1000) |
| }) |
| } |
| |
| } |
| } |
仿 vantUI-分页组件-简单版本
- 练习基本的鸿蒙线性布局
- 练习条件渲染
- 练习逻辑能力

| @Entry |
| @ComponentV2 |
| struct Index { |
| @Local |
| list: string[] = ['1', '2', '3', '4', '5'] |
| @Local |
| current: number = 4 |
| |
| build() { |
| Column() { |
| Row({ space: 2 }) { |
| Button("上一页") |
| .enabled(this.current != 0) |
| .backgroundColor("#fff") |
| .fontColor("#0094ff") |
| .stateStyles({ |
| disabled: { |
| .backgroundColor("#eee") |
| } |
| }) |
| .onClick(() => { |
| this.current-- |
| |
| }) |
| ForEach(this.list, (item: string, index: number) => { |
| Button(item) |
| .backgroundColor(this.current == index ? "#0094ff" : "#fff") |
| .fontColor(this.current == index ? "#fff" : "#0094ff") |
| .onClick(() => { |
| this.current = index |
| }) |
| }) |
| Button("下一页") |
| .enabled(this.current != this.list.length - 1) |
| .backgroundColor("#fff") |
| .stateStyles({ |
| disabled: { |
| .backgroundColor("#eee") |
| } |
| }) |
| |
| .fontColor("#0094ff") |
| .onClick(() => { |
| this.current++ |
| }) |
| } |
| } |
| .width("100%") |
| .height("100%") |
| .padding({ |
| top: 100 |
| }) |
| .backgroundColor("#eee") |
| } |
| } |
仿 vantUI-分页组件-复杂版本
- 练习基本的鸿蒙线性布局
- 练习条件渲染
- 练习复杂的逻辑能力

| @Entry |
| @ComponentV2 |
| struct Index { |
| @Local list: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] |
| @Local |
| test: number = 1 |
| @Local |
| showList: number[] = [1, 2, 3] |
| |
| change() { |
| if (this.test > 1 && this.test < this.list.length) { |
| this.showList = [this.test - 1, this.test, this.test + 1] |
| } |
| } |
| |
| previous() { |
| this.test-- |
| this.change() |
| } |
| |
| next() { |
| this.test++ |
| this.change() |
| } |
| |
| build() { |
| |
| Column() { |
| Row() { |
| Button("上") |
| .enabled(this.test != 1) |
| .backgroundColor("#ccc") |
| .onClick(() => { |
| this.previous() |
| }) |
| if (this.test > 2) { |
| Button("...") |
| .backgroundColor("#ccc") |
| .onClick(() => { |
| this.previous() |
| }) |
| } |
| |
| ForEach(this.showList, (item: number) => { |
| Button(item.toString()) |
| .backgroundColor(this.test == item ? Color.Blue : "#ccc") |
| .onClick(() => { |
| this.test = item |
| this.change() |
| }) |
| }) |
| |
| |
| if (this.test < this.list[this.list.length-1] - 1) { |
| Button("...") |
| .backgroundColor("#ccc") |
| .onClick(() => { |
| this.next() |
| }) |
| } |
| |
| Button("下") |
| .enabled(this.test != this.list[this.list.length-1]) |
| .backgroundColor("#ccc") |
| .onClick(() => { |
| this.next() |
| }) |
| } |
| } |
| |
| } |
| } |
小结
如果部分内容中的图片不存在,自己随机替换即可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了