在 App 中,列表数据加载是一个很常见的功能,几乎大多数 App 中都存在列表数据的展示,而对于大数据量的列表展示,为提高用户体验、减少服务器压力等,一般采用分页加载列表数据,首次只加载一页数据,当用户向下滑动列表到底部时再触发加载下一页数据。
为方便开发过程中快速实现列表分页的功能,对列表分页加载统一封装是必不可少的,这样在开发过程中只需关注实际的业务逻辑而不用在分页数据加载的处理上花费过多时间,从而节省开发工作量、提高开发效率。
0x00 效果
首先来看一下经过封装后的列表分页加载的效果:
封装后的使用示例代码:
State:
代码语言:javascript复制class ArticleListsState extends PagingState<Article>{
}
Controller:
代码语言:javascript复制class ArticleListsController extends PagingController<Article, ArticleListsState> {
final ArticleListsState state = ArticleListsState();
/// 用于接口请求
final ApiService apiService = Get.find();
@override
ArticleListsState getState() => ArticleListsState();
@override
Future<PagingData<Article>?> loadData(PagingParams pagingParams) async{
/// 请求接口数据
PagingData<Article>? articleList = await apiService.getArticleList(pagingParams);
return articleList;
}
}
View:
代码语言:javascript复制class ArticleListsPage extends StatelessWidget {
final controller = Get.find<ArticleListsController>();
final state = Get.find<ArticleListsController>().state;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("文章列表")),
body: buildRefreshListWidget<Article,ArticleListsController>(itemBuilder: (item, index){
return _buildItem(item);
}),
);
}
/// item 布局
Widget _buildItem(Article item) {
return Card(...);
}
}
0x01 实现
上面展示了通过封装后的列表分页加载实现的文章列表效果并附上了关键示例代码,通过示例代码可以看出,在使用封装后的列表分页加载功能时只需要关注数据请求本身和界面布局展示,而无需关注分页的具体细节,使列表分页加载的实现变得更简单。下面将通过代码介绍具体如何实现列表分页加载的封装。
整体介绍
在看具体实现之前,先带大家从整体结构、最终实现的功能、使用到的三方库上做一个整体介绍。
整体结构
整个列表封装分为三层,State、Controller、View。
- • State: 用于存放界面状态数据,一个复杂的界面可能存在很多的状态数据,为了便于对状态数据的维护将其统一放到 State 里,对于有列表分页加载的页面,其列表数据也统一封装到 State 里。
- • Controller: 页面业务逻辑处理。
- • View: 界面 UI 元素,即 Widget 。
实现功能
封装后的列表分页加载实现功能主要如下:
- • 列表数据显示
- • 下拉刷新
- • 上拉加载
- • 自动判断是否还有更多数据
- • 自动处理分页逻辑
- • 列表 item 点击事件封装
使用到的第三方库
- • pull_to_refresh[1]: 下拉刷新、下拉加载更多
- • GetX[2]: 依赖管理、状态管理
列表分页加载封装中 GetX 主要使用到了依赖管理和状态管理,当然 GetX 除了依赖管理还有很多其他功能,因本篇文章主要介绍列表分页的封装,不会过多介绍 GetX,关于 GetX 更多使用及介绍可参考以下文章:
- • Flutter之GetX集成及使用详解
- • Flutter 通过源码一步一步剖析 Getx 依赖管理的实现
- • Flutter之GetX依赖注入使用详解
- • Flutter之GetX依赖注入tag使用详解
具体实现
前面介绍了对于列表分页加载的封装整体分为三层:State、Controller、View,而封装的主要工作就是对这三层的封装,实现 PagingState
、PagingController
的基类以及 buildRefreshListWidget
函数的封装。
PagingState
PagingState
用于封装保存分页状态数据及列表数据,不涉及实际业务逻辑处理,源码如下:
class PagingState<T>{
/// 分页的页数
int pageIndex = 1;
///是否还有更多数据
bool hasMore = true;
/// 用于列表刷新的id
Object refreshId = Object();
/// 列表数据
List<T> data = <T>[];
}
PagingState
有一个泛型 T
为列表 data 的 item 类型 ,即列表数据 item 的数据实体类型。refreshId
刷新列表界面的 id,用于后面 Controller 刷新指定 Widget 使用,属于 GetX 状态管理的功能,具体可详阅 GetX 相关文章。其他变量的作用在注释里描述得很详细,这里就不作赘述了。
PagingController
PagingController
封装分页的逻辑处理,源码如下:
abstract class PagingController<M,S extends PagingState<M>> extends GetxController{
/// PagingState
late S pagingState;
/// 刷新控件的 Controller
RefreshController refreshController = RefreshController();
@override
void onInit() {
super.onInit();
/// 保存 State
pagingState = getState();
}
@override
void onReady() {
super.onReady();
/// 进入页面刷新数据
refreshData();
}
/// 刷新数据
void refreshData() async{
initPaging();
await _loadData();
/// 刷新完成
refreshController.refreshCompleted();
}
///初始化分页数据
void initPaging() {
pagingState.pageIndex = 1;
pagingState.hasMore = true;
pagingState.data.clear();
}
/// 数据加载
Future<List<M>?> _loadData() async {
PagingParams pagingParams = PagingParams.create(pageIndex: pagingState.pageIndex);
PagingData<M>? pagingData = await loadData(pagingParams);
List<M>? list = pagingData?.data;
/// 数据不为空,则将数据添加到 data 中
/// 并且分页页数 pageIndex 1
if (list != null && list.isNotEmpty) {
pagingState.data.addAll(list);
pagingState.pageIndex = 1;
}
/// 判断是否有更多数据
pagingState.hasMore = pagingState.data.length < (pagingData?.total ?? 0);
/// 更新界面
update([pagingState.refreshId]);
return list;
}
/// 加载更多
void loadMoreData() async{
await _loadData();
/// 加载完成
refreshController.loadComplete();
}
/// 最终加载数据的方法
Future<PagingData<M>?> loadData(PagingParams pagingParams);
/// 获取 State
S getState();
}
PagingController
继承自 GetxController
,有两个泛型 M
、S
,分别为列表 item 的数据实体类型和 PageState 的类型。
成员变量 pagingState
类型为泛型 S
即 PagingState
类型,在 onInit
中通过抽象方法 getState
获取,getState
方法在子类中实现,返回 PagingState
类型对象。
refreshController
为 pull_to_refresh
库中控制刷新控件 SmartRefresher
的 Controller
,用于控制刷新/加载完成。
refreshData
、loadMoreData
方法顾名思义是下拉刷新和上拉加载更多,在对应事件中调用,其内部实现调用 _loadData
加载数据,加载完成后调用 refreshController
的刷新完成或加载完成, refreshData
中加载数据之前还调用了初始化分页数据的 initPaging
方法,用于重置分页参数和数据。
_loadData
是数据加载的核心代码,首先创建 PagingParams
对象,即分页请求数据参数实体,创建时传入了分页的页数,值为 PagingState 中维护的分页页数 pageIndex
,PagingParams
实体源码如下:
class PagingParams {
int current = 1;
Map<String, dynamic>? extra = {};
Map<String, dynamic> model = {};
String? order = 'descending';
int size = 10;
String? sort = "id";
factory PagingParams.create({required int pageIndex}){
var params = PagingParams();
params.current = pageIndex;
return params;
}
}
字段包含当前页数、每页数据条数、排序字段、排序方式以及扩展业务参数等。此类可根据后台接口分页请求协议文档进行创建。
分页参数创建好后,调用抽象方法 loadData
传入创建好的参数,返回 PagingData
数据,即分页数据实体,源码如下:
class PagingData<T> {
int? current;
int? pages;
List<T>? data;
int? size;
int? total;
PagingData();
factory PagingData.fromJson(Map<String, dynamic> json) => $PagingDataFromJson<T>(json);
Map<String, dynamic> toJson() => $PagingDataToJson(this);
@override
String toString() {
return jsonEncode(this);
}
}
该实体包含列表的真实数据 data ,以及分页相关参数,比如当前页、总页数、总条数等,可根据后台分页接口返回的实际数据进行调整。其中 fromJson
、toJson
是用于 json 数据解析和转换用。
关于 json 数据解析可参考前面写的 : Flutter应用框架搭建(三)Json数据解析[9]
数据加载完成后,判断数据是否为空,不为空则将数据添加到 data 集合中,并且分页的页数加 1。然后判断是否还有更多数据,此处是根据 data 中的数据条数与分页返回的总条数进行比较判断的,可能不同团队的分页接口实现规则不同,可根据实际情况进行调整,比如使用页数进行判断等。
方法最后调用了 Controller 的 update 方法刷新界面数据。
流程如下:
View
View 层对 ListView
和 pull_to_refresh
的 SmartRefresher
进行封装,满足列表数据展示和下拉刷新/上拉加载更多
功能。其封装主要为 Widget 参数配置的封装,涉及业务逻辑代码不多,故未将其封装为 Widget 控件,而是封装成方法进行调用, 共三个方法:
- • buildListView: ListView 控件封装
- • buildRefreshWidget: 下拉刷新/上拉加载更多控件封装
- • buildRefreshListWidget: 带分页加载的 ListView 控件封装
其中前面两个是单独分别对 ListView 和 SmartRefresher 的封装,第三个则是前两者的结合。
buildListView:
代码语言:javascript复制Widget buildListView<T>(
{required Widget Function(T item, int index) itemBuilder,
required List<T> data,
Widget Function(T item, int index)? separatorBuilder,
Function(T item, int index)? onItemClick,
ScrollPhysics? physics,
bool shrinkWrap = false,
Axis scrollDirection = Axis.vertical}) {
return ListView.separated(
shrinkWrap: shrinkWrap,
physics: physics,
padding: EdgeInsets.zero,
scrollDirection: scrollDirection,
itemBuilder: (ctx, index) => GestureDetector(
child: itemBuilder.call(data[index], index),
onTap: () => onItemClick?.call(data[index], index),
),
separatorBuilder: (ctx, index) =>
separatorBuilder?.call(data[index], index) ?? Container(),
itemCount: data.length);
}
代码不多,主要是对 ListView 的常用参数包装了一遍,并添加了泛型 T
即列表数据 item 的类型。其次对 itemCount
和 itemBuilder
做了特殊处理, itemCount
赋值为 data.length
列表数据的长度;ListView 的 itemBuilder
调用了传入的 itemBuilder
方法,后者参数与 ListView 的参数有区别,传入的是 item 数据和下标 index, 且使用 GestureDetector
包裹封装了 item 点击事件调用onItemClick
。
buildRefreshWidget:
代码语言:javascript复制Widget buildRefreshWidget({
required Widget Function() builder,
VoidCallback? onRefresh,
VoidCallback? onLoad,
required RefreshController refreshController,
bool enablePullUp = true,
bool enablePullDown = true
}) {
return SmartRefresher(
enablePullUp: enablePullUp,
enablePullDown: enablePullDown,
controller: refreshController,
onRefresh: onRefresh,
onLoading: onLoad,
header: const ClassicHeader(idleText: "下拉刷新",
releaseText: "松开刷新",
completeText: "刷新完成",
refreshingText: "加载中......",),
footer: const ClassicFooter(idleText: "上拉加载更多",
canLoadingText: "松开加载更多",
loadingText: "加载中......",),
child: builder(),
);
}
对 SmartRefresher 参数进行封装,添加了 header 和 footer 的统一处理,这里可以根据项目实际需求进行封装,可以使用其他下拉刷新/上拉加载的风格或者自定义实现效果,关于 SmartRefresher
的使用请参考官网 : flutter_pulltorefresh[3]。
buildRefreshListWidget:
代码语言:javascript复制Widget buildRefreshListWidget<T, C extends PagingController<T, PagingState<T>>>(
{
required Widget Function(T item, int index) itemBuilder,
bool enablePullUp = true,
bool enablePullDown = true,
String? tag,
Widget Function(T item, int index)? separatorBuilder,
Function(T item, int index)? onItemClick,
ScrollPhysics? physics,
bool shrinkWrap = false,
Axis scrollDirection = Axis.vertical
}) {
C controller = Get.find(tag: tag);
return GetBuilder<C>(builder: (controller) {
return buildRefreshWidget(
builder: () =>
buildListView<T>(
data: controller.pagingState.data,
separatorBuilder: separatorBuilder,
itemBuilder: itemBuilder,
onItemClick: onItemClick,
physics: physics,
shrinkWrap: shrinkWrap,
scrollDirection: scrollDirection
),
refreshController: controller.refreshController,
onRefresh: controller.refreshData,
onLoad: controller.loadMoreData,
enablePullDown: enablePullDown,
enablePullUp: enablePullUp && controller.pagingState.hasMore,
);
}, tag: tag, id: controller.pagingState.refreshId,);
}
buildRefreshListWidget
是对前面两者的再次封装,参数也基本上是前面两者的结合,buildRefreshWidget
的 builder
传入的是 buildListView
。
为了将下拉刷新、上拉加载更多的操作进行统一封装,这里引入了 PagingController 的泛型 C
并通过 GetX 的依赖管理获取到当前的 PagingController
实例 controller:
- •
buildListView
的 data 传入 PagingState 的 data 即分页数据,即controller.pagingState.data
- • refreshController 传入 PagingController 中创建的 refreshController 对象,即
controller.refreshController
- • onRefresh / onRefresh 调用 PagingController 的
refreshData / loadMoreData
方法 - • enablePullUp 使用方法传入的 enablePullUp 和 PagingState 的 hasMore(是否有更多数据) 共同判断
列表数据加载完成后将自动刷新界面,这里使用了 GetBuilder
包裹 buildRefreshWidget
,并添加 tag 和 id 参数,其中 tag 是 GetX 依赖注入的 tag ,用于区分注入的实例, id 则为刷新的 id,可通过 id 刷新指定控件,这里传入的就是 PagingState 里定义的 refreshId
,即刷新指定列表。
整体 View 结构如下:
0x02 总结
经过上诉的封装后就能快速实现文章开头展示的列表分页加载效果,通过简单的代码就能实现完整的列表分页加载功能,让开发者关注业务本身,从而节省开发工作量、提高开发效率和质量。最后附上一张整体的结构关系图:
源码:flutter_app_core[4]
引用链接
[1]
pull_to_refresh: https://pub.dev/packages/pull_to_refresh
[2]
GetX: https://pub.dev/packages/get
[3]
flutter_pulltorefresh: https://github.com/peng8350/flutter_pulltorefresh
[4]
flutter_app_core: https://github.com/loongwind/flutter_app_core