Android当中的MVP模式(六)View 层 Activity 的基类--- BaseMvpActivity 的封装

摘要:使用封装之后的 MVP 模式实现一个新的界面,也就是 Vie 层,那么就需要去实现 IBaseView 接口,可能还需要针对当前要实现的界面情况,在 IBaseView 的基础之上派生出一个新的接口 IXxxView,之前的 SohuAlbumInfoActivity 用于展示搜狐电视剧主要信息的 View 就是这种情况,由 IBaseView 派生了一个 ISohuSerials ,再由 SohuAlbumInfoActivity 去实现,那么随着需要展示的界面越来越多,它们坐着大量重复的工作,我们就要像个方法来简化这个过程了。

回顾前几篇中 View 层的写法

根据MVP系列第二篇当中的分析, View 层的职责如下:

  1. Loading 状态的展示隐藏
  2. 接收 Presenter 层处理后的数据
  3. 接收 Presenter 层处理后的错误信息
  4. 接收 Presenter 层处理后的服务器拒绝信息

所以当就将着一些职责抽象成方法,放在 IBaseView 接口中,看看之前的的 IBaseView

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
/**
* Created by fanyuzeng on 2017/10/20.
* @author : ZengFanyu
*/
public interface IBaseView {
/**
* 进行耗时操作时的用户友好交互接口,比如显示ProgressBar
*
* @param isShow
* @author zfy
* @created at 2017/10/21/021 14:12
*/
void showProgress(boolean isShow);
/**
* 显示网络请求错的的接口
*
* @param errorCode
* @param errorDesc
* @param errorUrl
* @author zfy
* @created at 2017/10/21/021 14:14
*/
void showOkHttpError(int errorCode, String errorDesc, String errorUrl);
/**
* 现实服务器端请求错误的接口
*
* @param errorCode
* @param errorDesc
* @author zfy
* @created at 2017/10/21/021 14:14
*/
void showServerError(int errorCode, String errorDesc);
/**
* 请求成功或者失败之后,对应UI做出改变的接口
*
* @param isSuccess
* @author zfy
* @created at 2017/10/21/021 14:15
*/
void showSuccess(boolean isSuccess);

View 层每需要添加一个类, View 层的对象都需要在它的基础上去实现,比如说,在 MVP系列第三篇中,需要对搜狐视频电视剧频道的主要信息做分页展示,当时是在 IBaseView 的基础上派生出了一个 ISohuSerials

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);
}

然后再使用 SohuAlbumInfoActivity 去实现这个接口,对 IBaseViewISohuSerials 中的方法又做了一遍实现,但是这次的实现过程,跟MVP系列第二篇中的 LatestNewsTitleActivity 实现的功能几乎一致,并且这个时候,我就意识到 IBaseView 接口设计的缺陷,我们在 IBaseView 的基础上派生出 ISohuSerials 接口 ILatestNewsView 接口,无非就是要展示不同类型的数据,那这个功能完全可以整合进 IBaseView 接口中,至于不同页面的数据类型不同,我们完全可以使用泛型来解决。

下面就来解决这两个问题:

  1. 将展示 Presenter 层实例好的数据的方法,由派生接口整合至基类接口中,使用泛型解决数据类型不同的问题。
  2. 封装 BaseMvpActivity,实现共有逻辑,子类不重复处理 View 层基类接口(IBaseView)中的方法。

IBaseView 的重构

再回顾一下,之前要展示知乎日报的最新消息的标题内容,我写了一个 ILatestNewsView 接口,它长这样:

1
2
3
4
5
public inerface ILatestNewsView extends IBaseView {
void showLatestNewsTitle(List<String> titles)
}

后来又需要展示搜狐电视剧主要信息,于是写了一个 ISohuSerials ,:

1
2
3
4
5
public interface ISohuSerials extends IBaseView {
void showAlbumMainInfo(List<VideoInfo> videoList);
}

当时怎么想的,要整个这接口出来 - - !

现在把他们都整合进 IBaseView

1
2
3
4
5
6
7
8
9
10
public interface IBaseView<Data> {
//省略代码
/**
* 显示presenter层处理好之后的数据
* @param data data
*/
void showDataFromPresenter(Data data);

此处添加了一个泛型 Data ,它就可以用于指代上面两个接口中的 List<String> titlesList<VideoInfo> videoList ,或者是其他的数据了类型,然后在实现接口的类中去指明参数的类型就可以动态的更改它的类型了。

BaseMvpActivity 的封装

上述是对之前遗留问题的一个解决,从这儿开始才正式对基类 BaseMvpActivity 进行封装。

ToolBar 的统一处理

首先, Demo 是在 API 25 ,所以对 ToolBar 也要有良好的支持,所以首先是对 ToolBar 的封装,将 ToolBar 写到一个单独的 Layout 文件之中,方便其他文件引用。top_action_bar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<android.support.design.widget.AppBarLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<!-- android:popupTheme 用于自定义弹出的菜单的样式-->
<android.support.v7.widget.Toolbar
android:id="@+id/id_tool_bar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:titleTextColor="#ffffff"
>
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>

BaseMvpActivity 中的统一处理如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected void setSupportActionBar() {
if (mToolbar != null) {
setSupportActionBar(mToolbar);
}
}
protected void setActionBarIcon(int resId) {
if (mToolbar != null) {
mToolbar.setNavigationIcon(resId);
}
}
protected void setSupportArrowActionBar(boolean isSupport) {
getSupportActionBar().setDisplayHomeAsUpEnabled(isSupport);
}

这样处理了之后,在子类当中,就可以直接调用上述方法,就可以使用 ToolBar了, 当然,对 ToolBar 的自定义需要另外去处理。

BaseMvpActivity 的布局文件的处理

由于 BaseMvpActivity 是要作为 MVP 模式下,所有 View 层的基类,所以它自己需要有布局文件,将 IBaseView 中的接口实现,
activity_base_mvp

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
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include
layout="@layout/top_action_bar"/>
<TextView
android:id="@+id/id_tip_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:textSize="16sp"
android:text="tip"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/id_content_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<ProgressBar
android:id="@+id/id_progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="gone"/>
</RelativeLayout>
</LinearLayout>
  • 7 行的 include 文件,就是对上面 top_action_bar 的引用。
  • 10 行的 TextView 这里用来实现 IBaseViewshowOkHttpErrorshowServerError接口的。
  • 22 行的 FrameLayout 很重要,看 id 就知道了,它是用于展示子类页面的方法的,直接将子类的布局文件给 add 进来。类似于:

    1
    2
    3
    4
    View contentView = LayoutInflater.from(this).inflate(R.layout.activity_album_view, null);
    FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
    mContentContainer.addView(contentView, lp);
  • 27 行的 ProgressBar 就是用于实现 IBaseViewshowProgress 的。

BaseMvpActivity 对 IBaseView 的实现

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
/**
* @author:fanyuzeng
* @date: 2017/10/30 13:50
* @desc:
*/
public abstract class BaseMvpActivity<Data> extends AppCompatActivity implements IBaseView<Data> {
private static final String TAG = "BaseMvpActivity";
protected Toolbar mToolbar;
protected ProgressBar mProgressBar;
protected TextView mTipView;
protected FrameLayout mContentContainer;
protected Handler mHandler = new Handler(Looper.getMainLooper());
protected Context mContext;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_base_mvp);
mContext = this;
mToolbar = bindViewId(R.id.id_tool_bar);
mProgressBar = bindViewId(R.id.id_progress_bar);
mTipView = bindViewId(R.id.id_tip_content);
mContentContainer = bindViewId(R.id.id_content_container);
beforeInitViews();
initViews();
afterInitViews();
}
protected <T extends View> T bindViewId(int resId) {
return (T) findViewById(resId);
}
//统一处理ToolBar
@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() {
mTipView.setText("errorCode:" + errorCode + ",errorDesc:" + errorDesc + ",errorUrl:" + errorUrl);
}
});
}
@Override
public void showServerError(final int errorCode, final String errorDesc) {
mHandler.post(new Runnable() {
@Override
public void run() {
mTipView.setText("errorCode:" + errorCode + ",errorDesc:" + errorDesc);
}
});
}
@Override
public void showSuccess(final boolean isSuccess) {
mHandler.post(new Runnable() {
@Override
public void run() {
if (isSuccess) {
mContentContainer.setBackgroundResource(android.R.color.white);
} else {
mContentContainer.setBackgroundResource(R.color.colorAccent);
}
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
break;
default:
break;
}
return super.onOptionsItemSelected(item);
}
/**
* 子类实现,用于初始化控件
*/
protected abstract void initViews();
/**
* 子类实现 在初始化控件之后进行的操作
*/
protected abstract void afterInitViews();
/**
* 子类实现, 在初始化控件之前的操作
*/
protected abstract void beforeInitViews();
}

做了几点事情

  1. 实现了 IBaseView 中的接口
  2. ToolBar 做统一处理
  3. findViewById方法处理
  4. Menu Item 中返回按键的处理

还有一个问题,似乎少了一个方法?就是在上一小节中,整合进 IBaseView 接口中的 void showDataFromPresenter(Data data) ,还没有实现。


由于这里的 BaseMvpViewabstract 的,所以它可以不实现,也实现不了,因为实现这方法需要知道泛型参数 Data 的具体类型,所以这个函数是留给子类去实现的。

上面三个抽象方法也很好理解,就是用于子类初始化操作的,并且都在基类初始化之后才执行,这一点很重要,因为子类中是需要将布局文件给 add 到基类布局当中的,所以基类的组件也必须提前初始化好。

下面就看看子类中是如何处理的。

SohuAlbumInfoActivity 的重构

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
/**
* @author:ZengFanyu
* Function:
*/
public class SohuAlbumInfoActivity extends BaseMvpActivity<List<VideoInfo>> {
private static final String TAG = "SohuAlbumInfoActivity";
private PullLoadRecyclerView mRecyclerView;
private AlbumPresenter mAlbumPresenter;
private BasePaginationParam mParam = new BasePaginationParam(1, 10);
private VideoInfoAdapter mAdapter;
private boolean mIsFromRefresh = false;
// private View mContentView;
@Override
protected void beforeInitViews() {
mRecyclerView = new PullLoadRecyclerView(this);
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
mContentContainer.addView(mRecyclerView, lp);
// View contentView = LayoutInflater.from(this).inflate(R.layout.activity_album_view, null);
// FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
// mContentContainer.addView(contentView, lp);
}
@Override
protected void initViews() {
setSupportActionBar(); //表示当前页面支持ActionBar
setTitle(TAG);
setSupportArrowActionBar(true);
mAlbumPresenter = new AlbumPresenter(this, Album.class);
mTipView.setText(TAG);
// mRecyclerView = (PullLoadRecyclerView)mContentView.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);
mRecyclerView.setRefreshCompleted();
}
@Override
public void onLoadMore() {
mAlbumPresenter.loadingNext();
mRecyclerView.setLoadMoreCompleted();
}
});
}
@Override
protected void afterInitViews() {
}
@Override
public void showDataFromPresenter(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();
mTipView.setText(TAG);
}
});
}
}
}
  • 15 行的 beforeInitViews 方法,就是用于初始化子类的布局的,由于这个子类布局比较简单, 就是一个 RecyclerView ,所以可以直接用代码实现,然后给 add 进父类的 mContentContainer,或者用下面注释掉的,常规尝试来实现。
  • 5 行,泛型参数为 List<VideoInfo> ,这个参数就是用于上面提到的,未实现的方法当中的,指定了泛型参数的类型。
  • 26 行的 initViews 方法就用户初始化子类的 View
  • showDataFromPresenter 的写法和未封装之前是一样的。

LatestNewsTitleActivity 的重构

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
/**
* @author ZengFanyu
*/
public class LatestNewsTitleActivity extends BaseMvpActivity<List<String>> {
private ListView mListView;
private LatestNewsPresenter mBasePresenter;
LatestNewsAdapter mAdapter;
private View mContentView;
@Override
protected void beforeInitViews() {
mContentView = LayoutInflater.from(this).inflate(R.layout.activity_latest_news, null);
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
mContentContainer.addView(mContentView, lp);
}
@Override
protected void initViews() {
mBasePresenter = new LatestNewsPresenter(this, LatestNews.class);
mTipView.setText(LatestNews.class.getSimpleName());
mListView = (ListView) mContentView.findViewById(R.id.id_list_view);
Button btnLatestNews = (Button) mContentView.findViewById(R.id.id_btn_latest_news);
btnLatestNews.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mBasePresenter.requestServer(null);
}
});
}
@Override
protected void afterInitViews() {
}
@Override
public void showDataFromPresenter(List<String> titles) {
if (mAdapter != null) {
mAdapter.clear();
mAdapter = null;
}
mAdapter = new LatestNewsAdapter(titles, mContext);
mListView.setAdapter(mAdapter);
mAdapter.notifyDataSetChanged();
mTipView.setText(LatestNews.class.getSimpleName());
}
}

写法和上面一样,但是比起之前的代码量来说,已经少了很多了,并且对比这两个子类,都没有重复的实现方法,只专注于自己需要实现的逻辑。

还有其他的 View 层类和上述的实现过程类似,此处不再赘述。

下一篇这个系列的最后一篇准备些关于 MVP 模式在开发中使用,随着项目的复杂程度的提高, Presenter 会越来越臃肿的问题的解决思路。

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