页面使用的代码还没来得及封装, 现有代码已经经过测试人员严格测试.请放心使用. 如果有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: () {},

),

);

}

},

);

}

}

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐