ArkTS 数据建模与状态管理:保养数据表驱动 UI 全流程
本文基于 HarmonyOS Next API 24+,深入「车辆保养手册」应用的数据层。我们将以
MaintenanceItem与PartItem两个接口为切入点,讲透 ArkTS 静态数据建模、类型驱动渲染、派生数据计算、状态与渲染的耦合边界,以及把"40 条业务数据"组织成"驱动整个 UI 的数据源"的工程取舍。



一、ArkTS 类型系统在工程中的定位
1.1 ArkTS 不是 TypeScript 的超集
很多从 Web 前端转来的开发者第一次写 ArkTS 会下意识把它当作"加了装饰器的 TypeScript",这是个危险的认知。ArkTS 是 HarmonyOS Next 的应用层语言,它在 TypeScript 的语法外观之下,是一套围绕 ArkUI 响应式 UI 模型重新设计的强约束语言。
关键差异在三个方向:
- 不支持动态结构:ArkTS 禁用
any、unknown在 struct 字段、禁用运行时改写对象形状(如obj.newKey = ...)。本项目的两个接口MaintenanceItem与PartItem,所有字段类型都是string/number的原始组合,不存在可选字段或联合类型。 - 不支持 getter/setter:ArkTS 在 struct 内不支持
get xxx()形式的计算属性。派生数据必须用方法表达——这是本应用用getFilteredMaintenance()而不是get filteredMaintenance的根本原因。 - 装饰器是一等公民:
@State/@Builder/@Component/@Entry不是可选的语法糖,而是 ArkUI 响应式模型的执行核心。@State变量的赋值会触发依赖它的build()片段重渲染,这是语言运行时强制的。
1.2 接口设计的工程哲学
本应用的两个核心接口:
interface MaintenanceItem {
name: string
category: string
intervalKm: number
intervalMonth: number
note: string
}
interface PartItem {
name: string
category: string
lifeYear: number
lifeKm: number // 单位:万公里(与 MaintenanceItem 的公里不同)
replaceTip: string
}
这两个接口的形状直接决定了 UI 的渲染形状——每张卡片的字段映射到接口的一个字段。这种"接口字段 ↔ UI 元素"一一对应的建模方式,是声明式 UI 的精髓。
设计上有几个值得展开的取舍:
字段命名的业务一致性
MaintenanceItem.intervalKm 和 PartItem.lifeKm 名字不同——前者是"间隔",后者是"寿命"。这反映业务语义差异:
- 保养周期是周期性事件:每 5000 公里换一次机油,所以叫
interval(间隔) - 配件寿命是一次性事件:发电机用到 25 万公里就报废,所以叫
life(寿命)
UI 文案也跟着语义走:保养卡片显示"5000 公里 / 6 个月",配件卡片显示"15 年 / 60 万公里"。如果字段名都叫 km,UI 文案就要绕弯判断"这是间隔还是寿命",不如在数据层就把语义分开。
lifeKm 单位是万公里,不是公里
PartItem.lifeKm 的单位是万公里(数值 60 = 60 万公里),而 MaintenanceItem.intervalKm 的单位是公里(数值 5000 = 5000 公里)。这个单位差异在 UI 上被显式处理:
Text(`${item.intervalKm.toLocaleString()} 公里`) // 保养:5000 公里
Text(`或 ${item.lifeKm} 万公里`) // 配件:60 万公里
为什么不统一成公里?因为配件寿命的数值范围是 8-60(万公里),如果改成公里就是 80000-600000,数值跨度大且不直观——车主习惯说"发动机能用 60 万公里"而不是"发动机能用 600000 公里"。保养周期反过来,5000 公里是车主习惯表达。
这是一个"数据单位跟着用户语言走"的取舍。代价是 UI 渲染要显式处理单位差异,但收益是数据值与用户认知直接对齐,无需做转换。
category 用字符串而不是枚举
category: string 而不是 category: MaintenanceCategory 是个看似违反工程惯例的选择。理由有三:
- 跨接口共享:保养与配件的分类是同一组(发动机、底盘、电气、车身),用枚举要声明两个同义枚举或共享枚举,增加样板。
- 静态数据集:40 条数据都是编译期常量,分类值是写死的字符串,不存在"传入未知分类"的风险。
- 筛选逻辑简单:
item.category === this.activeCategory的字符串相等比对,足够清晰。
如果数据会从远端拉取、或支持用户自定义分类,就必须改成枚举或字符串字面量联合类型。但本应用的封闭数据集里,字符串字段是最省事的方案。
二、40 条数据的静态建模
2.1 模块级 const 而不是 struct 内部
const MAINTENANCE_DATA: MaintenanceItem[] = [
{ name: '机油及机滤', category: '发动机', intervalKm: 5000, intervalMonth: 6, note: '矿物机油5千公里,半合成7.5千,全合成1万公里' },
...
]
const PARTS_DATA: PartItem[] = [
{ name: '发动机总成', category: '发动机', lifeYear: 15, lifeKm: 60, replaceTip: '正常保养下可达整车寿命' },
...
]
三个数据集都声明在模块顶层(文件作用域),而不是 Index struct 内部。这个选择背后的工程逻辑:
- 数据是应用资产,不是组件状态:40 条保养规范是"知识库",不是"当前页面的状态"。把它们放进 struct 会混淆"资产"与"状态"的边界。
- 避免实例化重复创建:每次
Index组件实例化,struct 内部的字段都会重新初始化。如果数据在 struct 内,每次都会创建一份 40 条数据的数组——浪费内存且毫无意义。 - 类型推断更稳定:模块顶层
const X: Type[] = [...]让 TypeScript 类型推断一次性确定,struct 内字段则要受装饰器(@State等)的推断顺序影响。
2.2 数据集设计的业务深度
这 40 条数据不是随手拼的占位数据,每条都对应一个真实的保养决策点:
| 保养项 | 数值 | 业务依据 |
|---|---|---|
| 机油及机滤 | 5000 公里 / 6 月 | 矿物机油基线,全合成可延至 1 万 |
| 燃油滤芯 | 30000 公里 / 24 月 | 内置式(油箱内)可延至 8 万 |
| 刹车油 | 40000 公里 / 24 月 | 关键是含水量超 3% 必换,不只是里程 |
| 变速箱油(AT) | 60000 公里 / 36 月 | CVT 4 万、双离合 6 万,差异写在 note |
| 正时皮带 | 80000 公里 / 72 月 | 皮带车型必须按时换,链条车型免维护 |
| 蓄电池 | 80000 公里 / 48 月 | 启停 AGM 电池约 4-5 年,普通电池 3-4 年 |
每条数据的 note 字段不是占位文本,而是承载"什么时候该偏离基线"的判断标准——比如刹 deltaY车油的"含水量超 3% 必换"、刹车片的"厚度低于 3mm 必换"、轮胎的"胎纹低于 1.6mm 或侧壁鼓包必换"。这些数值阈值都来自行业通用规范,让数据集本身具备查阅价值。
2.3 数据集与 UI 卡片的字段映射
每条 MaintenanceItem 渲染成一张 MaintenanceCard,字段映射关系:
| MaintenanceItem 字段 | UI 元素 | 渲染处理 |
|---|---|---|
name |
卡片左上标题 | fontSize 15,Medium 字重 |
category |
卡片左上副标签 | 用 CATEGORY_COLORS 着成色 |
intervalKm |
卡片右上主数值 | toLocaleString() 千位分隔 |
intervalMonth |
卡片右上副数值 | 拼"或 X 个月" |
note |
卡片底部说明 | 二级字色 |
(category) |
卡片左侧色条 | 4×40vp 色条,CATEGORY_COLORS |
接口形状直接决定 UI 形状。声明式 UI 的"数据驱动"在这里体现得最直接——数据集改一个字段,UI 渲染就要跟着改;反过来,UI 想多显示一个东西,数据接口就要加一个字段。这种"接口 ↔ UI"的紧耦合不是缺点,而是声明式 UI 的核心红利:减少了中间转换层。
三、派生数据:从静态数据到渲染数据
3.1 过滤方法的设计
private getFilteredMaintenance(): MaintenanceItem[] {
if (this.activeCategory === '全部') {
return MAINTENANCE_DATA
}
return MAINTENANCE_DATA.filter((item: MaintenanceItem) => item.category === this.activeCategory)
}
这是"派生数据用方法"的标准范式。三个细节:
早返回避免无谓 filter
activeCategory === '全部' 时直接返回原数组引用,不执行 filter。这避免了一次无谓的数组复制——filter 总是返回新数组,而"全部"情况下渲染需要的就是原数组。
这是个微优化(20 条数据的 filter 是微秒级),但更重要的意义是语义清晰:返回原数组让阅读者一眼看出"全部就是原数据",而 filter(item => true) 要绕个弯。
filter 的类型推断
MAINTENANCE_DATA.filter((item: MaintenanceItem) => ...) 显式声明 item 类型是 MaintenanceItem。ArkTS 在严格模式下要求箭头函数参数显式标注类型——这不同于 TypeScript 的"上下文推断优先"。在 ArkUI 的 ForEach / filter / map 等回调里,都建议显式写出参数类型,避免编译器推断失败。
返回类型显式
方法签名 : MaintenanceItem[] 显式标注返回类型,不依赖推断。这是工程代码的稳健做法——显式返回类型让方法成为"类型契约",调用方不需要看实现就能知道返回形状。
3.2 比例条宽度的实时计算
private getBarWidth(value: number, max: number): string {
const percent = Math.min(value / max * 100, 100)
return `${percent}%`
}
这个方法把"数值 + 满量程"转换成"百分比字符串"。三个工程细节:
返回字符串而不是 number
ArkUI 的 .width() 接受三种类型:
string:可以是'100%'(百分比)、'16vp'(vp 单位)、'100px'(px 单位)number:默认单位 vp,例如.width(100)= 100vpResource:资源引用,如$r('app.color.X')
本应用要的是百分比,必须返回字符串 '80%'。如果返回 number 80,会被当成 80vp,完全不是百分比的意思。
Math.min 封顶
Math.min(value / max * 100, 100) 确保百分比不超过 100。当 value > max(例如某项间隔 10 万公里、满量程 10 万),计算结果 = 100%,进度条满格。如果不封顶,进度条会超出容器宽度,触发 ArkUI 的边界处理(通常是截断),但显式封顶更安全。
满量程是硬编码常量
调用处:
.width(this.getBarWidth(item.intervalKm, 100000)) // 保养里程,满量程 10 万公里
.width(this.getBarWidth(item.intervalMonth, 72)) // 保养时间,满量程 72 月 = 6 年
.width(this.getBarWidth(item.lifeYear, 15)) // 配件年限,满量程 15 年
.width(this.getBarWidth(item.lifeKm, 60)) // 配件里程,满量程 60 万公里
四个满量程常量是按数据集最大值选定的"标尺":
- 保养里程 100000 公里:正时皮带 8 万、刹 delta车盘 8 万、蓄电池 8 万,满量程取 10 万让最大值不顶格
- 保养时间 72 月:正时皮带 72 月是数据集最大值,满量程就取 72
- 配件年限 15 年:发动机总成 15 年是最大值,满量程取 15
- 配件里程 60 万公里:发动机总成 60 万是最大值,满量程取 60
如果数据集会动态变化,满量程应该用方法计算 Math.max(...allValues)。但本应用静态数据集里直接硬编码更直观——阅读者一眼能看出"满量程是 10 万",而方法调用要绕一层。
3.3 派生数据在 build() 里的执行频率
派生方法在 build() 里被多次调用:
// 顶部计数 chip
Text(this.currentTab === 0 ?
`${this.getFilteredMaintenance().length}项` :
`${this.getFilteredParts().length}项`)
// 内容列表
ForEach(this.getFilteredMaintenance(), (item: MaintenanceItem) => { ... })
每次 build() 重渲染(currentTab 或 activeCategory 变化触发),getFilteredMaintenance() 会被调用 2 次:一次算 .length、一次喂给 ForEach。
这是不是性能问题?对 20 条数据来说,filter 一次的开销约 10-50 微秒,两次 = 20-100 微秒,远低于人眼能感知的 16 毫秒帧阈值。完全不需要做 memo 缓存。
如果数据集上千条、过滤逻辑复杂(多层 join),就该把过滤结果缓存为 @State,在 activeCategory 变化的回调里手动重算。但本应用的数据规模,重算本身就是最省事的方案。
四、状态与渲染的耦合边界
4.1 @State 的响应式机制
@State currentTab: number = 0
@State activeCategory: string = '全部'
@State 是 ArkUI 的响应式装饰器。它的运行时机制:
- 赋值追踪:每次对
@State变量赋值,ArkUI 运行时会记录"该变量变了" - 依赖收集:在
build()执行期间,所有读取该变量的表达式都会被登记为"依赖该状态" - 重渲染触发:状态变化后,ArkUI 把所有依赖该状态的
build()片段标记为"脏",下一帧重新执行
本应用的依赖关系:
currentTab被 Tab 栏(高亮切换)、内容列表(if 分支)、顶部计数 chip 依赖activeCategory被分类筛选 chip 列(高亮切换)、内容列表(filter 结果)、顶部计数 chip 依赖
任何一个状态变化,这三处的渲染都会同步刷新。这是声明式 UI 的核心红利——开发者只管"状态怎么变","UI 怎么跟着变"由运行时自动完成。
4.2 状态变化的入口:onClick 回调
.onClick(() => {
this.currentTab = index as number
})
.onClick(() => {
this.activeCategory = cat
})
状态变化的唯一入口是用户交互的 onClick 回调。回调里直接对 @State 变量赋值,不需要调用 setState / notify 之类的显式通知 API——这是 ArkUI 与 React 的一个关键差异。
React 的 useState 返回 [value, setter],必须用 setter 才能触发重渲染;直接 value = X 不会触发。ArkUI 的 @State 是属性装饰器,直接赋值即触发,语法更接近原生直觉。
但代价是阅读时要留意——本应用里所有 this.currentTab = ... / this.activeCategory = ... 都是"会触发重渲染"的赋值,没有显式 setter 提示。在大型项目里,这要求团队约定"状态变量只在事件回调里改",避免在异步逻辑里无意改状态导致渲染时序错乱。
4.3 状态为什么不放 RxJS / Promise 里
本应用没有任何异步逻辑——所有数据是静态 const,所有交互是同步 onClick。如果数据从远端拉取,状态管理就要扩展:
@State maintenanceData: MaintenanceItem[] = []
@State isLoading: boolean = false
async function loadMaintenance() {
this.isLoading = true
try {
const res = await fetch('https://api.example.com/maintenance')
this.maintenanceData = await res.json()
} finally {
this.isLoading = false
}
}
异步状态的关键约束:赋值必须在 ArkUI 的执行上下文里。ArkUI 不像 React 有 act() 包装,但同样要求状态变化在合适的时序触发——例如不能在 setTimeout 回调里直接改 @State(实际能跑,但时序行为在某些场景下不稳定)。
本应用不涉及异步,因此状态管理极简。这是工具型应用的一个红利——没有远端依赖、没有缓存失效、没有并发请求,状态层只有"用户点了什么"这一个变量。
五、类型驱动的渲染分支
5.1 MaintenanceCard 与 PartCard 的分野
两个 @Builder 的渲染形状相似但字段映射不同:
@Builder
MaintenanceCard(item: MaintenanceItem) {
// ... 顶部:name + category + intervalKm + intervalMonth
// ... 进度条:里程条(满量程 100000)+ 时间条(满量程 72)
// ... 备注:item.note
}
@Builder
PartCard(item: PartItem) {
// ... 顶部:name + category + lifeYear + lifeKm
// ... 进度条:年限条(满量程 15)+ 里程条(满量程 60)
// ... 提示:item.replaceTip
}
为什么不抽一个泛化的 BaseCard?两个理由:
- 字段名不同:
intervalKmvslifeKm、intervalMonthvslifeYear、notevsreplaceTip。如果抽公共 Builder,要传一堆参数(标题、分类、数值 1、数值 2、说明、满量程 1、满量程 2…),参数列表会膨胀到 8+ 个。 - 单位差异:保养里程单位是公里(要
toLocaleString千位分隔),配件里程单位是万公里(直接拼"万公里")。这种单位差异硬塞进泛化 Builder 要传一堆格式化函数,得不偿失。
ArkUI 的 @Builder 不支持泛型(@Builder MaintenanceCard<T>(item: T) 不是有效语法),因此类型驱动的渲染分支只能用"两个独立 Builder"。这是 ArkUI 与 React泛化组件的一个差异——React 可以 <Card<T> item={item} />,ArkUI 必须具体类型具体 Builder。
5.2 build() 内 if 的类型收窄
if (this.currentTab === 0) {
ForEach(this.getFilteredMaintenance(), (item: MaintenanceItem) => {
this.MaintenanceCard(item)
})
} else {
ForEach(this.getFilteredParts(), (item: PartItem) => {
this.PartCard(item)
})
}
if 分支里 item 的类型是确定的——MaintenanceItem 或 PartItem,不需要联合类型。这归功于数据源方法的返回类型已经显式标注 : MaintenanceItem[] / : PartItem[]。
如果数据源是联合类型 MaintenanceItem | PartItem,就要在 Builder 里收窄:
ForEach(this.getMixedData(), (item: MaintenanceItem | PartItem) => {
if ('intervalKm' in item) {
this.MaintenanceCard(item)
} else {
this.PartCard(item)
}
})
但本应用 Tab 切换是互斥的——同一时刻只渲染一种类型,因此数据源方法已经分野,不需要联合类型。这是"状态分支 → 数据分支 → 渲染分支"三段对齐的标准做法。
六、分类色映射的 Record 实现
6.1 Record<string, string> 的选用
const CATEGORY_COLORS: Record<string, string> = {
'发动机': '#1F6FEB',
'底盘': '#FF8C1A',
'电气': '#16A34A',
'车身': '#9333EA'
}
Record<string, string> 是 TypeScript / ArkTS 的索引签名类型,等价于 { [key: string]: string }。选用 Record 而不是 enum 或 interface 的理由:
- 查询是 O(1):
CATEGORY_COLORS[item.category]直接哈希查表,比 enum + switch 简洁 - 数据形状就是字典:4 个键值对,没有方法、没有副作用,Record 是最贴合的形状
- 与业务字符串对接:
item.category是string,用 Record 可以直接索引;如果用 enum,要先做enumValueFromString转换
6.2 颜色值是硬编码 hex 而不是 $r
注意这里颜色用的是字符串 '#1F6FEB',而不是 $r('app.color.primary')。这是个有意的取舍:
$r资源引用支持运行时切换深色模式、支持资源文件 override- 硬编码 hex 不支持深色模式,但阅读时一眼能看出颜色
分类色是"装饰性色标"——它只用于左侧色条和分类文字,不参与"明暗模式适配"的核心色板。如果应用要做深色模式,分类色应该都进资源文件;但本应用目前只设计了浅色模式,分类色直接硬编码更直观。
更好的工程化做法是:把分类色也写成资源,让资源系统统一管理。但本应用选择了"核心色走资源、分类色走硬编码"的折中——核心色(背景、文字、边框)走资源因为它们是"会被深色模式切换的";分类色硬编码因为它们是"业务标识色,深浅模式都用同样的色"。
6.3 4 个分类的色相分布
4 个分类的色相选择:
| 分类 | hex | HSL 大致 | 视觉 |
|---|---|---|---|
| 发动机 | #1F6FEB |
蓝 | 主色调,最常见类别 |
| 底盘 | #FF8C1A |
橙 | 暖色,与蓝形成对比 |
| 电气 | #16A34A |
绿 | 中性色,机械感 |
| 车身 | #9333EA |
紫 | 区别于以上三种 |
4 个色相覆盖蓝、橙、绿、紫,色相轮上分布均匀,相互区分度高。这是数据可视化"分类色板"的基本要求——同屏出现的多个分类要色相差大,不能只用饱和度区分(色弱用户无法区分)。
七、数据集的健壮性约束
7.1 静态数据的"封闭性"红利
本应用的数据集是编译期常量,40 条数据写死在源码里。这带来一个工程红利:不需要任何运行时校验。
- 不需要验证
intervalKm > 0:写源码时就保证 - 不需要验证
category是 4 个值之一:写源码时就保证 - 不需要验证
name非空:写源码时就保证
这种"封闭数据集"的健壮性靠编译期保证,不靠运行时检查。ArkTS 的接口类型已经能在编译期拦截大多数形状错误(例如忘写 note 字段、把 intervalKm 写成字符串),剩下的业务约束(数值范围、字符串值域)靠代码评审保证。
7.2 如果数据从远端拉取
如果改成远端拉取,就要加运行时校验:
function validateMaintenanceItem(raw: unknown): MaintenanceItem {
if (typeof raw !== 'object' || raw === null) {
throw new Error('Invalid item shape')
}
const obj = raw as Record<string, unknown>
if (typeof obj.name !== 'string') throw new Error('name must be string')
if (typeof obj.intervalKm !== 'number' || obj.intervalKm <= 0) {
throw new Error('intervalKm must be positive number')
}
// ... 其他字段
return obj as MaintenanceItem
}
但本应用是封闭工具,数据来自"业内通用规范 + 编译者审定",没有远端污染风险。因此跳过所有运行时校验,直接信任编译期常量。
这是工程中"边界校验"原则的体现——只在数据进入系统的边界(远端入口、用户输入、文件读取)做校验,内部流转的数据只要类型对就信任。本应用的所有数据都在边界内(源码常量),因此零校验代码。
7.3 接口字段的"必须性"
两个接口的字段都是必填,没有可选字段(?)或默认值。这反映数据本质——每条保养项都有名字、分类、间隔、说明,没有"可能没有"的字段。
如果接口设计成 note?: string,UI 渲染就要处理 note 可能为空的场景:
if (item.note) {
Text('备注')
Text(item.note)
}
但本应用的数据集里每条都有 note,把它设计成可选字段是过度设计。声明式 UI 的接口应该反映"数据的真实形状",而不是"理论上可能的最宽形状"。
八、本篇小结
本章从 ArkTS 类型系统切入,完整覆盖了数据建模到 UI 驱动的全流程。关键点归纳:
- 接口形状决定 UI 形状:声明式 UI 的"数据驱动"不是抽象口号,而是字段 ↔ 元素的一一对应。每改一个字段就要想 UI 要不要跟着改。
- 派生数据用方法:ArkTS 不支持 struct getter,派生数据(过滤、计算)用方法表达。方法在
build()里被重复调用,对小数据集是可接受的。 - 状态最小化:只有用户交互的产物才做
@State,编译期常量用模块级const,派生数据用方法。三类数据各归各位。 - 类型驱动渲染分支:两种数据类型对应两个独立 Builder,不追求泛化。
if分支让类型在分支内自然收窄,不需要联合类型。 - 封闭数据集零运行时校验:数据来自源码常量,靠编译期类型保证健壮性。运行时校验只在数据进入边界做。
九、附录:本篇涉及的关键 ArkTS / TypeScript 差异
| 特性 | ArkTS | TypeScript | 本篇体现 |
|---|---|---|---|
| struct 内 getter | 不支持 | 支持 | 派生数据用方法 getFilteredMaintenance() |
@State 装饰器 |
必需 | 不存在 | 状态变量声明 |
| 箭头函数参数类型 | 必须显式 | 可省略推断 | filter((item: MaintenanceItem) => ...) |
@Builder 泛型 |
不支持 | 不适用 | 类型驱动用两个独立 Builder |
any 在 struct 字段 |
禁用 | 允许 | 所有字段都是具体类型 |
| 运行时改对象形状 | 禁用 | 允许 | 数据集是 const,从不改形状 |
十、附录:本篇涉及的数据接口完整定义
interface MaintenanceItem {
name: string
category: string
intervalKm: number // 单位:公里
intervalMonth: number // 单位:月
note: string
}
interface PartItem {
name: string
category: string
lifeYear: number // 单位:年
lifeKm: number // 单位:万公里(与 MaintenanceItem.intervalKm 不同)
replaceTip: string
}
十一、附录:本篇涉及的数据集统计
保养周期数据集(20 条)
| 分类 | 数量 | 代表项 |
|---|---|---|
| 发动机 | 9 | 机油、空气滤芯、燃油滤芯、火花塞、节气门、防冻液、正时皮带 |
| 底盘 | 7 | 刹 δ车油、变速箱油、差速器油、分动箱油、刹车片、刹车盘、轮胎 |
| 电气 | 2 | 蓄电池、发电机皮带 |
| 车身 | 4 | 空调滤芯、雨刮片、玻璃水 |
配件年限数据集(20 条)
| 分类 | 数量 | 代表项 |
|---|---|---|
| 发动机 | 8 | 发动机总成、水箱、三元催化、氧传感器、喷嘴、正时链条、排气管 |
| 底盘 | 5 | 减震器、悬挂球头、转向助力泵、半轴万向节、刹车分泵 |
| 电气 | 6 | 发电机、起动机、蓄电池、冷凝器、压缩机、ECU、点火线圈 |
| 车身 | 1 | 车门铰链 |
至此完整覆盖了"数据建模到 UI 驱动"的全流程。下一篇将深入 ArkUI 声明式 UI 的渲染机制,从 build() 执行模型讲到 @Builder 复用的边界。
更多推荐

所有评论(0)