Android当中的MVP模式(三)基于分页列表的封装

摘要:在上一篇中对MVP模式进行了封装,然后通过封装之后的类,实现了一个网络请求,但是请求到网络数据之后,就直接展示到了 View 层,并没有其他的操作,然而我们在开发过程中, 经常会用到分页加载,一般在滑动控件向上滚动,加载更多事件触发是调用,并且这个过程设计到两个参数,一个是 PageIndex :页码;一个是 PageSize 一页数据的大小, 分页加载就是通过在某一具体事件触发时,调用修改这两个或者一个参数,重新请求网络,从而拿到下一页的数据,这边文章还是基于MVP模式,对分页数据的请求进行封装。

presenter 层作为 MVP 模式的桥梁, 那就先从这一层开始说起吧。

Presenter

上一篇中对 Presenter 层的公共方法进行了抽取并且封装成了一个接口 IBasePresenter ,那么现在我们需要实现分页加载还有刷新的功能,那么在 IBasePresenter 接口的基础之上,在对其封装一个接口 IBasePeginationPresenter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 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();
}

也是一个泛型的接口,增加的三个方法 :

  1. refresh(Param param)View 层调用,用于通知 Model 层刷新数据
  2. loadingNext()View 层调用,用于通知 Model 层加载下一页数据
  3. hasMoreData()Model 层请求网络数据前调用做判断,是否还有下一页数据

有了针对分页刷新的接口之后,还需要有一个实现它的基类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/**
* @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主要是封装了摘要中提到的 PageIndexPageSize 两个参数,以及他们的 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 展示搜狐电视剧频道具体信息的接口
*
* @author:ZengFanyu
*/
public interface ISohuSerials extends IBaseView {
/**
* 展示搜狐视频API电视剧主要信息的方法
*
* @param videoList 处理好的VideoInfo集合
*/
void showAlbumMainInfo(List<VideoInfo> videoList);
}

此处的 VideoInfo 是一个JavaBean,对应的就是电视剧信息的实体类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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),并且是全中文的!直接在 ASInspection Results 窗口中显示,这 IDE 内置功能啥时候讲过中文反馈结果的?

《阿里巴巴Java开发规约》插件全球首发!

广告时间结束,言归正传!

这个 Activity 实现了 ISohuSerials 接口,布局文件和上一篇一样,只是把 ListView 换成了自定义的 PullLoadRecyclerView 了,这个RecycyclerView 支持上拉加载更多和下拉刷新, 这里不展开说了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/**
* @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.OnPullLoadMoreListenreonRefresh() 回调方法中,核心代码就是这一行 mAlbumPresenter.refresh(mParam); ,通知 Presenter 层去刷新数据, 至于 Presenter 层如何刷新。。 关我 View 层 X 事~
  • PullLoadRecycler.OnPullLoadMoreListenreonLoadMore() 回调方法中,也是直接调用 mAlbumPresenter.loadingNext()

下面说说 Presenter 层的代码

Presenter 层的具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* @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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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 就展示了一下电视剧的 主演、名字、导演、集数、更新时间的信息。

小结

通过上面的封装和例子,起码证明了这一套封装能够跑的通了,以后如果还有关于分页请求的需求,可以直接继承上面的基类来实现,无非就是修改paramData 两个泛型的参数。

  • 前者是请求 url 的参数,根据具体的业务需求,封装 BasePaginationParam 的子类即可。
  • 后者是服务器端返回数据的实体类,也是根据数据的结构来封装的,在 Android Studio 中有 Gson Formatter 这个插件,封装 JavaBean 插件也轻松很多,在结合上面提到的 Gson 注解,全套了。

下一篇准备封装一下 OkHttp ,然后将封装之后的 OkHttp 整合到当前框架中,当然了,还是以分页接在为例

共82.3k字
0%
.gt-container a{border-bottom: none;}