Android 当中的 MVP 模式(三)基于分页列表的封装
Android 当中的 MVP 模式(四)插曲-封装 OkHttp
Android 当中的 MVP 模式(五)封装之后的 OkHttp 工具在 Model 层的使用
Android 当中的 MVP 模式(六)View 层 Activity 的基类— BaseMvpActivity 的封装
摘要:在上一篇中对MVP模式进行了封装,然后通过封装之后的类,实现了一个网络请求,但是请求到网络数据之后,就直接展示到了 View
层,并没有其他的操作,然而我们在开发过程中, 经常会用到分页加载,一般在滑动控件向上滚动,加载更多事件触发是调用,并且这个过程设计到两个参数,一个是 PageIndex
:页码;一个是 PageSize
一页数据的大小, 分页加载就是通过在某一具体事件触发时,调用修改这两个或者一个参数,重新请求网络,从而拿到下一页的数据,这边文章还是基于MVP模式,对分页数据的请求进行封装。
presenter
层作为 MVP
模式的桥梁, 那就先从这一层开始说起吧。
Presenter
层的封装
上一篇中对 Presenter
层的公共方法进行了抽取并且封装成了一个接口 IBasePresenter
,那么现在我们需要实现分页加载还有刷新的功能,那么在 IBasePresenter
接口的基础之上,在对其封装一个接口 IBasePeginationPresenter
:
/**
* Created by fanyuzeng on 2017/10/23.
* Function:在IBasePresenter的基础上扩展的接口,适用于分页加载的情况
*/
public interface IBasePaginationPresenter<Param> extends IBasePresenter<Param> {
/**
* 刷新数据的接口
*
* @param param 访问服务器的参数
* @created at 2017/10/23 20:07
*/
void refresh(Param param);
/**
* 加载更多的接口
*
* @created at 2017/10/23 20:07
*/
void loadingNext();
/**
* 用于判断服务器端是否还有更多的数据
* @return true -还有更多数据 - false 没有更多的数据
*/
boolean hasMoreData();
}
也是一个泛型的接口,增加的三个方法 :
refresh(Param param)
在View
层调用,用于通知Model
层刷新数据loadingNext()
在View
层调用,用于通知Model
层加载下一页数据hasMoreData()
在Model
层请求网络数据前调用做判断,是否还有下一页数据
有了针对分页刷新的接口之后,还需要有一个实现它的基类:
/**
* @author:ZengFanyu
* @date:2017/10/20
*/
public abstract class BasePaginationPresenter<Param extends BasePeginationParam, Data> implements IBasePaginationPresenter<Param> {
private static final String TAG = "BasePaginationPresenter";
private IBaseModel mBaseModel;
private IBaseView mBaseListView;
private Param mParam;
private Class<Data> mClazz;
private Handler mHandler = new Handler(Looper.getMainLooper());
private boolean mHasMoreData=true;
/**
* 子类中调用,用于传递服务器返回的,处理好的结果
*
* @param data View层需要的数据类型
* @created at 2017/10/23 20:10
*/
public abstract void serverResponse(Data data);
/**
* 子类中调用,用于确认服务器端是否还有数据
*
* @return true-还有数据 false-没有数据
*/
public abstract boolean serverHaveMoreData();
public BasePaginationPresenter(IBaseView baseListView, Class<Data> Clazz) {
this.mBaseListView = baseListView;
mClazz = Clazz;
mBaseModel = new SohuAlbumModel(this);
}
@Override
public void refresh(Param param) {
requestServer(param);
}
@Override
public void loadingNext() {
if (mParam != null) {
int pageIndex = mParam.getPageIndex();
mParam.setPageIndex(pageIndex + 1);
requestServer(mParam);
}
}
@Override
public void requestServer(@Nullable Param param) {
mBaseListView.showProgress(true);
mParam = param;
Log.d(TAG, ">> requestServer >> ");
getModel().sendRequestToServer(param);
}
@Override
public void accessSuccess(String responseJson) {
mBaseListView.showProgress(false);
Gson gson = new Gson();
serverResponse(gson.fromJson(responseJson, mClazz));
mBaseListView.showSuccess(true);
}
@Override
public void cancelRequest() {
mBaseModel.cancelRequest();
}
@Override
public void okHttpError(final int errorCode, final String errorDesc, final String errorUrl) {
mHandler.post(new Runnable() {
@Override
public void run() {
mBaseListView.showOkHttpError(errorCode, errorDesc, errorUrl);
mBaseListView.showProgress(false);
mBaseListView.showSuccess(false);
}
});
}
@Override
public IBaseModel getModel() {
return mBaseModel;
}
@Override
public HashMap<String, String> getParams() {
return null;
}
@Override
public boolean hasMoreData() {
return ServerHaveMoreData();
}
}
- 在类申明时,可以看到 Param extends BasePeginationParam
,这里的 BasePeginationParam
主要是封装了摘要中提到的 PageIndex
和 PageSize
两个参数,以及他们的 Getter Seeter
方法。
- 重点看 IBasePeginationPresenter
中新增加的三个方法,refresh(Param param)
会重新调用一次 requestServer(Param param)
(此方法在上一篇也提过了,就是通知 Model
层获取数据);
- loadingNext()
,加载下一页数据的方法,就是将参数中的 PageIndex + 1
之后,重新调用 requestServer(Param param)
方法。此处只改变了页码,如果需要改变请求数据的条数,也是相应的在 loadingNext()
中修改 PageSize
的值。
- hasMoreData()
,这里返回抽象方法 serverhaveMoreData()
,这个方法是在子类中实现的,子类解析了数据之后,判断服务器是否还有数据返回。
然后有需要实现分页功能的 Presenter
就可以直接继承 BasePaginationPresenter
。
Model
层
由于 Model
层的职责比较单一,就是向数据源请求数据,并且返回给 Presenter
层,所以此处不需要额外封装接口或者是基类,只需要重新实现上一篇中提到的 IBaseModel
接口即可。
View
层
此处和请求一次数据相比较, View
层就是需要在两个事件触发的时候,重新设置参数通知 Presenter
去请求数据,然后再展示出来。这两个事件分别是:上拉到底时加载更多、下拉时刷新数据(当然可以别的)。
针对上一小节中封装类的具体实现
View
层的具体实现
主要是展示电视剧的主要信息,那么需要提供一个接口方法,给 Presenter
层调用,展示处理好的 JavaBean
:
/**
* 展示搜狐电视剧频道具体信息的接口
*
* @author:ZengFanyu
*/
public interface ISohuSerials extends IBaseView {
/**
* 展示搜狐视频API电视剧主要信息的方法
*
* @param videoList 处理好的VideoInfo集合
*/
void showAlbumMainInfo(List<VideoInfo> videoList);
}
此处的 VideoInfo
是一个JavaBean,对应的就是电视剧信息的实体类。
public class VideoInfo {
@SerializedName("main_actor")
private String mMainActor;
@SerializedName("total_video_count")
private int mTotalVideoCount;
@SerializedName("album_name")
private String mAlbumName;
@SerializedName("director")
private String mDirector;
@SerializedName("publish_time")
private String mPublishTime;
//Getter and setter methods
}
之前映射数据需要保证字段名和
Json
数据的字段名一致,其实本来把这个类的字段名改得一致就行啦,但是服务器端返回的数据字段,很多都是以“_”
进行连接,而不是使用驼峰命名法则,这个时候Gson
的@SerializedName
注解就派上用场了,注解中用服务器端返回值字段,成员变量仍然使用驼峰命名法。但是上个周末安装了最近
Alibaba 10 月 14 日
推出的Coding Guidelines
插件,发现代码中很多不规范的地方,并且人家规定了成员变量就必须要使用驼峰命名!所以我决定要按照这个插件的规范来写代码了,虽然现在进不了大厂,但是先熟悉大厂的代码规范也是好事,哈哈~ 咳咳,按照大厂的代码规范,成员变量的命名必须使用驼峰命名法!这个插件是真心好用,比如对类名要
javadoc
注释 参数、返回值、异常说明、此方法做什么事情、实现什么功能(领域模型相关命名除外,比如:DO、BO、DAO),并且是全中文的!直接在AS
的Inspection Results
窗口中显示,这IDE
内置功能啥时候讲过中文反馈结果的?广告时间结束,言归正传!
这个 Activity
实现了 ISohuSerials
接口,布局文件和上一篇一样,只是把 ListView
换成了自定义的 PullLoadRecyclerView
了,这个RecycyclerView
支持上拉加载更多和下拉刷新, 这里不展开说了。
/**
* @author:ZengFanyu
*/
public class SohuAlbumInfoActivity extends AppCompatActivity implements ISohuSerials {
private static final String TAG = "SohuAlbumInfoActivity";
private PullLoadRecyclerView mRecyclerView;
private Context mContext;
private ProgressBar mProgressBar;
private TextView mTip;
private RelativeLayout mContainer;
private AlbumPresenter mAlbumPresenter;
private BasePaginationParam mParam= new BasePaginationParam(1, 10);
private VideoInfoAdapter mAdapter;
Handler mHandler = new Handler(Looper.getMainLooper());
private boolean mIsFromRefresh = false;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_album_view);
mContext = this;
mAlbumPresenter = new AlbumPresenter(this, Album.class);
mContainer = (RelativeLayout) findViewById(R.id.id_success_content);
mTip = (TextView) findViewById(R.id.id_tip);
mProgressBar = (ProgressBar) findViewById(R.id.id_progress_bar);
mRecyclerView = (PullLoadRecyclerView) findViewById(R.id.id_recycler_view);
mRecyclerView.setLinearLayout();
mAdapter = new VideoInfoAdapter(mContext);
mAlbumPresenter.requestServer(mParam);
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.setOnPullLoadMoreListener(new PullLoadRecyclerView.OnPullLoadMoreListener() {
@Override
public void onRefresh() {
mIsFromRefresh = true;
mParam.setPageIndex(1);
mAlbumPresenter.refresh(mParam); //通知Presenter层刷新数据
mRecyclerView.setRefreshCompleted();
}
@Override
public void onLoadMore() {
mAlbumPresenter.loadingNext();
mRecyclerView.setLoadMoreCompleted(); //通知Presenter层加载下一页数据
}
});
}
@Override
public void showAlbumMainInfo(List<VideoInfo> albumList) {
if (mIsFromRefresh) {
mAdapter.cleanData();
mIsFromRefresh = false;
}
if (albumList != null && albumList.size() > 0) {
for (VideoInfo videoInfo : albumList) {
mAdapter.addData(videoInfo);
}
mHandler.post(new Runnable() {
@Override
public void run() {
mAdapter.notifyDataSetChanged();
}
});
}
}
@Override
public void showProgress(final boolean isShow) {
mHandler.post(new Runnable() {
@Override
public void run() {
if (isShow) {
mProgressBar.setVisibility(View.VISIBLE);
} else {
mProgressBar.setVisibility(View.GONE);
}
}
});
}
@Override
public void showOkHttpError(final int errorCode, final String errorDesc, final String errorUrl) {
mHandler.post(new Runnable() {
@Override
public void run() {
mTip.setText("http err:" + "errCode:" + errorCode + ",errDesc:" + errorDesc + ",errUrl:" + errorUrl);
}
});
}
@Override
public void showServerError(final int errorCode, final String errorDesc) {
mHandler.post(new Runnable() {
@Override
public void run() {
mTip.setText("server err:" + "errCode:" + errorCode + ",errDesc:" + errorDesc);
}
});
}
@Override
public void showSuccess(final boolean isSuccess) {
mHandler.post(new Runnable() {
@Override
public void run() {
if (isSuccess) {
mContainer.setBackgroundResource(android.R.color.white);
mTip.setText("Sohu Serials album");
} else {
mContainer.setBackgroundResource(R.color.colorAccent);
}
}
});
}
}
在上面代码中可以看到:
- 在
PullLoadRecycler.OnPullLoadMoreListenre
的onRefresh()
回调方法中,核心代码就是这一行mAlbumPresenter.refresh(mParam);
,通知Presenter
层去刷新数据, 至于Presenter
层如何刷新。。 关我View
层 X 事~ - 在
PullLoadRecycler.OnPullLoadMoreListenre
的onLoadMore()
回调方法中,也是直接调用mAlbumPresenter.loadingNext()
。
下面说说 Presenter
层的代码
Presenter
层的具体实现
/**
* @author:ZengFanyu
* Function:
*/
public class AlbumPresenter extends BasePaginationPresenter<BasePaginationParam, Album> {
private ISohuSerials mBaseListView;
private Handler mHandler = new Handler(Looper.getMainLooper());
private int mTotalCount;
public AlbumPresenter(ISohuSerials baseListView, Class<Album> CLazz) {
super(baseListView, CLazz);
this.mBaseListView = baseListView;
getModel().setRequestMethod(Constants.HTTP_GET_METHOD);
getModel().setRequestUrl(Constants.SOHU_SERIALS_URL);
}
@Override
public void serverResponse(Album album) {
mBaseListView.showAlbumMainInfo(album.getData().getVideos());
mHandler.post(new Runnable() {
@Override
public void run() {
mBaseListView.showProgress(false);
}
});
mTotalCount = album.getData().getCount();
}
@Override
public boolean serverHaveMoreData() {
//此处pageIndex是从1开始的, 实际使用需要注意pageIndex的起始值
int pageSize = mParam.getPageSize();
int pageIndex = mParam.getPageIndex();
return (pageIndex * pageSize) <= mTotalCount;
}
}
- 首先是要继承之前编写的
BasePaginationPresenter
类,泛型参数BasePaginationParam
可以根据实际需求进行拓展,基本使用在前面已经介绍过,此处不做赘述。 Album
是搜狐视频电视剧频道返回数据的实体类,上面提到的VideoInfo
包含在Album
里面,因为现在只需要展示VideoInfo
里的信息, 所以在serverRespomse
方法里,有一个转换mBaseListView.showAlbumMainInfo(album.getData().getVideos());
- 实现父类
BasePaginationPresenter
中的抽象方法serverHaveMoreData()
,思路就是 当前页面数 * 每一页的数据量,然后和 数据总量 比较大小。
Model
层的具体实现
1 /**
2 * @author:ZengFanyu
3 */
4 public class SohuAlbumModel<Param extends BasePaginationParam> implements IBaseModel<Param> {
5 private static final String TAG = "SohuAlbumModel";
6 private String url;
7 private int method;
8 private IBasePaginationPresenter mPaginationPresenter;
9
10 public SohuAlbumModel(IBasePaginationPresenter paginationPresenter) {
11 mPaginationPresenter = paginationPresenter;
12 }
13
14 @Override
15 public void sendRequestToServer(Param param) {
16 String validUrl = null;
17 if (param != null && !TextUtils.isEmpty(url)&&mPaginationPresenter.hasMoreData()) {
18 validUrl = getValidUrl(url, param);
19 Log.d(TAG, ">> sendRequestToServer >> " + "ValidUrl:" + validUrl);
20 }
21 Log.d(TAG,">> sendRequestToServer >> " + "check param,url and server have data or not!")
22 if (!TextUtils.isEmpty(validUrl)) {
23 HttpUtils.executeByGet(validUrl, new Callback() {
24 @Override
25 public void onFailure(Call call, IOException e) {
26 Log.d(TAG, ">> onFailure >> ");
27 e.printStackTrace();
28 mPaginationPresenter.okHttpError(Constants.URL_ERROR, e.getMessage(), url);
29 }
30
31 @Override
32 public void onResponse(Call call, Response response) throws IOException {
33 if (!response.isSuccessful()) {
34 Log.d(TAG, ">> onResponse >> " + "Not successful");
35 mPaginationPresenter.okHttpError(Constants.SERVER_ERROR, response.message(), url);
36 }
37
38 String responseJson = response.body().string();
39 Log.d(TAG, ">> onResponse >> " + "responseJson:" + responseJson);
40 mPaginationPresenter.accessSuccess(responseJson);
41
42 }
43 });
44 } else {
45 Log.d(TAG, ">> sendRequestToServer >> " + "Valid Url is empty");
46 }
47 }
48
49 private String getValidUrl(String url, Param param) {
50 return String.format(url, param.getPageIndex(), param.getPageSize());
51 }
52
53
54 @Override
55 public void setRequestUrl(String url) {
56 this.url = url;
57 }
58
59 @Override
60 public void setRequestMethod(int method) {
61 this.method = method;
62 }
63
64 @Override
65 public void cancelRequest() {
66 HttpUtils.cancelCall();
67 }
68 }
Model
层的实现还是跟之前的一样,直接实现 IBaseModel
接口即可。
- 在
17
行可以看到,mPaginationPresenter.hasMoreData()
,这个就是对服务器点是否还有数据可以返回的判断,如果这里返回false
那么就不回去进行网络请求,然后在22
行打印个Log
提醒。 - 在看看
49
行的getVaildUrl
方法,这个方法主要就是把传进来的param
参数拼接进url
中,形成有效的,可以请求到数据的Url
。
效果图
Item
就展示了一下电视剧的 主演、名字、导演、集数、更新时间的信息。
小结
通过上面的封装和例子,起码证明了这一套封装能够跑的通了,以后如果还有关于分页请求的需求,可以直接继承上面的基类来实现,无非就是修改param
和 Data
两个泛型的参数。
- 前者是请求
url
的参数,根据具体的业务需求,封装BasePaginationParam
的子类即可。 - 后者是服务器端返回数据的实体类,也是根据数据的结构来封装的,在
Android Studio
中有Gson Formatter
这个插件,封装JavaBean
插件也轻松很多,在结合上面提到的Gson
注解,全套了。
下一篇准备封装一下
OkHttp
,然后将封装之后的OkHttp
整合到当前框架中,当然了,还是以分页接在为例
个人博客地址 :CODER FRAMER BIGZ