Flutter 上拉加载更多终极解决方案实现

页面使用的代码还没来得及封装, 现有代码已经经过测试人员严格测试.请放心使用. 如果有bug欢迎提出. 具体效果可以参考 [视界北京] App

项目代码地址
对于移动开发,上拉加载更多是列表,中必不可少的一个功能, 由于上拉加载更多的逻辑相对来说比较复杂, 且变化多端, 因此 Android, IOS 都没有相应的上拉加载更多的控件提供. Flutter 作为新兴的跨平台开发方式也没有提供相应的Widget.

下面是我参考Android端加载更多, 开发的Flutter加载更多帮助类,可以适应大多数上拉加载更多的需求. 先来看一下效果图.

loadMore.gif

下面就来分析一下如何实现.

  1. 首先分析一下页面状态.

    数据加载状态

    1. 当前什么也没做(网络请求前,网络请求成功)
    2. 数据加载中
    3. 数据加载失败(业务逻辑错误)
    4. 数据加载网络异常

    数据状态

    1. 没有数据
    2. 有数据
      这样两种状态组合可以得到页面的八种状态,因此我们的加载更多要在这八种状态中进行切换.
  2. 其次我们来分析一下如何加载更多

    上拉加载更多的示例网上一搜一大把: 比如:

    1. 滑动到底部加载更多
    2. 滑动到还有多少个不可见项加载更多
    3. 某个特定项被加载时加载更多.

    显然这样的示例都无法满足实际开发时的需求.

    假设我们有这样的需求:

    1. 只在服务端有更多数据才允许加载更多
    2. 向上滑动时才允许加载更多
    3. 上一次调用过程没有发生错误和异常才允许加载更多
    4. 发生错误或异常后点击最后一项才允许加载更多.
  3. 代码实现

    用枚举表示数据加载状态

/// 数据加载状态
enum PageState {
  None, // 现在什么也没做(网络请求前,网络请求完成)
  Loading, // 加载中
  LoadingError, // 加载失败(业务逻辑错误)
  LoadingException, // 网络异常
}

用bool表示是否有数据

  /// 是否有数据
  bool get hasData => this.length > 0;

因此根据上面的分析我们可以得到如下加载更多基类

代码解析

三个需要重载的方法

  1. bool hasMore();
    根据具体的业务和服务返回数据清空 判断数据是否已经加载完成
  2. getRequest(bool isRefresh, int currentPage, int pageSize);
    根据参数调用后台服务
  3. Future handlerData(MODEL model, bool isRefresh);
    处理数据,将数据放入到数据列表中,通常在这里需要计算出 bool hasMore() 方法的返回值

其他方法及属性

  1. _mData = [];
    用于存储服务请求回来的列表数据
  2. PageState _pageState = PageState.None;
    存储页面当前状态
  3. bool get hasData => this.length > 0;
    页面是否已经加载了数据, 有些时候需要总是显示有数据时的页面, 可以重写这个方法返回 true
  4. 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: () {},
            ),
          );
        }
      },
    );
  }
}

项目代码地址

你可能感兴趣的:(Flutter 上拉加载更多终极解决方案实现)