
页面结构背后的组件选择
HarmonyOS NEXT 的 ArkUI 框架提供了声明式 UI 开发范式,在实际项目中,最常用的组件就是那五个:Button、Text、Image、TextField 和 Progress。很多刚接触 ArkTS 的开发者容易陷入一个误区------试图用一个组件完成所有需求,比如用 Text 模拟按钮,或者用 Image 加载超清图片导致内存溢出。
这篇文章直接拆开这五个组件,把每个常用属性都跑一遍。不会讲设计模式,也不扯架构,只聚焦于:怎么用、用在哪、容易在哪出问题。
环境说明
text
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机
核心实现:逐个击破
下面这段代码是把五个组件组合在一个页面里,演示各自的核心能力。代码直接在 @Entry 装饰的组件里运行即可。
Button - 不止是"点击"
很多人以为 Button 就是 <button>Click</button> 的翻版,但 ArkUI 的 Button 支持更细粒度的样式控制。
typescript
@Entry
@Component
struct ButtonDemo {
@State count: number = 0
build() {
Column() {
// 默认圆角按钮
Button('默认样式')
.onClick(() => {
this.count++
console.log('点击次数:' + this.count)
})
// 自定义背景色和圆角
Button('自定义样式')
.backgroundColor('#007AFF')
.borderRadius(20)
.width(200)
.height(48)
.fontColor(Color.White)
.fontSize(16)
.onClick(() => {
console.log('自定义按钮被点击')
})
// 图标+文字按钮,通过 Image 组件内嵌
Button() {
Image($r('app.media.icon'))
.width(20)
.height(20)
Text('保存')
.fontSize(14)
}
.backgroundColor(Color.Green)
.borderRadius(8)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
}
.justifyContent(FlexAlign.SpaceAround)
.height('100%')
.width('100%')
}
}
这里需要注意 :Button() 的构造参数如果传字符串或资源引用,其实是《常规按钮》模式。如果想在按钮里放自定义组件(如 Image+Text),必须使用空白构造函数 Button(),然后在代码块里直接写子组件。
实际项目里,很多人直接在字符串参数里拼接文字和图标占位符,这是行不通的。必须用 Button(() => { ... }) 这种闭包写法。
Text - 不仅仅是文本显示
Text 最容易被忽略的属性是 textOverflow 和 maxLines。很多新人在显示长文本时不加限制,导致布局撑爆。
typescript
@Entry
@Component
struct TextDemo {
build() {
Column() {
// 基础文本
Text('这是一段普通文本').fontSize(16)
// 带省略号的多行文本
Text('这是一段很长的文本,超出部分会显示为省略号。实际开发中如果不对长度做限制,布局会变得不可控。')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.fontSize(14)
.width(200)
// 富文本:不同颜色和字体
Text() {
Span('红色').fontColor(Color.Red).fontSize(20)
Span('蓝色').fontColor(Color.Blue).fontSize(16)
Span('加粗').fontWeight(FontWeight.Bold)
}
// 可点击文本
Text('点击我')
.fontColor(Color.Blue)
.decoration({ type: TextDecorationType.Underline })
.onClick(() => {
console.log('点击了可点击文本')
})
}
.justifyContent(FlexAlign.SpaceAround)
.height('100%')
.width('100%')
}
}
实际开发中,textOverflow 必须和 maxLines 同时使用才能生效 。只写 textOverflow 但不设置行数限制,在表现上非常诡异------它会截断文本但不会展示省略号。
Image - 图片加载的坑很多
Image 组件最常被问的问题就是:为什么图片显示不出来?
typescript
@Entry
@Component
struct ImageDemo {
@State imageWidth: number = 100
@State imageHeight: number = 100
build() {
Column() {
// 加载本地资源
Image($r('app.media.logo'))
.width(100)
.height(100)
.objectFit(ImageFit.Contain)
// 加载网络图片(需要网络权限)
Image('https://example.com/icon.png')
.width(200)
.height(200)
.objectFit(ImageFit.Cover)
.onError((err) => {
console.log('图片加载失败:' + JSON.stringify(err))
})
// 圆形裁剪
Image($r('app.media.avatar'))
.width(80)
.height(80)
.borderRadius(40)
.objectFit(ImageFit.Cover)
}
.justifyContent(FlexAlign.SpaceAround)
.height('100%')
.width('100%')
}
}
三个容易翻车的地方:
- 网络图片需要
ohos.permission.INTERNET权限 ,在module.json5里配置。不配置会报网络错误,但控制台日志非常隐晦。 objectFit默认是ImageFit.Cover,如果图片比例和组件尺寸不一致,会被裁剪掉一部分。很多人想要"完整显示"却得到"部分显示",就是因为不知道默认值是什么。onError回调里拿到的错误信息比较简陋 ,只有error字段,没有 HTTP 状态码。需要判断是网络问题还是图片资源不存在。
TextField - 输入框的正确用法
TextField 在 ArkUI 里支持输入类型、占位符、监听变化等。
typescript
@Entry
@Component
struct TextFieldDemo {
@State inputText: string = ''
build() {
Column() {
TextField({ placeholder: '请输入用户名' })
.width('80%')
.height(48)
.onChange((value: string) => {
this.inputText = value
console.log('输入内容:' + value)
})
TextField({ placeholder: '请输入密码', type: InputType.Password })
.width('80%')
.height(48)
.onChange((value: string) => {
console.log('密码输入长度:' + value.length)
})
Text('已输入:' + this.inputText)
.fontSize(14)
.margin({ top: 20 })
}
.justifyContent(FlexAlign.SpaceAround)
.height('100%')
.width('100%')
}
}
一个反直觉的设计 :TextField 的 onChange 回调在每次字符变化时触发,但在初始化赋值(比如用 @State 绑定初始值)时不会触发。如果需要在页面加载时做校验,必须在 onAppear 钩子里手动调用一次。
另外,type: InputType.Password 并不能让输入框完全变成密文,它在某些设备上默认会有一个"眼睛"图标用于切换明文显示。如果业务要求强制隐藏,需要额外处理。
Progress - 进度条的样式切换
Progress 组件在加载场景里非常常用,但很多人不知道它支持多种样式。
typescript
@Entry
@Component
struct ProgressDemo {
@State progressValue: number = 30
build() {
Column() {
// 线性进度条
Progress({ value: this.progressValue, total: 100, type: ProgressType.Linear })
.width(200)
.color(Color.Blue)
// 环形进度条
Progress({ value: this.progressValue, total: 100, type: ProgressType.Ring })
.width(80)
.height(80)
.color(Color.Green)
// 胶囊型进度条
Progress({ value: this.progressValue, total: 100, type: ProgressType.Capsule })
.width(200)
.height(40)
.color(Color.Orange)
Button('增加进度')
.onClick(() => {
if (this.progressValue < 100) {
this.progressValue += 10
}
})
}
.justifyContent(FlexAlign.SpaceAround)
.height('100%')
.width('100%')
}
}
需要注意 :Progress 的 type 属性在创建时设置后不能动态修改。如果需要在环形和线性之间切换,需要条件渲染两个不同的 Progress 组件。这个问题在官方文档里没有明确说明,属于实际踩坑经验。
常见问题 1:Button 的点击事件在组件频繁重建时失效
现象 :当页面的 @State 状态频繁变化时,Button 的 onClick 回调有时不触发。
原因 :ArkUI 的声明式渲染机制决定,每次状态变化会触发组件重建。如果 Button 的 onClick 闭包里引用了外部变量,且该变量在重建时被覆盖,可能导致回调函数被重新创建,旧的回调引用丢失。
解决方案 :确保 onClick 回调里不依赖组件的临时状态,直接修改 @State 变量是最稳妥的方式。如果确实需要复杂逻辑,建议使用 @Builder 构建按钮。
常见问题 2:Image 加载网络图片时出现白屏
现象:网络图片一直显示白色区域,无报错无日志。
原因 :最常见的原因是 ohos.permission.INTERNET 权限没有配置。其次可能是图片 URL 不支持 HTTPS,或者图片资源跨域。
解决方案:
- 检查
module.json5是否添加了ohos.permission.INTERNET。 - 在
onError回调里打印错误信息,判断是网络问题还是资源问题。 - 如果是 HTTP 协议,在
entry/src/main/resources/base/profile/network_config.json中配置允许明文流量。
最佳实践
- 不要在
build()中频繁创建对象 :比如new Text('xxx')这种写法,每次状态变化都会创建新的 Text 实例,导致 ArkUI 频繁触发组件重建。推荐直接使用字面量或@Builder。 - 推荐把状态集中到
@State或@Observed管理 :多个组件同步同一个数据时,如果各自维护本地@State,很容易出现状态不一致。用@Observed装饰的数据模型可以统一管理。 - 异步回调里不要直接修改 UI 状态 :比如网络请求的回调里直接赋值给
@State变量,如果在页面销毁后触发,会导致警告日志。建议在回调前判断isActive()状态。
FAQ
Q:为什么真机正常,模拟器不生效?
A:模拟器不支持某些硬件特性,比如摄像头、传感器权限。同时模拟器的网络请求权限配置和真机有差异,建议遇到奇怪问题先尝试真机测试。
Q:为什么页面返回后状态丢失?
A:当页面使用 @State 管理数据时,页面销毁后状态会丢失。如果需要保持状态,建议使用 @LocalStorage 或 AppStorage 持久化存储。
Q:为什么 Image 的 onError 回调里拿不到 HTTP 状态码?
A:这是 ArkUI 当前版本的限制。Image.onError 的回调参数只包含一个 error 字段,没有详细的状态信息。如果需要精确判断,建议封装一层网络请求先下载图片到本地,再用 Image 加载本地文件。