ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HarmonyOS API 22之前UI开发:组件创建、属性设置与状态管理实战

HarmonyOS API 22之前UI开发:组件创建、属性设置与状态管理实战 做HarmonyOS原生开发的朋友应该都有体会UI组件的创建和属性设置是每天写代码绕不开的基本功。在API 22之前HarmonyOS的UI开发已经形成了一套相对稳定的范式以ArkUI声明式开发为主开发者不再像早期Java UI那样手动控制View树而是通过Entry、Component这些装饰器声明页面再在build方法里把组件和属性方法按声明顺序组合起来。这篇文章不讲抽象概念就讲实际操作组件怎么建、属性怎么设、状态怎么联动、踩过哪些坑附带完整可复现的示例帮你把API 22之前这套写法的底层逻辑彻底理清楚。1. API22之前的UI开发范式为什么版本比想象中重要1.1 版本演进与声明式模式定型聊HarmonyOS的UI开发必须先聊API版本。标题里的“API 22之前”严格来说覆盖了一整段演进周期从API 9引入ArkUI声明式开发范式开始到API 12伴随HarmonyOS NEXT的ArkTS全面落地再到后续SDK持续迭代。API 9之前HarmonyOS还同时存在Java UI和JS UI两套写法那时候创建组件、设置属性基本都是命令式的Java UI里需要先拿到ComponentContainernew一个Text或Button再通过setText、setTextSize等方法逐个设置属性最后手动addComponent到容器里。JS UI里虽然写法靠近前端但支持的组件和自定义能力都比较有限遇到复杂交互经常会绕路。API 9之后ArkUI声明式范式成为主流组件的创建与属性设置变成了一段集中描述UI长什么样的结构体代码。这个转变本质上是从“告诉系统怎么做”变成“告诉系统我要什么”系统的渲染引擎自己会去diff、更新。API 22之前这套声明式模型已经酝酿并成熟组件创建、属性设置的操作规律也基本稳定下来之后版本更多是能力和性能的增强而核心心智模型没有推翻。1.2 声明式UI的心智模型状态驱动视图我把这套模型的精髓总结成一句话build方法里的代码就是UI的“图纸”状态变量是图纸上的“可替换零件”。当状态变化时系统重新“印发”图纸受影响的组件自动更新。举个例子命令式写法里如果你想把按钮文字从“登录”改成“登录中”需要先拿到按钮实例再调用setText方法。声明式写法里你只需要在组件属性里绑定一个状态变量State loginText: string 登录 ... Button(this.loginText) .onClick(() { this.loginText 登录中... })系统发现loginText变了自动重新渲染按钮。你不用关心按钮实例在内存里叫什么名字也不用自己调用任何刷新方法。这种心智模型和React的setState、Vue的ref/响应式状态一模一样如果之前写过前端上手几乎没有认知成本。理解这一点之后再去看“组件创建”和“属性设置”这两个操作就会发现他们其实是一件事的两面创建组件是把UI节点放进视图树属性设置是往节点上挂各种配置和事件。2. 组件创建从页面入口到自定义封装2.1 页面入口Entry装饰器与struct组件在API 22之前一个页面的标准外壳长这样Entry Component struct Index { build() { // 组件树从这里开始 } }Entry标记了一个页面的入口App启动时首先加载它。Component表示下面这个struct是一个自定义组件它可以是一个页面也可以是页面里的一块区域。struct里必须实现build方法build里只能有一个根组件可以是一个Column/Row等容器也可以是单个组件组件树通过嵌套形成布局层级。这里有个新手容易踩的坑Entry不一定是页面里唯一的Component一个页面可以由多个自定义组件组成。但Entry只能有一个且要写在能代表整个页面的那个组件上。如果把Entry写错位置IDE会直接报“Only one Entry is allowed”之类的错误。2.2 基础组件的创建要点在build方法里创建组件本质就是“调用组件的构造函数”。最常用的几个组件构造参数需要记清楚Column() { Text(这是一段文本) .fontSize(16) .fontColor(#333333) Button(点击我) .type(ButtonType.Capsule) .width(200) .height(48) Image($r(app.media.logo)) .width(120) .height(120) .borderRadius(60) }Text可以直接传入字符串或资源引用Button第一个参数是按钮文案第二个可选参数可以配置按钮类型比如轮播样式的Capsule或圆形的CircleImage的构造参数是图片来源可以是resources下的Media资源$r(app.media.xxx)也可以是网络地址字符串或者PixelMap内存图片。这里强调一下Image资源引用的用法API 22之前老项目里经常出现直接写图片路径字符串的情况比如Image(file:///data/xxx.png)在真机上经常因为权限问题加载失败。正确做法是先把图片放进resources/base/media目录然后用$r(app.media.xxx)引用。$r返回的是一个Resource对象它比裸字符串更可靠还能自动匹配不同屏幕密度下的资源。再比如列表组件List、滚动组件Scroll构造语法和Column类似但内部子组件有数量、懒加载等方面的限制这属于组件自身的规则和“属性设置”关系不大可以先不纠结。2.3 自定义组件与Builder复用机制如果一段UI在多个页面重复出现别复制粘贴。API 22之前的推荐做法有两种自定义组件或Builder函数。自定义组件的核心结构如下Component struct Avatar { Prop name: string Prop size: number 48 build() { Column() { Image($r(app.media.avatar)) .width(this.size) .height(this.size) .borderRadius(this.size / 2) Text(this.name) .fontSize(14) } } }在父组件中直接像基础组件一样使用它Avatar({ name: 张三, size: 64 })自定义组件的优势是自带封装性但注意要在struct里使用Prop、Link等装饰器同步状态不能直接传普通HashMap之类的复杂类型后面会专门讲状态同步。Builder则是轻量级复用方案它本质是一个生成UI描述的函数Builder function Greeting(text: string) { Text(text) .fontSize(20) .fontWeight(FontWeight.Bold) }在build中调用即可build() { Column() { Greeting(早上好) Greeting(中午好) Greeting(晚上好) } }Builder适合复用“组件片段”自定义组件适合复用“带状态的完整UI块”。这两者没有绝对优劣但API 22前后的官方样例里自定义组件出现频率明显高于Builder原因在于状态管理和生命周期更立体。3. 属性设置链式调用背后的设计逻辑3.1 属性方法为什么能链式调用在声明式UI中组件创建后马上被多个属性方法修饰。这类方法的返回值就是组件自身。比如Text().fontSize(20).fontColor(#333)fontSize返回Text对象fontColor继续在同一个对象上操作于是就可以一直点下去。这种设计让代码顺序和视觉声明顺序一致宽度在哪、间距在哪、字体在哪读代码的时候一目了然。相比之下命令式写法里属性分散在多个代码块维护时需要在类文件里上下跳转。链式调用有一个容易被忽略的优点属性顺序本身就是一种隐性文档。有经验的开发者会把“影响布局的尺寸类属性放在前面视觉类属性放中间事件绑定放最后”这样别人读起来能快速抓住重点。3.2 常见属性分类大盘点API 22之前ArkUI组件的属性方法虽然多但分类很清晰可以对照下面这个表来系统性记忆类别典型属性方法作用尺寸类width、height、size、constraintSize控制组件宽高支持数字vp、百分比布局类padding、margin、alignSelf、position、offset、flex控制组件在容器中的位置与边距视觉类backgroundColor、borderRadius、border、shadow、opacity、blur控制颜色、圆角、边框、阴影等文本类fontSize、fontColor、fontWeight、fontStyle、textAlign、lineHeight仅文本类组件可用事件类onClick、onTouch、onAppear、onDisAppear、onAreaChange绑定交互与感知事件属性设置不是只有样式事件绑定也属于属性设置范畴。在命令式UI里事件监听和属性设置是分开的两个体系在ArkUI里onClick本身就是挂在组件后的一个属性方法。理解了这一点就不会在写事件时忘了链式写法。3.3 属性设置的几个容易被忽略的规则链式写法看起来自由实际上有几个隐性规则是API 22之前的开发中我踩过最多的坑先设置尺寸再设置位置类属性。如果先写position后写width某些布局场景下width会被外部约束修正表现不符合预期。合理顺序是尺寸、边距、视觉、文本、事件。单位问题。数字默认单位是vp虚拟像素字体默认单位是fp跟随系统字体缩放。可以直接写数字也可以写100%但百分比的分母是父容器尺寸不是组件自己的初始尺寸。字体不能用30%这种写法必须用数字或资源。动态属性值别忘了用ternary表达式。很多场景下属性值要根据状态变化比如按钮不可用时透明度降低Button(this.label) .opacity(this.isLoading ? 0.5 : 1) .enabled(!this.isLoading)直接在属性方法里做三元判断是声明式UI最常用的写法比在外面写一堆if然后复制代码要清爽得多。不要在build方法里做耗时操作或随机值生成。build方法可能会被调用多次随机值会导致UI闪烁、状态不稳定。属性值应该来自状态变量或纯计算函数。3.4 完整登录页组件创建属性设置实操下面这个登录页聚集了前面说的组件创建和属性设置技巧比较有代表性Entry Component struct LoginPage { State username: string State password: string State isLogin: boolean false build() { Column() { // Logo容器 Column() { Image($r(app.media.logo)) .width(80) .height(80) .borderRadius(40) } .width(100%) .padding({ top: 80, bottom: 40 }) // 用户名输入框 TextInput({ placeholder: 请输入用户名, text: this.username }) .width(85%) .height(48) .backgroundColor(#F5F5F5) .borderRadius(8) .padding({ left: 16, right: 16 }) .onChange((value: string) { this.username value }) // 密码输入框 TextInput({ placeholder: 请输入密码, text: this.password }) .type(InputType.Password) .width(85%) .height(48) .backgroundColor(#F5F5F5) .borderRadius(8) .padding({ left: 16, right: 16 }) .margin({ top: 16 }) .onChange((value: string) { this.password value }) // 登录按钮 Button(this.isLogin ? 登录中... : 登录) .width(85%) .height(48) .margin({ top: 32 }) .backgroundColor(#007DFF) .borderRadius(24) .fontColor(#FFFFFF) .fontSize(16) .enabled(!this.isLogin) .onClick(() { if (!this.username || !this.password) { return } this.isLogin true // 模拟登录请求 setTimeout(() { this.isLogin false }, 2000) }) } .width(100%) .height(100%) .backgroundColor(#FFFFFF) } }这段代码里有两个关键点登录按钮的文案和禁用状态都跟着isLogin状态走实现了“点击后置灰并转文案”的用户反馈整个页面所有组件都是通过属性方法做样式微调没有一句手动刷新逻辑。这就是API 22之前HarmonyOS原生UI操作的日常形态。4. 状态管理驱动属性动态变化4.1 一个属性值是如何自动更新的前面提到属性方法可以绑定状态变量但很多人不知道底层实现逻辑。简单说装饰器会建立一条“依赖收集”链路当状态变量被读取比如Text(this.message)该组件会被自动标记为“依赖这个消息变量”当状态变量被赋值修改时系统会把依赖它的组件标记为脏并在下一帧重建这部分UI。所以不要在build里给状态变量赋值那会触发无限循环。正确做法是build里只读状态状态变量在事件回调、生命周期、定时器里修改。以下是一个计数器按钮的标准写法Entry Component struct Counter { State count: number 0 build() { Column() { Text(当前计数${this.count}) .fontSize(24) .fontWeight(FontWeight.Bold) Button(加一) .margin({ top: 20 }) .onClick(() { this.count }) } .width(100%) .height(100%) .justifyContent(FlexAlign.Center) } }count状态每次变化Text和Button都会重新跟渲染引擎同步一次但不会白白重建整个页面系统内部有diff机制。这也是声明式UI性能可靠的根基。4.2 Prop和Link的父子组件同步自定义组件的成员变量如果只是普通变量父组件改变值后子组件不会刷新。要让子组件的属性跟着父组件走需要用装饰器。Prop是单向同步父组件传值给子组件子组件内部赋值不会影响父组件。Link是双向同步子组件改了父组件对应的状态也改。做一个父子联动的界面演示一下Component struct ChildPanel { Link count: number build() { Column() { Text(子组件看到的数${this.count}) .fontSize(18) Button(子组件加一) .onClick(() { this.count }) } .padding(16) .borderRadius(8) .backgroundColor(#F0F0F0) .margin(8) } } Entry Component struct ParentPage { State count: number 0 build() { Column() { Text(父组件状态${this.count}) .fontSize(18) ChildPanel({ count: $count }) } .width(100%) .height(100%) .justifyContent(FlexAlign.Center) } }注意Link传值时父组件写法是count: $count前面带$符号。Prop传值时不需要带$直接写count: this.count。这两者非常容易记混API 22之前的老工程师基本都在这上面翻过车。4.3 生命周期钩子对组件创建的影响自定义组件有一组生命周期钩子aboutToAppear、aboutToDisappear以及页面级的onPageShow、onPageHide、onBackPress仅在Entry组件中生效。组件创建后、build执行前aboutToAppear会被调用这里适合做初始化赋值、请求数据。如果数据请求完成后需要改UI直接给状态变量赋值即可系统会自动刷新。我实际开发中踩过一个坑在aboutToDisappear里清空状态变量是不必要的因为组件销毁后状态也跟着没了强行赋值反而可能在销毁过程中触发不必要的渲染警告。生命周期的另一个作用是避免在build里写异步逻辑。比如想在页面加载后请求接口如果写在build里每次刷新都可能重复请求。放在aboutToAppear里则只会在组件实例创建后请求一次这才是正确归位。5. 常见问题与排查技巧实录5.1 属性不生效先查顺序和单位这是出现频率最高的问题。明明写了width: 200组件却还是铺满父容器。原因通常是父容器没有给子组件限制可用空间比如宽度写在Row的子组件上但Row自身宽度是100%子组件width被Row的flex属性拉长。单位写错比如数字直接写200pxArkUI不支持px单位直接忽略。属性顺序问题比如先设置position再设置width在某些场景下position会强制组件脱离文档流width表现异常。排查方法很固定先给组件加一个明显的背景色确定组件实际占据的区域再逐个注释属性方法看是哪一步影响的最后检查父容器约束。5.2 状态修改了UI却不刷新状态变了UI不动十有八九是下面三种情况现象原因解决子组件内修改普通变量没加State/Prop/Link补装饰器修改State对象的自建类属性自建类没有实现Observed装饰类上加Observed数组/对象需要额外观察异步回调里修改State但UI不更新忘记绑定this导致装饰器上下文丢失使用箭头函数或bind(this)API 22之前ArkTS对Observed和ObservedV2的支持逐步增强如果你在数组里push新元素希望UI变化光靠State是不够的还需要结合Observed和对象数组更新策略。最简单的处理方式是修改数组时不要原地push而是新创建一个数组赋值给State变量比如this.list [...this.list, newItem]这样系统一定能检测到变化。5.3 新手必看组件重绘性能排查表有些界面操作卡不是算法问题而是组件重绘太频繁。我排查性能问题时基本按这个顺序走状态变量粒度太大。一个State包含整个页面数据任何字段变化都导致全页面重绘。应该拆分成多个小状态或使用Observed局部观察。ForEach没有写key生成器。列表里item位置变化时如果key生成器不合理系统会重新创建组件而不是复用。比如ForEach(this.list, (item) Text(item), item item.id)key必须稳定且唯一。属性方法里写复杂表达式。build每次刷新都会重新计算整个属性链建议把复杂逻辑抽成方法尽管方法调用本身也有开销但至少比每次渲染都做复杂循环要好。5.4 一个小技巧调试UI组件用弹窗代替Log在真机上调试属性是否生效老用console.log看不到视图层级。可以临时用一个Button或者Text把关键状态显示在页面上比如在角落放一个Text内容直接绑定状态变量。实测下来这种“视觉化状态调试”比打日志更直观能快速定位是状态没更新还是组件属性写错。6. 关于API22之后这套经验没有过时我在写API 22之前这套UI创建和属性设置操作的时候一直在想一个问题这些经验会不会因为新版本而作废目前看不会。声明式UI的最大魅力在于设计哲学上的稳定——只要还是“组件构造 属性方法 状态驱动”这套骨架换多少API版本核心操作逻辑都不变。API 12之后HarmonyOS NEXT用ArkTS全面统一了UI开发语言之前用TypeScript写ArkUI的开发风格仍然被兼容后续新版本更多是能力增强而不是推翻既有写法。而且把这些基础的组件创建、属性设置方法掌握透了再去学新特性会非常快。因为新组件、新属性本质上还是在同一个build模型里加新的构造参数、新的属性方法。地基没变往上盖楼就轻松。最后分享一个个人习惯我喜欢在写每个页面前先在纸上把组件树结构画出来再对照结构写build代码。这样组件创建和属性设置的顺序就有参考依据代码里不容易出现嵌套混乱的情况。API 22之前如此之后我估计也如此。
返回列表