资讯详情

资讯详情

HarmonyOS 6 List下拉刷新实战:Refresh组件与数据链路设计详解

最近在做HarmonyOS 6的列表页改造正好遇到了List列表下拉刷新这个需求。之前项目里列表都是静态数据这次要接真实接口还得支持用户手指下拉触发重新加载踩了不少坑才把整个链路捋顺。我把这个List下拉刷新案例的完整实现思路、代码细节、参数调优和常见问题都整理在这篇文里给正在做鸿蒙应用开发的朋友一个可以直接抄作业的参考。这篇文章适合两类人看一类是刚接触鸿蒙ArkUI开发想搞明白List和Refresh怎么配合的新手另一类是已经在用List做展示但遇到刷新不触发、列表卡顿、数据重复等问题想找排查思路的进阶者。我会从最基础的设计拆解开始讲再给完整可运行的代码最后把几个高频问题逐一说明白。1. 整体设计思路拆解下拉刷新到底在解决什么问题1.1 下拉刷新的本质是“数据链路的重新激活”很多新手第一次写下拉刷新容易陷入一个误区以为只要在列表上套一个Refresh组件就完事了。实际上下拉刷新的核心动作是重新发起一次数据请求然后把返回的新结果替换掉旧数据最后让列表回到正常滚动状态。UI上的下拉动画只是给了用户一个明确的视觉反馈你触发了刷新系统正在重新获取内容。所以这个案例的第一层设计拆分是这样的手势层由Refresh组件负责监听手指的下拉动作计算下拉距离决定是否触发刷新。状态层用一个布尔状态标记当前是否处于刷新中这个状态同时驱动刷新动画的显示和收起。数据层列表数据源需要支持整体替换而不是在旧数据上做增量覆盖。视图层List负责高效渲染数据并要处理好刷新前后的滚动位置。这四个层次缺一不可。我见过有人只用State数组往里push数据结果刷新一次列表多了一倍重复项也有人没管刷新状态导致Refresh组件永远转个不停。这些问题不是组件库的问题是对数据链路理解不到位。1.2 为什么在HarmonyOS 6里推荐用Refresh包List而不是自己写手势HarmonyOS 6的ArkUI组件体系里下拉刷新有一个标准做法Refresh组件包裹滚动组件。你可以包List也可以包Scroll、Grid只要你包在Refresh里面它就能识别这个滚动区域的下拉手势。为什么不推荐自己用Touch事件去计算手指位移我刚开始也想自己写因为总觉得自绘手势会更灵活。实际试下来发现几个问题滚动容器会拦截手势你很难准确区分“我在滚动内容”和“我在下拉刷新”。下拉回弹动画要自己实现物理效果很难调到和系统一样顺手。刷新状态和滚动位置的关系需要手动同步容易在刷新过程中出现列表跳动。Refresh组件内部已经处理了这些基础逻辑。你只需要关注两件事刷新时做什么刷新后什么时候结束。这也是HarmonyOS推荐这种组合方式的原因——框架把通用能力封装好开发者专心处理业务逻辑。1.3 List组件在数据更新时的表现差异List组件和普通的Column布局最大的区别是视图复用。HarmonyOS的List配合LazyForEach只渲染当前可视区域内的ListItem上下滑动时会回收和复用节点。这意味着你替换数据源时List不会傻傻地全量重建所有item而是检测到数据变化后复用已有的节点只更新内容。这个特性对下拉刷新非常关键。如果你的数据源没有按规范生成稳定的key或者直接在原数组上做无序遍历List会认为数据发生了大范围变化从而销毁重建大量项。这个过程肉眼看起来就是刷新时列表闪一下甚至卡顿。所以在设计阶段我强烈建议给每个列表项定义一个稳定且唯一的id并且在LazyForEach里用它作为key。这个细节直接决定了下拉刷新动画结束后的流畅度。2. 核心代码实现从工程搭建到刷新逻辑落地2.1 工程准备与目录结构本次案例我使用的是DevEco Studio最新版本创建了一个空白的ArkTS工程。项目名可以随便取但建议在entry模块下新建一个pages/RefreshListPage.ets方便单独调试。在开始写代码前先检查两件事工程的compileSdkVersion是否支持你当前使用的Refresh API。HarmonyOS 6对应的SDK版本相对较新Refresh组件的枚举和事件回调参数名可能和你之前看到的旧教程有差异以你本地的API检查为准。是否开启了状态管理V2。HarmonyOS 6推荐使用更严格的类型状态管理但我这个案例为了兼容性还是用了标准的State装饰器跑起来没有任何问题。2.2 页面骨架Refresh组件包住List先看页面最外层结构我用的是下面这段代码Entry Component struct RefreshListPage { State items: ArrayItemData []; State isRefreshing: boolean false; private pageIndex: number 1; build() { Refresh({ refreshing: this.isRefreshing }) { List({ space: 12 }) { LazyForEach(this.items, (item: ItemData) { ListItem() { Row() { Text(item.title) .fontSize(16) .fontWeight(FontWeight.Medium) Text(item.desc) .fontSize(13) .fontColor(#666666) } .padding(16) .width(100%) .backgroundColor(Color.White) .borderRadius(12) } }, (item: ItemData) item.id) } .padding(16) .layoutWeight(1) } .width(100%) .height(100%) .onRefresh(() { this.loadData(true); }) .onStateChange((status: RefreshStatus) { if (status RefreshStatus.REFRESHING) { this.isRefreshing true; } else if (status RefreshStatus.IDLE) { this.isRefreshing false; } }) } }这里有几个要点需要说明。第一Refresh组件的refreshing参数控制的是刷新状态。正常情况下你需要在刷新结束时把它设回false但如果你用onRefresh回调去触发数据加载并且配合onStateChange来同步状态那么当Refresh组件自己回到IDLE状态时isRefreshing会被正确复位。这两个回调是配合使用的缺一个都容易出状态错乱。第二List的space属性控制项间距我设置为12视觉上比较紧凑。如果你需要更复杂的分组样式可以改成ListItemGroup但本案例不涉及。第三layoutWeight(1)是让List占满父容器剩余高度。如果你把List放在Column里别忘了这个设置否则列表底部会有空白区域下拉手势的响应区间也不对。2.3 数据源定义和模拟网络请求接下来是数据部分我创建了一个ItemData类用来表示列表项。为了模拟真实接口我封装了一个loadData方法内部用setTimeout模拟网络延迟。class ItemData { id: string; title: string; desc: string; constructor(id: string, title: string, desc: string) { this.id id; this.title title; this.desc desc; } }数据加载方法private loadData(isPullRefresh: boolean) { if (this.isRefreshing) { return; } this.isRefreshing true; // 模拟网络请求 setTimeout(() { const newItems: ItemData[] []; const base this.pageIndex * 20; for (let i 0; i 20; i) { newItems.push(new ItemData( item-${base i}, 标题 ${base i}, 这是第 ${base i} 条数据的描述信息 )); } if (isPullRefresh) { // 下拉刷新整组替换 this.items newItems; } else { // 上拉加载更多追加 this.items this.items.concat(newItems); } this.pageIndex; // 注意这里不能直接 this.isRefreshing false // 而是要等Refresh组件自己回到IDLE状态后由onStateChange统一处理 }, 800); }关于这段代码有三点实战经验分享第一刷新去重保护。我在方法开头判断了isRefreshing如果当前已经在刷新直接return。这个保护很重要因为用户在刷新动画还没结束时会连续下拉你不加判断就会同时发出多个请求最后数据互相覆盖。第二整组替换和追加要分开。下拉刷新应该替换整个数组上拉加载才是追加。我在代码里用isPullRefresh区分了这两种行为。如果你统一用concat每下拉一次后面就会多一截旧数据这个bug非常隐蔽。第三刷新状态的复位位置。我故意没有在setTimeout内部把isRefreshing置为false而是希望让实际的Refresh状态决定UI。如果网络请求早就返回了但Refresh动画还没走完强行把isRefreshing置false会导致动画突然中断视觉上很生硬。2.4 Refresh状态回调的细节处理再单独讲讲onStateChange这个回调。它在Refresh组件状态变化时触发最常见的状态值是IDLE和REFRESHING。我在回调里根据状态同步isRefreshing.onStateChange((status: RefreshStatus) { if (status RefreshStatus.REFRESHING) { this.isRefreshing true; } else if (status RefreshStatus.IDLE) { this.isRefreshing false; } })有人会问既然onRefresh里已经能发请求了为什么还要这个回调因为onRefresh只负责通知你“刷新动作发生了”但Refresh组件内部的动画是一个独立过程。如果网络请求在动画结束前就返回了你需要知道动画什么时候真正结束才能准确复位状态。这个同步逻辑也可以更简单完全依赖onRefresh在请求回调里手动把isRefreshing设为false。但那样做的话一旦网络特别快会出现刷新动画只闪了一下就被打断的现象。我个人还是推荐用状态回调驱动UI让系统的刷新动画完整走完观感更自然。3. 细节打磨刷新体验的隐藏价值点3.1 刷新阈值和回弹手感Refresh组件默认有一个触发刷新所需的下拉距离阈值。我在案例中用的是默认值实际项目里可以根据交互稿调整。HarmonyOS的Refresh组件支持传入offset参数用来控制下拉距离的基准值。你还可以提供一个自定义的Builder在下拉过程中显示自定义动画。比如很多App在下拉时会有一个拉伸的图片或者带有进度百分比文字这些都是通过Refresh的builder参数实现的。这里给一个简单的自定义刷新头示例Refresh({ refreshing: this.isRefreshing, offset: 100 }) { List() { } } .builder(() { Row() { Text(this.isRefreshing ? 正在刷新... : 下拉刷新) .fontSize(14) .fontColor(#999999) } .height(60) })需要注意自定义builder的布局不要写成固定高度死值最好根据下拉距离动态调整。否则会出现下拉距离超过builder高度后内容悬空的问题。实际调试时我习惯先跑一次记录builder高度的实时值再回到代码里调整间距。3.2 空数据和接口失败的处理下拉刷新不可能每次都成功接口失败、返回空列表都要有对应状态。我在项目里增加了一个isEmpty的派生状态用来显示空页面占位。一个简单做法是请求完成后判断this.items.length 0在List外面用if/else切换空态视图。这个和Refresh不冲突因为Refresh依然可以包裹空态这样即便列表没数据用户也能通过下拉刷新重新加载。接口失败场景更麻烦。我建议在请求失败时保留旧数据而不是清空items。你可以在loadData里加try/catch失败时弹一个Toast提示然后什么都不改。这样用户的阅读位置不会丢也不会因为刷新失败看到白屏。3.3 上拉加载更多与下拉刷新的协同一个列表页通常同时需要下拉刷新和上拉加载更多。List组件本身有onReachEnd回调会在滚动到底部时触发。我把它接到同一个loadData方法里但传参区别一下.onReachEnd(() { this.loadData(false); })这里要注意一个问题如果列表内容不满一屏onReachEnd会立即触发导致页面刚进来就疯狂加载。解决办法是加一个是否还有更多数据的判断没有更多时直接return。if (!this.hasMore) { return; }我在案例里没有展开这部分但实际项目一定要考虑。单纯的下拉刷新demo可以不做一旦接上分页这个细节就是必踩的坑。3.4 图片加载对刷新流畅度的影响列表项里如果带图片刷新时图片重新加载也会造成卡顿。HarmonyOS的Image组件会在数据变化时重新拉取资源尤其是远程图片处理不当就会在刷新动画过程中出现白块。优化思路有两种一是缩略图缓存二是使用Image占位图让图片列表项在数据替换时快速复用旧图等新图加载完成再替换。这个案例里我避免引入图片但实际项目中务必要考虑这个因素。列表的流畅度往往不是刷新组件的问题而是item渲染的瓶颈。4. 常见问题与排查技巧实录4.1 下拉刷新完全不触发如果Refresh包了List之后下拉没有任何反应优先检查三点检查Refresh组件是否设置了正确的宽高如果高度为0或者包裹内容被子容器溢出手势区域可能根本不在屏幕上。检查List的edgeEffect阻尼效果。HarmonyOS默认会给List边缘加阻尼需要把它设置为EdgeEffect.Spring或EdgeEffect.None否则下拉手势有可能被边缘效果吃掉。检查是否在Refresh里直接添加了scrollable子组件时内部手势发生冲突。可以把List的高度调小或者关闭内部滚动让Refresh直接响应。我在调试这个问题时最喜欢用HarmonyOS的HiLog打印手势回调看onTouch到底有没有收到事件。先把问题范围缩小到“组件没有响应手势”还是“响应了但没触发回调”再针对性地改。4.2 刷新动画一直转圈停不下来这个问题的本质是**refreshing参数没有在正确时机复位**。最容易出现的情况是你在onRefresh里发起了网络请求请求成功后直接把isRefreshing设为false但这时候Refresh组件的内部动画状态还没有回到IDLE两者状态冲突。表现就是动画在转但数据已经更新完毕。我的建议是抛弃“请求成功就立刻停止刷新”的思路改成由onStateChange统一控制。只要Refresh组件认为动画走完了自然会把IDLE状态回传给你你只需要在这个回调里同步isRefreshing就行。如果接口实在太快动画一闪而过你可以主动加一个最小刷新时长比如请求发起后至少等800ms再允许结束setTimeout(() { this.isRefreshing false; }, Math.max(800, Date.now() - this.refreshStartTime));注意这段代码要在请求成功之后调用不能放在请求前面。4.3 刷新后列表内容闪烁或跳动这个问题很大概率来自LazyForEach的key不稳定。如果你的key用的是index数据一旦变化所有item的key都变了List会强制重建所有可见项。正确做法是给每个数据项生成一个业务唯一的id比如数据库主键、接口返回的id绝对不要用数组下标。另外一个隐藏问题是更新this.items时如果你是直接修改数组元素而没有创建新数组那State装饰器可能无法感知变化。我上面的代码用的是this.items newItems直接赋值新数组这样状态系统才能确认变化发生。如果你用this.items[0] xxx这种写法在某些情况下UI不会刷新。4.4 快速下拉导致重复请求我在前面已经提过用isRefreshing做保护这里再展开一下。真实场景里用户在刷新动画尚未结束时会再次下拉如果不加保护每一次下拉都会触发一次接口请求最终多个请求返回后相互覆盖数据。加保护的位置有两个一是loadData入口判断二是onRefresh回调里判断。我推荐两个都加双保险。入口判断防止业务代码手动误触发回调判断拦截用户连续手势。下面是常见问题的速查表方便你排查时快速定位现象可能原因解决方案下拉无反应List边缘阻尼吃掉了手势设置EdgeEffect.Spring或None刷新动画不停止refreshing参数未正确复位由onStateChange统一管理状态刷新后列表闪烁LazyForEach使用index作为key改为业务唯一id数据重复下拉刷新使用了追加逻辑区分刷新和加载更多两种更新方式多次请求缺少状态保护在入口和回调中双重判断isRefreshing下拉时页面跳动item高度不稳定固定item高度或使用缓存组件4.5 性能排查思路从列表下滑流畅度反推问题如果刷新结束后列表滑动明显卡顿不要急着怀疑Refresh组件。先打开HiLog看掉帧记录再检查item里是否有复杂计算、大量嵌套布局、过多容器层级。HarmonyOS的ArkUI对容器层级很敏感。一个ListItem里嵌套五层Row/Column性能会断崖式下降。我的经验是列表项尽量扁平化能用一个Row解决的就不要嵌套Stack和Column。另外列表项的宽高要让系统能提前计算出来尽量减少measure阶段的耗时。下拉刷新本身只是一个触发机制真正的性能瓶颈往往在item渲染上。这个案例里我们用的是简单文本列表你看不出差异但换成富文本、图片、视频封面混合布局时优化差异会非常明显。5. 写在最后的个人实操体会这个List下拉刷新案例看起来不大我前前后后却调了快两天。最开始觉得Refresh组件是个现成的容器套上去就完事结果被状态管理、数据更新策略和手势冲突轮番教育。我个人最大的体会是下拉刷新不是一个UI功能而是一条数据链路。UI只是表象真正要设计的是刷新过程中状态怎么流转、数据怎么替换、用户怎么感知。如果你正在实现类似功能我的建议是先不做任何自定义视觉效果把最朴素的Refresh List跑通。确认下拉、转圈、数据替换、停止动画这四个环节稳定之后再逐步加入自定义刷新头、上拉加载、空态占位这些锦上添花的东西。不要一上来就堆代码先想清楚你的数据源是被替换还是被追加刷新开始后哪些操作应该被禁止以及刷新结束后列表应该停留在什么位置。最后分享一个小技巧调试下拉刷新时把模拟网络请求的延迟调到300ms和3000ms各跑一遍。300ms能验证状态复位是否及时3000ms能暴露重复下拉、界面假死这类边界问题。这两个极端场景都过了实际线上环境才会稳。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →