flutter 刷新、加载与占位图一站式服务(基于easy_refresh扩展)
前文
今天聊到的是滚动视图
的 刷新与加载
,对于这个老生常谈的问题解决方案就太多了,优秀的插件也屡见不鲜,譬如 pull_to_refresh、 easy_refresh、还有笔者前段时间使用过的 infinite_scroll_pagination 这些都是极具代表性的优秀插件。 那你在使用时有没有类似情况:
- 为了重复的样版式代码感到厌倦?
- 为了
Ctrl V+C
感到无聊? - 看到通篇类似的代码想大刀阔斧的整改?
- 更有甚者有没有本来只想自定义列表样式,却反而浪费更多的时间来完成基础配置?
现在我们来解决这类问题,欢迎来到走近科学探索发现 - Approaching Scientific Exploration and Discovery。(可能片场走错了:) )
注意
- 本文以
easy_refresh
作为刷新框架举例说明(其他框架的类似) - 本文案例demo以
getx
作为项目状态管理框架(与刷新无关仅作为项目基础框架构成,不喜勿喷)
正文
现在请出我们重磅成员mixin
,对于这个相信大家已经非常熟悉了。我们要做的就是利用easy_refresh
提供的refresh controller
对视图逻辑进行拆分,从而精简我们的样板式代码。
1、提炼逻辑,刷新控制与分页请求
首页拆离我们刷新和加载方法:
import 'dart:async';
import 'package:easy_refresh/easy_refresh.dart';
import 'state.dart';
mixin PagingMixin<T> {
/// 刷新控制器
final EasyRefreshController _pagingController = EasyRefreshController(
controlFinishLoad: true, controlFinishRefresh: true);
EasyRefreshController get pagingController => _pagingController;
/// 初始页码 <---- 单独提出这个的原因是有时候我们请求的起始页码不固定,有可能是0,也有可能是1
int _initPage = 0;
/// 当前页码
int _page = 0;
int get page => _page;
/// 列表数据
List<T> get items => _state.items;
int get itemCount => items.length;
/// 错误信息
dynamic get error => _state.error;
/// 关联刷新状态管理的控制器 <---- 自定义状态类型,后文会有阐述主要包含列表数据、初始加载是否为空、错误信息
PagingMixinController get state => _state;
final PagingMixinController<T> _state = PagingMixinController(
PagingMixinData(items: []),
);
/// 是否加载更多 <---- 可以在控制器中初始化传入,控制是否可以进行加载
bool _isLoadMore = true;
bool get isLoadMore => _isLoadMore;
/// 控制刷新结束回调(异步处理) <---- 手动结束异步操作,并返回结果
Completer? _refreshComplater;
/// 挂载分页器
/// `controller` 关联刷新状态管理的控制器
/// `initPage` 初始页码值(分页起始页)
/// `isLoadMore` 是否加载更多
void initPaging({
int initPage = 0,
isLoadMore = true,
}) {
_isLoadMore = isLoadMore;
_initPage = initPage;
_page = initPage;
}
/// 获取数据
FutureOr fetchData(int page);
/// 刷新数据
Future onRefresh() async {
_refreshComplater = Completer();
_page = _initPage;
fetchData(_page);
return _refreshComplater!.future;
}
/// 加载更多数据
Future onLoad() async {
_refreshComplater = Completer();
_page++;
fetchData(_page);
return _refreshComplater!.future;
}
/// 获取数据后调用
/// `items` 列表数据
/// `maxCount` 数据总数,如果为0则默认通过 `items` 有无数据判断是否可以分页加载, null为非分页请求
/// `error` 错误信息
/// `limit` 单页显示数量限制,如果items.length < limit 则没有更多数据
void endLoad(
List<T>? list, {
int? maxCount,
// int limit = 5,
dynamic error,
}) {
if (_page == _initPage) {
_refreshComplater?.complete();
_refreshComplater = null;
}
final dataList = List.of(_state.value.items);
if (list != null) {
if (_page == _initPage) {
dataList.clear();
// 更新数据
_pagingController.finishRefresh();
_pagingController.resetFooter();
}
dataList.addAll(list);
// 更新列表
_state.value = _state.value.copyWith(
items: dataList,
isStartEmpty: page == _initPage && list.isEmpty,
);
// 默认没有总数量 `maxCount`,用获取当前数据列表是否有值判断
// 默认有总数量 `maxCount`, 则判断当前请求数据list+历史数据items是否小于总数
// bool hasNoMore = !((items.length + list.length) < maxCount);
bool isNoMore = true;
if (maxCount != null) {
isNoMore = page > 1; // itemCount >= maxCount;
}
var state = IndicatorResult.success;
if (isNoMore) {
state = IndicatorResult.noMore;
}
_pagingController.finishLoad(state);
} else {
_state.value = _state.value.copyWith(items: [], error: error ?? '数据请求错误');
}
}
}
创建PagingMixin<T>
混入类型,泛型<T>
属于列表子项的数据类型
void initPaging(...)
:初始化的时候可以写入基本设置(可以不调用)
Future onRefresh()
Future onLoad()
:供外部调用的刷新加载方法
FutureOr fetchData(int page)
:由子类集成重写,主要是完成数据获取方法,在获取到数据后,需要调用方法void endLoad(...)
来结束整个请求操作,通知视图刷新
PagingMixinController
继承自ValueNotifier
,是对数据相关状态的缓存,便于独立逻辑操作与数据状态:
class PagingMixinController<T> extends ValueNotifier<PagingMixinData<T>> {
PagingMixinController(super.value);
dynamic get error => value.error;
List<T> get items => value.items;
int get itemCount => items.length;
}
// flutter 关于easy_refresh更便利的打开方式
class PagingMixinData<T> {
// 列表数据
final List<T> items;
/// 错误信息
final dynamic error;
/// 首次加载是否为空
bool isStartEmpty;
PagingMixinData({
required this.items,
this.error,
this.isStartEmpty = false,
});
....
}
完成这两个类的编写,我们对于逻辑部分的拆离已经完成了。
2、简化使用,刷新框架的封装
下面是对easy_refresh
的使用,封装:
class PullRefreshControl extends StatelessWidget {
const PullRefreshControl({
super.key,
required this.pagingMixin,
required this.childBuilder,
this.header,
this.footer,
this.locatorMode = false,
this.refreshOnStart = true,
this.startRefreshHeader,
});
final Header? header;
final Footer? footer;
final bool refreshOnStart;
final Header? startRefreshHeader;
/// 列表视图
final ERChildBuilder childBuilder;
/// 分页控制器
final PagingMixin pagingMixin;
/// 是否固定刷新偏移
final bool locatorMode;
@override
Widget build(BuildContext context) {
final firstRefreshHeader = startRefreshHeader ??
BuilderHeader(
triggerOffset: 70,
clamping: true,
position: IndicatorPosition.above,
processedDuration: Duration.zero,
builder: (ctx, state) {
if (state.mode == IndicatorMode.inactive ||
state.mode == IndicatorMode.done) {
return const SizedBox();
}
return Container(
padding: const EdgeInsets.only(bottom: 100),
width: double.infinity,
height: state.viewportDimension,
alignment: Alignment.center,
child: SpinKitFadingCube(
size: 25,
color: Theme.of(context).primaryColor,
),
);
},
);
return EasyRefresh.builder(
controller: pagingMixin.pagingController,
header: header ??
RefreshHeader(
clamping: locatorMode,
position: locatorMode
? IndicatorPosition.locator
: IndicatorPosition.above,
),
footer: footer ?? const ClassicFooter(),
refreshOnStart: refreshOnStart,
refreshOnStartHeader: firstRefreshHeader,
onRefresh: pagingMixin.onRefresh,
onLoad: pagingMixin.isLoadMore ? pagingMixin.onLoad : null,
childBuilder: (context, physics) {
return ValueListenableBuilder(
valueListenable: pagingMixin.state,
builder: (context, value, child) {
if (value.isStartEmpty) {
return _PagingStateView(
isEmpty: value.isStartEmpty,
onLoading: pagingMixin.onRefresh,
);
}
return childBuilder.call(context, physics);
},
);
},
);
}
}
创建PullRefreshControl
类型,设置必须属性pagingMixin
和 childBuilder
,前者是我们创建的PagingMixin
对象(可以是任何类型,只要支持混入就可以了),后者是对我们滚动列表的实现。 其他的都是对 easy_refresh
的属性配置,参考相关文档就行了。
到这里我们减配版的封装就完成了,使用方式如下:
3、分门别类,进一步简化
但是我们并没有完成我们前文所说的简化操作,还是需要一遍又一遍创建重复的滚动列表,所以我们继续:
/// 快速构建 `ListView` 形式的分页列表
/// 其他详细参数查看 [ListView]
class SpeedyPagedList<T> extends StatelessWidget {
const SpeedyPagedList({
super.key,
required this.controller,
required this.itemBuilder,
this.scrollController,
this.padding,
this.header,
this.footer,
this.locatorMode = false,
this.refreshOnStart = true,
this.startRefreshHeader,
double? itemExtent,
}) : _separatorBuilder = null,
_itemExtent = itemExtent;
const SpeedyPagedList.separator({
super.key,
required this.controller,
required this.itemBuilder,
required IndexedWidgetBuilder separatorBuilder,
this.scrollController,
this.padding,
this.header,
this.footer,
this.locatorMode = false,
this.refreshOnStart = true,
this.startRefreshHeader,
}) : _separatorBuilder = separatorBuilder,
_itemExtent = null;
final PagingMixin<T> controller;
final Widget Function(BuildContext context, int index, T item) itemBuilder;
final Header? header;
final Footer? footer;
final bool refreshOnStart;
final Header? startRefreshHeader;
final bool locatorMode;
/// 参照 [ScrollView.controller].
final ScrollController? scrollController;
/// 参照 [ListView.itemExtent].
final EdgeInsetsGeometry? padding;
/// 参照 [ListView.separator].
final IndexedWidgetBuilder? _separatorBuilder;
/// 参照 [ListView.itemExtent].
final double? _itemExtent;
@override
Widget build(BuildContext context) {
return PullRefreshControl(
pagingMixin: controller,
header: header,
footer: footer,
refreshOnStart: refreshOnStart,
startRefreshHeader: startRefreshHeader,
locatorMode: locatorMode,
childBuilder: (context, physics) {
return _separatorBuilder != null
? ListView.separated(
physics: physics,
padding: padding,
controller: scrollController,
itemCount: controller.itemCount,
itemBuilder: (context, index) {
final item = controller.items[index];
return itemBuilder.call(context, index, item);
},
separatorBuilder: _separatorBuilder!,
)
: ListView.builder(
physics: physics,
padding: padding,
controller: scrollController,
itemExtent: _itemExtent,
itemCount: controller.itemCount,
itemBuilder: (context, index) {
final item = controller.items[index];
return itemBuilder.call(context, index, item);
},
);
},
);
}
}
...
归纳我们所需要的使用方式(我这里只写了ListView/GridView),构创建快速初始化加载列表的方法,将我们仅需要的Widget Function(BuildContext context, int index, T item) itemBuilder
单个元素的创建(因为对于大多列表来说我们仅关心单个元素样式)暴露出来,简化PullRefreshControl
的使用。
对比前面的使用方式,现在更加简洁了,总计代码也就十几行吧。
4、总结
到这里就结束啦,文章也仅算是对繁杂重复使用的东西进行一些归纳总结,没有特别推崇的意思,更优秀的方案也比比皆是,所以仁者见仁了各位。
转载自:https://juejin.cn/post/7242512226917417015