# 分页

分页也是APP中必不可少的功能, 一般配合 [滚动到底部自动加载的控件](https://ryan-8.gitbook.io/android-architecture-journey/widgets/refresh_and_load) 一起使用. 但是光有控件还不够, 我们还需要在Presenter中去控制页数, 什么时候能加载, 什么时候不能加载. 下拉刷新的时候还需要重置页数. 这些代码都是可以复用的, 因此就有了BasePaginationPresenter.

## 旧的模式

旧的分页模式将分页的通用逻辑都写在BasePaginationPresenter中，如控制页数，判断页数等。一旦分页逻辑有变化就需要改动BasePaginationPresenter，甚至可能影响到Presenter的写法，甚是丑陋。如果想扩展另外一种分页模式，除了改代码别无他法。因此催生的重构分也逻辑的想法。

## 新的模式

新的分页模式将分页的逻辑抽取出来，并使用策略＋工厂模式封装，并将BasePaginationPresenter继续细化为使用不同分页策略的Presenter，这样使得一旦分页逻辑有变，只需要修改策略或者BasePaginationPresenter的子类，而不用去动BasePaginationPresenter。扩展也只需要新建一个策略以及BasePaginationPresenter的子类。

下面来看看写法，这里以页数分页为例－即根据服务器返回的总页数以及当前页数来判断是否能分页：

首先，Presenter需要使用PagePresenter，PagePresenter是BasePaginationPresenter的子类，BasePaginationPresenter中需要的是BasePaginationView类型的view接口，也就是说Activity/Fragment必须实现继承自BasePaginationView的view接口.

BasePaginationView定义:

```
/**
 * 带有分页功能的BaseView
 */
public interface BasePaginationView extends BaseView {
    /**
     * 一次页面加载完成操作
     */
    void onLoadingCompleted();

    /**
     * 所有页面均加载完成
     */
    void onAllPageLoaded();
}
```

一般情况下, 我们需要在onLoadingCompleted()中隐藏加载框, 在onAllPageLoaded()中禁止自动加载.

如何使用PagePresenter和BasePaginationView? 首先我们需要这样定义我们的Activity/Fragment以及Presenter和View:

```
public class HomeFragment extends BaseFragment<HomeView, HomePresenter>
        implements HomeView
```

```
public interface HomeView extends BasePaginationView
```

```
public class HomePresenter extends PagePresenter<HomeView>
```

接着按照下面的代码来写Presenter中需要做分页的方法. 如获取热门产品的方法:

```
    public void getTopProducts(final boolean reload) {
        if (!doPagination(reload)) return;
        if (reload) view.showLoading();

        api.getTopProducts(getPageNo(), getPageSize())
                .compose(new ResponseTransformer<>(this.<BaseData<List<String>>>bindToLifeCycle()))
                .subscribe(new PaginationSubscriber<BaseData<List<String>>>(view, this, reload) {
                    @Override
                    protected void onDataNotNull(BaseData<List<String>> data) {
                        view.renderData(reload, data.data);
                    }

                    @Override
                    protected Object getCondition(BaseData<List<String>> data, boolean dataNotNull) {
                        return data.page.pageCount;
                    }

                    @Override
                    protected List getListResult(BaseData<List<String>> data, boolean dataNotNull) {
                        if (dataNotNull) return data.data;
                        return null;
                    }
                });
    }
```

关键的地方就在`if (!doPagination(reload)) return;`这一句。doPagination方法位于BasePaginationPresenter中，翻页的通用逻辑就定义在该方法中。如果doPagination返回了false, 则意味当前不能再加载数据了, 需要return退出函数。如果为true, 则加载下一页数据。

一般分页接口都需要当前的页数, 以及每页加载多少条数据, 我们只需要使用PagePresenter的getPageNo()与getPageSize()方法即可。当然不同的分页策略可能需要的参数都不一样，我们需要根据具体的策略具体对待。

与普通网络请求不一样，这里需要使用PaginationSubscriber而不是ResponseSubscriber来订阅请求结果。接着请求下一页数据成功之后, 服务器会返回总页数, 这个时候我们需要在PaginationSubscriber的getCondition中返回总页数。并且在getListResult中返回服务器返回的List集合。最后，翻页成功并且返回的数据不为空的情况下，会回调PaginationSubscriber的onDataNotNull方法。如果返回为空，会回调onDataIsNull方法。默认此方法会调用BaseView的showEmptyHint方法，当然也可以选择覆写。

到此翻页的逻辑算是完成了一大部分. 接着看看Activity/Fragment如何去实现BasePaginationView的两个方法:

```
    @Override
    public void onLoadingCompleted() {
        ptrAutoLoadMoreLayout.complete();
    }

    @Override
    public void onAllPageLoaded() {
        ptrAutoLoadMoreLayout.disableLoading();
    }
```

全部是转发PtrAutoLoadMoreLayout的方法. 我们也可以覆写BaseView的showLoading与hideLoading方法, 覆盖BaseActivity的默认加载框, 而使用PtrAutoLoadMoreLayout的下拉式加载:

```
    @Override
    public void showLoading() {
        ptrAutoLoadMoreLayout.setRefreshing();
    }

    @Override
    public void hideLoading() {
        ptrAutoLoadMoreLayout.complete();
    }
```

接下来, 由于要配合PtrAutoLoadMoreLayout一起实现分页功能, 因此肯定需要实现PtrAutoLoadMoreLayout的RefreshLoadCallback接口:

```
    @Override
    public void onRefreshing(PtrFrameLayout frame) {
        ptrAutoLoadMoreLayout.enableLoading();
        if (!frame.isAutoRefresh()) presenter.getTopProducts(true);
    }

    @Override
    public void onLoading(PtrFrameLayout frame) {
        presenter.getTopProducts(false);
    }
```

&#x20;**注意: 由于PtrLollipopLayout/PtrAutoLoadMoreLayout的setRefreshing会回调RefreshCallback/RefreshLoadCallback的onRefreshing方法, 所以在onRefreshing中我们需要判断当前是不是自动刷新的**`if (!frame.isAutoRefresh())`**, 自动刷新就不要请求数据了, 不然就会重复执行两次. 又因为我们覆写了BaseActivity的showLoading方法为**`ptrAutoLoadMoreLayout.setRefreshing()`**, 同样每次showLoading的话就会回调onRefreshing一次, 因此在Presenter中建议加上**`if (reload) view.showLoading()`**.**&#x20;

到此, 整个分页功能就完成了. 可以看到整体还是不是很简单的. BasePaginationPresenter也只是抽象了通用的分页的逻辑，实现则交由具体的分页策略。下面看看关键方法doPagination:

```
    /**
     * 分页操作, 具体页面效果需自行实现
     * @param reload 是否是刷新操作, true为刷新, false则为加载下一页
     * @return 分页是否成功, 当当前页码超过总页数时会返回false
     */
    protected boolean doPagination(boolean reload) {
        boolean can = strategy.canDoPagination(reload);
        if (can) strategy.doPagination(reload);
        else view.onAllPageLoaded();
        return can;
    }
```

接着来看看分页策略PaginationStrategy接口的定义：

```
public interface PaginationStrategy {
    /**
     * 根据分页条件判断是否能分页
     */
    boolean canDoPagination(boolean reload);

    /**
     * 分页具体逻辑
     */
    void doPagination(boolean reload);

    /**
     * 获取分页条件
     */
    Object getCondition();

    /**
     * 设置分页条件
     */
    void setCondition(Object c);
}
```

根据页数分页的具体逻辑实现类则是PageStrategy。PageStrategy内部维护了一个Page类，有当前页数，每页个数，总页数等字段。PageStrategy实现了PaginationStrategy，接着来看看canDoPagination，doPagination以及setCondition的实现：

```
    protected Page page = new Page(1, 15, -1);

    @Override
    public boolean canDoPagination(boolean reload) {
        return reload || page.pageNo < page.pageCount;
    }

    @Override
    public void doPagination(boolean reload) {
        if (reload) page.pageNo = 1;
        else page.pageNo++;
    }

    @Override
    public void setCondition(Object c) {
        page.pageCount = (int) c;
    }
```

细心的朋友可能会疑惑了, doPagination方法中仅仅回调了onAllPageLoaded方法, 而没有onLoadingCompleted方法. 那么onLoadingCompleted方法是在何处调用的? 下面看看ResponseHandler类的resetLoadingStatus方法（不知道ResponseHandler的同学可以参考[ResponseSubscriber](https://ryan-8.gitbook.io/android-architecture-journey/corlibsdesign_md/rxjava)，PaginationSubscriber继承自ResponseSubscriber）:

```
public void resetLoadingStatus() {
    if (view != null) {
        if (view instanceof BasePaginationView) {
            BasePaginationView paginationView = (BasePaginationView) view;
            paginationView.onLoadingCompleted();
        }
        view.hideLoading();
    }
}
```

到此, 整个分页的逻辑就很清晰了. 分页功能是由分页策略（BasePaginationPresenter，PaginationStrategy）, Activity/Fragment（BasePaginationView）, PaginationSubscriber以及PtrAutoLoadMoreLayout四方合作的结果.

## 扩展分页策略

如何扩展分页策略？这里以页书分页的一个变种为例－－根据数据集合个数来判断是否能分页，分页逻辑则和页数分页一样。这里我们只需要新建两个类：

* ListResultStrategy

  ListResultStrategy需要实现PaginationStrategy，但由于其分页逻辑与PageStrategy一致，因此可以直接继承PageStrategy：

  ```
  public class ListResultStrategy extends PageStrategy {

    private List lastResult;

    @Override
    public boolean canDoPagination(boolean reload) {
        return reload || (lastResult != null && lastResult.size() >= page.getPageSize());
    }

    @Override
    public void setCondition(Object c) {
        lastResult = (List) c;
    }
  }
  ```
* ListPagePresenter

  由于两种分页需要的条件一样，因此ListPagePresenter类似于PagePresenter

  ```
  public abstract class ListPagePresenter<T extends BasePaginationView> extends BasePaginationPresenter<T> {

      private ListResultStrategy pageStrategy;

      public ListPagePresenter() {
          setPaginationStrategy(StrategyFactory.getStrategy(StrategyFactory.ListResultStrategy));
          pageStrategy = (ListResultStrategy) strategy;
      }

      /** 获取当前页数下标 **/
      public int getPageNo() {
          return pageStrategy.getCondition().getPageNo();
      }

      /** 获取当前每页个数 **/
      public int getPageSize() {
          return pageStrategy.getCondition().getPageSize();
      }

      /** 设置每页个数 **/
      public void setPageSize(int size) {
          pageStrategy.setPageSize(size);
      }
  }
  ```

  只需要在构造函数里设置策略为ListResultStrategy。
* StrategyFactory

  前面提到分页使用了策略＋工厂模式，StrategyFactory就是一个工厂类，一个非常简单的没有接口的工厂类，并且该工厂是一个枚举。这里为何要多此一举的使用工厂模式，而不是直接new出来具体的策略？主要是因为策略模式的缺陷：具体的策略必须暴露出去，并且还要由上层模块初始化，这不合适，高层次模块对低层次模块应该仅仅处在“接触”的层次上，而不是“耦合”的关系，否则维护的工作量会非常大。正好工厂方法可以帮我们生产指定的对象。

  在新建了策略类和Presenter之后，就需要在StrategyFactory添加一个属性`ListResultStrategy("com.corelibs.pagination.strategy.ListResultStrategy")`，策略名加上路径即可。

  使用不同的策略在写法上的不同只有继承不同的Presenter，获取不同的分页条件，以及getCondition的实现不同。
