news 2026/10/1 19:13:40

HarmonyOS ArkTS声明式UI基础组件实战:从Text到List与状态管理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HarmonyOS ArkTS声明式UI基础组件实战:从Text到List与状态管理

1. 先把思路切换过来:ArkTS声明式UI不等于"写标签调样式"

如果你是从Web前端或者后端顺手学Harmony的,第一周大概率会有一个共同的困惑:翻官方文档时每个基础组件都能看懂,Text是文本,Image是图片,Button是按钮,可真要自己动手拼一个登录页、一个商品列表,反而不知道从哪儿下手。

这其实是思路还没切换过来的问题。很多做Web的同学会下意识把Harmony的组件类比成Bootstrap那套东西——写个div,加个类名,样式就上去了。但在HarmonyOS的ArkTS声明式开发里,UI不是"先画出来再绑定数据"的静态标签,而是"当前数据状态对应什么样的界面"。基础组件的用法,本质上就三件事:参数控制显示内容,属性控制样式效果,事件控制交互动作。任何一个组件,你只要把这三点拆清楚,基本就掌握了七八成。

举个最直观的例子,一个最简单的页面:

@Entry @Component struct Index { @State message: string = 'Hello Harmony' build() { Column({ space: 10 }) { Text(this.message) .fontSize(24) .fontColor(Color.Black) Button('点击我') .onClick(() => { this.message = '你点了我' }) } .width('100%') .padding(20) } }

这一段代码几乎包含了所有基础组件的共同规律:Colum负责布局,Text负责展示字符串,Button负责触发事件,@State让数据一变、界面自动跟着变。所以我在带新人时,特别不建议一开始抱着API文档从头翻到尾,那样翻完就忘。先把这种"状态驱动界面刷新"的思维建立起来,再去记具体组件,效率会高得多。

2. 天天都要用的四个高频组件:Text/Image/Button/TextInput

2.1 Text不能只当"一段文字"用

Text是全项目里出镜率最高的组件,没有之一。表面上它就是显示字符串,但实际上日常开发中有一堆细节容易踩。

第一是字体单位。Harmony里推荐用fp(font pixel)表示字体大小,用vp(virtual pixel)表示尺寸间距。最早我用px写习惯了,直接写Text组件的fontSize时给个16就完事。但在不同分辨率设备上,16这个裸数值默认是vp,和px并不等价。建议写成fontSize(16)其实OK,系统会按vp处理,但如果你在perviewer的工程配置里改了像素单位,记得统一。

第二是截断和换行。很多需求是标题最多显示两行、超出用省略号。这个在Text组件里有一套组合写法:

Text('这是一段很长的商品标题,超过两行应该显示省略号') .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) .textAlign(TextAlign.Start)

注意一个坑:maxLines如果设为1、同时没有写textAlign,默认的对齐在部分场景下会让人感觉文字"靠左对齐但底部有空白",其实不是空白,是行高撑起来了。遇到这种显示的"怪问题",先检查是不是行高、内边距、最大行数三者叠加的结果,而不是急着去调样式。

第三是富文本。同一个Text里想显示不同颜色和字号的文字,很多人第一反应是拆成多个Text再横向排,其实用Span更省事:

Text() { Span('¥') .fontSize(14) .fontColor(Color.Red) Span('199') .fontSize(28) .fontColor(Color.Red) .fontWeight(FontWeight.Bold) Span('/件') .fontSize(14) .fontColor('#999999') }

2.2 Image的填图模式决定UI是否"变形"

Image组件本身不复杂,Image(src)里支持三种来源:本地资源$r('app.media.icon')、网络地址、base64字符串。但一旦图片尺寸和组件尺寸不一致,objectFit就成了决定视觉质量的关键参数。

  • ImageFit.Contain:保持宽高比,完整展示图片,多余区域留空。适合商品大图、Logo。
  • ImageFit.Cover:保持宽高比,铺满整个区域,超出部分裁剪。适合轮播图、头像背景。
  • ImageFit.Fill:不保持宽高比,强行拉伸填满。适合尺寸已裁好的装饰图。

我见过最多的视觉事故,就是轮播图用了Contain,结果大图左右两侧露出黑色背景条;或者头像用了Fill导致人脸被拉变形。实际项目里,轮播图背景一般会设置成深色或模糊底图,配合Cover再做一层插值:

Image('https://example.com/banner.png') .width('100%') .height(180) .objectFit(ImageFit.Cover) .interpolation(ImageInterpolation.High)

再说一个不算冷门的细节:本地图片一定要按目录规范放。资源默认放在resources/base/media下,如果按设备类型区分,则放在resources/dark/media、resources/tablet/media这类限定目录里。项目里不按限定目录放,等到适配深色模式、平板时,就会面临大量重复返工。

2.3 Button的形态与状态反馈

Button在Harmony里有三种预设形态,ButtonType.Capsule胶囊形、ButtonType.Circle圆形、ButtonType.Normal矩形。其中Capsule用得最多,它自带圆角表现,在视觉上非常贴合移动端风格。

实际开发中我很少用默认构造函数去写一个光秃秃的文字按钮,因为真实业务里按钮不只是"一个字加上一个点击事件"那么简单。我们经常需要自定义背景色、圆角、甚至带图标:

Button({ type: ButtonType.Normal, stateEffect: true }) { Row({ space: 4 }) { Image($r('app.media.ic_add')) .width(16) .height(16) Text('加入购物车') .fontSize(16) } } .backgroundColor('#FF6B00') .width('100%') .height(44) .borderRadius(8) .onClick(() => { // 业务逻辑 })

这里有个易忽视的点:stateEffect控制按下时是否有按压效果。如果设为false,点击按钮时完全没有任何视觉反馈,用户会以为没点到。大家出于"去默认样式"的惯性把它关掉后,一定要自己补按压缩放或变色反馈,否则交互体验非常生硬。

2.4 TextInput输入框:占位符、类型与实时监听

TextInput是表单页的核心组件,虽然API简单,但开发中涉及输入类型的场景经常被忽略。比如手机号输入框,键盘应该是数字键盘,密码框需要做隐藏输入。

TextInput({ placeholder: '请输入手机号' }) .type(InputType.PhoneNumber) .maxLength(11) .enterKeyType(EnterKeyType.Go) .onChange((value: string) => { this.phone = value }) TextInput({ placeholder: '请输入密码' }) .type(InputType.Password) .showPasswordIcon(true) .onChange((value: string) => { this.password = value })

showPasswordIcon默认是会显示那个小眼睛切换明文/密文的,如果产品要求不显示,需要显式设置为false。另外,如果你发现输入框文字和占位符在视觉上看起来"没对齐",往下查一层,通常不是TextInput的问题,而是外层布局容器的alignItems或padding造成的偏移。

这四个组件搞明白,一个基础的信息录入页面、一个商品展示页面的"骨架"就已经有了。下面是这四个高频组件的常用属性和典型使用场景速查表:

组件核心参数常用属性典型场景最常见的翻车点
Text字符串/字符串资源fontSize、fontColor、maxLines、decoration标题、正文、价格截断效果不生效
Image$r资源或网络地址objectFit、alt、interpolation商品图、头像、轮播图片变形、白屏
Buttonlabel/自定义子组件type、stateEffect、backgroundColor提交、跳转、删除无点击反馈
TextInputplaceholdertype、maxLength、showPasswordIcon登录、搜索、验证码键盘类型不对

3. 组件能"组合成界面",靠的是布局容器

单个组件是一块块积木,把积木搭起来的,是Column、Row、Stack这些布局容器组件。很多新手写页面时会陷入"每一个元素都写绝对坐标"的误区——拿CSS的绝对定位思维硬套,结果一到不同尺寸的设备上,界面直接乱掉。Harmony的布局思路是父容器决定子组件怎么排,你只要告诉容器"从哪对齐、要不要换行、间距多少",系统自己计算位置。

3.1 Column/Row线性布局是地基

Column是纵向排列,Row是横向排列,这两个是最常用的布局容器。它们有几个核心属性值得专门记一下:

  • space:子组件之间的间距,比在每个子组件上单独加margin要好维护得多。
  • alignItems:在交叉轴上的对齐方式。Column里控制水平方向,Row里控制垂直方向。
  • justifyContent:在主轴上分配空间。想置底放按钮,就用VerticalAlign.Bottom。
  • layoutWeight:权重分配,相当于Flex布局里的flex-grow。

我举个实际例子。底部栏加内容区的经典结构,主内容区域占满剩余空间,底部按钮始终贴底,代码写出来是这样:

Column() { // 内容区,占据剩余空间 Scroll() { Column() { // 各种业务内容 } .width('100%') } .layoutWeight(1) // 底部按钮 Button('立即购买') .width('100%') .height(48) } .width('100%') .height('100%')

这里的关键就是.layoutWeight(1),它让Scroll把父容器减去按钮高度后剩下的空间全部吃掉。没有这个属性,你拿height('80%')去硬凑,遇到小屏设备就会按钮顶出屏幕或者内容区过短。能用layoutWeight解决的问题,就不要用百分比硬猜。

3.2 Stack层叠、Flex弹性与响应式栅格

Stack是层叠容器,所有子组件会叠加在一起,适合做"图片上显示播放按钮""头像右下角带一个红点"这种场景。它有一个alignContent属性,控制子组件整体在Stack中的对齐位置,默认是中心。比如红点定位在头像右下角:

Stack({ alignContent: Alignment.BottomEnd }) { Image($r('app.media.avatar')) .width(48) .height(48) .borderRadius(24) // 小红点 Text('3') .fontSize(10) .fontColor(Color.White) .backgroundColor('#FF3B30') .borderRadius(8) .padding({ left: 4, right: 4, top: 1, bottom: 1 }) }

Flex组件则更接近传统弹性布局,适合处理"一行多个元素水平分布、宽度自适应"的场景。默认Flex是横向排列,通过direction可以切换成纵向,justifyContent支持FlexAlign.SpaceBetween这种两端对齐的经典模式。

至于GridRow/GridCol栅格组件,说实话基础项目里用到的频率没那么高,但做平板、折叠屏适配时绕不开,它的核心价值是让同一套界面在宽屏下自动排多列。先记住有这个东西,等真正做多设备适配时再深入。

3.3 布局容器的"隐形坑":默认宽高和子组件对齐

老实说,基础组件里最容易被忽略的,是容器默认的宽度和子组件的对齐方式。

  • Column默认宽度是"包裹内容",不是撑满全屏。想让它占满,必须写.width('100%')或按需设置。
  • Row里的子组件默认是垂直方向居中对齐,如果你希望它们顶对齐,需要手动设alignItems(VerticalAlign.Top)。
  • Stack里的子组件默认全部居中叠放,不同子组件位置如果忘了改alignContent,就会出现两个元素叠在一起的现象。

这个"隐形坑"看起来低级,但几乎所有新手第一周都会碰到。排查方法很简单:在预览器里逐层点选组件,看它的布局蓝色框线到底占了多大区域,问题基本一眼就能定位。

4. 列表组件List:性能差异从渲染机制开始

4.1 为什么不要用Scroll+Column硬撸列表

早期我用ArkTS写列表,第一反应是Scroll套一个Column,再ForEach循环生成一个个卡片。界面确实能出来,但数据一多(50条以上)就开始掉帧,尤其在低端设备上滑动时明显卡顿。

原因并不神秘:Scroll+Column把所有子组件一次性全部创建并渲染了。哪怕你只需要看到屏幕上的5个卡片,系统还是把下面45个也提前创建好。这在页面复杂、图片多的时候,内存和渲染性能都会急剧恶化。

List组件的底层是按需渲染机制。它只会创建当前可视区域内以及附近预加载的一小部分列表项,滑走之后还会回收。两者的性能差距,在商品流、消息流这种长列表场景下,完全是数量级的差别。

所以只要列表项结构是统一的,别犹豫,优先用List。

4.2 LazyForEach与数据源:让列表真正支持懒加载

List搭配ForEach只能实现语法上的"循环渲染",数据量大了依然会一次性创建。要实现真正的按需加载,需要配合LazyForEach。它的数据源必须实现IDataSource接口,代码结构如下:

class ProductDataSource implements IDataSource { private products: ProductModel[] = [] totalCount(): number { return this.products.length } getData(index: number): ProductModel { return this.products[index] } registerDataChangeListener(listener: DataChangeListener): void { this.listener = listener } unregisterDataChangeListener(listener: DataChangeListener): void { this.listener = undefined } addProduct(product: ProductModel): void { this.products.push(product) this.listener?.onDataAdd(this.products.length - 1) } }

写完数据源之后,在List里这样挂载:

List({ space: 12 }) { LazyForEach(this.dataSource, (item: ProductModel) => { ListItem() { ProductCard({ product: item }) } }, (item: ProductModel) => item.id) } .width('100%') .layoutWeight(1)

这里第三个参数keyGenerator特别重要,它告诉框架每个列表项的唯一标识。如果省略,框架会使用默认的遍历序号作为键,一旦数据发生增删,会出现列表项复用错乱、UI状态串掉的诡异问题。我在实际项目中吃过一次大亏:在"收藏列表取消收藏"的场景里,没有提供稳定的keyGenerator,导致取消一条后,后面的卡片全部显示成"已收藏"的状态,花了半天才定位到是这个原因。

4.3 列表项交互:点击、加载更多与滚动定位

List组件里的每个子组件通常都用ListItem包一层,点击事件可以加在ListItem上,也可以加在ListItem内部的业务组件上。常见的"点赞、取消收藏"这类操作,最好在子组件内部去处理,同时阻止事件冒泡,避免误触发ListItem的外层点击跳转。

滚动定位是另一个高频需求,比如Tab切换后列表要滚到指定位置,或者点"回到顶部"。做法是给List设置一个Scroller:

private scroller: Scroller = new Scroller() build() { Column() { List({ scroller: this.scroller }) { // 列表内容 } Button('回到顶部') .onClick(() => { this.scroller.scrollToIndex(0, true) }) } }

scrollToIndex是滚到指定索引,scrollEdge是滚到边缘,这两个方法基本覆盖了大多数"程序控制滚动"的需求。这里有个性能小技巧:如果列表项高度不一,precise参数建议设为false,性能更好;高度固定时设true,定位更准确。

5. 让组件"活"起来:状态管理与组件更新

5.1 @State:组件内部数据的"自动刷新开关"

Harmony的声明式UI里,最核心的机制就是状态驱动刷新。你用@State修饰一个变量,当这个变量改变时,所有依赖它的组件会自动重新渲染,不需要手动操作DOM或者调用刷新方法。

@State count: number = 0 build() { Column() { Text(`当前计数: ${this.count}`) .fontSize(20) Button('加一') .onClick(() => { this.count++ }) } }

这段代码里,每次count++,Text里的文字会立刻更新。这个机制让页面逻辑变得非常直观——页面长什么样,是状态算出来的,而不是通过一步步操作改出来的。

但有一个限制必须清楚:@State只能观察一层变化。如果你用@State修饰一个对象,然后修改对象的某个嵌套属性,界面不会刷新。

@State user: User = { name: '张三', address: { city: '北京' } } // 这样改,页面不会刷新 this.user.address.city = '上海' // 这样改,页面才会刷新 this.user = { ...this.user, address: { ...this.user.address, city: '上海' } }

解决"深层次对象属性变更不刷新"有两种方式:一是用@Observed和@ObjectLink去修饰嵌套类与属性,二是改变量时重新给整个对象赋值。前者适合项目规模大、类嵌套深的情况,后者胜在简单直接。

5.2 @Prop和@Link:父子组件之间如何传值

实际项目里,一个页面不可能全在一个组件里写完,必然要拆出子组件。这时父组件的数据想传给子组件,就用@Prop或@Link。

  • @Prop:单向同步。父组件传值给子组件,子组件内部修改不会影响父组件。
  • @Link:双向同步。子组件修改会直接影响父组件的源数据。

用一句话来记:子组件只是展示用,用@Prop;子组件还能编辑这个值,用@Link。

举例,商品卡片组件,只展示外部传入的商品数据,那就用@Prop;一个包含开关状态的设置项,子组件内部需要切换开关并同步给父组件保存,那就用@Link。

// 子组件 @Component export struct ToggleItem { @Prop title: string @Link isOn: boolean build() { Row() { Text(this.title) .layoutWeight(1) Toggle({ type: ToggleType.Switch, isOn: this.isOn }) .onChange((value: boolean) => { this.isOn = value }) } } }

5.3 条件渲染与循环渲染:给组件加"逻辑分支"

有了状态驱动之后,界面上最常见的需求是"满足某个条件才显示某块内容"。Harmony里用if/else、ForEach、LazyForEach实现条件渲染和循环渲染。

Column() { if (this.isLogin) { Text(`欢迎回来,${this.username}`) } else { Button('去登录') } ForEach(this.tagList, (tag: string) => { Text(tag) .margin(4) }, (tag: string) => tag) }

条件渲染中有一个很隐蔽的坑:if内部的组件状态在条件切换时会被销毁重建。比如你在某个if分支里输入了文本框内容,切换到别的分支再切回来,输入内容就没了。如果希望保留,需要把状态提升到条件判断之外,用@State保存起来,而不是依赖子组件内部的临时状态。

5.4 给新手的忠告:状态不要"滥用"

多组件之间的状态一旦关联复杂,代码就会变得特别难维护。比如A组件的状态要影响B组件,B组件的状态又反过来影响C组件,这种情况下继续用@State+@Link硬传,最后会陷入"改一个变量,五个页面联动"的灾难。

应对方法有两个梯度:数据层级较深时用@Provide和@Consume跨层级传递状态;再复杂就到应用级状态管理工具,比如引入@StorageLink、AppStorage等。新手阶段不用急着全上,但一定要有"状态复杂度到一定阈值后,该换更高层方案了"的意识,否则后面重构成本会非常大。

6. 界面写多了才会遇到的坑:调试与排查

代码写到一定量,真正花时间的地方往往不在"写组件",而在于界面出了莫名其妙的问题后怎么定位。下面这几个坑是我在实际开发中反复踩过的,写出来给大家省点时间。

6.1 组件"消失"了,先看高度是不是0

有个非常常见的情况:在Column里放了一个Image,预览器里怎么都显示不出来,代码也没报错。排查到最后往往是Image的高度或者宽度为0。尤其当你用Image($r('app.media.xxx'))时,如果媒体资源本身尺寸为0,或者父容器没有给Image确定尺寸,组件就会"塌"成一条线。

定位方法很简单:点击DevEco Studio预览器右上角的蓝色小图标,打开ArkUI Inspector视图,选中看不到的组件,看它的布局框线。如果显示宽高为0,要么给Image显式宽高,要么给它设置objectFit的同时给父容器宽度约束。

6.2 文字"灰灰的"或者"点不动"

如果页面里的Text按钮明明有onClick事件,但点上去没反应,先检查是不是父容器或兄弟组件盖住了它。Stack布局里,后来的子组件默认盖在先前的组件上面,如果有个全屏透明的组件盖在按钮上方,事件就被拦截了。

还有一个隐蔽场景:Button组件的.backgroundColor如果设置成半透明,而底层又恰好有深色背景,视觉上按钮的文字会显得"灰灰的"。这不是按钮失效,是颜色叠加的问题。把背景改为不透明白/黑,文字颜色就正常了。

6.3 预览器和真机表现不一致

Harmony的Previewer在开发效率上是极大的提升,但它和真机渲染在某些场景下确实有差异,主要集中在字体渲染、暗黑模式、部分动效上。遇到"预览器正常但真机布局乱了"的情况,不要怀疑代码玄学,先在预览器里切换设备类型,实在不行跑一下模拟器或者真机。组件式开发最忌讳的就是"预览器看着没问题就直接交付",它只能帮你验证布局的大方向,不能覆盖全部真机特性。

6.4 资源引用规范:小细节省大麻烦

我接手过不少项目,最大的隐性问题不在组件逻辑,而在资源引用混乱。有人把图片直接塞在pages目录下的图片文件夹里,有人用字符串路径访问图片,还有人把颜色值硬编码在十几个文件里。

Harmony的推荐做法是:

  • 图片统一放resources/base/media,访问就走$r('app.media.xxx')。
  • 字符串放resources/base/element/string.json,访问走$r('app.string.xxx')。
  • 颜色放resources/base/element/color.json,访问走$r('app.color.xxx')。

这样做的最大好处,是后续做深色模式、多语言时,不需要去每一条代码里改文字和颜色,只需新增限定目录下的资源文件,系统会自动匹配。很多团队一开始图省事硬编码,等适配需求一到,光改资源就得多花两三天时间。

7. 写在最后的一个小建议

如果让我给刚接触Harmony基础组件的人一个学习路径,我会建议按照"高频文本展示 -> 图片与按钮交互 -> 布局组合 -> 列表懒加载 -> 状态管理"这个顺序,每学一个组件就拼一个真实的小页面,而不是背完API再做项目。

基础组件这块,谁都能在几天内做到"会用",但真正做到"选型不犹豫、性能不拉胯、样式不返工",靠的还是多写多踩坑。我自己参数忘了的时候,也会去翻API文档,但真正常用的核心点和坑,反而是那些记录在项目注释和排错记录里的东西。希望这篇文章里的经验和教训,能让你少走几段弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 19:11:58

Java工程师做AI:落地方向与实战路径(模型网关/RAG/Agent)

每次一聊到 Java 工程师往 AI 领域转型,我都能隔着屏幕感受到一股焦虑:“现在满屏都是 Python 和 PyTorch,我是不是得推倒重来?”“大厂不都在卷大模型训练吗,搞 Java 的还有位置吗?”我的答案一直很直接&a…

作者头像 李华
网站建设 2026/10/1 19:09:30

YOLOv5模型转换实战:PyTorch转ONNX导出、验证与优化部署全指南

简介:YOLOv5是目前广泛使用的高效实时目标检测模型,在图像识别、自动驾驶、安防监控等场景中均有重要应用。PyTorch动态计算图虽便于训练与调试,但实际部署时常需要转换成ONNX这种开放模型交换格式,以便跨框架复用并利用ONNX Runt…

作者头像 李华
网站建设 2026/10/1 19:09:11

COZE低代码AI平台:5大核心能力+2类交付物实战解析

1. 项目概述:为什么“5.2平台一:COZE”突然成为高频搜索词? 最近两周,我在三个不同行业的客户群里都看到同一个词被反复提起——“5.2平台一:COZE”。不是“coze怎么用”,也不是“coze和dify哪个强”&#…

作者头像 李华
网站建设 2026/10/1 19:08:38

AI辩论系统:用多智能体对抗检验投资观点,拆穿逻辑陷阱

1. 整体设计思路:为什么是“辩论”而不是“预测”圈子里一直有个争论:AI到底能不能拿来炒股?我的答案很土——别让机器替你下单,但可以让机器替你“挨打”。这套系统做完之后,我最大的感受是:它压根不是在跟…

作者头像 李华
网站建设 2026/10/1 19:08:36

散列函数六种构造方法详解:从原理到工程选型实战

散列函数看起来是个有点“学院派”的概念,但只要你写过缓存、设计过数据库表,或者哪怕只是用过HashMap,你就已经在跟它打交道了。散列函数的核心任务,就是把一个任意长度的关键字,通过某种规则映射到一个固定范围的地址…

作者头像 李华
网站建设 2026/10/1 19:07:20

Python二手房房价预测全流程:爬虫、清洗、建模到可视化

简介:基于Python的深圳二手房房价预测与分析可视化项目,数据来自链家,面向计算机相关专业的毕业设计、课程设计及项目实战学习者,也适合有Python基础、希望了解数据采集、房价回归预测和数据可视化流程的中级开发者。资源共13个文…

作者头像 李华