页面使用的代码还没来得及封装, 现有代码已经经过测试人员严格测试.请放心使用. 如果有bug欢迎提出. 具体效果可以参考 [视界北京] App
项目代码地址
对于移动开发,上拉加载更多是列表,中必不可少的一个功能, 由于上拉加载更多的逻辑相对来说比较复杂, 且变化多端, 因此 Android, IOS 都没有相应的上拉加载更多的控件提供. Flutter 作为新兴的跨平台开发方式也没有提供相应的Widget.
下面是我参考Android端加载更多, 开发的Flutter加载更多帮助类,可以适应大多数上拉加载更多的需求. 先来看一下效果图.
loadMore.gif
下面就来分析一下如何实现.
-
首先分析一下页面状态.
数据加载状态
- 当前什么也没做(网络请求前,网络请求成功)
- 数据加载中
- 数据加载失败(业务逻辑错误)
- 数据加载网络异常
数据状态
- 没有数据
- 有数据
这样两种状态组合可以得到页面的八种状态,因此我们的加载更多要在这八种状态中进行切换.
-
其次我们来分析一下如何加载更多
上拉加载更多的示例网上一搜一大把: 比如:
- 滑动到底部加载更多
- 滑动到还有多少个不可见项加载更多
- 某个特定项被加载时加载更多.
显然这样的示例都无法满足实际开发时的需求.
假设我们有这样的需求:
- 只在服务端有更多数据才允许加载更多
- 向上滑动时才允许加载更多
- 上一次调用过程没有发生错误和异常才允许加载更多
- 发生错误或异常后点击最后一项才允许加载更多.
-
代码实现
用枚举表示数据加载状态
/// 数据加载状态
enum PageState {
None, // 现在什么也没做(网络请求前,网络请求完成)
Loading, // 加载中
LoadingError, // 加载失败(业务逻辑错误)
LoadingException, // 网络异常
}
用bool表示是否有数据
/// 是否有数据
bool get hasData => this.length > 0;
因此根据上面的分析我们可以得到如下加载更多基类
代码解析
三个需要重载的方法
-
bool hasMore();
根据具体的业务和服务返回数据清空 判断数据是否已经加载完成 -
getRequest(bool isRefresh, int currentPage, int pageSize);
根据参数调用后台服务 -
Future
handlerData(MODEL model, bool isRefresh);
处理数据,将数据放入到数据列表中,通常在这里需要计算出bool hasMore()
方法的返回值
其他方法及属性
-
_mData = [];
用于存储服务请求回来的列表数据 -
PageState _pageState = PageState.None;
存储页面当前状态 -
bool get hasData => this.length > 0;
页面是否已经加载了数据, 有些时候需要总是显示有数据时的页面, 可以重写这个方法返回true
-
Future
obtainData([bool isRefresh = false]) async
用于页面请求数据
/// [DATA] 列表中的数据的数据类型
/// [MODEL] 服务返回的数据结构对应的数据类
abstract class DataLoadMoreBase extends ListBase {
final _mData = [];
@override
DATA operator [](int index) {
return _mData[index];
}
@override
void operator []=(int index, DATA value) {
_mData[index] = value;
}
@override
int get length => _mData.length;
@override
set length(int newLength) => _mData.length = newLength;
final _pageSize = 20;
int _currentPage = 1;
/// 使用 BehaviorSubject 会保留最后一次的值,所有监听是会受到回调
final _streamController = new BehaviorSubject>();
/// 页面状态
PageState _pageState = PageState.None;
/// 是否有数据
bool get hasData => this.length > 0;
/// 是否有业务错误
bool get hasError => _pageState == PageState.LoadingError;
/// 是否有网络异常
bool get hasException => _pageState == PageState.LoadingException;
/// 是否加载中
bool get isLoading => _pageState == PageState.Loading;
/// 页面状态
PageState get pageState => _pageState;
/// 页面通过监听stream变化更新界面
Stream> get stream => _streamController.stream;
/// 拉取数据
/// [isRefresh] 是否清空原来的数据
@mustCallSuper
Future obtainData([bool isRefresh = false]) async {
if (isLoading) return true;
_pageState = PageState.Loading;
onStateChanged(this);
var success = false;
try {
success = await _loadData(isRefresh);
if (success) {
// 加载数据成功
_pageState = PageState.None;
} else {
// 加载数据业务逻辑错误
_pageState = PageState.LoadingError;
}
} catch (e) {
// 网络异常
_pageState = PageState.LoadingException;
}
onStateChanged(this);
return success;
}
/// 加载数据
/// [isRefresh] 是否清空原来的数据
Future _loadData([bool isRefresh = false]) async {
int currentPage = isRefresh ? 1 : _currentPage + 1;
MODEL model = await getRequest(isRefresh, currentPage, _pageSize);
bool success = await handlerData(model, isRefresh);
if (success) _currentPage = currentPage;
return success;
}
/// 是否还有更多数据
@protected
bool hasMore();
/// 构造请求
/// [isRefresh] 是否清空原来的数据
/// [currentPage] 将要请求的页码
/// [pageSize] 每页多少数据
@protected
Future getRequest(bool isRefresh, int currentPage, int pageSize);
/// 重载这个方法,必须在这个方法将数据添加到列表中
/// [model] 本次请求回来的数据
/// [isRefresh] 是否清空原来的数据
@protected
Future handlerData(MODEL model, bool isRefresh);
/// 发送状态变更消息
void onStateChanged(DataLoadMoreBase source) {
if (!_streamController.isClosed) _streamController.add(source);
}
/// 释放资源
void dispose() {
_streamController.close();
}
}
4. 代码使用
使用的时候只需要简单继承上面的类, 并在页面中监听列表滚动即可实现上拉加载,下拉刷新
首先实现一下数据加载逻辑处理类
class _DataLoader extends DataLoadMoreBase {
bool _hasMore = true;
int _id; // 请求时的参数
_DataLoader(this._id);
@override
Future getRequest(bool isRefresh, int currentPage, int pageSize) async {
// 这里模拟网络请求
var list = List();
for (var i = 0; i < 10; i++) {
var article = Article(title: "Article$currentPage $_id $i");
list.add(article);
}
await Future.delayed(Duration(seconds: 2));
return Model(data: list, message: "加载成功", code: 0);
}
@override
Future handlerData(Model model, bool isRefresh) async {
// 1. 判断是否有业务错误,
// 2. 将数据存入列表, 如果是刷新清空数据
// 3. 判断是否有更多数据
if (model == null || model.isError()) {
return false;
}
if (isRefresh) clear();
// todo 实际使用时这里需要修改
addAll((model.data as List).map((d){
return d as Article;
}));
_hasMore = length < 100;
return true;
}
@override
bool hasMore() => _hasMore;
}
5. 页面实现
class LoaderMoreDemo extends StatefulWidget {
final int _id;
const LoaderMoreDemo(this._id, {Key key}) : super(key: key);
@override
_LoaderMoreDemoState createState() => _LoaderMoreDemoState();
}
class _LoaderMoreDemoState extends State with AutomaticKeepAliveClientMixin {
/// 数据加载类
_DataLoader _loader;
@override
bool get wantKeepAlive => true;
@override
void initState() {
_loader = _DataLoader(widget._id);
_loader.obtainData(false);
super.initState();
}
@override
void dispose() {
_loader.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[200],
appBar: AppBar(
title: Text('加载更多示例'),
),
body: StreamBuilder>(
stream: _loader.stream,
builder: (context, snapshot) {
/// 监听滑动结束广播
return NotificationListener(
onNotification: (notification) {
if (notification.depth != 0) return false;
if (notification.metrics.axisDirection != AxisDirection.down) return false;
if (notification.metrics.pixels < notification.metrics.maxScrollExtent) return false;
/// 如果没有更多, 服务返回错误信息, 网络异常,那么不允许上拉加载更多
if (snapshot.data == null ||
!snapshot.data.hasMore() ||
snapshot.data.hasError ||
snapshot.data.hasException) return false;
// 加载更多
_loader.obtainData(false);
return false;
},
/// 下拉刷新
child: RefreshIndicator(
child: _buildList(snapshot.data),
onRefresh: () => _loader.obtainData(true),
));
}),
);
}
Widget _buildList(DataLoadMoreBase dataLoader) {
/// 初始化时显示的View
if (dataLoader == null) {
return Container(
child: Center(child: new Text('欢迎光临...')),
);
}
/// 没有数据时候显示的View构建
if (!dataLoader.hasData) {
return LoadingEmptyIndicator(dataLoader: dataLoader);
}
/// 渲染数据 ,这里数据+1 1表示最后一项,用于显示加载状态
return ListView.separated(
itemCount: dataLoader.length + 1,
physics: const AlwaysScrollableScrollPhysics(),
separatorBuilder: (content, index) {
return new Container(height: 0.5, color: Colors.grey);
},
itemBuilder: (context, index) {
if (index == dataLoader.length) {
return LoadingIndicator(dataLoader: dataLoader);
} else {
return Material(
color: Colors.white,
child: new InkWell(
child: Padding(
padding: EdgeInsets.all(32),
child: Text(dataLoader[index].title),
),
onTap: () {},
),
);
}
},
);
}
}
项目代码地址