一, 简述
现在大家都开始整 mvvm 了,我才开始弄 mvp, 不得不说,真是老掉牙了。然而,相对于 MVC 而言,MVP 在安卓设计中确实是一个很大的进步。
二, MVC 和 MVP 对比
传统的 Android MVC 的角色分布:
- View: xml 布局文件
- Model:业务逻辑和实体模型
- Controller:Activity
然而以 xml 格式做的 View 能做的事情实在太少,所以最终都委托到了 activity 上去了,结果臃肿的 activity 就被很多人的诟病了。
MVP 结构则更改了这样的一个现象,虽然代码量增加了
MVP 角色分布:
- View: Activity, 显示,用户交互
- Presenter: 负责 View 与 Model 的交互
- Model: 业务逻辑和实体模型
其中的变化模型为:
到
图片来自 鸿洋大神的博客
原图地址这里,但是这篇文章貌似被博主删除了。
本篇博客参考鸿洋大神的博客后按照自己的理解写的一个实现。
三,一个简单的 DEMO
既然要消化 MVP 的思路,自然不能只是照抄一下而已。这里做一个下面这样的 Demo
下面是结构图:
首先,我们看看 Model
Bean 结构:
package me.leo.mvp.bean;
public class News {
private int id;
private String title;
private String abstracts;
public String getAbstracts() {
return abstracts;
}
public String getTitle() {
return title;
}
public void setAbstracts(String abstracts) {
this.abstracts = abstracts;
}
public void setTitle(String title) {
this.title = title;
}
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}
}
按照一般的浏览模式,需要有下拉刷新和上滑刷新,分别对应 update 和 reqMore, 对应网络请求这里同样需要一个回调接口了。
public interface INewsBiz {
void update(int newsId, OnMoreNewsListener moreNewsListener);
void reqMore(int newsId, OnMoreNewsListener moreNewsListener);
}
public interface OnMoreNewsListener {
void updateSuccess(List<News> newsList);
void reqSuccess(List<News> newsList);
void loadFailed(String errInfo);
}
那么 Biz 的具体实现就用 Thread.sleep 来模拟了, 在 update 的时候和 req 的时候根据所给定的 ID 生成其后面的和前面的 News, 到 1030 的时候 告诉 Presenter 没有了。
public class NewsBiz implements INewsBiz {
private final static int NEWS_NUM = 10;
private final static int NEWS_ID_START = 1000;
private final static int NEWS_ID_TO_FAILED = 1030;
@Override
public void reqMore(final int newsId, final OnMoreNewsListener moreNewsListener) {
new Thread(new Runnable() {
@Override
public void run() {
try
{
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
List<News> list = new ArrayList<>(NEWS_NUM);
int end = newsId - NEWS_NUM - 1;
for(int i = newsId -1; i > end; i--){
News news = new News();
news.setId(i);
news.setTitle("news title " + i);
news.setAbstracts("new abstract " + i);
list.add(news);
}
moreNewsListener.reqSuccess(list);
}
}).start();
}
@Override
public void update(final int newsId, final OnMoreNewsListener moreNewsListener) {
new Thread(new Runnable() {
@Override
public void run() {
try
{
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int start;
int end;
if (newsId == NEWS_ID_TO_FAILED){
moreNewsListener.loadFailed("没有啦~~~");
return;
}
if (newsId == NEWS_ID_INVALID){
end = NEWS_ID_START - NEWS_NUM;
start = NEWS_ID_START;
}else {
end = newsId;
start = end + NEWS_NUM;
}
List<News> list = new ArrayList<>(NEWS_NUM);
for(int i = start; i > end; i--){
News news = new News();
news.setId(i);
news.setTitle("news title " + i);
news.setAbstracts("new abstract " + i);
list.add(news);
}
moreNewsListener.updateSuccess(list);
}
}).start();
}
}
根据下拉刷新和上滑刷新,我们对应的 View 需要这样的一些功能
public interface INewsMoreView {
int NEWS_ID_INVALID = -1;
/**
* @return 下拉刷新时当前最新的 ID
*/
int getNewsId();
/**
* @return 上滑查看以前的时候的最老的 ID
*/
int getLastNewsId();
/**
* 显示刷新状态
*/
void showLoading();
/**
* 刷新完成后结束状态
*/
void hideLoading();
/**
* 下拉刷新成功
* @param newsList 下拉刷新成功后得到的数据
*/
void updateSuccess(List<News> newsList);
/**
* 上滑刷新成功
* @param newsList 上滑刷新成功后得到的数据
*/
void reqSuccess(List<News> newsList);
/**
* 显示刷新失败的原因
* @param errInfo 刷新失败的原因
*/
void showFailed(String errInfo);
}
Model 和 View 都有了,该我们的 Presenter 上场了,这里对于具体的 Presenter 就有三个事儿要做了,第一个,初始化,第二个,下拉,第三个,上滑,那么实现起来也挺简单的嘛。
public class MoreNewsPresenter implements OnMoreNewsListener{
private INewsMoreView newsView;
private INewsBiz newsBiz;
private Handler mHandler;
/**
* 实例化 Biz 内容,接入 View, 获取 Main Looper 来更新 View
* @param newsView View
* @param mainLooper View 对应的 Looper
*/
public MoreNewsPresenter(INewsMoreView newsView, Looper mainLooper){
this.newsView = newsView;
this.mHandler = new Handler(mainLooper);
this.newsBiz = new NewsBiz();
}
public void update(){
newsView.showLoading();
newsBiz.update(newsView.getNewsId(), this);
}
public void init(){
newsView.showLoading();
newsBiz.update(INewsMoreView.NEWS_ID_INVALID, this);
}
public void reqMore(){
newsView.showLoading();
newsBiz.reqMore(newsView.getLastNewsId(), this);
}
@Override
public void loadFailed(final String errInfo) {
mHandler.post(new Runnable() {
@Override
public void run() {
newsView.showFailed(errInfo);
newsView.hideLoading();
}
});
}
@Override
public void updateSuccess(final List<News> newsList) {
mHandler.post(new Runnable() {
@Override
public void run() {
newsView.updateSuccess(newsList);
newsView.hideLoading();
}
});
}
@Override
public void reqSuccess(final List<News> newsList) {
mHandler.post(new Runnable() {
@Override
public void run() {
newsView.reqSuccess(newsList);
newsView.hideLoading();
}
});
}
}
这样,除了具体的 View 已经全完了,那么我们就简单实现一下 View 吧。要下拉刷新,用 Google 官方的吧,简单好用,最重要的还是好看,那么就有了下面这样的一个主界面了:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="me.leo.mvp.ui.MainActivity">
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/news_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ListView
android:id="@+id/news_list"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.v4.widget.SwipeRefreshLayout>
</RelativeLayout>
对应 ListView 肯定有一个 Item 项,这里就放了两个 TextView 进去了。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="me.leo.mvp.ui.MainActivity">
<TextView
android:id="@+id/news_title"
android:textSize="28sp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/news_abstract"
android:textSize="20sp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
嗯, xml 写出来了,先把 adapter 准备好吧,我不喜欢 simple adapter 应该没人打我。
public class NewsAdapter extends BaseAdapter{
private List<News> list = new ArrayList<>();
private LayoutInflater mInflater;
public NewsAdapter(Context context){
this.mInflater = LayoutInflater.from(context);
}
@UiThread
public void update(List<News> list){
this.list.addAll(0, list);
notifyDataSetChanged();
}
@UiThread
public void addMore(List<News> list){
this.list.addAll(list);
notifyDataSetChanged();
}
@Override
public int getCount() {
return list.size();
}
@Override
public Object getItem(int position) {
return list.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null){
convertView = mInflater.inflate(R.layout.news_item, parent, false);
holder = new ViewHolder(convertView);
}else{
holder = (ViewHolder) convertView.getTag();
}
News news = list.get(position);
holder.mTitle.setText(news.getTitle());
holder.mAbstract.setText(news.getAbstracts());
return convertView;
}
private static class ViewHolder{
TextView mTitle;
TextView mAbstract;
ViewHolder(View v){
this.mTitle = v.findViewById(R.id.news_title);
this.mAbstract = v.findViewById(R.id.news_abstract);
v.setTag(this);
}
}
}
那么就剩 activity 了
public class MainActivity extends AppCompatActivity implements INewsMoreView,
SwipeRefreshLayout.OnRefreshListener, AbsListView.OnScrollListener{
private SwipeRefreshLayout mSwipeLayout;
private NewsAdapter mAdapter;
private MoreNewsPresenter mPresenter;
private ListView mNewsList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Point p = new Point();
getWindowManager().getDefaultDisplay().getSize(p);
Log.e("MainActivity", p.toString());
mAdapter = new NewsAdapter(this);
mPresenter = new MoreNewsPresenter(this, getMainLooper());
mSwipeLayout = (SwipeRefreshLayout)findViewById(R.id.news_container);
mSwipeLayout.setOnRefreshListener(this);
mNewsList = (ListView)findViewById(R.id.news_list);
mNewsList.setAdapter(mAdapter);
mNewsList.setOnScrollListener(this);
mPresenter.init();
}
@Override
public void reqSuccess(List<News> newsList) {
mAdapter.addMore(newsList);
}
@Override
public void updateSuccess(List<News> newsList) {
mAdapter.update(newsList);
}
@Override
public int getLastNewsId() {
if (mAdapter.getCount() == 0){
return INewsMoreView.NEWS_ID_INVALID;
}
return ((News)mAdapter.getItem(mAdapter.getCount()-1)).getId();
}
@Override
public int getNewsId() {
if (mAdapter.getCount() == 0){
return INewsMoreView.NEWS_ID_INVALID;
}
return ((News)mAdapter.getItem(0)).getId();
}
@Override
public void showFailed(String errInfo) {
Toast.makeText(this, errInfo, Toast.LENGTH_SHORT).show();
}
@Override
public void showLoading() {
mSwipeLayout.setRefreshing(true);
}
@Override
public void hideLoading() {
mSwipeLayout.setRefreshing(false);
}
@Override
public void onRefresh() {
mPresenter.update();
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == SCROLL_STATE_IDLE){
int invisibleNum = mAdapter.getCount() - mNewsList.getLastVisiblePosition();
if (invisibleNum < 5 && !mSwipeLayout.isRefreshing()){
mPresenter.reqMore();
}
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {}
}
是的,用了这个 MVP 之后,代码就开始像简单的堆砌了。虽然代码量多了很多,但是,不用动脑子真的好舒服。。。
四,小结
MVP 相对于原来的 MVC 代码量是增大了一些,但是各部分的职责更加单一,功能更加明确,比 MVC 更加解耦了。这种条件下,单元测试等测试方案更加顺利,极端的情况下甚至可以不在 android 条件下运行。
然而,听说新的 MVVM 比这个更好用,那么,下一篇,就看看 MVVM吧。
五,链接
本文 MVP 实现思路主要参考鸿洋大神的这篇博客