Android 基于 MVP 框架的下拉刷新、上拉加载页面,View和Presenter层基类封装
来源:互联网 发布:soap rest 知乎 编辑:程序博客网 时间:2024/05/20 23:57
前言
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 基于 MVP 框架的下拉刷新、上拉加载页面,View和Presenter层基类封装
- Android 中所有View的上拉加载下拉刷新
- android 打造真正的下拉刷新上拉加载recyclerview(四):自动加载和其他封装
- android 打造真正的下拉刷新上拉加载recyclerview(四):自动加载和其他封装
- 你想要的下拉刷新和上拉加载框架
- Android MVP设计框架模板 之 漂亮ListView上拉刷新下拉加载更多
- Android MVP设计框架模板 之 漂亮ListView上拉刷新下拉加载更多
- android下拉刷新和上拉加载更多的框架pulltoreflesh
- Android 实现RecyclerView的下拉刷新和上拉加载
- ListView封装实现下拉刷新和上拉加载
- RecyclerView封装--添加下拉刷新和上拉加载更多
- Android所有View通用下拉刷新上拉加载控件
- 自定义下拉刷新上拉加载View
- 实战MVP请求数据OKHttp封装RecyclerView上拉刷新下拉加载
- 微信小程序实现页面下拉刷新和上拉加载
- Android 下拉刷新 上拉加载更多框架实现
- android 打造真正的下拉刷新上拉加载recyclerview(三):下拉刷新上拉加载
- android 打造真正的下拉刷新上拉加载recyclerview(三):下拉刷新上拉加载
- Window下的Shell:PowerShell
- ARM学习笔记4
- QZOI被虐记
- python学习day1
- scala的数组、映射、元组和集合
- Android 基于 MVP 框架的下拉刷新、上拉加载页面,View和Presenter层基类封装
- 5. ESP8266固件的编译(RTOS SDK固件)
- 最长公共子序列 java 代码实现
- leetcode 268r
- 「游族杯」上海市高校程序设计邀请赛暨华东师范大学第九届 ECNU Coder 程序设计竞赛 (重现)
- java各种类型占字节数
- QML ListView悬浮标题栏
- Qt学习笔记1:初步认识控件
- Java重写与重载