Android MVP 模型的使用

本文对比了Android中的MVC和MVP架构,详细解释了MVP的各个角色及其优势,通过一个DEMO展示了MVP的实现过程,强调了MVP在代码解耦和测试方面的优势,并提到了MVVM作为更新的架构选择。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一, 简述
现在大家都开始整 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: 业务逻辑和实体模型

其中的变化模型为:
MVC

MVP

图片来自 鸿洋大神的博客
原图地址这里,但是这篇文章貌似被博主删除了。
本篇博客参考鸿洋大神的博客后按照自己的理解写的一个实现。

三,一个简单的 DEMO
既然要消化 MVP 的思路,自然不能只是照抄一下而已。这里做一个下面这样的 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 实现思路主要参考鸿洋大神的这篇博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值