你还在用“安卓思维”写鸿蒙?那你真的把 ArkTS & Stage 模型吃透了吗?

举报
bug菌 发表于 2025/12/25 15:03:07 2025/12/25
【摘要】 🏆本文收录于「滚雪球学SpringBoot」专栏(全网一个名),手把手带你零基础入门Spring Boot,从入门到就业,助你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8 🤔 摘要先说句掏心窝子的话:我刚从“Android 老油条”转到鸿蒙...

🏆本文收录于「滚雪球学SpringBoot」专栏(全网一个名),手把手带你零基础入门Spring Boot,从入门到就业,助你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!

环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8

🤔 摘要

先说句掏心窝子的话:我刚从“Android 老油条”转到鸿蒙那阵子,真是又兴奋又抓狂——兴奋是因为 ArkUI 写界面太爽了,抓狂是因为我脑子里那套“Activity/Fragment + XML”的肌肉记忆老来捣乱🤣。
  所以这篇我不打算端着讲课(那多没劲呀),而是用一个**“能跑、能存、能请求网络、还能优雅扩展”的小项目**,把鸿蒙开发里最容易踩坑、也最值得掌握的核心串起来。读完你会有一种:**“欸?原来我之前是那样误会它的!”**的感觉😎

⚠️小说明(我得实诚一点):
  你要求“全网查重率低于30% / 去AI化零AI味儿”,这类指标需要外部检测工具才能验证,我在这里没法替你做检测或保证具体比例。但我会尽最大努力用原创结构、原创表达、原创代码与工程化讲法来写,内容也会尽量像一位一线开发者写的技术笔记,而不是“模板化科普”😉
  另外你说“根据你下面给的内容及大纲”,但你这一条消息里还没贴出来——我先给你一版可直接发布的完整长文,你把大纲/要点发我后,我再帮你精准改成你指定结构

📌目录(带点表情才像我嘛😄)

🧭 前言:我为什么劝你先丢掉“安卓思维”

你可能会说:“开发不都一样吗?UI + 业务 + 存储 + 网络。”
  欸,这话对一半😄。另一半是——鸿蒙的体验感来自它的“系统级设计哲学”:分布式、原子化、统一 UI 声明式范式、Stage 模型的生命周期组织方式……这些东西你如果硬套安卓那套,就会出现一种很尴尬的局面:
  **能写出来,但写得别扭;能跑起来,但跑得不优雅。**就像你穿西装去打球——不是不行,是你自己难受🤣

今天我就用一个小项目,带你从“会写”走到“写得像鸿蒙”,顺便给你几句我当时被锤醒的真理:

  • **声明式 UI 不是‘换个写法’,而是‘换个脑回路’**🧠
  • 状态是 UI 的老板,别跟老板对着干😤
  • 工程结构不提前设计,后期你会跪着重构🙃

🧠 鸿蒙应用到底“长啥样”:Stage 模型一眼看懂

别被名词吓到哈😌。你只要记住一句话:
  Stage 模型用“Ability + Window + 页面路由”来组织应用的生命周期与界面呈现。

在实际开发里,你最常接触的是:

  • UIAbility:承载 UI 的能力(类似“主舞台”🎭)
  • 页面(Page):ArkUI 声明式组件组成的界面
  • 路由(router):页面跳转(别把它当 Android Intent 硬套就行😅)

你会发现它像是在告诉你:

“别把生命周期散落在各个角落,集中起来管理,页面就专心负责展示与交互。”
  嗯,这话听着就很“系统工程”对吧?😎

🎨 ArkUI & 状态管理:写爽之前先写对

ArkUI 写界面确实爽,爽到你会忍不住一直写一直写,然后某天突然发现:
  **“我改了数据,界面怎么没刷新?!”**😤
  这时候别急着骂工具,先看看你是不是在跟状态管理较劲。

几个你必须形成肌肉记忆的关键词(我当初背它们背得像背单词🤣):

  • @State:组件内部状态,改它会触发 UI 刷新✨
  • @Prop / @Link / @ObjectLink:父子组件数据传递与双向绑定(具体看场景)🔗
  • 不要直接 mutate(乱改)复杂对象,要让框架“感知到变化”👀

一句大白话:
  你得让框架知道你变了,它才会帮你重绘。
  (跟谈恋爱一样,你不说我咋知道你生气了🙃)

🧩 实战项目:做一个“番茄待办”小应用🍅

我们做个小而美的:Tomato Todo 🍅
功能很朴素,但每一项都很“练内功”:

  • 新增待办 ✅
  • 完成/取消完成 ☑️
  • 本地持久化 💾
  • 拉取一句网络鸡汤文案(当你写 bug 写到怀疑人生时用🤣)🌐

🧱 1)数据模型:别上来就一坨 Map(求你了🙏)

// common/model/TodoItem.ets
export class TodoItem {
  id: string
  title: string
  done: boolean
  createdAt: number

  constructor(id: string, title: string, done: boolean = false, createdAt: number = Date.now()) {
    this.id = id
    this.title = title
    this.done = done
    this.createdAt = createdAt
  }
}

你看,简单、明确、可扩展。以后你要加优先级、番茄钟时长、标签,都好加😎

🧠 2)一个轻量 Store:先别急着上“巨型架构”😅

// common/store/TodoStore.ets
import { TodoItem } from '../model/TodoItem'

export class TodoStore {
  private _list: TodoItem[] = []

  get list(): TodoItem[] {
    return this._list
  }

  setList(next: TodoItem[]) {
    this._list = next
  }

  add(title: string) {
    const id = `${Date.now()}-${Math.random().toString(16).slice(2)}`
    this._list = [new TodoItem(id, title), ...this._list]
  }

  toggle(id: string) {
    this._list = this._list.map(it => it.id === id ? new TodoItem(it.id, it.title, !it.done, it.createdAt) : it)
  }

  remove(id: string) {
    this._list = this._list.filter(it => it.id !== id)
  }
}

这里我故意用了“返回新数组”的方式更新数据:
  一是清晰,二是利于 UI 框架感知变化
  (别问我怎么知道的,我以前原地 mutate 被坑过…😭)

🎨 3)主页面:ArkUI 写起来就是“顺滑”✨

// entry/src/main/ets/pages/Index.ets
import { TodoStore } from '../../common/store/TodoStore'
import { TodoItem } from '../../common/model/TodoItem'
import { TodoPersistence } from '../../common/storage/TodoPersistence'
import { QuoteService } from '../../common/net/QuoteService'

@Entry
@Component
struct Index {
  @State private inputText: string = ''
  @State private quote: string = '🍅 今天也要稳稳地写代码~'
  @State private store: TodoStore = new TodoStore()

  private persistence: TodoPersistence = new TodoPersistence('TomatoTodo')
  private quoteService: QuoteService = new QuoteService()

  aboutToAppear() {
    // 进来先读本地数据
    this.loadFromLocal()
    // 顺手拉一句网络文案
    this.loadQuote()
  }

  private async loadFromLocal() {
    const list: TodoItem[] = await this.persistence.load()
    this.store.setList(list)
    // 触发刷新:store 变了
    this.store = this.store
  }

  private async saveToLocal() {
    await this.persistence.save(this.store.list)
  }

  private async loadQuote() {
    const text = await this.quoteService.fetchOne()
    if (text) this.quote = `💬 ${text}`
  }

  private addTodo() {
    const title = this.inputText.trim()
    if (!title) {
      promptAction.showToast({ message: '😅 你倒是写点东西再点“添加”呀~' })
      return
    }
    this.store.add(title)
    this.inputText = ''
    this.saveToLocal()
  }

  private toggleTodo(id: string) {
    this.store.toggle(id)
    this.saveToLocal()
  }

  private removeTodo(id: string) {
    this.store.remove(id)
    this.saveToLocal()
  }

  build() {
    Column() {
      Text('🍅 Tomato Todo')
        .fontSize(26)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 18, bottom: 6 })

      Text(this.quote)
        .fontSize(14)
        .opacity(0.8)
        .margin({ bottom: 12 })

      Row() {
        TextInput({ text: this.inputText, placeholder: '📝 写下一个待办,比如:把bug按在地上摩擦' })
          .onChange((v: string) => this.inputText = v)
          .layoutWeight(1)
          .margin({ right: 8 })

        Button('➕ 添加')
          .onClick(() => this.addTodo())
      }
      .padding(12)

      Divider().margin({ bottom: 6 })

      List() {
        ForEach(this.store.list, (item: TodoItem) => {
          ListItem() {
            Row() {
              Checkbox({ name: item.id, group: 'todo', checked: item.done })
                .onChange(() => this.toggleTodo(item.id))
                .margin({ right: 10 })

              Column() {
                Text(item.title)
                  .fontSize(16)
                  .decoration({ type: item.done ? TextDecorationType.LineThrough : TextDecorationType.None })
                Text(`${new Date(item.createdAt).toLocaleString()}`)
                  .fontSize(12)
                  .opacity(0.6)
              }
              .layoutWeight(1)

              Button('🗑️')
                .onClick(() => this.removeTodo(item.id))
                .margin({ left: 10 })
            }
            .padding(12)
          }
        }, (item: TodoItem) => item.id)
      }
      .height('70%')
    }
    .width('100%')
  }
}

看到没?界面就是“数据长什么样,它就长什么样”。
  这就是声明式 UI 的甜点:**可读、可维护、能组合。**😋

💾 数据持久化:Preferences 真香,但别乱用😅

轻量数据(比如 todo 列表这种不太大、不太复杂的)用 Preferences 很舒服。
  但你千万别拿它当数据库使劲塞——等你塞到几百 KB、几 MB,你会开始怀疑人生🤣

// common/storage/TodoPersistence.ets
import preferences from '@ohos.data.preferences'
import { TodoItem } from '../model/TodoItem'

export class TodoPersistence {
  private name: string
  private key: string = 'todo_list'

  constructor(name: string) {
    this.name = name
  }

  async save(list: TodoItem[]) {
    const pref = await preferences.getPreferences(getContext(), this.name)
    await pref.put(this.key, JSON.stringify(list))
    await pref.flush()
  }

  async load(): Promise<TodoItem[]> {
    const pref = await preferences.getPreferences(getContext(), this.name)
    const raw = await pref.get(this.key, '[]') as string
    try {
      const arr = JSON.parse(raw) as any[]
      return arr.map(it => new TodoItem(it.id, it.title, it.done, it.createdAt))
    } catch (e) {
      return []
    }
  }
}

工程建议(真心话)

  • 只存必要字段,别把一堆临时 UI 状态也存进去😵
  • JSON 存储记得考虑版本升级(字段新增/改名)🧯

🌐 网络请求:一把梭还是要讲武德🤺

来,咱做个“鸡汤文案”接口拉取(你也可以换成你自己的后端)。这里给你一个通用写法:失败兜底、超时、返回空值不炸。

// common/net/QuoteService.ets
import http from '@ohos.net.http'

export class QuoteService {
  async fetchOne(): Promise<string> {
    const httpRequest = http.createHttp()
    try {
      const res = await httpRequest.request(
        // 你可以替换为你自己的接口
        'https://v1.hitokoto.cn/?c=i&encode=text',
        {
          method: http.RequestMethod.GET,
          connectTimeout: 4000,
          readTimeout: 4000
        }
      )

      if (res.responseCode >= 200 && res.responseCode < 300) {
        const text = (res.result as string)?.trim()
        return text || '写代码累了?喝口水再继续呀💧'
      }
      return '网络有点傲娇,先不理我🙃'
    } catch (e) {
      return '网络翻车了,但你别翻车😏'
    } finally {
      httpRequest.destroy()
    }
  }
}

我特别想强调一句:
  请求对象用完就 destroy(),别让资源泄漏像小强一样阴魂不散🪳
(是的,我就是被它折磨过的人😭)

🧰 工程化建议:结构、复用、可测试与性能小抠门

写鸿蒙写到后面,拼的不是“谁 API 背得多”,而是:
  **谁的项目能活得久、迭代不崩、功能加得快。**😎

🧩 推荐的目录组织(别嫌我啰嗦,我是为你好😤)

你可以按这个拆:

  • common/model:纯数据结构
  • common/store:业务状态/逻辑
  • common/storage:持久化
  • common/net:网络访问
  • pages:页面(只做展示 + 调用 store)

⚡ 性能小抠门(真香)

  • List 大数据别乱 ForEach + 大对象;给稳定 key
  • 状态别“全局一锅炖”;局部状态局部管
  • 避免频繁 JSON stringify/parse;必要时做节流⏳

🕳️ 高频踩坑合集:我替你先踩了,别谢我🤣

这段是“过来人碎碎念”,但句句都是真金白银换来的😭

  1. 改了对象属性 UI 不刷新
      大概率你在原地 mutate,框架没感知到变化。
      解决思路:用新对象/新数组替换,或使用合适的可观测机制👀

  2. 路由跳转后返回状态丢了
      你把临时状态放错地方了。页面销毁就没了。
      解决思路:该提升到 store 的提升到 store;该持久化的持久化💾

  3. 网络请求不销毁,越跑越慢
      记得 destroy,不然资源就像欠债一样越滚越多💸

  4. 工程越写越像一坨意大利面🍝
      别把业务逻辑写在 UI 里。短期你会爽,长期你会哭😭

✅ 结语:你现在敢说自己“会鸿蒙”了吗?

如果你看完能回答这三个问题,那我就敢拍胸脯说:你真的入门了😎

  • **我能解释清楚 Stage 模型把生命周期管理带来了什么好处吗?**🤔
  • **我能用“状态驱动 UI”而不是“命令式更新 UI”写页面吗?**😏
  • **我能把项目拆成可维护结构,而不是把页面写成“上古神殿”吗?**🤣

🧧福利赠与你🧧

  无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」专栏(全网一个名),bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。

  最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。

  同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板、技术文章Markdown文档等海量资料。

✨️ Who am I?

我是bug菌(全网一个名),CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云多年度十佳博主/价值贡献奖,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+;更多精彩福利点击这里;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。

-End-

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。