首页 > 其他分享 >HarmonyOS NEXT开发实战:实现高效下拉刷新与上拉加载组件(二)刷新核心逻辑与空页面集成

HarmonyOS NEXT开发实战:实现高效下拉刷新与上拉加载组件(二)刷新核心逻辑与空页面集成

时间:2024-12-27 10:30:00浏览次数:6  
标签:void mComponent number NEXT HarmonyOS 刷新 currentPage data 加载

前言:

在上一篇文章中,我们深入探讨了如何在HarmonyOS中实现一个功能完备的空页面组件。现在,我们将进入下拉刷新和上拉加载功能的核心逻辑实现。这不仅仅是技术实现,更是对用户体验的深刻理解。本文将详细介绍如何将空页面与下拉刷新、上拉加载逻辑相结合,打造一个既高效又用户友好的交互体验。

一、核心逻辑的构建

在开发下拉刷新和上拉加载功能时,我们首先需要定义几个关键字段:页面总数、开始页数、页面数据条数。这些字段是分页请求的基础,也是我们实现逻辑的起点。

那么在我们的核心工具类里需要对网络请求做成一个函数参数,进行外部请求
代码示例:

private requestData:(currentPage:number,pageSize:number)=>void

接下来,我们需要实现外部调用监听,以便我们的加载逻辑能够与外部进行沟通。这包括刷新完成、加载完成、数据为空监听等。

代码示例:

export interface PullRefreshListener<T> {
  refreshCompleted:()=>void; 
  loadMoreCompleted:()=>void;
  emptyPage:()=>void;
  setData:(data:T[], isRefreshLast:boolean)=>void;
  lastData:()=>void;
  moreLoadFail:(error:BaseError)=>void;
  onl oadFail:(error:BaseError)=>void;
}

二、下拉刷新与上拉加载的实现

在实现下拉刷新和上拉加载时,我们需要考虑多种状态,包括数据为空、加载错误等。核心逻辑包括判断数据是否为空,是否到达最后一页,以及如何处理加载错误。

核心逻辑代码:

import { BaseError } from '@kangraoo/baselibrary/src/main/ets/exception/NetworkError';
import { Log } from '@kangraoo/utils';

export interface PullRefreshListener<T> {
  refreshCompleted:()=>void;
  loadMoreCompleted:()=>void;
  emptyPage:()=>void;
  setData:(data:T[], isRefreshLast:boolean)=>void;
  lastData:()=>void;
  moreLoadFail:(error:BaseError)=>void;
  onl oadFail:(error:BaseError)=>void;
}

export class PullRefreshList<T>{

  //页面总数
  readonly PAGE_COUNT_SIZE:number = 10
  //当前第几页
  readonly CURRENT_PAGE:number = 1

  private isRefreshLast:boolean = true
  //开始
  private currentPage:number
  //页数
  private pageSize: number

  //网络请求数据啥的
  private requestData:(currentPage:number,pageSize:number)=>void

  private pullRefreshListener:PullRefreshListener<T>;

  constructor(requestData: (currentPage: number, pageSize: number) => void, pullRefreshListener: PullRefreshListener<T>
    ,currentPage?: number, pageSize?: number) {
    this.currentPage = currentPage??this.CURRENT_PAGE;
    this.pageSize = pageSize??this.PAGE_COUNT_SIZE;
    this.requestData = requestData;
    this.pullRefreshListener = pullRefreshListener;
  }

  private makeCurrentPage(){
    this.currentPage++;
    Log.debug(`当前 ${this.currentPage}`);
    this.isRefreshLast = false;
  }

  //刷新
  refreshData() {
    this.isRefreshLast = true;
    this.currentPage = 1;
    this.requestData(this.currentPage,this.pageSize);
  }

  ///刷新已经加载的数据(是数据存在的情况才可以刷新)
  refreshLoadData(){
    this.isRefreshLast = true;
    Log.debug(`当前${this.currentPage} 总共 ${this.pageSize}*${this.currentPage}`);
    this.requestData(1,this.pageSize*(this.currentPage--));
  }

  ///加载
  loadMore() {
    this.isRefreshLast = false;
    this.requestData(this.currentPage,this.pageSize);
  }


  dataError(error:BaseError) {
    this.pullRefreshListener.loadMoreCompleted();
    this.pullRefreshListener.refreshCompleted();
    if (this.isRefreshLast) {
      this.pullRefreshListener.onLoadFail(error);
    } else {
      this.pullRefreshListener.moreLoadFail(error);
    }
  }



  dataSucces(data:T[]|null, total:number) {
    this.pullRefreshListener.loadMoreCompleted();
    this.pullRefreshListener.refreshCompleted();
    if (total === 0) {
      if (this.isRefreshLast) {
        this.pullRefreshListener.setData([], this.isRefreshLast);
        this.pullRefreshListener.emptyPage();
      }
    } else {
      if (data === null || data.length===0) {
        if (this.isRefreshLast) {
          this.pullRefreshListener.setData([], this.isRefreshLast);
          this.pullRefreshListener.emptyPage();
        }
      } else {
        Log.debug(`page${this.currentPage},total${total}`);
        this.pullRefreshListener.setData(data, this.isRefreshLast);
        if (this.pageSize * this.currentPage >= total) {
          this.pullRefreshListener.lastData();
        }
        this.makeCurrentPage();
      }
    }
  }


}

三、控件选择与基础逻辑

选择合适的控件对于实现下拉刷新和上拉加载至关重要。我们选择了系统控件Refresh,它提供了天然的下拉刷新处理和页面定制化功能。

为此我们需要做上篇文章的空页面与refresh控件的结合

首先需要熟悉几个变量

空页面状态 layoutType
上拉刷新结束 finished
上拉加载中 loading
下拉刷新 isRefreshing
刷新状态 refreshStatus

其次我们熟悉几个方法
下拉刷新包裹的内容,一般是list 或者 其他列表 content
下拉调用的方法 onRefreshing
点击空页面上的刷新按钮 onButtonRefreshing

核心代码:

@Preview
@Component
export struct PullRefreshWidget {

  public mCmpController: PullRefreshController|null = null;

  aboutToAppear(): void {
    if (this.mCmpController!=null) {
      this.mCmpController.attach(this); //绑定控制器
    }
  }

  @State isRefreshing: boolean = false
  @State
  refreshStatus: RefreshStatus = RefreshStatus.Inactive
  @Link finished: boolean

  @Link loading: boolean

  @Link moreLoadFail: boolean

  @BuilderParam
  content:()=>void

  onRefreshing?:()=>void

  onButtonRefreshing?:()=>void



  @Builder
  baseRefresh(){
    Refresh({
      refreshing : $$this.isRefreshing,
      builder: this.customRefreshComponent()
    }){
      this.content()
    }.onRefreshing(()=>{
      if(this.onRefreshing){
        this.onRefreshing()
      }
    })
    .onStateChange(async (status) => {
      this.refreshStatus = status
    })
    .height("100%")
  }

  @State
  layoutType : EmptyStatus =  EmptyStatus.none

  build() {
    EmptyWidget({
      child : ()=>{
        this.baseRefresh()
      },
      layoutType : this.layoutType,
      refresh : ()=>{
        if(this.onButtonRefreshing){
          this.onButtonRefreshing()
        }
      }
    })

  }

  @Builder
  customRefreshComponent()
  {
    Stack()
    {
      Row()
      {
        LoadingProgress().height(32)
        Text(this.getTextByStatus()).fontSize(16).margin({left:20})
      }
      .alignItems(VerticalAlign.Center)
    }
    .align(Alignment.Center)
    .clip(true)
    .constraintSize({minHeight:32}) // 设置最小高度约束保证自定义组件高度随刷新区域高度变化时自定义组件高度不会低于minHeight
    .width("100%")
  }

  getTextByStatus() {
    switch (this.refreshStatus) {
      case RefreshStatus.Drag:
        return Application.getInstance().resourceManager.getStringSync($r("app.string.continue_pull_down").id)
      case RefreshStatus.OverDrag:
        return Application.getInstance().resourceManager.getStringSync($r("app.string.release_to_load").id)
      case RefreshStatus.Refresh:
        return Application.getInstance().resourceManager.getStringSync($r("app.string.loading").id)
    }
    return ""
  }

}

export class PullRefreshController{

  private mComponent: PullRefreshWidget | null = null;

  attach(component: PullRefreshWidget) {
    this.mComponent = component;
  }
  refreshCompleted(){
    if(this.mComponent!=null){
      this.mComponent.isRefreshing = false;
    }
  }
  loadMoreCompleted() {
    if(this.mComponent!=null){
      this.mComponent.finished = false
      this.mComponent.moreLoadFail = false
      this.mComponent.loading = false
    }
  }
  lastData(){
    if(this.mComponent!=null){
      this.mComponent.finished = true
    }
  }
  moreLoadFail(){
    if(this.mComponent!=null){
      this.mComponent.moreLoadFail = true;
    }
  }
  emptyPage(){
    if(this.mComponent!=null){
      this.mComponent.layoutType = EmptyStatus.nodata
    }
  }
  nonePage(){
    if(this.mComponent!=null){
      this.mComponent.layoutType = EmptyStatus.none
    }
  }
  onl oadFail(){
    if(this.mComponent!=null){
      this.mComponent.layoutType = EmptyStatus.fail
    }
  }

}

四、数据源的选择与实现

在业务上,为了使用瀑布流,我采用了WaterFlow作为数据组件。同时,我们实现了一个数据源类,以支持数据的动态加载和更新。

首先由于我采用LazyForEach 关于数据源需要做一个 封装类

BasicDataSource 是实现 IDataSource来写一些方法

// Basic implementation of IDataSource to handle data listener
export class BasicDataSource<T> implements IDataSource {
  private listeners: DataChangeListener[] = [];

  private originDataArray: T[] = [];

  public totalCount(): number {
    return this.originDataArray.length;
  }
  public getData(index: number): T {
    return this.originDataArray[index];
  }

  // 注册改变数据的控制器
  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener)
    }
  }

  // 注销改变数据的控制器
  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener)
    if (pos >= 0) {
      this.listeners.splice(pos, 1)
    }
  }

  // 通知控制器数据重新加载
  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded()
    })
  }

  // 通知控制器数据增加
  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index)
    })
  }

  // 通知控制器数据变化
  notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(index)
    })
  }

  // 通知控制器数据删除
  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index)
    })
  }

  // 通知控制器数据位置变化
  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach(listener => {
      listener.onDataMove(from, to)
    })
  }

  // 指定位置添加一个数据
  public addData(index: number, data: T): void {
    this.originDataArray.splice(index, 0, data);
    this.notifyDataAdd(index);
  }

  // 在第一个位置增加数据
  public add1stItem(data: T): void {
    this.addData(0,data)
  }

  // 添加一个数据
  public pushData(data: T): void {
    this.originDataArray.push(data);
    this.notifyDataAdd(this.originDataArray.length - 1);
  }

  // 在指定索引位置删除一个元素
  public deleteItem(index: number): void {
    this.originDataArray.splice(index, 1)
    this.notifyDataDelete(index)
  }

  // 删除第一个元素
  public delete1stItem(): void {
    this.deleteItem(0)
  }

  // 删除最后一个元素
  public deleteLastItem(): void {
    this.originDataArray.splice(-1, 1)
    this.notifyDataDelete(this.originDataArray.length)
  }

  // 清除数据
  public clearData () {
    this.originDataArray = []
    this.notifyDataReload()
  }

  // 设置新数据
  public setData(dataArray: T[]){
    this.originDataArray = dataArray
    this.notifyDataReload()
  }

  //添加list数据
  public addDatas(dataArray: T[]){
    let l = this.originDataArray.length
    this.originDataArray.push(...dataArray)
    this.notifyDataAdd(l-1)
    // this.originDataArray.push(...dataArray)
    // this.notifyDataReload()
  }

}

我的数据类是ExperienceListResponse,我需要实现这个数据源

class WaterFlowDataSource extends BasicDataSource<ExperienceListResponse> {

}

五、完整的下拉刷新实现

最后,我们将所有组件和逻辑整合在一起,实现一个完整的下拉刷新功能。这包括数据的加载、状态的更新以及用户交互的处理。

完整代码:


@Component
export struct MyPullRefreshWidget{

  @State list:ExperienceListResponse[] = []

  dataSource: WaterFlowDataSource = new WaterFlowDataSource()

  mCmpController: PullRefreshController = new PullRefreshController()

  pullRefreshList :PullRefreshList<ExperienceListResponse> = new PullRefreshList((currentPage,pageSize)=>{
    setTimeout(()=>{
      QuickResponsitory.getInstance().experienceList(currentPage,pageSize).then(value=>{
        LibLoading.hide();
        if (value instanceof SuccessData) {
          let data = value as (SuccessData<ApiResult<ExperienceListResponse[]>>)
          this.list = data.data?.data ?? []
          this.pullRefreshList.dataSucces(this.list,data.data?.page?.totalCount??0)
        } else if (value instanceof ErrorData) {
          this.pullRefreshList.dataError(value.error)
        }
      })
    },1000)

  },{
    refreshCompleted:()=>{
      this.mCmpController.refreshCompleted()
    },
    loadMoreCompleted:()=> {
      this.mCmpController.loadMoreCompleted()
    },
    emptyPage:()=> {
      this.mCmpController.emptyPage()
    },
    setData:(data:ExperienceListResponse[], isRefreshLast:boolean)=>{
      if(isRefreshLast){
        this.mCmpController.nonePage()
        this.dataSource.setData(data)
      }else{
        this.dataSource.addDatas(data)
      }
    },
    lastData:()=> {
      this.mCmpController.lastData()
    },
    moreLoadFail:(error:BaseError)=>{
      this.mCmpController.moreLoadFail()
    },
    onl oadFail:(error:BaseError)=>{
      this.mCmpController.onLoadFail()
    }
  })

  aboutToAppear(): void {
    LibLoading.show();
    this.pullRefreshList.refreshData()
  }

  @State finished: boolean = false // 是否已经加载完成

  @State loading: boolean = false

  @State moreLoadFail: boolean = false

  @Builder
  itemFoot() {
    if (this.finished) {
      Row() {
        Text($r("app.string.no_more_data"))
          .fontSize(12)
      }
      .width("100%")
      .height(40)
      .justifyContent(FlexAlign.Center)
    } else {
      if (this.loading) {
        // 正在加载中
        Row({ space: 10 }) {
          Text($r("app.string.loading_data"))
            .fontSize(12)
          LoadingProgress()
            .width(20)
            .height(20)
        }
        .width("100%")
        .height(40)
        .justifyContent(FlexAlign.Center)
      }else {
        if(this.moreLoadFail){
          Row() {
            Text($r("app.string.data_loading_failed"))
              .fontSize(12)
          }
          .width("100%")
          .height(40)
          .justifyContent(FlexAlign.Center)
        }
      }
    }
  }

  @Builder
  dataContent(){
    WaterFlow({footer:this.itemFoot()}){
      LazyForEach(this.dataSource,(item:ExperienceListResponse,index:number)=>{
        FlowItem(){
          ExperienceListItem({experience:item}).padding(4)
        }
      },(item:ExperienceListResponse,index:number)=>{
        return item.id
      })
    }
    .layoutDirection(FlexDirection.Column)
    .columnsTemplate("1fr 1fr")
    .onReachEnd(()=>{
      // 阀门控制
      if (!this.loading && !this.finished) {
        this.loading = true
        this.pullRefreshList.loadMore()
      }
    })
  }

  build() {
    PullRefreshWidget({
      mCmpController:this.mCmpController,
      content:()=>{
        this.dataContent()
      },
      onRefreshing:()=>{
        this.pullRefreshList.refreshData()
      },
      onButtonRefreshing:()=>{
        LibLoading.show();
        this.pullRefreshList.refreshData()
      }
    ,finished:this.finished,loading:this.loading,moreLoadFail:this.moreLoadFail})
  }
}

五、深入解析与经验分享

在实现下拉刷新与上拉加载的过程中,我遇到了一些挑战,比如如何确保数据加载的流畅性,如何处理网络请求的异常,以及如何与空页面进行有效的集成。通过不断的测试和优化,我们找到了一些解决方案,使得整个组件不仅功能强大,而且用户体验良好。

总结:

通过本文,我们不仅学习了如何在HarmonyOS中实现下拉刷新和上拉加载的核心逻辑,还了解了如何将这些逻辑与空页面组件相结合,以提供更加丰富和流畅的用户体验。

标签:void,mComponent,number,NEXT,HarmonyOS,刷新,currentPage,data,加载
From: https://www.cnblogs.com/wangerdan115/p/18634958

相关文章

  • 鸿蒙Next状态管理V2 - @Once初始化用法总结
    一、概述@Once装饰器用于实现变量仅在初始化时同步一次外部传入值,后续数据源更改时不会将修改同步给子组件。其必须搭配@Param使用,且不影响@Param的观测能力,仅拦截数据源变化,与@Param装饰变量的先后顺序不影响实际功能,并且在搭配使用时可在本地修改@Param变量的值。二、装饰器使......
  • Next.js 14 部署运维:从开发到生产的最佳实践
    在完成Next.js14应用的开发后,如何将其高效地部署到生产环境并进行可靠的运维管理是一个关键问题。本文将详细介绍Next.js14的部署策略和运维最佳实践。部署准备工作1.环境配置管理//next.config.js/**@type{import('next').NextConfig}*/constnextConfig=......
  • Next.js 14 性能优化:从首屏加载到运行时优化的最佳实践
    在现代Web应用中,性能优化直接影响用户体验和业务转化。Next.js14提供了多种内置的性能优化特性,今天我们就来深入探讨如何充分利用这些特性,以及一些实用的优化技巧。图片和字体优化1.图片优化Next.js的Image组件供了强大的图片优化功能://components/OptimizedIm......
  • Next.js 14 基础入门:从项目搭建到核心概念
    Next.js14带来了许多激动人心的新特性,包括局部渲染、ServerActions增强等。作为一名前端开发者,我最近在项目中升级到了Next.js14,今天就来分享一下从项目搭建到实际应用的完整过程。项目初始化首先,让我们创建一个全新的Next.js14项目:#使用create-next-app创建......
  • Next.js 14 基础入门:从项目搭建到核心概念
    Next.js14带来了许多激动人心的新特性,包括局部渲染、ServerActions增强等。作为一名前端开发者,我最近在项目中升级到了Next.js14,今天就来分享一下从项目搭建到实际应用的完整过程。项目初始化首先,让我们创建一个全新的Next.js14项目:#使用create-next-app创建项目n......
  • HarmonyOs DevEco studio小技巧39--LazyForEach:性能优化与懒加载
    在鸿蒙系统的应用开发中,LazyForEach是一个极为重要的工具,它在处理列表数据展示等场景时展现出独特的性能优化和懒加载特性,为应用开发带来诸多显著优势。一、LazyForEach在鸿蒙中的优势(一)高效的内存管理在处理大量数据时,传统的组件渲染方式可能会一次性创建所有数据对应的......
  • 华为鸿蒙HarmonyOS Next基础开发教程
    华为鸿蒙HarmonyOSNext基础开发教程1.开发环境准备安装DevEcoStudioDevEcoStudio是华为为HarmonyOS应用开发提供的集成开发环境(IDE)。您可以从华为的官方网站下载并安装DevEcoStudio。配置开发环境确保您的计算机上安装了以下软件:JDK:Java开发工具包,用于支持Java语言开......
  • 【FAQ】HarmonyOS SDK 闭源开放能力 — IAP Kit(4)
    1.问题描述:发布了一个订阅,看日志显示订阅发布成功了,但是在消费的时候没有值,这个是什么原因?人脸活体检测返回上一页App由沉浸式变为非沉浸式多了上下安全区域。解决方案:对于公共事件来说就是提供这个能力,需要调用方保证时序,订阅成功之后再发广播才能收到。2.问题描述:微信支......
  • 鸿蒙Next状态管理V2-Local装饰器总结
    一、引言在鸿蒙Next的开发中,状态管理是构建高效、响应式应用的关键部分。@Local装饰器作为状态管理V2中的重要特性,为开发者提供了一种有效的方式来管理组件内部状态。本文将对@Local装饰器进行全面总结,包括其功能、使用方法、与@State装饰器的对比以及常见问题的解决方法等。二......
  • 首次访问网站加载失败但刷新后可正常显示的原因及解决方案
    首次访问网站时页面加载失败但在刷新后能够正常显示的现象确实令人困惑。这种现象背后可能存在多个潜在因素,下面我们将详细介绍一些常见原因及其对应的解决办法,帮助您更好地理解和处理此类问题。浏览器缓存与DNS解析问题:浏览器缓存和DNS解析是影响首次加载速度的关键环节。当......