最简单的 MVP 理解

前言

一个好的软件总是离不开好的架构,不管是前端后端。
在Android中,我已知的设计模式有:MVC,MVP、MVVM、Clean,其中各自的优劣不再这里展开,有需要的自行Google。
这里探讨一下MVP,在很多的文章中,都讲很多的概念性的东西,时常把人讲的云里雾里,对于刚接触的人,就算是理解了,怎么实际应用都不知道。
因此本文就用最简单最常见的来介绍MVP,其实架构是一种很活的东西,谁说你必须使用某种模式?谁规定代码一定要这么写才是对的?我认为只有在变化中能不断适应的,才是王道。难道后来你会了另一种模式,就不能在已有的项目中应用了吗?
我认为只要是你逻辑清晰,分层合理,你想怎么玩都行,甚至不用任何所谓的模式,注意:前提是分层一定要清晰,层与层之间的界限要清晰明了。

先不管概念,来一段简单的代码先

需求:用户输入账号密码,点击登录按钮进行登录。

代码如下:注意,只是作为示范用。有所删减,看得懂意图就好。

activity_login.xml:
如图,xml 的代码就不贴了,很简单。

LoginActivity.java:

public class LoginActivity
        extends AppCompatActivity
{
    private EditText etAccount;
    private EditText etPwd;

    @Override
    protected void onCreate(
            @Nullable
                    Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        etAccount = (EditText) findViewById(R.id.et_account);
        etPwd = (EditText) findViewById(R.id.et_pwd);
    }

    // 响应登录按钮
    public void onLogin(View view)
    {
        String account = etAccount.getText().toString();
        String pwd = etPwd.getText().toString();

        // TODO 这里省掉了空判断

        // 发起请求
        RequestParams params = new RequestParams();
        params.add("account", account);
        params.add("pwd", pwd);
        new AsyncHttpClient().get("url", params, new Login());
    }

    // 登录请求回调
    private class Login
            extends AsyncHttpResponseHandler
    {
        @Override
        public void onSuccess(int statusCode, Header[] headers, byte[] responseBody)
        {
            if (responseBody != null)
            {
                LoginResponse response = JSON.parseObject(new String(responseBody),
                        LoginResponse.class);
                if (response != null)
                {
                    if (response.getStatus() == 0)
                    {
                        Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();

                        // TODO 去到主界面之类的

                        // 然后结束掉登录
                        finish();
                    }
                    else
                    {
                        Toast.makeText(LoginActivity.this, "登录失败," + response.getMsg(),
                                Toast.LENGTH_SHORT).show();
                    }
                }
            }
            else
            {
                onFailure(statusCode, headers, null, null);
            }
        }

        @Override
        public void onFailure(int statusCode, Header[] headers, byte[] responseBody,
                              Throwable error)
        {
            Toast.makeText(LoginActivity.this, "登录失败,请检查网络", Toast.LENGTH_SHORT).show();
        }
    }
}

很简单吧?就是输入和发起登录。
其中网络库使用的是:
android-async-http
JSON解析使用的是:
FastJSON Android版本

分析,以上代码一共分为多少层?有什么缺陷?

  • UI层(View):界面的显示,控件的绑定和操作,用户的输入和操作,都属于UI层需要处理的。比如上述代码中的findViewById,按钮响应,Toast,跳转到其他Activity等操作。在Android中,Activity、Fragment都属于View。
  • 业务逻辑层(Presenter):登录请求的发起,结果的接收和处理,通知UI层界面更新,都属于业务逻辑的范围。比如上述代码中的请求发起和JSON解析,判断等。
  • 数据层(Model):去服务器请求数据,这里不只是云服务器的请求,数据库,文件,智能设备,任何数据源,只要是增删改查的,都属于数据层的工作。比如上述代码的网络库异步请求。

从分析来看,上述一个简单的需求实际上有三个层的存在,而却全部写在View中,对于新手来说,这样类似的代码是再正常不过了。
一般来说,简单的需求,项目小,这样写也不会造成什么问题的,但是一旦项目越来越大,并且需求改动也越来越多的时候,就成了一种灾难了,比如无休止的复制和粘贴。

举个例子:
现在项目中加入了启动页,要求在启动页判断先前是否已有用户登录过,如果有,则取出账号密码进行登录,登录成功去到主界面,失败则去到登录页;
如果没有,直接跳转到登录页。

再用上面的写法,也就是加个启动页,然后复制登录的那段代码,再改改回调处理的,听起来好像没事,但不觉得重复了吗?

使用MVP模式重写

先看一张类图


你肯定会说:什么?一个简单的功能,居然需要这么多类文件,这不是更加烦琐,工作量更加大了吗?
别急,继续看。下面我们就按上面分析的来写。

LoginResponse.java :JSON 解析需要的数据类

public class LoginResponse
{
    private int status;
    private String msg;

    ...省略掉 set/get
}

再来看两个 Base 类:

BasePresenter.java:

/**
 * 所有Presenter的父接口
 */
public interface BasePresenter
{
    // TODO 在这里可以声明一些Presenter的通用方法
}

BaseView.java:

/**
 * 所有View的父接口
 */
public interface BaseView
{
    // TODO 在这里可以声明一些View的通用方法
}

不知道定义两个 Base 是用来干嘛的,没关系,再来思考关于这个登录界面的两个问题:

  • 1.需要我们处理的用户操作有哪些?
  • 2.界面�显示相关的,需要我们做的有哪些?

针对以上问题,解答如下:

  • 1.只有登录需要我们处理,其他的诸如输入账号密码,点击按钮这种操作�不需要我们做。至于账号密码的空判断,已经包含在登录这个操作里了。
  • 2.点击登录按钮后,需要显示进度条登录成功需要显示成功或直接去到主页面之类的,登录失败需要隐藏进度条,�提示登录失败的原因之类的。

因此我们可以�把这些操作和显示都归类到一个地方,称为契约类(Contract)

LoginContract.java:

/**
 * 登录契约类,声明了View和Presenter该有的操作,方便管理
 */
public interface LoginContract
{
    // 定义界面中所有的 UI 状态
    interface View
            extends BaseView
    {
        void loginSuccess(); // 登录成功

        void loginFailure(String msg); // 登录失败

        void showLoading(boolean isShowLoading); // 是否显示加载中
    }

    // 定义了所有的用户操作
    interface Presenter
            extends BasePresenter
    {
        void login(String account, String pwd); // 登录
    }
}

定义契约类的目的是方便管理,也能理清你的逻辑。

好了,�以上都是准备工作,实际的 Model、View、Presenter 相关的具体类还没写。继续看。

Model:LoginRequest.java

public final class LoginRequest
{
    // 单例
    private LoginRequest()
    {
    }

    private static class SingletonHolder
    {
        private static final LoginRequest SINGLETON = new LoginRequest();
    }

    public static LoginRequest getInstance()
    {
        return SingletonHolder.SINGLETON;
    }

    public void login(String account, String pwd, final LoginCallback callback)
    {
        // 发起请求
        RequestParams params = new RequestParams();
        params.add("account", account);
        params.add("pwd", pwd);
        new AsyncHttpClient().get("url", params, new AsyncHttpResponseHandler()
        {
            @Override
            public void onSuccess(int statusCode, Header[] headers, byte[] responseBody)
            {
                callback.onSuccess(statusCode, responseBody);
            }

            @Override
            public void onFailure(int statusCode, Header[] headers, byte[] responseBody,
                                  Throwable error)
            {
                callback.onFailure(statusCode, responseBody, error);
            }
        });
    }

    // 对外暴露的接口
    public interface LoginCallback
    {
        void onFailure(int statusCode, byte[] responseBody, Throwable error);

        void onSuccess(int statusCode, byte[] responseBody);
    }
}

Model 类不负责逻辑的处理,只是负责增删改查,以及必要的保存住自己的状态,比如你这个 Model 表示一个智能开关设备,那么开关的状态你得保存起来,以便�状态改变的时候发出通知,以及外面的人来你这拿状态的时候,你得给人家正确的状态。
这里的 Model 表示登录,login 方法被调用后,将登录结果通过 LoginCallback 回传给调用者就完成了职责。

再来看 View。

View:�LoginActivity.java:

public class LoginActivity
        extends AppCompatActivity
        implements LoginContract.View // 实现了�契约类中的接口
{
    private EditText etAccount;
    private EditText etPwd;

    private LoginContract.Presenter loginPresenter;

    @Override
    protected void onCreate(
            @Nullable
                    Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        // 创建Presenter,使View和Presenter,Presenter和Model关联起来,这一步暂且忽略也可以,等看到 Presenter 了再回来看。
        loginPresenter = new LoginPresenter(this, LoginRequest.getInstance());

        etAccount = (EditText) findViewById(R.id.et_account);
        etPwd = (EditText) findViewById(R.id.et_pwd);
    }

    public void onLogin(View view)
    {
        String account = etAccount.getText().toString();
        String pwd = etPwd.getText().toString();

        // 告知Presenter发起登录
        loginPresenter.login(account, pwd);
    }

    @Override
    public void showLoading(boolean isShowLoading)
    {
        // 显示和隐藏进度条
    }

    @Override
    public void loginSuccess()
    {
        /*
        比如取消进度条,进入到主页面
         */
    }

    @Override
    public void loginFailure(String msg)
    {
        /*
        取消进度条,显示登录错误提示,比如密码错误、账号不存在之类的
         */
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }
}

可以看到,View 中没有任何的逻辑处理和数据获取,能做的��只是跟界面相关的操作,向�外界发出请求,以及�暴露给外界操作界面的方法。注意:View 中不能有任何的业务逻辑处理,只能有和 View 相关的操作。

可以看到,Model 和 View 是完全分开的,没有任何的直接关联。下一步,我们需要通过 Presenter 将他们关联起来。

Presenter:LoginPresenter.java

public class LoginPresenter
        implements LoginContract.Presenter
{
    // Presenter �持有 View 和 Model 的引用
    private LoginContract.View loginView;
    private LoginRequest loginRequest;

    public LoginPresenter(LoginContract.View loginView, LoginRequest loginRequest)
    {
        this.loginView = loginView;
        this.loginRequest = loginRequest;
    }

    @Override
    public void login(String account, String pwd)
    {
        // 账号密码不对的话,直接失败
        if (TextUtils.isEmpty(account.trim()) || TextUtils.isEmpty(pwd))
        {
            loginView.showLoading(false);
            loginView.loginFailure("账号密码不对");
            return;
        }

        loginView.showLoading(true);

        loginRequest.login(account, pwd, new LoginRequest.LoginCallback()
        {
            @Override
            public void onFailure(int statusCode, byte[] responseBody, Throwable error)
            {
                loginView.loginFailure("登录错误的提示信息");
            }

            @Override
            public void onSuccess(int statusCode, byte[] responseBody)
            {
                if (responseBody != null)
                {
                    LoginResponse response = JSON.parseObject(new String(responseBody),
                            LoginResponse.class);
                    if (response != null)
                    {
                        if (response.getStatus() == 0)
                        {
                            loginView.loginSuccess();
                        }
                        else
                        {
                            loginView.loginFailure("登录错误的提示信息");
                        }
                    }
                    else
                    {
                        loginView.loginFailure("登录错误的提示信息");
                    }
                }
                else
                {
                    loginView.loginFailure("登录错误的提示信息");
                }
            }
        });
    }
}

可以看到,所有的业务逻辑都在 Presenter 里面了。

看看以上的代码是不是符合下面这张图:

Model 和 View 是完全分离的,以上通过小实例目的是为了让大家理解并用起来,更复杂彻底的 MVP ,可以查看 Google 的官方 Sample:
googlesamples/android-architecture

还有一个开源项目:
android10/Android-CleanArchitecture

MVP 的好处

  • Model 只有一个,View 只有一个,而 Presenter 可以有多个,但是一个 View 至少对应一个 Presenter,还是那句话,架构是很灵活的,你都把 Model 和 View 分开了,低耦合已经实现了,怎么关联他们,你看着办咯。

  • 设计图出来了,接口还没好,你可以专注先写 View,完全不用管数据。设计图没好,接口好了,你可以先写 Model,测试接口是否正常,完全不用管 View 是如何设计的。等到都设计好了,�你再把 Model 和 View 关联起来专注写逻辑。是不是觉得无比的清爽?

  • 非常适合于大型的项目,但是要避免过度设计和正确的抽象。

MVP 的坏处

  • 类爆炸

结语:

本文的目的是让没有玩过 MVP 的设计快速入门的,�理解了以上内容,�进阶的内容可自己 Google,很多这方面的资料。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容