你写鸿蒙应用还在“到处塞 @State”?不怕后期维护把你反噬到怀疑人生吗?
本文分享了鸿蒙应用开发中状态管理的实战经验。作者从自身踩坑经历出发,提出鸿蒙开发既不能照搬Web思路,也不应沿用Android模式,而应采用声明式UI+数据驱动的规范写法。通过一个待办清单案例,演示了如何构建网络请求+本地缓存+状态管理的闭环流程: 数据分层:模型层定义数据结构,缓存层使用Preferences存储,服务层封装网络请求 状态集中:ViewModel统一管理业务逻辑,处理数据加载、缓
👋 你好,欢迎来到我的博客!我是【菜鸟学鸿蒙】
我是一名在路上的移动端开发者,正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来,也为了和更多同路人互相启发,我决定把探索 HarmonyOS 的过程都记录在这里。
🛠️ 主要方向:ArkTS 语言基础、HarmonyOS 原生应用(Stage 模型、UIAbility/ServiceAbility)、分布式能力与软总线、元服务/卡片、应用签名与上架、性能与内存优化、项目实战,以及 Android → 鸿蒙的迁移踩坑与复盘。
🧭 内容节奏:从基础到实战——小示例拆解框架认知、专项优化手记、实战项目拆包、面试题思考与复盘,让每篇都有可落地的代码与方法论。
💡 我相信:写作是把知识内化的过程,分享是让生态更繁荣的方式。
如果你也想拥抱鸿蒙、热爱成长,欢迎关注我,一起交流进步!🚀
前言:我当年也嘴硬,直到项目把我教育了一顿😅
刚开始写鸿蒙(尤其是 ArkUI/ArkTS 这一套),我也挺飘的:
“状态?不就 @State 一把梭?”
“页面传参?随便来个全局变量不就完事儿?”
“网络请求?直接在 onAppear 里开干,快得很!”
结果呢?
上线两周——需求一改,页面一炸;再改一次,状态像面条一样缠成一坨;再来一次,热更新都救不了我😂。
所以今天我就想用一种很“人话”的方式聊聊:
在鸿蒙里做一个稍微“像样”的应用,状态、网络、缓存、页面生命周期到底该怎么串起来,既不玄学,也不硬背概念——还能让你未来少掉几根头发。
1. 先把“坑位”摆正:鸿蒙前端不是 Web,但也别当成 Android 写
我见过最离谱的两种写法:
- 把 ArkUI 当 Web:各种状态乱飞,组件里随手改全局,最后你自己都不知道谁在驱动 UI。
- 把 ArkUI 当 Android:在生命周期里堆业务,页面一多,逻辑像“自助火锅”——啥都往里下。
鸿蒙这套更像:
声明式 UI + 数据驱动渲染 + 明确的状态归属。
你写得越“规矩”,它越顺滑;你越“随缘”,它越给你颜色看。
2. 状态管理的底线:别让 UI 组件背业务债
2.1 我自己的“土法但很稳”的规则(亲测不容易翻车)
- UI 组件只管展示与交互:点击、输入、选择……到此为止。
- 业务逻辑放到 ViewModel/Store:请求、转换、校验、缓存策略都在这里。
- 状态只流向一个方向:数据更新 → UI 自动刷新,不要反复互相改来改去。
你可能会问:
“我就一个页面,至于吗?”
我反问你一句:一个页面能保持永远只有一个页面吗?(产品经理:当然不能😈)
3. 来点真家伙:做一个“待办清单 + 网络拉取 + 本地缓存”的小闭环
下面这个例子我故意选得很“落地”:
- 首次进入:先读本地缓存(秒开)
- 然后:发起网络请求刷新
- 下拉刷新:重新拉取
- 新增/完成:更新 UI + 写缓存
代码以 ArkTS/ArkUI 风格展示(你可以按项目实际 API 适配)。
3.1 数据模型:别花里胡哨,先能跑再说🧱
export interface TodoItem {
id: string
title: string
done: boolean
updatedAt: number
}
3.2 简易缓存层:Preferences(轻量、够用)
用 Preferences 存 JSON,适合“小而美”的数据。大数据请上数据库(下面也会提)。
import preferences from '@ohos.data.preferences'
export class TodoCache {
private static KEY = 'todos_cache_v1'
static async save(context: Context, todos: TodoItem[]): Promise<void> {
const pref = await preferences.getPreferences(context, { name: 'app_pref' })
await pref.put(TodoCache.KEY, JSON.stringify(todos))
await pref.flush()
}
static async load(context: Context): Promise<TodoItem[]> {
const pref = await preferences.getPreferences(context, { name: 'app_pref' })
const raw = await pref.get(TodoCache.KEY, '[]') as string
try {
return JSON.parse(raw) as TodoItem[]
} catch (e) {
return []
}
}
}
3.3 网络层:别把请求散落在页面里(求你了🙏)
这里用一个“服务类”集中管理。你也可以换成你常用的封装方式。
export class TodoService {
// 这里用伪接口示意:你换成真实后端地址即可
static async fetchTodos(): Promise<TodoItem[]> {
// 假装网络延迟,不然你看不出“先缓存后刷新”的意义🤣
await new Promise<void>((r) => setTimeout(() => r(), 600))
const now = Date.now()
return [
{ id: '1', title: '把状态管理写规矩一点', done: false, updatedAt: now },
{ id: '2', title: '别在 onAppear 里乱塞业务', done: true, updatedAt: now }
]
}
}
3.4 ViewModel:把“人间烟火气”的业务都关进笼子里🧠
todos是页面状态源init():先缓存再网络refresh():只管拉取与落盘toggleDone():改状态后写缓存
export class TodoViewModel {
todos: TodoItem[] = []
loading: boolean = false
errorMsg: string = ''
constructor(private context: Context) {}
async init(): Promise<void> {
// 1) 先读缓存:让页面“先活过来”
this.todos = await TodoCache.load(this.context)
// 2) 再拉网络:让数据“再体面一点”
await this.refresh()
}
async refresh(): Promise<void> {
this.loading = true
this.errorMsg = ''
try {
const list = await TodoService.fetchTodos()
this.todos = list
await TodoCache.save(this.context, this.todos)
} catch (e) {
this.errorMsg = '网络有点闹情绪…稍后再试?'
} finally {
this.loading = false
}
}
async add(title: string): Promise<void> {
const item: TodoItem = {
id: `${Date.now()}`,
title: title.trim(),
done: false,
updatedAt: Date.now()
}
this.todos = [item, ...this.todos]
await TodoCache.save(this.context, this.todos)
}
async toggleDone(id: string): Promise<void> {
this.todos = this.todos.map(t => t.id === id ? { ...t, done: !t.done, updatedAt: Date.now() } : t)
await TodoCache.save(this.context, this.todos)
}
}
3.5 页面 UI:UI 只做 UI,别当“全能打工人”😤
这里用声明式写法:
@State只持有 ViewModel 的“可观察映射”(简单起见直接触发刷新)。
如果你项目更复杂,建议引入更明确的可观察模式或状态容器。
@Entry
@Component
struct TodoPage {
@State private vm: TodoViewModel | null = null
@State private inputText: string = ''
@State private version: number = 0 // 用来触发重渲染(简化演示)
aboutToAppear() {
const context = getContext(this) as Context
const model = new TodoViewModel(context)
this.vm = model
model.init().then(() => {
this.version++
})
}
private bump() {
this.version++
}
build() {
const vm = this.vm
Column({ space: 12 }) {
Text('Todo - 小闭环示例')
.fontSize(22)
.fontWeight(FontWeight.Bold)
Row({ space: 8 }) {
TextInput({ text: this.inputText, placeholder: '写点要做的事…' })
.onChange(v => this.inputText = v)
.layoutWeight(1)
Button('新增')
.onClick(async () => {
if (!vm) return
if (!this.inputText.trim()) {
promptAction.showToast({ message: '你倒是写点字呀🤣' })
return
}
await vm.add(this.inputText)
this.inputText = ''
this.bump()
})
}
if (vm?.errorMsg) {
Text(vm.errorMsg).fontColor(Color.Red)
}
Row({ space: 10 }) {
Button(vm?.loading ? '刷新中…' : '下拉/点我刷新')
.onClick(async () => {
if (!vm) return
await vm.refresh()
this.bump()
})
Text(vm ? `共 ${vm.todos.length} 条` : '初始化中…')
}
List() {
ForEach(vm?.todos ?? [], (item: TodoItem) => {
ListItem() {
Row({ space: 10 }) {
Checkbox({ name: item.id, group: 'todo', select: item.done })
.onChange(async () => {
if (!vm) return
await vm.toggleDone(item.id)
this.bump()
})
Text(item.title)
.decoration({ type: item.done ? TextDecorationType.LineThrough : TextDecorationType.None })
.layoutWeight(1)
Text(item.done ? '✅' : '⏳')
}
.padding(12)
}
}, item => item.id)
}
.height('70%')
}
.padding(16)
}
}
看到这,你应该会有个直觉:
页面不再“抓狂”,逻辑不再“乱窜”,扩展也更舒服。
是的,这就是“把复杂度关进笼子里”的快乐🙂。
4. 再往深一点:为什么“先缓存后网络”这么香?
我以前也觉得缓存是“锦上添花”。后来被用户教育了:
用户不管你架构多优雅,他只在乎两件事:
- 我点开能不能立刻看到东西?
- 数据是不是最新的?
“先缓存后网络”刚好同时满足:
- 缓存让你秒开(体验好)
- 网络让你可更新(数据新)
你可能会说:
“我数据很少,真的需要吗?”
我还是那句反问:你能保证它永远很少吗?(不能,对吧😏)
5. 真实项目里我会怎么升级它(别只会写 Demo 啊喂😎)
5.1 数据多了:Preferences 换成数据库
- 列表几十条:Preferences 还能忍
- 上千条、还要检索:请上数据库(比如关系型或你们团队封装的存储层)
5.2 网络复杂了:加拦截器与统一错误码映射
- 401/403/500 统一处理
- 弱网重试策略
- 埋点记录耗时与失败率(别靠猜)
5.3 状态更复杂:引入更明确的单向数据流
- UI → Action → Reducer/Store → State → UI
- 你会发现“改需求”不再像拆炸弹
5.4 性能更敏感:减少无意义重渲染
我上面例子用了 version++ 来简化演示,但真实项目更推荐:
- 让“可观察对象”本身驱动刷新
- 或把状态拆得更细,避免整页重绘
6. 我踩过的雷(你别再踩一遍了😭)
- 在 onAppear 里直接写一堆请求 + 数据转换 + UI 状态拼装
当时觉得“快”,后来维护觉得“快死”。 - 把列表 item 的 key 用 index
删除/插入后你就知道什么叫“鬼畜错乱”。 - 状态到处散落
一旦出现“这个按钮点了,另一个页面怎么也变了”,你会开始怀疑宇宙。
结语:写鸿蒙不是难,难的是“你愿不愿意对未来的自己好一点”
我一直觉得写代码像谈恋爱:
你前期图省事,后期就得还债;
你前期讲原则,后期就会很省心。
所以我最后也送你一句很直白的话:
别让 UI 背业务债,别让生命周期变成垃圾桶。
你会感谢今天的自己——真的。
📝 写在最后
如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!
我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!
感谢你的阅读,我们下篇文章再见~👋
✍️ 作者:某个被流“治愈”过的 移动端 老兵
📅 日期:2025-11-05
🧵 本文原创,转载请注明出处。
更多推荐


所有评论(0)