👋 你好,欢迎来到我的博客!我是【菜鸟学鸿蒙】
   我是一名在路上的移动端开发者,正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来,也为了和更多同路人互相启发,我决定把探索 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. 再往深一点:为什么“先缓存后网络”这么香?

我以前也觉得缓存是“锦上添花”。后来被用户教育了:
用户不管你架构多优雅,他只在乎两件事:

  1. 我点开能不能立刻看到东西?
  2. 数据是不是最新的?

“先缓存后网络”刚好同时满足:

  • 缓存让你秒开(体验好)
  • 网络让你可更新(数据新)

你可能会说:
“我数据很少,真的需要吗?”
我还是那句反问:你能保证它永远很少吗?(不能,对吧😏)

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
🧵 本文原创,转载请注明出处。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐