☰
用ArkTS写计数器,吃透HarmonyOS声明式UI与状态管理
2026/10/12 6:38:12 网站建设 项目流程

1. 先说结论:这个小项目到底在练什么?

HarmonyOS 6的开发入门,绕来绕去还是得回到计数器上。别觉得它太简单——这个Demo虽然只有两个按钮和一个数字,但它把ArkTS声明式开发的状态管理、组件封装、事件绑定、条件渲染这些核心概念全串起来了。认认真真写一遍计数器,你对HarmonyOS应用开发的整个脉络会清晰很多。

这篇文章写给两类人:一类是刚把开发环境装好、还没写过一行HarmonyOS代码的新手;另一类是做过Android或前端开发、想快速了解ArkUI开发范式的技术迁移者。前者可以通过它迈出第一步,后者可以通过它看到一个熟悉又陌生的世界——熟悉的是界面元素,陌生的是"状态驱动UI"这套思维方式。

简单说明一下这个项目的最终效果:屏幕中间一个很大的数字,下方两个按钮,一个加一、一个减一,数字小于等于0时减一按钮自动置灰。功能就这么点,但它背后的知识点足够你消化一阵子。

我在文中用到的环境和代码,以HarmonyOS 6开发环境为基准。不同版本IDE的SDK API编号可能略有出入,但ArkTS的语法、ArkUI的组件模型和状态管理机制是一脉相承的,你本地SDK版本比我新或者比我旧,都不影响理解。

1.1 为什么把计数器当成HarmonyOS的第一道菜

很多初学者喜欢一上来就搞复杂的登录页、列表页、网络请求,结果被一堆概念砸晕。计数器这个项目最大的好处是"逻辑单一、界面干净",你能把全部注意力放在框架本身上,而不是业务逻辑。

它覆盖了应用开发最核心的链路:界面长什么样(UI)、用户点了怎么办(事件)、数据变了怎么刷新(状态管理)、代码怎么组织(组件拆分)。这条链路无论做多复杂的App都跑不掉。先在一个极简场景里把链路走通,后面面对复杂业务时才不会手忙脚乱。

另外还有一个很现实的原因:HarmonyOS的开发资料虽然越来越多,但很多教程一上来就堆概念,ArkTS、ArkUI、Stage模型、Ability、Page……新手很容易被劝退。计数器是官方文档和各类课程里最常出现的例子,资料齐全,遇到问题容易搜到答案。拿它做起点,等于给自己铺了一条平滑的入门斜坡。

1.2 需要提前准备的基本功

在动手之前,有几点背景知识值得先理清,不然写代码时会觉得处处是"魔法"。

HarmonyOS 6的应用开发主语言是ArkTS,它是TypeScript的超集,加上了ArkUI的装饰器和状态管理语法。如果你写过一点TS或JS,上手成本很低;就算完全没写过,凭直觉也能看懂大部分代码,因为它的核心思路是"描述界面长什么样,而不是一步步命令界面怎么画"。

界面框架叫ArkUI,采用声明式范式。传统命令式UI里,你要手动调用各种方法去更新界面,比如"把这个文本改成100""把这个按钮禁用掉";声明式UI里,你只需要声明数据和界面的关系,数据一变,界面自动跟着变。这个转变是第一道门槛,也是计数器项目最值得体会的点。

工程模型上,HarmonyOS 6采用Stage模型,一个应用可以有多个Ability,每个UIAbility负责一个界面入口。计数器虽然只有一个页面,但理解这个模型的骨架非常重要,因为后面所有应用都是在这个骨架上长出来的。

1.3 项目完成后的知识清单

动手之前,先列一个"完成清单",做完之后对照检查,确保不是照抄代码,而是真的吃透了。

第一,你能熟练地说出ArkTS的装饰器有哪些,@Entry、@Component、@State各自负责什么。第二,你能不用看文档,随手写一个带状态的自定义组件。第三,你能解释清楚"状态变化如何触发UI刷新"这件事。第四,你能在模拟器和真机上把HAP包跑起来并完成调试。

如果这几点都能做到,这个项目就算真正过关了。下面开始正式进入实操环节。

2. 环境搭建与工程骨架

工欲善其事,必先利其器。开发HarmonyOS应用,第一步是把官方IDE装好。这一步本身没什么难度,但有不少细节值得留意,版本选错、SDK没配齐,后面会浪费很多时间。

2.1 IDE安装与SDK配置

前往官方渠道下载DevEco Studio最新版,安装过程和其他IDE没有本质区别,一路下一步即可。装完之后不要急着新建工程,先确认几个环境项:

一是SDK是否完整。首次启动IDE会自动提示下载HarmonyOS SDK,包含ArkTS编译工具链、模拟器镜像、系统API等。国内网络环境下这个下载过程可能需要一些时间,建议挂一个稳定的网络等它跑完,不要中途取消。

二是检查SDK目录下的API版本。不同API版本对ArkTS语法和组件能力的支持有差异。HarmonyOS 6对应的SDK版本号以你本地IDE提示为准,但我建议始终使用最新稳定版,不要为了兼容旧设备特意降级——对于学习阶段的项目,新版本能让你体验到最完整的能力。

三是确认开发模式。HarmonyOS 6支持Stage模型,新建工程时选择这个模型即可。旧版本的FA模型已经不在主线学习路径里,不用去管。

2.2 创建工程:别小看这一步的选择

打开IDE,选择新建项目,会看到一堆模板:Empty Ability、List Ability、Login Ability等等。初学者直接选"Empty Ability",也就是空页面模板。有的版本里叫"Empty Page",本质上是一样的。

给项目起名的时候,注意以下几点。项目名建议用英文小写加数字,不要带中文和空格,例如"counterdemo"。包名是应用在系统中的唯一标识,默认会生成一个基于域名的倒写格式,例如"com.example.counterdemo"。学习项目不用太纠结包名,但养成好习惯——用你自己的域名倒写,别用默认的example,避免后面发布时到处改。

工程创建完成后,IDE会为你在本地生成一整套代码和配置。这时不要急着看代码,先花五分钟把工程目录过一遍。很多人忽略这一步,导致后面想加配置找不到文件,出了问题也不知道去哪看日志。

2.3 读一遍工程目录,比写代码更值钱

一个标准的Stage模型工程,有几个关键位置:

  • AppScope/:应用级配置,包含应用图标、应用名称等信息。
  • entry/:应用主模块,代码基本都在这里。
  • entry/src/main/ets/:ArkTS源码目录,里面又分为entryability和pages等子目录。
  • entry/src/main/ets/pages/:页面文件所在目录,默认有一个Index.ets。
  • entry/src/main/resources/:资源目录,字符串、颜色、图片等资源都放在这里,分目录管理。
  • entry/src/main/module.json5:模块配置文件,声明Ability、页面路由、权限等关键信息。
  • entry/ohosTest/:测试代码目录,后续进阶可以在这里写单元测试。

这个目录结构初看有点繁琐,但它背后是有逻辑的:应用级配置、模块级配置、页面代码、资源文件各司其职。养成"通过目录找文件"的习惯,比在IDE里到处翻要高效得多。

举个例子,你想改应用在桌面显示的名称,去AppScope里的配置文件改;你想改页面标题栏,去页面所在目录找对应的代码文件;你想加一个图片资源,去resources里放文件并引用。这个对应关系理清了,工程结构就不再是一堆陌生文件夹了。

3. 用ArkUI画出计数器界面

环境没问题之后,正式开始写界面。ArkUI的界面代码以组件树的形式组织,一个页面由多个组件嵌套而成,最外层是根容器,里面放各种子组件。计数器界面很简单:一个文本组件显示数字,一个行容器放两个按钮。

3.1 声明式UI:从XML思维切换到状态思维

如果你以前写过Android,肯定会疑惑:为什么界面不写在XML里?ArkUI选择直接在代码里用组件函数描述界面,好处是界面和逻辑写在一起,不用来回切换文件。

想象你正在给朋友描述一个界面:页面中央有一行,行里有左中右三块。在ArkUI里,你写的代码差不多就是这个描述本身。

@Entry @Component struct Index { build() { Column() { Text('0') Row() { Button('-') Button('+') } } } }

这段代码现在已经能渲染出一个基本界面了,只是很简陋。注意Column和Row这两个容器组件:Column让子组件纵向排列,Row让子组件横向排列。嵌套关系一定要想清楚再写,UI层级越复杂,这点越重要。

3.2 第一个版本的界面代码

为了让界面好看一点,加上样式和布局参数。我的做法是:先写结构,再调样式,不要一步到位,因为调试样式时视图刷新很快,逐步调整反而效率高。

@Entry @Component struct Index { build() { Column() { Text('0') .fontSize(96) .fontWeight(FontWeight.Bold) .fontColor('#0B0B0B') .margin({ bottom: 40 }) Row() { Button('-') .width(120) .height(48) .margin({ right: 16 }) Button('+') .width(120) .height(48) } } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .backgroundColor('#F5F5F5') } }

几个关键样式属性说明一下。justifyContent(FlexAlign.Center)是让Column容器内的所有子组件在主轴方向居中,配合宽度和高度各占100%,数字和按钮就整体居中显示了。width和height支持数字或字符串,数字单位默认是vp,也就是虚拟像素,系统会自动适配不同分辨率的屏幕。这一点很省心,你不需要像传统Android开发那样为不同屏幕密度准备多套尺寸。

3.3 样式细节:让按钮和数字像样

样式不能光好看,还要好点。两个按钮的尺寸略微区别对待:减号按钮因为会频繁触发,我习惯把它做得稍微大一点,触点区域大,误触概率低;加号按钮同理。按钮高度大于48vp能较好地适应手指点按。

再补充两个提升质感的小细节。一是给按钮设置圆角,默认按钮样式在6.0里已经有圆角了,但如果你用的版本默认值不理想,可以显式设置borderRadius。二是点击按钮时给一个按压反馈,比如背景色变化,让用户感知到"点到了"。ArkUI里可以给按钮添加stateEffect,它会自动处理按压效果。

Button('-') .width(120) .height(48) .borderRadius(12) .stateEffect(true)

到这里,界面部分先告一段落。你会发现代码量不多,但每个属性后面隐藏着不少调整空间。界面不是一次性写完美的,后面跑起来看一眼,再回来微调,这是正常节奏。

4. 状态管理与交互逻辑

界面画出来了,但按钮点了没反应,数字也不会变。接下来就是计数器的灵魂——状态管理。

4.1 @State装饰器到底做了什么

在ArkUI里,一个变量如果加了@State装饰器,它就不再是普通变量,而是"被观察"的状态。当这个变量的值发生变化时,所有依赖它的UI组件会自动重新渲染。这个机制,官方叫"状态管理",理解起来其实特别像电路里的总开关。

你把@State修饰的变量想象成一个开关,UI组件想象成灯泡。灯和开关之间有一根隐形的线,只要开关拨动,灯自己就会亮或灭,不需要你手动去拉每盏灯。

@State count: number = 0

在计数器里,这个状态就是count。你的界面里,Text('0')显示的内容和它绑定,按钮点击时要改的也是它。只要count一变,数字自动刷新,这就是声明式UI的核心体验。

新手最容易犯的错误是:忘记加@State,然后直接修改变量,发现界面死活不刷新。排查方向很简单——先检查变量有没有被@State装饰。

4.2 事件绑定与点击响应

按钮要响应点击,在ArkUI里用的是onClick事件。

Button('+') .onClick(() => { this.count++ }) Button('-') .onClick(() => { this.count-- })

onClick接收一个箭头函数,函数里写点击后要执行的逻辑。这里有一点要特别注意:必须使用this.count来引用组件中的状态变量,不能直接写裸的count。很多初学者在onClick里写count++,然后报错说找不到变量,原因就在这里。

为什么是this?因为count是Index这个组件实例的属性,访问实例属性必须通过this。箭头函数的写法能保证this指向正确,如果你改用普通function定义回调,this就会丢失,这也是一个经典坑。

到这里,一个能加能减的计数器已经可以跑了。跑起来点几下,数字确实会变,说明状态管理链路通畅了。

4.3 边界控制:减到0怎么办

功能看起来完成了,但实际用一下就会发现:一直点减号,数字变成负数,这不符合"计数器"的直觉。在很多业务场景里,你也要学会对用户输入做约束。

给减号按钮加一层判断:只有当前数字大于0时,才允许减一;否则按钮置灰,不可点击。ArkUI里控制按钮是否可用的属性是enabled。

Button('-') .enabled(this.count > 0) .onClick(() => { this.count-- })

enabled接收一个布尔值,true表示可点,false表示禁用。当count等于0时,减号按钮自动变灰,点击无效;一旦count变成1,按钮自动恢复。这个逻辑加完之后,计数器才符合真实使用预期。

这里有个关于"边界条件"的思考方法值得多说一句:写任何功能,都要问自己三个问题——最小值是多少?最大值是多少?达到边界时用户会看到什么?计数器的最小边界是0,最大不设限,边界时按钮置灰。这种思考方式会在后面所有项目里反复用到。

4.4 代码优化:抽一个自定义子组件

界面和逻辑都写完了,但如果你以后想复用这个计数器,比如在一个页面放两个计数器,现在的代码会让Index越来越臃肿。更好的做法是,把计数器独立成一个子组件,然后在页面里引用它。

@Component struct Counter { @State count: number = 0 build() { Column() { Text(`${this.count}`) .fontSize(96) .fontWeight(FontWeight.Bold) .margin({ bottom: 40 }) Row() { Button('-') .width(120) .height(48) .enabled(this.count > 0) .onClick(() => { this.count-- }) Button('+') .width(120) .height(48) .onClick(() => { this.count++ }) } } } } @Entry @Component struct Index { build() { Column() { Counter() } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } }

注意Counter是自定义组件,它用@Component装饰,和页面长得一样。区别在于页面用@Entry标记,自定义组件则不需要。把组件的样式和状态封装在自身内部,外部引用时一行代码就行,这是组件化的基本模式。

组件化带来一个额外的好处:每个组件的状态是独立的。你在页面里放两个Counter,它们互不干扰,各自维护各自的count。这一点在开发复杂应用时极其重要。

5. 在模拟器和真机上跑起来

代码写完了,最终目标是在设备上跑起来。HarmonyOS开发提供了Previewer、模拟器、真机三条路径。三条路径我都建议试一遍,因为它们的侧重点不同。

5.1 Previewer快速验证

DevEco Studio自带界面预览器,不用启动模拟器就能看到当前页面的渲染效果。它的优势是快,几乎实时刷新,适合写界面时边改边看,能显著缩短开发循环。

使用Previewer有个小技巧:在预览器顶部可以切换不同尺寸的设备模型,包括折叠屏、平板、手机等。虽然计数器界面简单,但看它在不同屏幕尺寸下的布局表现还是有价值的。比如你会发现,在平板上布局可能显得太居中,数字过大——这些都能通过Previewer提前发现。

不过Previewer也有局限:它主要验证界面呈现,对交互事件、网络请求、系统能力API的覆盖不全。很多问题只有真机才能暴露出来。

5.2 本地模拟器运行

模拟器能提供比Previewer完整得多的运行环境。在IDE的设备管理器里,选择一个你需要的模拟器镜像下载并创建。启动模拟器后,点击运行按钮,IDE会自动完成编译、打包、安装,整个过程全自动,对新手很友好。

这里想提醒一个细节:模拟器启动可能比较慢,第一次启动甚至需要几分钟。这不是你的操作有问题,是HarmonyOS模拟器在冷启动时要初始化完整的系统环境。启动过程中不要反复点击设备管理器,也不要关闭IDE,耐心等一下就好。

模拟器跑起来之后,你就能用鼠标点击按钮、观察界面变化了。注意观察两个细节:一是点击按钮时数字能否即时刷新,二是减号按钮在数字为0时是否真的置灰。这些都是对状态管理的直接验证。

5.3 真机调试与自动签名

模拟器能跑通,但真实体验还是要在真机上验证。HarmonyOS设备也支持开发者模式,打开方式和其他主流系统类似:进入设置,连续点击版本号,开启开发者选项,再打开USB调试。用数据线连接电脑后,设备上会弹出调试授权提示,点击允许即可。

这里有一个容易卡住的环节:签名。HarmonyOS应用安装到真机需要签名证书,但新手不用去研究证书配置的细节,IDE提供自动签名功能,会用本地生成的调试证书为你的应用签名。前提是你已经在IDE里完成了开发者账号的登录授权。这一步跟着IDE的引导走就行,几分钟搞定。

5.4 常见问题速查表

盘点一下我在教学过程中最常遇到的几个问题:

现象常见原因解决思路
Previewer一片空白页面代码有编译错误看IDE的编译日志,逐一修复语法问题
模拟器一直黑屏镜像冷启动慢等待2-5分钟,期间不要操作设备管理器
真机点击运行提示未签名自动签名未配置登录开发者账号并完成自动签名向导
安装时报版本不兼容SDK版本与设备系统不匹配检查设备系统版本与SDK API映射关系
按钮点击无反应事件绑定写错或变量没加@State先检查变量装饰器,再检查onClick中的this引用
界面中文显示乱码字符串硬编码未使用资源文件使用resources中的stringResource

这张表里的问题都很典型,记住大概率能救你一次。

6. 做完之后还能往哪走

计数器只是个起点,做完之后如果就此打住,收获会打个折扣。花点时间回头看一遍代码,你会看到很多可以继续延伸的线索。

6.1 把计数器变成通用组件

现在Counter组件的count状态是内部维护的,外部页面无法控制它的值。在一些场景里,你可能需要外部能读取或重置计数器的值。这时候就需要用到@Prop或@Link装饰器,实现父子组件之间的状态同步。

比如加一个重置按钮,让外部页面能一键把count清零。你可以定义一个属性让外部传值进来,或者通过事件把内部状态抛出去。这些都属于状态管理的进阶用法,计数器项目是一个非常好的练习场景。

再去看看官方文档里的状态管理章节,你会看到@State、@Prop、@Link、@Provide、@Consume、@Watch等一串装饰器。它们解决的是不同场景下的状态共享问题。别被数量吓到,记住一个核心原则:哪些数据需要被UI观察、这些数据归属于哪个组件、哪些数据要在组件间共享——想清楚这三点,就不会选错装饰器。

6.2 下一步学习路线

既然计数器已经帮你走通了"创建工程—写界面—管理状态—跑真机"的完整链路,下一步就可以往更有实际价值的方向走。

第一优先级是列表。做一个待办事项列表,把ForEach循环渲染、@State数组、增删改查这些基本功练扎实。这个项目能让你彻底理解"数据驱动UI"的威力——数组一变,整列都刷新。

第二优先级是和系统能力打交道。做一个天气页面,调定位API、网络API、权限申请,感受一下Promise回调和权限请求是怎么玩的。到这一步,你已经能做出一个真正能上桌面的小应用了。

第三优先级才是架构进阶。等你写过几个项目之后,再去研究MVVM模式、路由管理、组件通信框架这些抽象概念,会发现它们突然变得好懂。这就像学游泳,先在浅水区玩熟了,再去学技术动作,效率高得多。

最后再分享一个我的个人习惯:每学一个新知识点,都在计数器的代码上做一次小实验。比如把数字改成动画效果,给按钮加个震动反馈,或者把样式抽成公共资源。小项目的好处是试错成本低,可以放心大胆折腾。这个习惯让我当初从只会复制代码,慢慢变成真的理解框架的设计意图。你也可以试试。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询