Android 基于 MVP 框架的下拉刷新、上拉加载页面,View和Presenter层基类封装
2017-05-21 15:52
751 查看
前言
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和数据的关系,其他相同的处理都已经封装到基类中,非常方便。相关文章推荐
- Android RecyclerView下拉刷新和上拉加载封装
- Android快速开发框架Android_BaseLib,集成了常用工具类,自定义View控件,Base基类封装,常用开源框架
- RxAndroid+Retrofit+GreenDao+MVP框架---通用基类封装(三)
- 基于开源框架Glide加载Gif资源图到Android ImageView中
- Android基于JsBridge封装的高效带加载进度的WebView
- 基于Android官方Paging Library的RecyclerView分页加载框架
- Android基于JsBridge封装的高效带加载进度的WebView
- Android基于JsBridge封装的高效带加载进度的WebView
- RxAndroid+Retrofit+GreenDao+MVP框架---通用基类封装(二)
- 使用MVP注册登录模块+封装的OKhttp,拦截器+QQ第三方登录+RecyclerView+SpringView上拉加载下拉刷新网络数据
- Android RecyclerView开源框架(下拉刷新、底部加载更多)
- 基于Android官方Paging Library的RecyclerView分页加载框架
- 基于Android官方Paging Library的RecyclerView分页加载框架
- 毕业设计之android混合模式开发第一天--具有下拉刷新和页面加载等待的WebView搭建
- Android当中的MVP模式(六)View 层 Activity 的基类--- BaseMvpActivity 的封装
- android快速开发框架--快速实现 页面 加载中 加载失败 无数据等状态以及下拉刷新和自动加载
- android 打造真正的下拉刷新上拉加载recyclerview(四):自动加载和其他封装
- android 打造真正的下拉刷新上拉加载recyclerview(四):自动加载和其他封装
- 基于RecyclerView的封装,仿qq侧拉删除效果,实现下拉刷新,上拉加载更多,添加header,添加footer
- 基于开源框架Glide加载Gif资源图到Android ImageView中