# 分页

分页也是APP中必不可少的功能, 一般配合 [滚动到底部自动加载的控件](/android-architecture-journey/widgets/refresh_and_load.md) 一起使用. 但是光有控件还不够, 我们还需要在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](/android-architecture-journey/corlibsdesign_md/rxjava.md)，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的实现不同。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ryan-8.gitbook.io/android-architecture-journey/rests/pagination.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
