首页 / 资讯中心 / 文章详情

鸿蒙API 22之前UI组件创建与属性设置实战解析

鸿蒙API 22之前UI组件创建与属性设置实战解析 ★ FEATURED ARTICLE
前阵子把一个以 API 22 之前的版本为基础构建的鸿蒙应用工程翻出来维护代码里大量 UI 组件创建和属性设置的操作让我一度很不适应。倒不是说那会儿的写法有多难而是后来版本的 ArkUI 把很多细节藏得更深了再回头看这些老工程才发现基础操作里其实藏着不少设计取舍。所以就有了这篇整理专门围绕 API 22 之前的 HarmonyOS UI 组件创建、属性设置等操作把当时最常用的那套写法、背后的原理以及容易踩的坑系统地梳理一遍。如果你正在维护老工程或者想在鸿蒙开发早期代码中快速定位 UI 相关逻辑这篇内容应该能帮你省下不少时间。1. API 22之前鸿蒙UI开发到底处于哪个阶段1.1 先厘清API版本与UI范式的对应关系很多人听到API 22之前会下意识觉得这是远古代码其实远没有那么夸张。鸿蒙的API版本迭代速度比较快API 22之前的阶段恰恰是声明式UI逐渐成为主流的时期。早期鸿蒙生态里出现过基于Java的UI写法也出现过类似小程序思路的JS UI但到了API 22之前的稳定开发期绝大多数工程已经默认使用ArkUI声明式开发范式编程语言是基于TypeScript/ArkTS这一套。这个阶段的典型特征非常明显你在工程代码里几乎找不到new Text()、addView()这类命令式语句。UI的创建方式变成了在 struct 里写组件、在 build() 里声明结构组件实例的生命周期由框架托管不需要开发者手动销毁。也就是说你只需要告诉框架界面长什么样剩下的渲染、刷新、布局计算都不用手工干预。之所以强调API 22之前这个节点是因为前后两个阶段虽然都属于声明式但不少工程在版本升级后发现组件属性被重新归类、部分写法出现兼容性警告甚至某些看似没问题的代码在更高版本上行为发生了变化。比如曾经可以直接给组件设置margin和padding的四方向简写后期版本对同类属性做了统一约束。这种差异让API 22之前成了一个值得单独拿出来讲的窗口期。1.2 命令式与声明式两套思维的根本差异要理解这套UI操作方式最关键的是完成一次思维切换从操作对象切换到描述状态。命令式UI开发中你要创建一个按钮通常需要做三步创建对象、设置属性、丢到父容器里。后续要改按钮文字还得拿到这个对象引用调用类似setText(新文案)的方法再手动触发刷新。整套流程强调的是我做了一个动作UI产生了变化。声明式UI则反过来。你写的Text(旧文案)只是声明这个位置有一段文本组件文本具体显示什么可能来自一个状态变量。当状态变量改变时框架自己会重新执行组件构建流程把新值渲染到界面上。开发者不再需要关心哪段代码负责刷新UI只需要关心哪个变量的变化会导致文本变化。我刚开始切换到这套思路时最大的障碍就是总想在代码里找到某个组件对象然后调用方法。后来习惯了才意识到API 22之前这套声明式模型真正想传达的是UI是状态的投影。组件创建只是一次声明的执行属性设置是声明的一部分整体的逻辑始终围绕状态如何映射到界面展开而不是如何操作某个界面元素。2. 组件创建的基础动作struct、build()与组件实例是怎么落地的2.1 一个最简单组件的完整结构拆解在API 22之前的鸿蒙工程里新建一个页面组件通常是这样的Entry Component struct MyPage { State message: string Hello HarmonyOS build() { Column({ space: 12 }) { Text(this.message) .fontSize(20) .fontColor(Color.Blue) Button(点击更新) .width(120) .height(40) .onClick(() { this.message 内容已更新 }) } .width(100%) .height(100%) .padding(16) } }把这个结构拆开看Entry标记了页面入口Component表示这是一个自定义组件struct MyPage则承载了组件的属性声明和UI描述。真正创建组件的地方在build()内部Column、Text、Button这些组件构造函数被调用时组件实例就已经生成了。你没看错调用Text(Hello HarmonyOS)本身就是创建组件这个动作。这里有个容易让新人困惑的点组件实例在哪里被保存了答案是没有显式保存。在API 22之前这套写法中组件实例由框架内部管理开发者拿不到也不会去拿。你想修改一个组件不是通过它的引用来操作而是修改它绑定的数据源再让框架重新进入build()流程完成前后版本的对比与局部更新。2.2 为什么build()里不允许写命令式逻辑组件创建集中在build()中但build()并不是一个普通函数它有很强的约束这里只能进行UI描述和声明式条件渲染不能写this.count这类修改状态的命令式代码。为什么因为build()会被框架反复调用。状态一旦变化框架会重新执行build()来生成新的UI描述树然后与新进入的树做对比。如果build()本身会改变状态就会形成一个循环状态变 → 重跑build → 状态又变 → 再重跑build最终界面卡死或触发异常。很多人在老工程里遇到点击按钮后界面卡住的问题排查到最后往往就是有人在build()里写了副作用代码。正确做法是把修改状态的动作放到事件回调里比如onClick、onTouch让状态变更发生在声明式渲染流程之外。这个原则从API 22之前一直保持到现在理解了它你看老代码时的很多疑惑都会解开。3. 属性设置的核心玩法链式调用与常用属性速查3.1 点方法链式调用的底层逻辑API 22之前的UI属性设置最标志性的写法是一长串点方法链。比如给一个文本组件设置字号、字体颜色、字重、行高代码会连在一起Text(this.message) .fontSize(20) .fontColor(Color.Blue) .fontWeight(FontWeight.Medium) .lineHeight(28)为什么能这样一条链写到底因为每个属性方法执行完成后都会把当前组件实例返回出来以便继续设置下一个属性。这是典型的构建者模式。你看到的.fontSize(16).fontColor(Color.Red)并不是在一份静态配置文件里罗列参数而是依次在组件实例上调用方法每个方法内部也会做一次属性值的校验与存储。理解这一点对排查问题很重要。链式调用是有先后顺序的如果同一个属性被连续设置两次后设置的值会覆盖前面的值。比如Text(text) .fontSize(16) .fontSize(24)最终渲染出来的字号是24。这种覆盖逻辑在API 22之前很常见而且它不会报错只会默默按最后一次赋值生效。如果你发现某个属性怎么设都不生效先检查是不是后面又设了一次同类型属性别急着怀疑框架。3.2 必须吃透的通用属性与单位陷阱API 22之前组件属性大体分两类通用属性和专属属性。通用属性几乎每个组件都能用属于必须吃透的基础内容。属性作用注意事项width / height设置宽高支持数字、百分比、auto数字默认单位vppadding / margin内边距 / 外边距可四方向单独设置也支持简写形式backgroundColor背景色支持十六进制颜色值borderRadius圆角四个方向可分别设置borderWidth / borderColor边框粗细与颜色需要与圆角搭配检查显示效果fontSize / fontColor文字大小与颜色fontSize单位默认fp受字体缩放影响fontWeight字重使用FontWeight枚举或数值opacity透明度0到1之间的数值visibility可见性可设为显示、隐藏、占位隐藏align内容对齐控制子组件或内容的对齐方式layoutWeight权重在父容器中按权重分配剩余空间这些属性里最容易出问题的就是单位。API 22之前的阶段官方推荐的尺寸单位是vp虚拟像素字号推荐用fp。vp会根据屏幕密度自动换算fp则在用户调整系统字体大小后会跟着变化。当时很多老工程里直接写死了一堆px值导致在不同型号设备上布局忽大忽小。后来项目里统一把所有px换成了vp问题立刻少了一大半。另一个容易被忽视的是constraintSize它可以约束组件尺寸的最小值和最大值。比如Text(内容) .constraintSize({ minWidth: 100, maxWidth: 300 })这在文本内容不固定时需要提前想清楚否则内容一旦变长组件会突破预期布局把页面撑乱。4. 布局容器与子组件的挂载顺序4.1 Row/Column/Stack三种基础容器的本质组件创建不是孤立的几乎所有界面都是一层一层嵌套出来的。API 22之前的布局体系里最核心的三个容器是Row、Column和Stack。Row的主轴方向是水平子组件从左到右排列Column的主轴方向是垂直子组件从上到下排列Stack则没有主轴概念子组件按栈式层叠排列后面的组件会盖在前面的组件之上。单看表面很简单但要注意这些容器本质上也是组件也可以设置宽高、背景、内外边距它们和普通文本、按钮没有本质区别。我见过不少老工程整个页面只用了一个大Column所有内容从上往下堆。这样做不是不行但一旦遇到左右并排、底部悬浮这类布局就会非常别扭。正确做法是先想清楚结构层级再动手写代码。比如一个典型的列表卡片布局外层用Column撑竖向卡片内部用Row放头像和文字头像和文字再各自包一层容器。这样每层职责清晰后续改样式也方便。4.2 子组件在容器里的对齐、权重与自适应容器里放子组件光放进去还不够还得控制它们的位置关系。API 22之前控制位置主要靠两组属性容器自身的alignItems、justifyContent以及子组件自身的layoutWeight。alignItems控制交叉轴方向的对齐方式justifyContent控制主轴方向的对齐方式。举个例子Column的主轴是垂直方向所以justifyContent决定子组件是顶部对齐、居中对齐还是底部对齐alignItems则决定子组件是左对齐、居中还是右对齐。这两对概念很容易记混我在项目里通常先口头确认一下方向先问这个容器主轴是哪条线再决定用哪个属性控制哪个方向。layoutWeight则是解决剩余空间怎么分配的关键。两个按钮要在屏幕上等宽排列可以这样写Row() { Button(左边).layoutWeight(1) Button(右边).layoutWeight(1) } .width(100%)layoutWeight按数值比例分配空间两个都是1就是均分。这个属性在自适应布局里非常常用比起手动计算百分比要可靠得多。但要注意设置了layoutWeight的子组件如果同时设置了固定宽度固定宽度会先被满足剩下的空间才按权重分配。这个优先级关系我一开始没搞清导致页面在实际设备上出现了明显的左右不对称。5. 状态驱动的属性更新State和它的小伙伴们5.1 状态变量怎么反过来改变属性前文反复提到状态改变UI自动刷新这套机制在API 22之前主要靠装饰器实现。最常见的是State。被State修饰的变量一旦发生变化依赖它的组件就会重新渲染。注意依赖它这三个字。如果状态变量在build()里被用于某个组件的属性比如Text(this.message)那么 message 一变文本组件就会用新值重建。但如果有状态变量没有被任何UI属性引用它的变化不会触发渲染因为框架不知道它影响什么。这个机制导致了一个经典坑有些开发者会把一个临时数组也用State修饰然后用循环去改数组内容界面却纹丝不动。原因往往是数组虽然有变化但界面上只读取了数组的某个字段而那个字段没有在渲染函数里被真正引用。属性设置也一样可以动态绑定。高度、颜色、间距都可以直接引用状态变量State boxHeight: number 100 State themeColor: string #FF6600 build() { Column() { Text(动态高度盒子) } .height(this.boxHeight) .backgroundColor(this.themeColor) }这个时候属性设置就不再是写死的配置而是对状态的即时映射。API 22之前这种写法已经非常成熟是动态界面最基础的实现方式。5.2 父子组件属性同步的几种姿势单页面内用State足够但组件一旦拆分父子组件间就要解决状态同步问题。API 22之前的方案主要有Prop、Link、Provide和Consume。Prop表示单向数据同步父组件传值给子组件子组件内部修改这个值不会反向影响父组件。它适合那种子组件只展示偶尔内部处理一下的场景。Link则是双向同步父子组件共享同一个状态源任何一方修改另一方都会跟着变。它适合子组件的操作要反馈到父组件的场景。我在老工程里见过一个典型错误父组件传了一个对象给子组件子组件内部直接改了对象里的某个字段结果父组件完全没有感知。原因就在于用了Prop拿到的实际上是一份拷贝修改不会回传。解决方式要么改成Link要么在子组件里通过事件回调通知父组件去修改数据。明白了这两者的边界很多状态同步问题其实都能未卜先知。跨层级共享状态则用Provide和Consume由某个祖先组件提供数据孙组件直接注入使用避免一层层透传。这套组合拳在API 22之前已经足够覆盖绝大多数页面场景。6. 事件绑定与交互组件的卡点6.1 onXxx事件的挂载方式组件创建好、属性设置完接下来就是让它响应交互。API 22之前的事件绑定和属性设置共用一套链式写法非常直观Button(点击我) .onClick(() { console.info(按钮已被点击) }) .onKeyEvent((event) { console.info(收到按键事件) })不只是点击常见事件还有onTouch、onAppear、onDisAppear。onTouch能拿到手指按下、移动、抬起等不同状态适合自定义手势场景onAppear和onDisAppear则用于监听组件出现在界面上或消失的时机。老工程里有一个特别容易犯的问题在循环渲染列表时直接把事件回调写成onClick(this.handler)导致事件触发时拿不到正确的作用域。正确做法是使用箭头函数让回调能捕获当前上下文。6.2 事件回调里的参数捕获与闭包陷阱如果要在一个列表里给每个点击项绑定处理函数并且需要知道点击的是哪一项通常会在循环中写类似下面的代码ForEach(this.itemList, (item: ItemModel) { Row() { Text(item.name) } .onClick(() { this.handleItemClick(item) }) }, (item: ItemModel) item.id)这里的关键是item被箭头函数捕获到了。如果顺手写成了this.handleItemClick(item)但不加箭头函数回调执行时变量可能已经发生变化。尤其当itemList在渲染过程中被修改时闭包捕获的变量可能引用了一个过时的对象点击后拿到的数据跟界面显示不一致。我在排查这类问题时有个固定思路先确认事件回调本身有没有触发再确认回调里拿到的参数是不是当前渲染周期的值最后确认状态变量是否正确更新。很多点击没反应的怪问题实际上都是回调里操作了错误的对象而不是事件绑定本身失效。7. 我在实际项目中踩过的坑与最终建议7.1 五个高频问题与排查思路把API 22之前的UI操作遇到过的问题收敛一下下面这五类出现频率最高基本覆盖了绝大多数老工程的疑难杂症。问题现象根本原因处理方式状态变量改了界面没反应变量没有用State修饰或者界面没有引用该变量检查装饰器检查渲染函数里是否真正读取了这个变量修改数组后列表不刷新用了push等方法就地修改数组框架无法感知创建新数组并整体重新赋值让State变量引用变化设备上布局大小不一致代码里使用了px单位全部换算成vp字号使用fp子组件改动不回传给父组件使用Prop却想要双向同步改成Link或通过事件回调通知父组件修改点击回调拿到错误的列表项闭包捕获了循环变量或回调写法不正确使用箭头函数捕获当前渲染周期内的数据项这里的核心思路是先把问题分到数据层还是UI层。数据层出了问题UI不管怎么刷新都不会对UI层出了问题则要回到链式属性设置和布局层级里找答案。7.2 维护老工程的一点经验沉淀如果项目里大量代码还停留在API 22之前的写法不建议一上来就推翻重写。UI组件的创建和属性设置本质上是很确定的事情只要理解了声明式描述 状态驱动 链式设置这套逻辑老代码的可读性其实比想象中要高。我会先给每个页面的组件结构画一个层级图标注清楚哪些是容器、哪些是叶子组件、哪些属性是动态绑定的。然后对照状态变量清单逐个确认这些变量在build()里被哪些组件引用。这一遍做完整个页面的UI骨架和属性更新路径就完全清晰了后续不管是修bug还是做功能扩展心里都会踏实很多。最后再多说一句API 22之前的写法虽然和后来的一些“新特性”相比少了几分便利但它把组件创建和属性设置这类基础动作摆在了明面上反而很适合用来理解声明式UI的核心机制。看懂这套老代码再去看任何新版本鸿蒙项目的UI逻辑都会有一种豁然开朗的感觉。
阅读完成 · 觉得有帮助?
咨询建站