# ExperimentalPaging **Repository Path**: jackiehou/ExperimentalPaging ## Basic Information - **Project Name**: ExperimentalPaging - **Description**: No description available - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2025-06-19 - **Last Updated**: 2025-12-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: 实现了HarmonyOS下的paging分页加载 ## README # ExperimentalPaging ### 简介 参考Android paging3的设计,实现了HarmonyOS下的paging分页加载 - 跟踪获取前一页或后一页所需要的参数; - 当用户滚动到现有数据的末尾时,自动请求下一页; - 跟踪加载状态,并支持List、Gird、WaterFlow。为失败的加载提供简便的重试功能; - 支持状态管理V1和状态管理V2 ![](https://pic1.imgdb.cn/item/694694a229a616e52861d1f2.gif) ### 安装教程 ``` ohpm install @jackiehou/experimental-paging ``` ### 使用说明 ```ts import { LoadParams, LoadResult, LoadState, PagingSource } from '@jackiehou/experimental-paging' ``` 继承PagingSource,如果使用LazyForEach则还需要implements IDataSource接口,使用Repeat则无需implements IDataSource ```ts const PAGE_SIZE = 20//一页的item的个数 //继承PagingSource并实现LazyForEach的IDataSource接口,如果使用Repeat则无需implements IDataSource class PagingDataSource extends PagingSource implements IDataSource { private listeners: DataChangeListener[] = [];//如果使用Repeat,则无需使用DataChangeListener public array: Array = []; constructor(array?: Array) { /* 第一个参数 pageSize 一页的个数 * 第二个参数 prefetchDistance 当滑动到末尾/头部小于prefetchDistance个item的时候,触发加载,默认值pageSize乘以2 * 第三个个参数 initialLoadSize 初始化第一页请求的个数,默认值pageSize乘以3 */ super(PAGE_SIZE, PAGE_SIZE * 2, PAGE_SIZE * 3) this.array = array ? array : [] } //PagingSource会调用此方法,在这里需要更新全部数据 reloadData(items: ItemData[]): void { this.array.splice(0, this.totalCount(), ...items) this.notifyDataReload()//如果使用Repeat,则无需调用notifyDataReload } //PagingSource会调用此方法,在这里添加上、下一页的数据 batchAdd(startIndex: number, items: ItemData[]): void { this.array.splice(startIndex, 0, ...items) //如果使用Repeat,则无需调用notifyDatasetChange this.notifyDatasetChange([{ type: DataOperationType.ADD, index: startIndex, count: items.length, key: items.map((item) => JSON.stringify(item)) }]) } //PagingSource会调用此方法,在这里调用服务器接口获取分页数据 async load(loadParams: LoadParams): Promise> { //当前页的请求参数,如果loadParams.key为undefined表示初始化加载 const key = loadParams.key !== undefined ? loadParams.key : 1 /*初始化从第1页开始加载数据*/ try { //模拟网络耗时 await sleep(1000) //根据key和loadParams.loadSize得到当前页的数据 const items = getData((key - 1) * PAGE_SIZE, loadParams.loadSize, key, PAGE_SIZE) return { state: LoadState.SUCCEED, //加载成功 prevKey: undefined, //返回上一页的请求的key,undefined表示上一页没有数据 nextKey: key <= 10 ? key + (loadParams.loadSize / PAGE_SIZE) : undefined, //返回下一页的请求的key,undefined表示下一页没有数据 data: items //返回的数据 } }catch (e){ console.log(`load error key = ${key} error = ${JSON.stringify(e)}`) return {state: LoadState.ERROR}//返回错误状态 } } //PagingSource和IDataSource需要实现的方法,返回总数 totalCount(): number { return this.array.length } //...省略实现LazyForEach的IDataSource接口需要实现的方法,如果使用的是Repeat则不用考虑 } ``` #### LazyForEach ```ts @Component struct sample { pagingSource = new PagingDataSource() @State isListAppeared: boolean = false//List组件是否挂载 aboutToAppear(): void { this.pagingSource.refreshData() } aboutToDisappear(): void { this.pagingSource.release() } build() { Stack() { if (!this.isListAppeared && this.pagingSource.refresh === LoadState.Loading) { LoadingProgress() .color(Color.Blue).width('35%').aspectRatio(1) } else { if (this.pagingSource.refresh !== LoadState.ERROR) { List() { LazyForEach(this.pagingSource, (item: ItemData) => { ListItem() { Row() { Column() .height(100) .width(100) .backgroundColor(item.color) Text(item.name).padding(10) }.width('100%').padding(5).height(110) } }, (item: ItemData) => JSON.stringify(item)) } .size({ width: '100%', height: '100%' }) .padding(15) .onScrollIndex((start: number, end: number) => { //在这里必须要调用此方法 this.pagingSource.onVisibleAreaChanged(start, end) }) .onAppear(() => { this.isListAppeared = true }) .onDisAppear(() => { this.isListAppeared = false }) } else { Button('重试').onClick(() => { this.pagingSource.refreshData() }) } } }.width('100%').height('100%') } } ``` #### Repeat ```ts @ComponentV2 struct sampleV2 { @Local array :Array = [] pagingSource = new PagingDataSource(this.array) @Local isListAppeared: boolean = false//List组件是否挂载 aboutToAppear(): void { this.pagingSource.refreshData() } aboutToDisappear(): void { this.pagingSource.release() } build() { Stack() { if (!this.isListAppeared && this.pagingSource.refresh === LoadState.Loading) { LoadingProgress() .color(Color.Blue).width('35%').aspectRatio(1) } else { if (this.pagingSource.refresh !== LoadState.ERROR) { List() { Repeat(this.array) .each((ri: RepeatItem) => { ListItem() { Row() { Column() .height(100) .width(100) .backgroundColor(ri.item.color) Text(ri.item.name).padding(10) }.width('100%').padding(5).height(110) } }) .key((item) => JSON.stringify(item)) } .size({ width: '100%', height: '100%' }) .padding(15) .onScrollIndex((start: number, end: number) => { //在这里必须要调用此方法 this.pagingSource.onVisibleAreaChanged(start, end) }) .onAppear(() => { this.isListAppeared = true }) .onDisAppear(() => { this.isListAppeared = false }) .scrollBarWidth(15) } else { Button('重试').onClick(() => { this.pagingSource.refreshData() }) } } }.width('100%').height('100%') } } ``` #### Refresh下拉刷新+paging分页加载参考[示例entry](https://gitee.com/jackiehou/ExperimentalPaging) ### 使用限制 - 支持List,Gird,WaterFlow使用Refresh下拉刷新+paging下一页预加载 - 目前仅支持List+LazyForEach使用paging进行上一页预计加载 - 暂不支持Gird,WaterFlow上一页预计加载(由于没有类似[maintainVisibleContentPosition](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-list#maintainvisiblecontentposition12) 属性,会导致Gird,WaterFlow自动向上翻页,这里可以考虑使用List.lines()属性替换Grid) - List使用Repeat的上一页预计加载的使用限制(见[maintainVisibleContentPosition](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-list#maintainvisiblecontentposition12) 说明) ### 接口说明 #### 抽象类PagingSource | 方法名/字段名 | 参数/类型 | 说明 | |----------------------|--------------------------------------------------------------------|---------------------------------------------------------------------| | Key | 泛型 | 请求该页的请求参数,例如number类型的page,string类型的id等 | | Value | 泛型 | 数组的item的class类型 | | pageSize | number | 一页的个数,在构造函数传入 | | prefetchDistance | number | 当滑动到末尾距离还小于prefetchDistance个item时候,触发加载数据,默认值:pageSize乘以2 ,也在构造函数传入 | | initialLoadSize | number | 初始化第一页请求的个数,默认值:pageSize乘以3,也在构造函数传入 | | prepend | LoadState | 翻上一页的状态 | | refresh | LoadState | 刷新的状态 | | append | LoadState | 翻下一页的状态 | | prevKey | Key\|undefined | 上一页的key,用于触发请求上一页的数据,undefined表示到头了,没有上一页的数据了 | | nextKey | Key\|undefined | 下一页的key,用于触发请求上一页的数据,undefined表示到底了,没有下一页的数据了 | | minVisible | number | List等容器显示区域内第一个子组件的索引值 | | maxVisible | number | List等容器显示区域内最后一个子组件的索引值 | | retryOnFailure | boolean | append/prepend失败,minVisible、maxVisible改变后是否自动重试,默认值false | | refreshData | - | 刷新数据 | | retry | isNext:boolean true:翻下页重试,false:翻上页重试 | 重试请求上页或者下页的数据 | | onVisibleAreaChanged | minVisible: number, maxVisible: number | 列表可显示的区域的index发生改变,在list等列表容器的onScrollIndex回调中调用该方法 | | onPrependStateChange | old: LoadState, now: LoadState | 在头部加载的状态发生改变,子类可重写该方法但一定要加super | | onRefreshStateChange | old: LoadState, now: LoadState | 刷新的状态发生改变,子类可重写该方法但一定要加super | | onAppendStateChange | old: LoadState, now: LoadState | 在底部加载的状态发生改变,子类可重写该方法但一定要加super | | totalCount | return:number | 整个数组的总数,抽象方法需要子类去实现 | | reloadData | items: Array | 刷新成功后的数据,抽象方法需要子类去实现 | | batchAdd | startIndex: number, items: Array | 上页/下页加载成功后的数据,抽象方法需要子类去实现 | | load | loadParams: LoadParams return: Promise | 加载数据,抽象方法需要子类去实现 | #### LoadState枚举 | 枚举名称 | 说明 | |------------|------| | NotLoading | 没有加载 | | Loading | 加载中 | | ERROR | 加载失败 | | SUCCEED | 加载成功 | #### LoadParams\ | 名称 | 类型 | 说明 | |----------|--------|-------------------------------------------| | key | Key泛型 | 用于触发请求数据的key,例如number类型的page,string类型的id等 | | loadSize | number | 用于请求的pageSize | #### LoadResult\ | 名称 | 类型 | 说明 | |---------|-------------------------|-----------------| | prevKey | Key泛型\|undefined | 上一页的key | | nextKey | Key泛型\|undefined | 下一页的key | | data | Array\|undefined | 当前key请求返回的的列表数据 | | nextKey | LoadState | 当前key请求返回的状态 | #### 仓库地址 https://gitee.com/jackiehou/ExperimentalPaging #### 开源协议 本项目基于 [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0.html) ,请自由的享受和参与开源。