HarmonyOS ArkTS声明式UI实战:可刷新排行榜页面开发全解析

HarmonyOS ArkTS声明式UI实战:可刷新排行榜页面开发全解析 1. 项目概述与核心价值最近在捣鼓HarmonyOS应用开发想找个能综合练习声明式UI和状态管理的实战案例排行榜页面是个绝佳的选择。它麻雀虽小五脏俱全几乎涵盖了日常开发中80%的UI组件交互场景列表渲染、数据绑定、组件通信、状态管理甚至还能捎带上生命周期函数。很多新手朋友学完基础语法后面对一个完整的页面常常不知从何下手感觉知识点是散的。这个“可刷新的排行榜”项目就是帮你把这些散落的珍珠串成项链。这个页面最终要实现的效果是一个顶部带刷新按钮的标题栏一个展示排名的列表点击列表项能切换样式点击系统返回键还有二次确认的交互。听起来简单但里面用到的State、Prop、Link、Builder以及ForEach循环渲染正是构建复杂HarmonyOS应用的基石。我会带你从零开始不仅复现功能更重点拆解每个技术选择背后的“为什么”比如为什么这里用Link而不是PropBuilder到底解决了什么痛点。无论你是刚接触ArkTS的新手还是想深化对HarmonyOS UI框架理解的中级开发者这个案例都能让你获得即学即用的、能直接搬到你自己项目里的代码经验和设计思路。2. 环境准备与工程创建2.1 开发环境配置详解工欲善其事必先利其器。首先确保你的开发环境是匹配的。根据官方推荐我们使用DevEco Studio 3.1 Release版本SDK选择API 9。为什么是API 9因为它提供了稳定的声明式开发范式支持并且兼容性广。如果你安装了更高版本通常也向下兼容但为了避免一些潜在的、未被发现的兼容性问题在学习和复现特定案例时严格遵循推荐的版本是最稳妥的做法。注意安装DevEco Studio时务必勾选ArkTS相关的工具链。有时候默认安装可能不全导致后续编译报错。安装完成后打开IDE在Settings或Preferences SDK Manager中确认OpenHarmony SDK和Toolchains都已正确安装。硬件方面我们以润和RK3568开发板为例系统为OpenHarmony 3.2 Release。选择RK3568是因为它在社区和教学场景中非常普及资料丰富烧录和调试流程成熟。当然如果你手头是其他符合标准系统要求的开发板如Hi3516DV300等整体代码逻辑是完全通用的仅在设备连接和签名配置环节略有差异。2.2 工程创建与结构初始化环境就绪后我们开始创建工程。打开DevEco Studio选择Create Project。在模板选择页面我们找到Empty Ability模板。这里有个关键点为什么不选Empty Ability (ArkTS)之外的模板因为其他模板如JS或eTS旧范式的语法和项目结构与我们即将使用的声明式ArkTS不同Empty Ability模板为我们提供了一个最纯净的、基于声明式UI的起点避免了无关代码的干扰。创建工程时Project Name可以命名为RankListDemoBundle Name按自己习惯定义即可。Save Location注意不要放在中文或过深路径下以防一些工具链处理时出现意外错误。点击Finish后DevEco Studio会自动构建项目。项目创建完成后我们花几分钟看一下自动生成的核心目录结构这对后续代码组织至关重要entry/src/main/ets/ ├── entryability │ └── EntryAbility.ts // 应用入口管理应用生命周期 ├── pages │ └── Index.ets // 默认生成的首页我们将改造它 └── resources // 存放字符串、颜色、图片等资源我们的主要工作将在pages目录下进行。但为了更好的代码组织我们会参照案例创建更清晰的结构。在ets目录下右键新建目录common公共常量、model数据模型、view自定义组件、viewmodel视图模型。这是一种常见的、利于维护的架构模式将UI、数据和逻辑分离。3. 核心概念与设计思路拆解在动手写代码前我们必须把几个核心装饰器和工作原理吃透。很多开发者踩坑不是因为代码写不出来而是因为用错了状态管理方式导致数据流混乱页面更新不符合预期。3.1 状态管理装饰器State, Prop, Link 的本质区别这是HarmonyOS声明式开发中最关键的一环。你可以把它们理解为组件之间数据流动的“管道协议”。State组件内部的状态源。它装饰的变量是组件内部的状态数据可以初始化。当这个变量的值发生变化时它会驱动当前组件的build()方法重新执行从而更新UI。它是数据变化的“发源地”。在本案例中页面是否刷新数据isSwitchDataSource这个最根本的状态就应该用State来管理。Prop从父组件来的单向数据流。Prop装饰的变量必须从父组件初始化通常通过参数传入它接收来自父组件通常是State或Link的数据。子组件可以修改Prop变量的值但这个修改仅限于子组件内部不会反向通知父组件。这就像父亲给了儿子一本书儿子可以在书上做笔记修改但父亲手里的书不会自动出现这些笔记。案例中列表项ListItemComponent是否切换数据源isSwitchDataSource这个状态是从父页面传下来的且列表项内部的修改不需要通知父页面这就非常适合用Prop。Link与父组件的双向绑定。Link装饰的变量同样从父组件初始化但它与父组件对应的State变量建立了双向连接。任何一方修改这个数据另一方都会同步更新。这就像父子共用一份云端文档任何一方的编辑都会实时同步给对方。案例中标题栏组件TitleComponent里的刷新状态isRefreshData需要与页面的刷新状态同步点击标题栏的刷新按钮页面列表要立刻感知并刷新这就必须使用Link。实操心得如何快速决定用哪个问自己两个问题1. 这个状态是当前组件“私有”的还是需要从外部传入2. 子组件对状态的修改是否需要通知父组件私有且自用 -State从外来子改不需通知父 -Prop从外来子改需同步父 -Link。3.2 Builder可复用的UI描述块Builder装饰的方法不是一个执行逻辑的函数而是一个UI描述的定义。它允许你将一段UI结构封装起来像函数一样在build()方法或其他Builder方法中调用。它的核心价值在于代码复用与整洁避免在build()方法中堆砌巨量的UI代码尤其是当某段结构如列表项、卡片被多次使用时。逻辑与UI分离可以将一些带有简单逻辑如条件判断的UI片段抽离出来让主build()方法更清晰。 在本案例中整个排行榜列表区域RankList被抽离成一个Builder方法这样主页面的build()结构就变得非常简洁标题、表头、列表一目了然。3.3 渲染控制语法ForEach 与条件渲染动态列表是移动端应用的标配。ArkTS提供了ForEach方法来遍历数组并生成对应的组件集合。ForEach( arr: Arrayany, // 数据源数组 itemGenerator: (item: any, index?: number) void, // 生成每个项目的UI keyGenerator?: (item: any, index?: number) string // 为每个项目生成唯一键 )关键点在于keyGenerator。这个可选的参数极其重要。框架通过这个“键”来识别数组中的每一项。当数组发生变化增、删、改、排序时高效的key能帮助框架精准地知道哪些组件需要更新、移动或销毁而不是粗暴地重新渲染整个列表这能极大提升长列表的性能。最佳实践是使用数据项中真正唯一且稳定的标识符如item.id而不是数组索引index因为索引在数组变动时并不稳定。条件渲染if/else则用于根据状态动态显示不同的UI分支在本案例中用于控制显示圆形排名数字还是普通数字。4. 数据模型与视图模型构建4.1 定义实体类 (RankData.ets)数据是应用的血液。我们先在viewmodel目录下创建RankData.ets定义一个表示排行榜单项数据的实体类。// RankData.ets export class RankData { name: string; // 名称 vote: string; // 票数或分数 constructor(name: string, vote: string) { this.name name; this.vote vote; } }这里使用class而不是interface是因为我们后续可能需要实例化并包含一些方法。字段类型定义为string是为了展示方便vote在实际项目中可能是number类型用于计算。4.2 创建视图模型 (RankViewModel.ets)视图模型ViewModel负责准备和管理UI所需的数据和状态它将业务逻辑从UI组件中剥离出来。在viewmodel目录下创建RankViewModel.ets。// RankViewModel.ets import { RankData } from ./RankData; export class RankViewModel { // 数据源一 private dataSource1: ArrayRankData [ new RankData(项目A, 12560), new RankData(项目B, 11023), new RankData(项目C, 9820), new RankData(项目D, 8754), new RankData(项目E, 7601), ]; // 数据源二 private dataSource2: ArrayRankData [ new RankData(任务一, 98%), new RankData(任务二, 87%), new RankData(任务三, 76%), new RankData(任务四, 65%), new RankData(任务五, 54%), ]; // 获取数据源根据状态决定返回哪一个 getRankList(isSwitch: boolean): ArrayRankData { return isSwitch ? this.dataSource1 : this.dataSource2; } }为什么要把数据源放在ViewModel里而不是直接写在页面组件里这遵循了关注点分离原则。页面组件RankPage只应该关心如何渲染UI和响应用户交互至于数据从哪里来本地模拟、网络请求、如何加工应该由ViewModel负责。这样当未来需要将模拟数据替换为网络接口时你只需要修改ViewModel页面组件几乎不用动大大提升了代码的可维护性和可测试性。5. 自定义组件封装与实践5.1 标题组件 (TitleComponent.ets) 与 Link 实战在view目录下创建TitleComponent.ets。这个组件包含一个刷新按钮点击后需要通知父页面刷新列表。// TitleComponent.ets Component export struct TitleComponent { Link isRefreshData: boolean; // 双向绑定父页面的刷新状态 State title: Resource $r(app.string.title_default); // 组件内部标题状态 build() { Row() { // 左侧标题 Text(this.title) .fontSize(20) .fontWeight(FontWeight.Bold) .layoutWeight(1) // 使用权重布局占满剩余空间 // 右侧刷新按钮区域 Row() { Image($r(app.media.ic_refresh)) // 刷新图标需提前放入resources目录 .height(24) .width(24) .onClick(() { // 点击按钮切换刷新状态。这个变化会通过Link同步到父组件 this.isRefreshData !this.isRefreshData; // 可以在这里添加按钮点击动画效果例如旋转 }) } .height(100%) .justifyContent(FlexAlign.End) // 内容右对齐 } .width(100%) .height(56) .padding({ left: 12, right: 12 }) .backgroundColor($r(app.color.title_bg)) } }关键解析Link isRefreshData: 这个变量通过Link与父页面中的某个State变量绑定。当子组件内修改this.isRefreshData时父页面中对应的状态变量也会立刻改变从而触发父页面重新渲染。State title: 这个标题文字如果只是静态展示其实可以用常规变量。这里用State是为了演示即使组件内部有State也不影响它通过Link与父组件通信。在实际项目中如果标题是固定的完全可以去掉State直接用private title: Resource。layoutWeight(1): 这是弹性布局Flex中的权重属性。它让左侧的Text组件占据Row中除右侧刷新按钮区域外的所有剩余空间是实现左右布局的常用技巧。5.2 列表头部组件 (ListHeaderComponent.ets)这个组件比较简单用于显示列表的表头如“排名”、“名称”、“票数”。它不接受动态状态只依赖传入的样式参数。// ListHeaderComponent.ets Component export struct ListHeaderComponent { // 通过构造函数传入的参数属于常规变量变化不会触发UI更新 private paddingValue: Padding | Length 0; private widthValue: Length 0; // 构造函数用于接收父组件传递的参数 constructor(padding: Padding | Length, width: Length) { this.paddingValue padding; this.widthValue width; } build() { Row() { Text($r(app.string.rank)) .fontSize(14) .width(15%) .fontColor($r(app.color.font_secondary)) Text($r(app.string.name)) .fontSize(14) .width(60%) .fontColor($r(app.color.font_secondary)) Text($r(app.string.votes)) .fontSize(14) .width(25%) .fontColor($r(app.color.font_secondary)) } .width(this.widthValue) .padding(this.paddingValue) .backgroundColor($r(app.color.list_header_bg)) } }注意这个组件没有使用任何状态管理装饰器State、Prop、Link。它的所有属性都在创建时通过构造函数一次性设置之后不再改变。这种组件称为无状态组件性能开销最小。在设计中应尽可能将组件设计为无状态的。5.3 列表项组件 (ListItemComponent.ets) 与 Prop 实战这是最复杂的子组件它演示了Prop的单向绑定和组件内部State的管理。// ListItemComponent.ets Component export struct ListItemComponent { // 从父组件传入的索引和数据显示 private index?: number; private name?: string; Prop vote: string ; // 票数单向绑定 Prop isSwitchDataSource: boolean false; // 是否切换数据源标志单向绑定 // 组件内部状态控制文字颜色是否变化 State isChange: boolean false; build() { Row() { // 排名列根据索引决定显示圆形还是普通数字 Column() { if (this.index ! undefined this.index 3) { // 前三名显示为圆形背景 Text(this.index.toString()) .textAlign(TextAlign.Center) .fontSize(16) .fontColor(Color.White) .backgroundColor($r(app.color.top3_bg)) .borderRadius(20) .width(40) .height(40) } else { // 其他名次普通显示 Text(this.index?.toString() ?? ) .fontSize(14) .textAlign(TextAlign.Center) .width(40) } } .width(15%) // 名称列 Text(this.name ?? ) .fontSize(16) .fontWeight(this.isChange ? FontWeight.Bold : FontWeight.Normal) .fontColor(this.isChange ? $r(app.color.primary) : Color.Black) .width(60%) // 票数列 Text(this.vote) .fontSize(14) .fontColor(this.isChange ? $r(app.color.primary) : Color.Gray) .width(25%) .textAlign(TextAlign.End) } .width(100%) .height(60) .padding({ left: 12, right: 12 }) .backgroundColor(Color.White) .onClick(() { // 点击事件 // 1. 修改内部状态触发自身UI更新文字颜色/加粗 this.isChange !this.isChange; // 2. 修改Prop变量。注意这个修改不会通知父组件 this.isSwitchDataSource !this.isSwitchDataSource; }) } }深度解析与避坑指南Prop的“单向性”实验注意onClick事件中我们同时修改了this.isChangeState和this.isSwitchDataSourceProp。修改State会立刻让当前组件重新渲染所以你点击列表项它的颜色会变。但是修改Prop变量isSwitchDataSource这个变化不会传回给父组件RankPage。因此父组件中用于控制数据源切换的那个状态isSwitchDataSource我们稍后会在主页定义并不会改变。这就直观验证了Prop的单向数据流特性。条件渲染的运用我们使用if/else根据排名索引index来决定是渲染一个带圆形背景的文本还是普通文本。这种根据状态动态构建UI的能力是声明式UI的核心。样式与逻辑分离建议实际项目中像40、15%、$r(app.color.primary)这样的魔法数字和颜色值应该抽取到common/constants目录下的常量文件中统一管理例如Style.ets或Color.ets。这极大方便了后期主题切换和整体样式调整。6. 主页面集成与核心逻辑实现6.1 主页面结构 (RankPage.ets) 与 Builder 使用现在我们把所有零件组装起来。修改pages目录下的Index.ets或新建RankPage.ets作为主页面。// RankPage.ets import { TitleComponent } from ../view/TitleComponent; import { ListHeaderComponent } from ../view/ListHeaderComponent; import { ListItemComponent } from ../view/ListItemComponent; import { RankViewModel } from ../viewmodel/RankViewModel; import { RankData } from ../viewmodel/RankData; import prompt from ohos.promptAction; Entry Component struct RankPage { // 控制数据源切换的核心状态 State isSwitchDataSource: boolean false; // 视图模型实例提供数据 private rankViewModel: RankViewModel new RankViewModel(); // 用于处理返回键的生命周期变量 private clickBackTimeRecord: number 0; private readonly BACK_PRESS_INTERVAL: number 2000; // 2秒内再次点击退出 // 获取当前应显示的数据列表 private getCurrentData(): ArrayRankData { return this.rankViewModel.getRankList(this.isSwitchDataSource); } build() { Column() { // 1. 标题栏组件传递State变量建立Link绑定 TitleComponent({ isRefreshData: $isSwitchDataSource }) // 2. 列表头部 ListHeaderComponent({ paddingValue: { left: 16, right: 16 }, widthValue: 100% }) .margin({ top: 10, bottom: 10 }) // 3. 列表区域使用Builder方法构建使结构更清晰 this.RankList() } .width(100%) .height(100%) .backgroundColor($r(app.color.page_bg)) } // Builder 修饰的方法用于描述列表UI Builder RankList() { Column() { List() { // 使用ForEach循环渲染数据 ForEach( this.getCurrentData(), // 数据源数组 (item: RankData, index?: number) { ListItem() { // 创建每一个列表项组件 ListItemComponent({ index: (index ?? 0) 1, // 排名从1开始 name: item.name, vote: item.vote, isSwitchDataSource: this.isSwitchDataSource // 传递Prop }) } }, (item: RankData) item.name // 关键使用name作为唯一键实际项目应用id ) } .width(100%) .height(100%) .divider({ strokeWidth: 1, color: $r(app.color.divider) }) } .padding({ left: 16, right: 16 }) .width(100%) .alignItems(HorizontalAlign.Start) } }代码精讲$操作符在向子组件传递State变量时使用$符号如$isSwitchDataSource创建对状态变量的引用。对于TitleComponent它需要Link绑定所以传$isSwitchDataSource。对于ListItemComponent它只需要Prop单向接收所以直接传this.isSwitchDataSource的值。Builder RankList()将整个列表的构建逻辑抽离出来。这样做之后主build()方法变得非常清爽易于阅读和维护。Builder方法里可以访问组件内的状态如this.isSwitchDataSource。ForEach的键生成器这里简单使用了item.name作为键。但在真实项目中这是一个隐患如果列表数据中存在同名项会导致键冲突可能引发渲染错误。最佳实践是确保数据模型中有唯一标识符如id并使用item.id作为键。数据获取通过getCurrentData()方法从ViewModel获取数据。将isSwitchDataSource状态传递给ViewModel由ViewModel决定返回哪一套数据实现了状态与数据源的解耦。6.2 自定义组件生命周期onBackPress 实践HarmonyOS的自定义组件有一系列生命周期回调onBackPress是其中比较常用的一个它在用户点击系统返回键时触发。// 在RankPage结构体内与build()方法同级 Entry Component struct RankPage { // ... 其他状态和属性 // 系统返回键点击事件回调 onBackPress(): boolean { const currentTime new Date().getTime(); // 判断是否在2秒内第二次点击 if (currentTime - this.clickBackTimeRecord this.BACK_PRESS_INTERVAL) { // 2秒内再次点击返回false系统处理退出 prompt.showToast({ message: 再见, duration: 1000 }); return false; } else { // 第一次点击或距离上次点击超过2秒 prompt.showToast({ message: 再按一次退出应用, duration: 2000 }); // 记录本次点击时间 this.clickBackTimeRecord currentTime; // 返回true表示自己消费了这次返回事件阻止系统默认退出行为 return true; } } }生命周期函数要点onBackPress、onPageShow、onPageHide这些生命周期函数仅对用Entry装饰的、作为页面入口的组件生效。普通组件没有这些回调。onBackPress需要返回一个boolean值。返回true表示组件自己已经处理了返回逻辑系统不再执行默认的返回动作如关闭页面。返回false则表示交由系统处理。这里实现的是一个常见的“再按一次退出”功能。通过记录上次点击时间判断是否为连续快速点击从而提升用户体验防止误触退出。7. 资源定义与常量管理一个规范的项目离不开良好的资源管理。在resources目录下的对应文件中进行定义。字符串资源 (src/main/resources/base/element/string.json):{ string: [ { name: app_name, value: 排行榜Demo }, { name: title_default, value: 排行榜 }, { name: rank, value: 排名 }, { name: name, value: 名称 }, { name: votes, value: 票数 }, { name: prompt_text, value: 再按一次退出应用 } ] }颜色资源 (src/main/resources/base/element/color.json):{ color: [ { name: page_bg, value: #F5F5F5 }, { name: title_bg, value: #FFFFFF }, { name: primary, value: #007DFF }, { name: list_header_bg, value: #F0F0F0 }, { name: font_secondary, value: #666666 }, { name: top3_bg, value: #FF6A00 }, { name: divider, value: #E0E0E0 } ] }媒体资源将刷新按钮的图标如ic_refresh.png放入src/main/resources/base/media/目录。常量文件 (common/constants/Style.ets): 在ets/common/constants/下创建Style.ets统一管理尺寸、边距等。// Style.ets export class Style { // 尺寸 static readonly TITLE_BAR_HEIGHT: Length 56; static readonly LIST_ITEM_HEIGHT: Length 60; static readonly LIST_HEADER_HEIGHT: Length 40; static readonly ICON_SIZE: Length 24; // 边距 static readonly PAGE_PADDING: Length 16; static readonly ITEM_PADDING_HORIZONTAL: Length 12; // 颜色也可直接引用资源这里提供另一种管理思路 static readonly COLOR_PRIMARY: string #007DFF; static readonly COLOR_BACKGROUND: string #F5F5F5; }然后在组件中引入并使用import { Style } from ../common/constants/Style;并使用Style.TITLE_BAR_HEIGHT。这种方式比硬编码要优雅得多后期修改样式只需改动这一个文件。8. 真机调试与常见问题排查8.1 连接设备与运行确保RK3568开发板已烧录正确的OpenHarmony镜像并通过USB连接电脑。在DevEco Studio中打开File Project Structure Project Signing Configs配置正确的签名证书对于真机调试通常需要使用debug证书。在顶部工具栏的设备选择器中选择你的RK3568设备。点击绿色的运行按钮或使用快捷键ShiftF10。DevEco Studio会自动编译、打包、安装并运行应用到开发板上。8.2 常见问题与解决方案实录在实际开发中你几乎一定会遇到下面这些问题。这里是我踩过坑后总结的排查思路问题一页面空白控制台无报错或报错“组件未正确构建”。可能原因1Entry装饰器缺失或位置错误。Entry必须且只能装饰一个作为应用入口的组件。检查你的主页面struct是否被Entry正确装饰。可能原因2build()方法返回值错误或结构异常。build()方法必须返回一个合法的UI组件如Column、Row、Stack等。检查build()方法内所有括号是否配对最后一行是否是组件。排查技巧尝试先构建一个极简页面比如只返回一个Text(Hello)的Column确认基础环境无误后再逐步添加复杂组件。问题二点击标题栏刷新按钮列表数据无变化。可能原因1Link绑定失败。检查TitleComponent中isRefreshData变量是否用Link装饰并且在父页面RankPage中传递时是否使用了$符号$isSwitchDataSource。可能原因2数据源未响应状态变化。检查RankPage中ForEach的数据源是否依赖于那个被Link绑定的状态变量isSwitchDataSource。确保getCurrentData()方法或直接用于ForEach的数组能根据isSwitchDataSource的值返回不同的数据。排查技巧在TitleComponent的onClick事件和RankPage的build()方法开始处使用console.log打印状态变量的值观察点击前后状态是否同步变化以及build()方法是否被调用。问题三列表滚动卡顿或数据更新时闪烁。可能原因ForEach的keyGenerator未设置或设置不当。这是性能问题的首要怀疑对象。如果未提供键或使用数组索引作为键当数组变化时框架无法高效复用组件可能导致全部重新渲染。解决方案确保你的RankData实体类有一个唯一且稳定的字段如id并在ForEach中将其作为键(item: RankData) item.id。排查技巧对于复杂列表项可以给组件添加一个aboutToAppear生命周期回调在里面打印日志观察哪些项被重新创建了。问题四Prop变量在子组件内修改但父组件看不到变化。这是预期行为不是问题。Prop的设计就是单向数据流。如果你需要子组件的修改通知父组件应该使用Link。请重新审视你的组件通信设计。问题五真机调试时提示“安装失败”或“签名错误”。可能原因1签名配置错误。确认在Signing Configs中为debug类型配置了正确的证书文件.p12和Profile文件.p7b。通常DevEco Studio在创建项目时会自动生成调试证书确保其未被删除或损坏。可能原因2设备上已存在相同包名但签名不同的应用。卸载设备上已有的同名应用再重新安装。可能原因3网络权限等敏感权限未声明。如果应用需要网络等功能需在module.json5文件的requestPermissions字段中声明。问题六资源$r(app.string.xxx)引用报错或显示为空。可能原因1资源ID拼写错误或不存在。仔细检查string.json中name字段的值是否与代码中引用的完全一致注意大小写。可能原因2资源文件未正确同步或编译。尝试点击Build Clean Project然后Build Rebuild Project清理并重新构建项目。排查技巧对于图片资源还要检查其是否放在正确的目录base/media下以及格式是否受支持。9. 项目总结与扩展思考走完整个排行榜页面的开发流程你会发现一个看似简单的功能背后串联起了HarmonyOS声明式开发的精髓用State、Prop、Link构建清晰的数据流用Builder和组件化拆分复杂UI用ForEach高效处理列表再用生命周期函数处理边界交互。这个项目完全可以作为你下一个实际应用的起点。你可以尝试以下扩展练习把知识真正变成自己的能力接入真实数据将RankViewModel中的模拟数据替换为从网络API获取。学习使用ohos.net.http模块发起请求并在请求返回后通过State更新数据源观察列表如何自动刷新。增加交互复杂度为列表项添加“点赞”或“投票”按钮点击后票数增加。思考这个“票数”状态应该放在哪里Item内部State还是提升到父页面的State数组里不同的选择对数据流和性能有什么影响。实现下拉刷新利用Refresh组件或List的onScroll事件实现经典的下拉刷新列表功能这比点击标题栏刷新更符合移动端习惯。优化性能如果列表数据量很大比如1000条尝试实现列表项回收、图片懒加载等优化策略感受ArkUI引擎的优化能力。开发中最宝贵的经验往往来自于解决那些文档里没写的“坑”。比如记住Link变量不能本地初始化ForEach的键一定要稳定唯一无状态组件能提升性能就尽量用。把这些细节内化成习惯你构建的HarmonyOS应用自然会更加稳健和高效。