拓冰建站拓冰建站
首页 / 资讯中心 / 正文

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/TextInput2.1 Text不能只当一段文字用Text是全项目里出镜率最高的组件没有之一。表面上它就是显示字符串但实际上日常开发中有一堆细节容易踩。第一是字体单位。Harmony里推荐用fpfont pixel表示字体大小用vpvirtual 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 为什么不要用ScrollColumn硬撸列表早期我用ArkTS写列表第一反应是Scroll套一个Column再ForEach循环生成一个个卡片。界面确实能出来但数据一多50条以上就开始掉帧尤其在低端设备上滑动时明显卡顿。原因并不神秘ScrollColumn把所有子组件一次性全部创建并渲染了。哪怕你只需要看到屏幕上的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设置一个Scrollerprivate 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 }) } }这段代码里每次countText里的文字会立刻更新。这个机制让页面逻辑变得非常直观——页面长什么样是状态算出来的而不是通过一步步操作改出来的。但有一个限制必须清楚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组件这种情况下继续用StateLink硬传最后会陷入改一个变量五个页面联动的灾难。应对方法有两个梯度数据层级较深时用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文档但真正常用的核心点和坑反而是那些记录在项目注释和排错记录里的东西。希望这篇文章里的经验和教训能让你少走几段弯路。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门