前言:

在上一篇文章中,我们深入探讨了如何在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;
  onLoadFail:(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;
  onLoadFail:(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
    }
  }
  onLoadFail(){
    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()
    },
    onLoadFail:(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中实现下拉刷新和上拉加载的核心逻辑,还了解了如何将这些逻辑与空页面组件相结合,以提供更加丰富和流畅的用户体验。