您的位置:首页 > 移动开发 > Android开发

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和数据的关系,其他相同的处理都已经封装到基类中,非常方便。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐