第一个鸿蒙应用

要点

写鸿蒙应用之前先记住一句话:ArkUI 是声明式的,build() 里写的不是”创建一个按钮”的命令,而是”界面应该长什么样”的描述。状态(被装饰器标记的数据)变了,框架自己算出哪块 UI 该刷新。这篇从 Empty Ability 模板出发,先跑一个计数器,再升级成一个能增删的待办列表,最后把它和你熟悉的 React / Vue / Flutter 放在一起对照,说清心智差异。

入口链路:从模板到页面

环境搭建那篇(见系列首篇《鸿蒙开发环境搭建》)里创建的 Empty Ability 工程,运行起来显示 Hello World,中间经过了三层:

层 文件 职责
UIAbility entryability/EntryAbility.ets 应用级生命周期入口;在 onWindowStageCreate 里加载页面
页面 pages/Index.ets @Entry 标记的根组件,一个页面一个文件
组件 任意 struct @Component 标记的复用单元,页面就是组件树的根

EntryAbility 里一行 loadContent('pages/Index') 把首页挂到窗口上。入门阶段这层基本不用动,所有界面代码都从 Index.ets 开始。页面路径需要在 resources/base/profile/main_pages.json 里注册,新增页面忘了注册会直接报错——这是新手高频坑之一。

第一个界面:计数器

把 Index.ets 整个替换成:

@Entry
@Component
struct Index {
  // @State:组件内部状态,变化会自动触发相关 UI 刷新
  @State count: number = 0

  build() {
    // build() 是"描述",不是"命令":框架根据当前状态渲染这棵树
    Column({ space: 16 }) {
      Text(`点了 ${this.count} 次`)
        .fontSize(28)
      Button('加一')
        .onClick(() => {
          // 直接改状态,不需要任何"通知界面"的 API
          this.count += 1
        })
      Button('清零')
        .onClick(() => {
          this.count = 0
        })
    }
    .padding(24)
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

预期现象:预览器里点”加一”,数字立刻变;点”清零”归零。注意代码里没有 setText、没有手动刷新——this.count += 1 之后界面自己变了,这就是声明式的全部要点。属性用链式调用(.fontSize().padding())挂在组件后面,ArkUI 没有 CSS,样式、布局、事件都是组件属性。

声明式范式:三个装饰器打底

ArkTS 在 TypeScript 基础上扩展出的 UI 语法核心就三样:

装饰器 作用 类比
@Component 把 struct 声明为 UI 组件 React 的函数组件本身
@Entry 标记页面根组件(每个页面一个) 路由入口组件
@Builder 抽出可复用的 UI 片段(无独立状态) 最接近”函数式子组件”的东西

@Builder 的用法:

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

  // 复用 UI 片段:参数是值传递,适合纯展示
  @Builder label(text: string) {
    Text(text).fontSize(14).fontColor('#666')
  }

  build() {
    Column() {
      this.label('计数器 Demo')
      Text(`${this.count}`).fontSize(32)
    }
  }
}

升级:一个待办应用

// 数据模型:字段显式声明类型,符合 ArkTS 严格约束
class Todo {
  id: number
  title: string

  constructor(id: number, title: string) {
    this.id = id
    this.title = title
  }
}

@Entry
@Component
struct Index {
  @State input: string = ''
  @State todos: Array<Todo> = []
  private nextId: number = 1   // 私有且非状态:只用于生成 id,不驱动 UI

  build() {
    Column({ space: 12 }) {
      Row({ space: 8 }) {
        TextInput({ placeholder: '输入待办', text: this.input })
          .layoutWeight(1)          // 占满剩余宽度,类似 flex: 1
          .onChange((v: string) => { this.input = v })
        Button('添加')
          .onClick(() => {
            const title: string = this.input.trim()
            if (title.length === 0) {
              return
            }
            // @State 观测数组第一层的增删,push 后列表自动刷新
            this.todos.push(new Todo(this.nextId, title))
            this.nextId += 1
            this.input = ''
          })
      }
      .width('100%')

      List({ space: 8 }) {
        ForEach(this.todos, (item: Todo) => {
          ListItem() {
            Row({ space: 8 }) {
              Text(item.title).layoutWeight(1)
              Button('删')
                .onClick(() => {
                  // 按条件过滤生成新数组,再整体赋值
                  this.todos = this.todos.filter((t: Todo) => t.id !== item.id)
                })
            }
          }
        }, (item: Todo) => item.id.toString())   // key 生成器:靠 id 而不是数组下标
      }
      .layoutWeight(1)

      if (this.todos.length === 0) {
        Text('暂无待办').fontColor('#999')
      }
    }
    .padding(20)
    .width('100%')
    .height('100%')
  }
}

三个值得停下来的点。第一,filter 返回新数组后整体赋值是最稳的删除方式,id 过滤天然正确。第二,ForEach 的第三个参数是 key 生成器,用业务 id(item.id)而不是下标——用下标时”删除中间一项”会导致 key 错位、界面和内存数据对不上。第三,if (this.todos.length === 0) 这种条件渲染直接写在 build() 里,状态一变框架重新求值,不需要 v-if 那样的指令。

与 React / Vue / Flutter 的心智对照

维度 ArkUI React Vue 3 Flutter
声明状态 @State 计数器 useState ref / reactive State 字段
修改状态 直接赋值,可变 setXxx 或不可变更新 直接改 .value 必须包 setState
视图更新粒度 框架按状态依赖做细粒度更新 整个函数组件重跑 + diff 组件级重跑 + diff 整棵 build 重跑 + diff
UI 复用 @Builder / @Component 函数组件 组件 Widget 组合

最值得注意的差异是”可变状态”:React 训练出来的肌肉记忆是”状态不可变、每次造新对象”(为什么非得这样,见《React Hooks 心智模型》与《React 渲染与重渲染》),而 ArkUI 反过来,鼓励直接改 this.count、直接 push 数组——观测机制在装饰器层完成。Flutter 开发者可以对照《第一个 Flutter 应用》:setState 标记这步在 ArkUI 里不存在,但换来的是观测规则本身要学(下一节的坑就是观测边界的坑)。

常见问题

Q1: 改了嵌套对象的属性,界面不动

V1 状态装饰器(@State 等)只观测第一层:数组项的增删、对象整体替换能看到,但 this.todos[0].title = 'x' 这种深层修改不会触发刷新。三种解法:整对象替换、用 V2 的 @ObservedV2/@Trace(属性级深度观测)、或把嵌套拆平。详见下一篇语法要点的状态管理一节。

Q2: ForEach 列表增加或删除后显示错乱

九成是 key 生成器的问题:没写(默认用 index 且有告警)、或 key 不唯一。用稳定业务 id 做 key,删除场景立刻恢复正常。

Q3: 代码明明没错,预览器却白屏

预览器只渲染 UI,不支持部分系统 API(http 请求是典型)。换模拟器或真机再判断代码对错,别在预览器上白耗时间。

最佳实践与反模式

反模式一:找”拿到 Text 控件实例再 setText”的 API。ArkUI 里不存在控件句柄,一切界面变化都走”改状态 → 框架刷新”,Web 的 DOM 操作思维在这里没有落点。

反模式二:把所有状态都堆在 @Entry 页面顶层。状态先放离使用它最近的组件;跨组件共享再上 @Provide/@Consume 或 V2 方案,全局状态膨胀后谁都不敢动。

实践建议:每写一个界面,能口头回答三个问题——状态有哪些、哪个交互改哪个状态、哪块 UI 依赖哪个状态。答得上来,声明式就算入门了。

练习

  • 给待办加”完成”功能:点标题划线。先试直接改 item.done 观察(不刷新的)现象,再用整对象替换修好它
  • 把空态提示改成”共 N 项”,体会 Text 如何依赖状态自动更新
  • 给每个列表项加创建时间并用 @Builder 渲染成副标题

下一步

这篇跑通了”状态驱动界面”,但刻意绕开了语言层:为什么必须有类型标注、哪些 TypeScript 写法在这里编译不过、@State 背后的 V1/V2 两套状态管理体系是怎么回事,都在下一篇《ArkTS 语法要点:TypeScript 的严格方言》展开;查表需求直接跳系列末篇《ArkTS / ArkUI 基础知识参考字典》。

版本核实(撰写时检索,2024-12):本文语法(@Entry/@Component/@Builder、@State、ForEach、List/TextInput/Button)基于 HarmonyOS NEXT 5.0(API 12)的 ArkTS/ArkUI 声明式范式,在当前 Release SDK 下可用;状态管理 V2(@ObservedV2/@Trace 等)自 API 12 起引入。组件参数与 API 细节以 developer.huawei.com 官方文档为准。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇