分页

分页也是APP中必不可少的功能, 一般配合 滚动到底部自动加载的控件 一起使用. 但是光有控件还不够, 我们还需要在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);
    }

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

到此, 整个分页功能就完成了. 可以看到整体还是不是很简单的. 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,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的实现不同。

Last updated