为什么写个按钮还要“修仙”?——鸿蒙 ArkUI 框架从底层心法到界面实战你真的懂了吗?

举报
喵手 发表于 2025/10/31 17:19:26 2025/10/31
【摘要】 开篇语哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,...

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛

  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。

  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!

前言

第一次上手 ArkUI(ArkTS 声明式 UI)的时候,我的内心是复杂的:语法像写段子,界面像搭乐高,状态一动百川响应。可一到真项目,你会发现——生命周期卡点、事件回调陷阱、样式风格统一这些“人间烟火”,才是决定你代码能不能“发量保全”的关键。今天这篇,我就把ArkUI 的声明式架构 → 生命周期与事件绑定 → 自定义组件与样式管理一条龙讲清楚,顺带塞几个可直接抄走的实战片段,少一点玄学,多一点落地。


目录

  • 前言:为什么一定要先换一副“声明式大脑”
  • 01|ArkUI 声明式 UI 架构(状态驱动、树对齐、最小重绘)
  • 02|组件生命周期与事件绑定(该出手时就出手)
  • 03|自定义组件与样式管理(把界面做出“团队味儿”)
  • 04|完整小实战:习惯打卡卡片(状态、事件、样式、动画一次用全)
  • 05|性能与易踩坑清单(不翻车才是王道)
  • 结语:漂亮界面背后,是靠谱的工程秩序

前言:为什么一定要先换一副“声明式大脑”

命令式 UI 的思路是“发生了什么 → 我应该怎么改 DOM/视图”;声明式则是“状态是什么 → 视图就长什么样”。ArkUI 把后者做到极致:你只要维护好状态,框架就负责对齐(reconcile)组件树、计算最小重绘,并在生命周期节点上给你安全插手的机会。好处?心智负担小可测试性高并发风险低


01|ArkUI 声明式 UI 架构

1.1 一眼明白的“状态 → 视图”

@Entry
@Component
struct CounterPage {
  @State count: number = 0

  build() {
    Column() {
      Text(`Count: ${this.count}`).fontSize(22).padding(12)
      Row() {
        Button('-').onClick(() => this.count--)
        Button('+').onClick(() => this.count++)
      }.justifyContent(FlexAlign.SpaceAround).width('60%')
    }.height('100%').justifyContent(FlexAlign.Center)
  }
}

要点@State 改变 → 框架对齐组件树 → 只重建变动分支 → 最小量刷新。你不需要“手动 setXXX 重绘”;ArkUI 会保证可预测的同步渲染顺序

1.2 状态传递的四大角色

  • @State:组件内部可变状态;本组件改、仅本组件重建。
  • @Prop父传子的只读输入;父变则子随之更新。
  • @Link:父子双向联动的“引用态”,适合表单子项回写父级。
  • @Provide/@Consume跨层级依赖注入(主题、会话、全局设置)。
@Provide theme = { primary: '#0A84FF', radius: 16 }
@Consume theme  // 子孙组件取用,无需层层透传

1.3 Reconciliation(对齐)策略的工程意义

  • 纯函数式 buildbuild() 只根据当前状态拼装 UI,不要做副作用(发请求、订阅……)。
  • 稳定节点关键属性id()key() 可帮助 ArkUI 识别同一逻辑节点,减少无效重建。
  • 长列表优化List/LazyForEach 合理设置 cachedCountsticky,避免“一滑就卡”。

02|组件生命周期与事件绑定

2.1 生命周期时间线(常用)

  • aboutToAppear()组件初次进入渲染树前后的一瞬;适合一次性初始化(轻量)。
  • onPageShow() / onPageHide():页面级可见性变化(路由返回、进入后台);适合恢复/挂起逻辑。
  • aboutToDisappear()组件即将离开渲染树;释放资源、取消订阅。
  • onBackPress():拦截返回;做“二次确认”或状态保存。

小贴士:IO/网络不要塞进 build。想要请求数据?在 aboutToAppear() 中触发,然后用 @State 驱动视图刷新。

@Component
struct FeedList {
  @State items: Article[] = []
  private cancel?: () => void

  aboutToAppear() {
    const { cancel, promise } = fetchFeed() // 你自己的请求封装
    this.cancel = cancel
    promise.then(list => this.items = list).catch(_ => showToast('Load failed'))
  }

  aboutToDisappear() {
    this.cancel?.()
  }

  build() {
    List() {
      ForEach(this.items, (it) => ListItem() { Text(it.title) })
    }
  }
}

2.2 事件绑定的三条铁律

  1. 回调是无名英雄.onClick()、手势 .gesture() 都会闭包当前状态;别在回调里引入过期引用(异步更新时注意)。
  2. 节流/防抖要自带:高频手势(拖拽/滚动)里做 throttle;表单输入做 debounce。
  3. 副作用有生有死:在 aboutToDisappear()清理计时器、监听、订阅,别“魂飞魄散”。
Button('Save')
  .onClick(async () => {
    if (this.saving) return
    this.saving = true
    try { await api.save(this.form) } finally { this.saving = false }
  })

03|自定义组件与样式管理

3.1 自定义组件:把“拼装”变“复用”

// 可复用的卡片容器
@Component
export struct Card {
  @Prop title: string
  // slot 让父组件塞内容进来
  build() {
    Column() {
      Text(this.title).fontSize(18).fontWeight(FontWeight.Medium).padding({ top: 12, bottom: 8 })
      // 内容插槽
      // @ts-ignore: 伪示例,具体以版本 API 为准
      Slot()
    }
    .backgroundColor($r('app.color.cardBg'))
    .borderRadius($r('app.float.radius'))
    .padding(12)
    .shadow({ radius: 12, color: '#00000022', offsetY: 4 })
  }
}

使用:

Card({ title: 'Profile' }) {
  Row() {
    Image($r('app.media.avatar')).width(48).height(48).borderRadius(24)
    Column() {
      Text('Cat Coder').fontSize(16)
      Text('“咖啡续命,代码续杯。”').fontColor('#666')
    }.margin({ left: 12 })
  }
}

3.2 资源化样式:团队协作的“唯一真相源”

把颜色、字号、圆角、间距抽到资源里(resources/base/element/resources/base/profile/theme.json等),不同主题共享一套 key,按需切皮。

示例:resources/base/profile/theme.json

{
  "app": {
    "color": {
      "primary": "#0A84FF",
      "text": "#111111",
      "muted": "#666666",
      "cardBg": "#FFFFFF",
      "cardBgDark": "#1C1C1E"
    },
    "float": {
      "radius": 16,
      "gap": 12
    }
  }
}

在 ArkTS 使用:

.backgroundColor(isDark ? $r('app.color.cardBgDark') : $r('app.color.cardBg'))
.borderRadius($r('app.float.radius'))
.padding($r('app.float.gap'))

3.3 主题切换与全局态

@Provide/@Consume 存放主题状态,全局切换一次、全局生效。

@Entry
@Component
struct AppRoot {
  @Provide themeMode: 'light' | 'dark' = 'light'

  build() {
    Column() {
      Toggle('Dark mode', this.themeMode === 'dark')
        .onChange(v => this.themeMode = v ? 'dark' : 'light')
      HomePage()
    }
  }
}

@Component
struct HomePage {
  @Consume themeMode: 'light' | 'dark'

  build() {
    const isDark = this.themeMode === 'dark'
    Column() {
      Text('Hello ArkUI').fontColor(isDark ? '#EEE' : '#111')
    }.backgroundColor(isDark ? '#000' : '#FFF')
  }
}

3.4 动画与微交互(animateTo/Transition

@State expanded: boolean = false

Card({ title: 'More' }) {
  Text(this.expanded ? '收起' : '展开')
}
.onClick(() => {
  animateTo({ duration: 200, curve: Curve.EaseInOut }, () => {
    this.expanded = !this.expanded
  })
})
.height(this.expanded ? 240 : 80)

04|完整小实战:习惯打卡卡片(状态、事件、样式、动画一次拉满)

目标:做一个可复用的“习惯卡片”,支持勾选完成长按重命名主题自适配入场过渡

// HabitCard.ets
export type Habit = { id: string; name: string; done: boolean }

@Component
export struct HabitCard {
  @Link model: Habit
  @Consume themeMode: 'light' | 'dark'

  @State editing: boolean = false
  @State nameDraft: string = ''

  private bg() {
    return this.themeMode === 'dark' ? $r('app.color.cardBgDark') : $r('app.color.cardBg')
  }

  aboutToAppear() {
    // 入场轻微缩放,提升“活着”的感觉
    animateTo({ duration: 160 }, () => {})
  }

  build() {
    Row() {
      Checkbox({ name: this.model.name, group: 'habit' })
        .select(this.model.done)
        .onChange(v => this.model.done = v)

      if (!this.editing) {
        Text(this.model.name)
          .fontSize(16)
          .margin({ left: 12 })
          .onLongPress(() => {
            this.editing = true
            this.nameDraft = this.model.name
          })
      } else {
        TextInput({ text: this.nameDraft, placeholder: 'Rename habit' })
          .onChange(v => this.nameDraft = v)
          .onSubmit(() => this.commit())
      }

      Blank().layoutWeight(1)

      Button(this.editing ? 'Save' : 'Edit')
        .onClick(() => this.editing ? this.commit() : (this.editing = true, this.nameDraft = this.model.name))
    }
    .padding($r('app.float.gap'))
    .backgroundColor(this.bg())
    .borderRadius($r('app.float.radius'))
    .shadow({ radius: 12, color: '#00000022', offsetY: 4 })
    .transition(TransitionEffect.OPACITY) // 列表中增加/删除的渐隐渐现
  }

  private commit() {
    const v = this.nameDraft.trim()
    if (v.length === 0) { showToast('Name required'); return }
    this.model.name = v
    this.editing = false
  }
}

列表页使用:

@Entry
@Component
struct HabitListPage {
  @Provide themeMode: 'light' | 'dark' = 'light'
  @State habits: Habit[] = [
    { id: '1', name: 'Drink water', done: false },
    { id: '2', name: 'Walk 5,000 steps', done: true }
  ]

  build() {
    Column() {
      Row() {
        Text('Habits').fontSize(24).fontWeight(FontWeight.Bold)
        Blank().layoutWeight(1)
        Toggle('Dark', this.themeMode === 'dark')
          .onChange(v => this.themeMode = v ? 'dark' : 'light')
      }.padding(12)

      List({ space: 12 }) {
        ForEach(this.habits, (h, i) => ListItem() {
          HabitCard({ model: this.habits[i] as any }) // Link 传入
        }, (h) => h.id)
      }.cachedCount(6).padding(12)
    }
  }
}

你收获了什么?

  • @Link 让子组件直接改父级数据,简单粗暴但范围要控制
  • 文案编辑走长按进入编辑提交校验,用户体验更顺滑。
  • 主题切换、入场过渡、列表缓存,一次性合体。

05|性能与易踩坑清单

5.1 性能 7 招

  1. 长列表用 LazyList + ForEach 时指定 cachedCount,避免一次渲染全量。
  2. 稳定 keyForEach(..., item => item.id),减少“整行换新”。
  3. 轻 build、重副作用管理build() 只拼 UI;副作用放 aboutToAppear(),并在 aboutToDisappear() 清理。
  4. 图片策略:大图裁剪+占位骨架;滚动时延迟加载,停下再渲染高清。
  5. 节流手势:拖拽/滑动事件里做 throttle(如每 16ms 一次)。
  6. 避免过深组件嵌套:把“含逻辑的小片段”做成扁平可复用组件,比套娃强。
  7. 动画克制animateTo 时长 150~250ms 体感最佳;避免在重布局节点上频繁动画。

5.2 易踩坑 7 条

  • 在 build 里发请求:会被多次调用,轻则重复请求,重则死锁 UI。
  • 忘记清理定时器/订阅:页面切走还在跑,内存、CPU 一起炸。
  • Link 滥用:数据可被深层子组件“随手改”,复杂页面请用 @Provide/@Consume 或集中管理。
  • 回调闭包旧状态:异步回调里用到的状态可能已变,注意取最新值或通过 Link 传引用。
  • 主题资源硬编码:颜色/间距直接写死字符串,换皮必痛;一律走 $r()
  • 动画卡顿:动画中修改布局重排属性(如宽高)过多;尽量动不触发布局的属性(透明度、变换)。
  • 过度抽象:为复用而复用,结果样式与逻辑绑死;组件只做“一件事”,组合胜过继承。

结语:漂亮界面背后,是靠谱的工程秩序

ArkUI 的美,不在“招式花哨”,而在状态即真相、生命周期有秩序、样式有中枢。当你的组件像砖,样式像模数,交互像水,项目就能既开发迅速长期可维护。下一次同事夸你页面顺滑,别害羞:这不是玄学,是你把工程做对了。😉


一页速记(带走就能用)

  • 架构:状态驱动 → build 纯函数 → 框架对齐 → 最小重绘。
  • 状态State/Prop/Link/Provide&Consume 各司其职,别混。
  • 生命周期:初始化/订阅 → aboutToAppear(),清理 → aboutToDisappear(),显隐 → onPageShow/Hide()
  • 事件:回调注意闭包;高频手势节流;副作用有生有死。
  • 样式:资源化、主题化、参数化(颜色/间距/圆角);统一 key,统一切换。
  • 复用:小而美组件+Slot;Link 谨慎,复杂场景上升到 Provide。
  • 性能:长列表 Lazy、稳定 key、轻 build、克制动画、图像延迟加载。

… …

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

… …

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。


版权声明:本文由作者原创,转载请注明出处,谢谢支持!

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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