Android 基于 MVP 框架的下拉刷新、上拉加载页面,View和Presenter层基类封装

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/kbkaaa/article/details/72598828

前言

Android 项目开发中经常遇到列表式页面,并且需要实现下拉刷新,上拉到底后加载下一页的功能,这里结合我们项目正在使用的 MVP 框架,介绍一种基类封装方案,实现 View、Adapter、数据处理Presenter层的基类封装,后续继承这几个类,简单地重写下 UI 布局,网络请求即可实现下拉刷新,上拉加载功能。

老规矩,先上 Github 和 App 下载链接:
App下载地址: http://a.app.qq.com/o/simple.jsp?pkgname=chenyu.jokes
微信扫描下载APP:
App二维码
这里写图片描述
源码地址: https://github.com/zhongchenyu/jokes
由于后续代码可能会做重构,本文介绍的代码保存在 demo3_BaseScroll 分支,请 checkout。

View 层封装

View 层我们封装了 BaseScrollActivity 和 BaseScrollFragment 两个基类,分别用在需要使用 Activity 和 Fragment 的地方,这里先介绍下 BaseScrollActivity 。

UI 布局

要求所有继承的子类 Activity 必须包含一个 SwipeRefreshLayout ,再在其内部包含一个 RecyclerView。SwipeRefreshLayout 用于实现下拉刷新,而上拉加载需要通过 RecyclerView 的 OnScrollListener 实现。

  <android.support.v4.widget.SwipeRefreshLayout android:id="@+id/refreshLayout"
      android:layout_width="match_parent" android:layout_height="match_parent">
    <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView"
        android:layout_width="match_parent" android:layout_height="match_parent"/>
  </android.support.v4.widget.SwipeRefreshLayout>

BaseScrollActivity 封装

再看一下 BaseScrollActivity 的代码:

package chenyu.jokes.base;

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.widget.Toast;
import butterknife.BindView;
import butterknife.ButterKnife;
import chenyu.jokes.R;
import java.util.ArrayList;
import nucleus.view.NucleusAppCompatActivity;

/**
 * Created by chenyu on 2017/5/15.
 */

public abstract class BaseScrollActivity<Adapter extends BaseScrollAdapter, P extends BaseScrollPresenter, M>
    extends NucleusAppCompatActivity<P> implements BaseRxView<M> {
  @BindView(R.id.recyclerView) public RecyclerView recyclerView;
  @BindView(R.id.refreshLayout) public SwipeRefreshLayout refreshLayout;
  private int currentPage = 1;
  private int previousTotal = 0;
  private boolean loading = true;
  private boolean noMoreData = false;
  protected Adapter mAdapter;
  protected boolean needLoadMore = true;

  public abstract int getLayout();

  public abstract Adapter getAdapter();

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(getLayout());
    ButterKnife.bind(this);
    mAdapter = getAdapter();
    recyclerView.setAdapter(mAdapter);
    LinearLayoutManager layoutManager = new LinearLayoutManager(this);
    recyclerView.setLayoutManager(layoutManager);
  }

  @Override protected void onPostCreate(@Nullable Bundle savedInstanceState) {
    super.onPostCreate(savedInstanceState);
    initListener();
    getPresenter().loadPage(1);
  }

  private void initListener() {
    refreshLayout.setColorSchemeResources(R.color.colorPrimary);
    refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
      @Override public void onRefresh() {
        mAdapter.clear();
        getPresenter().loadPage(1);
        currentPage = 1;
        previousTotal = 0;
        mAdapter.notifyDataSetChanged();
        refreshLayout.setRefreshing(false);
      }
    });

    if (needLoadMore) {
      recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
          super.onScrolled(recyclerView, dx, dy);

          if (noMoreData) {
            return;
          }

          int totalItemCount = recyclerView.getAdapter().getItemCount();
          int lastVisibleItem =
              ((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition();
          if (loading) {
            if (totalItemCount > previousTotal) {
              loading = false;
              previousTotal = totalItemCount;
            }
          }
          if (!loading && lastVisibleItem >= totalItemCount - 1) {//(totalItemCount - visibleItemCount) <= firstVisibleItem

            loading = true;
            currentPage++;
            onLoadMore();
            previousTotal = totalItemCount;
          }
        }
      });
    }
  }

  @Override public void onItemsNext(ArrayList<M> items) {
    if (items.isEmpty()) {
      noMoreData = true;
      loading = false;
      return;
    }
    mAdapter.addAll(items);
    mAdapter.notifyDataSetChanged();
    loading = false;
  }

  @Override public void onItemsError(Throwable throwable) {
    Toast.makeText(this, throwable.getMessage(), Toast.LENGTH_SHORT).show();
  }

  public void onLoadMore() {
    getPresenter().loadPage(currentPage);
  }

  @Override protected void onDestroy() {
    super.onDestroy();
    mAdapter.clear();
  }
}

类定义

首先看下类的定义:

public abstract class BaseScrollActivity<Adapter extends BaseScrollAdapter, P extends BaseScrollPresenter, M>
    extends NucleusAppCompatActivity<P> implements BaseRxView<M>

我们定义的一个抽象类,因为有两个抽象函数需要子类去实现,分别是:

  public abstract int getLayout();

  public abstract Adapter getAdapter();

getLayout() 用于指定 layout 资源,getAdapter() 用于指定 RecyclerView 的 Adapter,子类里直接 return 需要的值就行。

父类 nucleus.view.NucleusAppCompatActivity 来自 nucleus。Nucleus 是一个 Android MVP 框架,具体用法可以参考我之前的博文:使用MVP+Retrofit+RxJava实现的的Android Demo (上)使用Nuclues库实现MVP

用到了3个泛型:<Adapter extends BaseScrollAdapter, P extends BaseScrollPresenter, M> 分别是 RecyclerView 需要用到的 Adapter ,Presenter, 数据模型 M,除了M,都是继承自我们自己封装的基类。

还有一个接口 implements BaseRxView<M>,代码如下:

package chenyu.jokes.base;

import java.util.ArrayList;

/**
 * Created by chenyu on 2017/5/20.
 */

public interface BaseRxView<Model> {
  void onItemsNext(ArrayList<Model> model);

  void onItemsError(Throwable throwable);
}

两个函数,分别在数据请求成功和失败时调用,单独把这两个提取到一个接口里,主要是为了使 BaseScrollActivity 和 BaseScrollFragment 能实现同一个接口,后面可以只封装一个 Presenter 类。

初始化

接下来变量声明,在 onCreate() 函数里进行 RecyclerView 的初始化,包括给 mAdapter 赋值并设置给 RecyclerView,LayouManager的设置。

加载首页数据,添加监听器

然后在 onPostCreate() 里初始化下拉和上拉的 Listener,并通过getPresenter().loadPage(1); 语句,调用 Presenter 的方法来加载第一页的数据。

为什么不放在 onCreate() 里呢?这是考虑到子类的 onCreate() 里可能还会有其他的初始化操作,比如基类变量protected boolean needLoadMore = true; 这个是用来控制是否添加上拉加载监听器的,默认为 true,考虑到有些时候可能只要下拉刷新,但数据的获取没有分页,不需要上拉加载更多,那么子类可以在 onCreate() 里把 needLoadMore 设置成false。这个需要在 initListener() 之前执行,如果基类中把 initListener() 放在onCreate() 里,那子类只能在调用 super.onCreate() 之前对 needLoadMore 进行赋值了,虽然也能实现效果,但是不优雅。

另外子类也可能需要对 Presenter 进行一些初始化,需要在加载第一页的数据之前执行,因此getPresenter().loadPage(1); 也要放在 onPostCreate() 里。

放到onStart()、onResume() 也是不合适的,因为这两个回调可能在 Activity 生命周期里可能被回调多次,但是添加 Listener 和加载首页数据,只需要执行一次,onPostCreate() 是最佳选择。

再看一下下拉刷新监听器的代码:

refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
      @Override public void onRefresh() {
        mAdapter.clear();
        getPresenter().loadPage(1);
        currentPage = 1;
        previousTotal = 0;
        mAdapter.notifyDataSetChanged();
        refreshLayout.setRefreshing(false);
      }
    });

这个实现一下 SwipeRefreshLayout 自带的监听接口就可以,注意要先将Adapter的数据情况,再重新去加载第一页数据,否则老的数据并没有被刷新,只是把新数据加到了最后面。同时要将各种翻页要用到的变量复位到初始值。

再看下上拉加载下一页的 Listener 代码:

if (needLoadMore) {
      recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
          super.onScrolled(recyclerView, dx, dy);

          if (noMoreData) {
            return;
          }

          int totalItemCount = recyclerView.getAdapter().getItemCount();
          int lastVisibleItem =
              ((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition();
          if (loading) {
            if (totalItemCount > previousTotal) {
              loading = false;
              previousTotal = totalItemCount;
            }
          }
          if (!loading && lastVisibleItem >= totalItemCount - 1) {
            loading = true;
            currentPage++;
            onLoadMore();
            previousTotal = totalItemCount;
          }
        }
      });
    }

这里主要是用了RecyclerView 的 OnScrollListener,在滑动 RecyclerView 列表时进行检测,如果列表中最后一个可见元素的 ID 是 总元素个数减一,则认为列表已经被拉到最低端,这是将 currentPage自加一,并调用 onLoadMore() 函数来加载下一页数据。

而LoadMore() 也是调用 Presenter 的函数:

  public void onLoadMore() {
    getPresenter().loadPage(currentPage);
  }

另外还有几个 Boolean 变量来进行控制加载流程:

if (needLoadMore) { ... }

needLoadMore,用于控制是否添加上拉加载 Listener,默认为 true,如果子类中设置为 false,则不添加Listener,用于数据一次性加载完成,不需要分页加载的场景。

noMoreData,没有下一页数据,初始化为false,如果加载下一页时获得的是空数据,说明已经加载完全部数据,没有下一页了,则置为true,为true时,Listener直接返回,不执行任何动作。

if (noMoreData) {
            return;
          }

loading ,表示是否正在请求数据,启动加载下一页前置为 true,加载完成后置为false,如果loading 为 true,触发监听器时,不会执行加载动作,主要为了防止网络不好,加载缓慢时,上拉到底会多次触发加载同一页的问题。

数据请求结束后的操作:

  @Override public void onItemsNext(ArrayList<M> items) {
    if (items.isEmpty()) {
      noMoreData = true;
      loading = false;
      return;
    }
    mAdapter.addAll(items);
    mAdapter.notifyDataSetChanged();
    loading = false;
  }

  @Override public void onItemsError(Throwable throwable) {
    Toast.makeText(this, throwable.getMessage(), Toast.LENGTH_SHORT).show();
  }

onItemsNext,onItemsError 这个两个函数由Presenter在完成请求后选择调用哪个,如果请求成功,则调用 onItemsNext,首先会判断下数据是否为空,如果为空,则将noMoreData 置为 true,如果不为空,则将数据添加到Adapter中,更新 UI,将loading 置为 false。

BaseScrollFragment 封装

BaseScrollActivity 基本就封装这些,BaseScrollFragment 基本是一样的,主要是Fragment和Activity生命周期不同,对应代码的执行位置也不同,这里只贴一下代码:

package chenyu.jokes.base;

import android.os.Bundle;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import butterknife.BindView;
import butterknife.ButterKnife;
import chenyu.jokes.R;
import java.util.ArrayList;
import nucleus.view.NucleusSupportFragment;

/**
 * Created by chenyu on 2017/3/6.
 */

public abstract class BaseScrollFragment<Adapter extends BaseScrollAdapter, P extends BaseScrollPresenter, M>
    extends NucleusSupportFragment<P> implements BaseRxView<M> {

  @BindView(R.id.recyclerView) public RecyclerView recyclerView;
  @BindView(R.id.refreshLayout) public SwipeRefreshLayout refreshLayout;
  private int currentPage = 1;
  private int previousTotal = 0;
  private boolean loading = true;
  private boolean noMoreData = false;
  protected Adapter mAdapter;
  protected SwipeRefreshLayout.OnRefreshListener listener;

  public abstract int getLayout();

  public abstract Adapter getAdapter();

  @Override public View onCreateView(LayoutInflater inflater, ViewGroup container,
      Bundle savedInstanceState) {
    View view = inflater.inflate(getLayout(), container, false);
    return view;
  }

  @Override public void onViewCreated(View view, Bundle state) {
    super.onViewCreated(view, state);
    ButterKnife.bind(this, view);
    mAdapter = getAdapter();
    recyclerView.setAdapter(mAdapter);
    LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
    recyclerView.setLayoutManager(layoutManager);
    initListener();
    getPresenter().loadPage(1);
  }

  private void initListener() {
    refreshLayout.setColorSchemeResources(R.color.colorPrimary);
    listener = new SwipeRefreshLayout.OnRefreshListener() {
      @Override public void onRefresh() {
        mAdapter.clear();
        getPresenter().loadPage(1);
        currentPage = 1;
        previousTotal = 0;
        mAdapter.notifyDataSetChanged();
        refreshLayout.setRefreshing(false);
      }
    };
    refreshLayout.setOnRefreshListener(listener);

    recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
      @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        if (noMoreData) {
          return;
        }
        int totalItemCount = recyclerView.getAdapter().getItemCount();
        int lastVisibleItem =
            ((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition();
        if (loading) {
          if (totalItemCount > previousTotal) {
            loading = false;
            previousTotal = totalItemCount;
          }
        }
        if (!loading && lastVisibleItem >= totalItemCount - 1) {

          loading = true;
          currentPage++;
          onLoadMore();
          previousTotal = totalItemCount;
        }
      }
    });
  }

  @Override public void onItemsNext(ArrayList<M> items) {

    if (items.isEmpty()) {
      noMoreData = true;
      loading = false;
      return;
    }

    mAdapter.addAll(items);
    mAdapter.notifyDataSetChanged();
    loading = false;
  }

  @Override public void onItemsError(Throwable throwable) {
    Toast.makeText(getActivity(), throwable.getMessage(), Toast.LENGTH_SHORT).show();
  }

  public void onLoadMore() {
    getPresenter().loadPage(currentPage);
  }

  @Override public void onDestroyView() {
    super.onDestroyView();
    mAdapter.clear();
  }
}

Adapter 封装

RecyclerView的Adapter,为了减少重复代码,我们也提取一些公共操作进行封装,先上代码:

package chenyu.jokes.base;

import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import butterknife.ButterKnife;
import java.util.ArrayList;

/**
 * Created by chenyu on 2017/3/3.
 */

public abstract class BaseScrollAdapter<Model, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {


  protected ArrayList<Model> mItems = new ArrayList<>();
  protected ViewGroup parent;


  public abstract int getLayout();

  @Override public VH onCreateViewHolder(ViewGroup parent, int viewType) {
    View view = LayoutInflater.from(parent.getContext()).inflate(
        getLayout(),parent,false);
    this.parent = parent;
    return getViewHolder(view);
  }

  protected abstract VH getViewHolder(View view) ;
  @Override public void onBindViewHolder(VH holder, int position){
    ButterKnife.bind(this,holder.itemView);
  }

  @Override public int getItemCount() {
    return mItems.size();
  }

  public void addAll(ArrayList<Model> items) {
    mItems.addAll(items);
  }

  public void add(Model item) {
    mItems.add(item);
  }
  public void clear() {
    mItems.clear();
  }

  public void remove(int index) {
    mItems.remove(index);
  }

}

BaseAdapter 也是抽象函数,有两个抽象函数需要子类实现,getLayout(),子类中直接return需要的layout 资源, getViewHolder 子类中return 需要的ViewHolder:

public abstract int getLayout();
 protected abstract VH getViewHolder(View view) ;

Adapter 中定义了一个 ArrayList类 mItems,用于保存数据,并公开了若干对 mItems 进行增删的函数。

其他几个函数也是实现一些初始化操作。

子类需要做的有,实现抽象函数,定义一个ViewHolder类,实现onBindTo函数。

BaseScrollPresenter 封装

BaseScrollPresenter做的主要是把第一个网络请求封装起来,先上代码:

package chenyu.jokes.base;

import android.os.Bundle;
import chenyu.jokes.app.AccountManager;
import java.util.ArrayList;
import nucleus.presenter.RxPresenter;
import rx.Observable;
import rx.functions.Action2;
import rx.functions.Func0;

import static rx.android.schedulers.AndroidSchedulers.mainThread;
import static rx.schedulers.Schedulers.io;

/**
 * Created by chenyu on 2017/3/7.
 */

public abstract class BaseScrollPresenter<View extends BaseRxView, Model>
    extends RxPresenter<View> {
  protected int mPage;
  private final int INIT_LOAD = 1;

  @Override protected void onCreate(Bundle savedState) {
    super.onCreate(savedState);

    restartableFirst(INIT_LOAD,
        new Func0<Observable<ArrayList<Model>>>() {
          @Override public Observable<ArrayList<Model>> call() {
            return loadPageRequest()
                .subscribeOn(io())
                .observeOn(mainThread());
          }
        },
        new Action2<View, ArrayList<Model>>() {
          @Override public void call(View view,
              ArrayList<Model> items) {
            view.onItemsNext(items);
          }
        },
        new Action2<View, Throwable>() {
          @Override public void call(View view, Throwable throwable) {
            view.onItemsError(throwable);
          }
        }
    );
  }

  protected abstract Observable<ArrayList<Model>> loadPageRequest();

  public void loadPage(int page) {
    mPage = page;
    start(INIT_LOAD);
  }

}

父类是 RxPresenter,也是 Nucleus 框架的内容,是负载异步处理数据请求的类,可以和 View 绑定。
两个泛型 <View extends BaseRxView, Model>,第一个View需要实现了 BaseRxView 接口,可以是BaseScrollActivity 或者 BaseScrollFragment,这就是定义 BaseRxView 的好处,否则就需要为aseScrollActivity 和 BaseScrollFragment 分别封装一个 BasePresenter 类了。Model 是第一个网络请求需要的数据模型,也就是加载首页,刷新,上拉加载时用到的数据模型,如果对应的View还有其他网络请求,可以使用其他数据模型,在子类定义就行,与这个泛型无关。

有一个抽象函数子类必须实现,返回数据请求接口的数据,可能是网络请求,或者从本地数据库获取数据等,返回类型是 RxJava 的 Observable 泛型为 ArrayList<Model>

  protected abstract Observable<ArrayList<Model>> loadPageRequest();

在 onCreate() 中用 restartableFirst() 函数注册数据请求,这个是 RxJava 的形式,如果请求成功,则调用 View 的 onItemsNext() 函数,请求出错则调用 onItemsError() 函数。

再看下 loadPage 函数,这个就是刚才在 View 中通过 getPresenter().loadPage(page) 来调用的那个,先给mPage赋值,再启动请求。

  public void loadPage(int page) {
    mPage = page;
    start(INIT_LOAD);
  }

子类实现

介绍完了基类的封装,接下来看下子类如何方便快捷地实现效果了。

View层:

@RequiresPresenter(FunPicPresenter.class)
public class FunPicFragment extends BaseScrollFragment<FunPicAdapter,FunPicPresenter, Data>{

  @Override public FunPicAdapter getAdapter() {
    return new FunPicAdapter();
  }

  @Override public int getLayout() {
    return R.layout.fragment_fun_pic;
  }
}

实现下getAdapter() 和 getLayout() 即可。

Adapter

public class FunPicAdapter extends BaseScrollAdapter<Data, FunPicAdapter.FunPicViewHolder> {

  @Override public int getLayout() {
    return R.layout.item_fun_pic;
  }

  @Override protected FunPicViewHolder getViewHolder(View view) {
    return new FunPicViewHolder(view);
  }

  @Override public void onBindViewHolder(FunPicViewHolder holder, int position) {
    super.onBindViewHolder(holder, position);
    holder.content.setText(mItems.get(position).getContent());
    Uri uri = mItems.get(position).getUri();           Picasso.with(holder.itemView.getContext()).load(uri).into(holder.img);    
  }

  public static class FunPicViewHolder extends RecyclerView.ViewHolder {
    @BindView(R.id.content) public TextView content;
    @BindView(R.id.img) public ImageView img;

    public FunPicViewHolder(View view) {
      super(view);
      ButterKnife.bind(this, view);
    }
  }
}

定义一个内部类 ViewHolder,实现抽象函数getLayout() 和 getViewHolder() 函数,再实现下 UI 和数据的绑定关系即可。

Presenter层

public class FunPicPresenter extends BaseScrollPresenter<FunPicFragment, Data>{

  @Override protected Observable<ArrayList<Data>> loadPageRequest() {
    return App.getServerAPI().getFunPic(getSendToken(), mPage);
  }
}

实现下loadPageRequest() 函数,返回网络请求结果就行。

这样就完成了一个页面。

以下是我的应用中的几个列表页面,都是用这个方式实现的,看看效果图:
这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

总结

使用我们封装好的基类,子类只需要再实现两三个函数,简单的几行代码,就可以实现列表页面的下拉刷新和上拉加载下一页的功能了。不同的页面,主要是要定义不同的 UI,以及UI和数据的关系,其他相同的处理都已经封装到基类中,非常方便。

猜你喜欢

转载自blog.csdn.net/kbkaaa/article/details/72598828
今日推荐