从天地初开到MVC再到MVP

从天地初开到MVC再到MVP

首先解释一下什么是“天地初开”,因为我实在是词穷了,想不到更好的词来表示我刚入行时写代码的状态。所谓的“天地初开”指的是我刚接触Android开发时,当时是没有所谓的架构的概念,什么解耦和啊,层次分明啊,单一职责啊之类的,当时完全是能把功能实现就万事大吉了,所以代码都是混在一起写的,就像是天地初开时的混沌状态一样,都是融合在一起的

其实网上关于MVC和MVP的文章有很多了,写的都很好,那我为什么还要写这篇博客呢?
俗话说,一千个读者就有一千个哈姆雷特。
关于Android方面的架构,每个人的观点也不一样,有的人就喜欢用MVC,虽然耦合度稍微高一些,但是用起来很舒服,而有的人觉得MVP更好,虽然产生的文件多一些,但是耦合度更低。
关于MVC好还是MVP好,这个我觉得也是仁者见仁,智者见智的。其实,适合自己的就是最好的。

这里写图片描述

好了,废话一大堆,不扯了,进入正题。

本篇博客主要是模拟实现一个登录的功能,从最基础的写法开始,然后过渡到mvc模式,然后一步一步的对mvc进行改良,最后过渡到mvp模式。最后大家根据自己的实际情况应该就能选择一个适合自己的写法了。


需求分析


首先我们来分析下需求,因为模拟的登录业务,所以比较简单。老司机略过即可。

首先是布局文件,很简单,就两个输入框和一个按钮。

<?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="com.yzq.mvpdemo.mvc1.Mvc1Activity">

    <EditText
        android:id="@+id/userNameEt"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入用户名" />

    <EditText
        android:id="@+id/pwdEt"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入密码" />

    <Button
        android:id="@+id/loginBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:text="登录" />
</LinearLayout>

大概长这样
这里写图片描述

界面搭好了,我们来梳理下逻辑。
逻辑很简单,大概就分三步
1.用户点击登录按钮的时候拿到输入框里的数据
2.判空过后通过网络请求登录接口(这里我用延时任务模拟)
3.拿到请求的结果后判断成功还是失败,成功就跳转到主页面,失败就给出提示。


天地初开

下面我们来看一下我刚入行时是怎么写的。写法很简单,就是所有的东西都在Activity中完成的,包括业务处理,界面的显示跳转之类的。下面来看代码。

package com.yzq.mvpdemo.chaos;

import android.os.Bundle;
import android.os.Handler;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import com.yzq.mvpdemo.LoginResultBean;
import com.yzq.mvpdemo.R;

import static com.yzq.mvpdemo.R.layout.activity_chaos;

/*
*
* 最简单粗暴但也是耦合度最高的写法
* 想当初我刚入行的时候就是这么写的
*
* */
public class ChaosActivity extends AppCompatActivity implements View.OnClickListener {


    private EditText mUserNameEt;//用户名
    private EditText mPwdEt;//密码
    private Button mLoginBtn;//登录按钮

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(activity_chaos);
        initView();
    }

    private void initView() {
        mUserNameEt = findViewById(R.id.userNameEt);
        mPwdEt = findViewById(R.id.pwdEt);
        mLoginBtn = findViewById(R.id.loginBtn);
        mLoginBtn.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            default:
                break;
            case R.id.loginBtn:
                String userName = mUserNameEt.getText().toString().trim();
                String pwd = mPwdEt.getText().toString().trim();
                if (TextUtils.isEmpty(userName) || TextUtils.isEmpty(pwd)) {
                    Toast.makeText(this, "用户名或密码不能为空", Toast.LENGTH_SHORT).show();
                    return;
                }
                login();
                break;
        }
    }

    /*登录操作*/
    private void login() {

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                /*模拟登录操作*/
                LoginResultBean resultBean = new LoginResultBean();
                if (resultBean.getCode() == 0) {
                    /*登录成功*/
                    Toast.makeText(ChaosActivity.this, resultBean.getMsg(), Toast.LENGTH_SHORT).show();
                }

            }
        }, 3000);
    }

    @Override
    public void onBackPressed() {
        super.onBackPressed();
        finish();
    }


}

这里写图片描述
一顿操作猛如虎,好了,完成了,看起来也很好嘛,没什么问题。
这么写确实没什么大问题,肯定也是能实现的,这种写法我们在Activity里既处理了登录逻辑,又负责了控制界面。
但是登录只是一个比较简单的业务,当你的某个Activity里业务逻辑很多的话,而你的逻辑和界面控制代码都写在Activity里的时候,Activity的代码就会很多,分分钟上千行代码,读起来就很费劲。想当初我就接手的一个项目就是这样,一个Activity里代码1500多行,改的我怀疑人生。而且这样写的耦合度很高,比如一般的APP会在启动时有个自动登录的功能,那这样的话我需要把登录的逻辑代码复制一遍到启动界面,很麻烦。

实际上这种写法我们也可以看作是mvc模式的一种

xml布局可以看作是view(视图)层
model(数据模型)层就是返回的数据LoginBean
Activity就是Controller(控制器)层。只不过这里的Activity既处理了业务逻辑有处理的视图逻辑

当我们写代码有一定的时间后,我们就不仅仅局限于只要实现功能就好,这个时候我们开始试着想解决一些问题
比如
1.activity在业务比较复杂的时候代码是在是太多了,代码都混在一起,不好维护,怎么能减少activity的代码,并且让代码逻辑更清晰呢?
2.登录的逻辑不只是在登录界面会调用,有时候在启动界面可能会有自动登录的逻辑,怎么能复用登录的业务逻辑呢?

对于这个例子我们暂时需要解决的就是这两个问题。下面,我们就来一步一步的解决问题。


MVC

Model:数据模型
View:视图
Controller:控制器

关于MVC的概念大家可以去网上百度一下,有很多。我这里就简单的介绍一下。

所谓的MVC就是根据需求将代码分层。每个层做自己该做的事。
M就是Model层,负责提供数据模型
V就是View层,就是我们的xml布局文件和Activity,负责界面相关的操作,比如显示弹窗,显示Toast,给控件赋值,跳转界面之类的
C就是Controller层,也就是俗称的控制层,主要是用来处理业务逻辑的


我个人觉得在Android上的mvc跟传统的mvc其实有点不一样。
还有就是我觉得架构是一种思想,并不是固定一层不变的,每个人的理解不一样,就可能出现多种表现形式。

例如
1.xml布局作为View层,activity作为控制层,model就是数据模型(一开始就是这种写法,缺点已经在上面指出了)

2.activity作为view层,新建一个类(xxxController)作为控制层,model就是数据模型。

我们先来看看这一种写法。

package com.yzq.mvpdemo.mvc1;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import com.yzq.mvpdemo.R;


/*
*
* 将Activity当做是View层
*
* */
public class MvcActivity extends AppCompatActivity implements View.OnClickListener {
    
    public EditText mUserNameEt;
    public EditText mPwdEt;
    private Button mLoginBtn;
    private MvcController controller;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvc);
        initView();
    }
    private void initView() {
        mUserNameEt = (EditText) findViewById(R.id.userNameEt);
        mPwdEt = (EditText) findViewById(R.id.pwdEt);
        mLoginBtn = (Button) findViewById(R.id.loginBtn);
        mLoginBtn.setOnClickListener(this);
        /*新建一个类作为controller*/
        controller = new MvcController(this);
    }
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            default:
                break;
            case R.id.loginBtn:
                controller.login();
                break;
        }
    }
}

我们在Activity创建了一个controller, controller = new MvcController(this);,并且把登录的逻辑丢给了controller处理。

我们来看看controller类

package com.yzq.mvpdemo.mvc1;

import android.os.Handler;
import android.text.TextUtils;
import android.widget.Toast;

import com.yzq.mvpdemo.LoginResultBean;

/**
 * Created by yzq on 2018/1/17.
 * 用来处理逻辑的控制层
 */

public class MvcController {

    private MvcActivity activity;

    public MvcController(MvcActivity activity) {
        this.activity = activity;
    }

    public void login() {
        /*拿到输入框的数据*/
        String userName = activity.mUserNameEt.getText().toString().trim();
        String pwd = activity.mPwdEt.getText().toString().trim();
        /*判空*/
        if (TextUtils.isEmpty(userName) || TextUtils.isEmpty(pwd)) {
            Toast.makeText(activity, "用户名或密码不能为空", Toast.LENGTH_SHORT).show();
            return;
        }

        /*模拟登录*/
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                /*模拟登录操作*/

                LoginResultBean resultBean = new LoginResultBean();//这里我可以看做是Model层
                if (resultBean.getCode() == 0) {
                    /*登录成功*/
                    Toast.makeText(activity, "登录成功", Toast.LENGTH_SHORT).show();
                }
            }
        }, 3000);
    }
}

可以看到,我们把activity传递过来了,然后我们的获取输入数据,判空,登录逻辑,甚至是连界面的一些显示提示信息的功能都在controller中完成了。其实这种写法我也用了很久,因为用起来确实简单粗暴,而且似乎也减少了activity中的代码。
但是慢慢的发现这种写法不符合单一职责的设计思想,因为controller既处理的业务,而且还处理了界面的逻辑。而且,这样做的话,Activity里需要被操作的控件就的修饰符就必须声明为public,这也是很不合理的地方。这样的controller也只能接收指定类型的Activity,代码还是不好复用。同样的,当业务变得复杂时会导致controller层的代码过多。

既然这样,我们来解决一下上面出现的问题。
首先,我们来解决一下单一职责的问题。这个就比较简单,上面的方式,我们是直接在controller中处理了界面显示的逻辑,那么我们不在controller中处理界面显示逻辑不就好了,我们可以在Activity中定义一些方法,然后在controller中通过activity调用界面相关的方法即可。

改良版本1

activity代码:

package com.yzq.mvpdemo.mvc.demo2;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import com.yzq.mvpdemo.R;



/*
*
* 将Activity当做是View层
*
* */
public class MvcActivity extends AppCompatActivity implements View.OnClickListener {

    privateEditText mUserNameEt;
    privateEditText mPwdEt;
    private Button mLoginBtn;
    private MvcController controller;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvc);
        initView();
    }
    private void initView() {
        mUserNameEt = (EditText) findViewById(R.id.userNameEt);
        mPwdEt = (EditText) findViewById(R.id.pwdEt);
        mLoginBtn = (Button) findViewById(R.id.loginBtn);
        mLoginBtn.setOnClickListener(this);

        /*新建一个类作为controller*/
        controller = new MvcController(this);
    }
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            default:
                break;
            case R.id.loginBtn:
                String userName = mUserNameEt.getText().toString().trim();
                String pwd = mPwdEt.getText().toString().trim();
                if (TextUtils.isEmpty(userName) || TextUtils.isEmpty(pwd)) {
                    Toast.makeText(this, "用户名或密码不能为空", Toast.LENGTH_SHORT).show();
                    return;
                }

                controller.login(userName,pwd);
                break;
        }
    }


    /*成功的回调*/
    public void onSuccess() {
        Toast.makeText(this,"登录成功",Toast.LENGTH_SHORT).show();
    }


}

可以看到,我们在activity中提供了一个onsuccess的方法,以便在controller中调用,同时,控件修饰符也不用再用public修饰了,取值的操作也放到了activity中了。
controller代码:

package com.yzq.mvpdemo.mvc.demo2;

import android.os.Handler;

import com.yzq.mvpdemo.LoginResultBean;
import com.yzq.mvpdemo.mvc.demo1.*;

/**
 * Created by yzq on 2018/1/17.
 * 用来处理逻辑的控制层
 */

public class MvcController {

    private MvcActivity activity;

    public MvcController(MvcActivity activity) {
        this.activity = activity;
    }

    public void login(String userName, String pwd) {
           /*模拟登录*/
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                /*模拟登录操作*/

                LoginResultBean resultBean = new LoginResultBean();//这里我可以看做是Model层

                if (resultBean.getCode() == 0) {
                    /*登录成功*/
                    activity.onSuccess();
                }
            }
        }, 3000);

    }
}

可以看到,controller只负责了处理业务,界面相关的操作交给了activity了。似乎解决了单一职责的问题。

但是,代码复用的问题还没有解决,因为controller中显式的持有了MvcActivity 类型的实例,也就是说controller和activity耦合在一起了,我没法在其他activity实例化controller并调用login方法。
下面我们来解决下这个问题。

改良版本2

既然我们想复用逻辑代码,那也就是说我们的controller不能只接收指定类型的activity了。
解决这个也很简单,我们可以定义一个接口用来描述登录视图是要处理那些逻辑,然后Activity实现这个接口即可,controller中接收的参数类型就不是固定类型的Activity了,而是实现了该接口的activity都可以。

文件结构

这里写图片描述

LoginView接口代码:

/**
 * Created by yzq on 2018/1/17.
 * 登录视图接口,定义了登录界面需要处理的功能
 */

public interface LoginView {

    void onSuccess();
}

controller代码


/**
 * Created by yzq on 2018/1/17.
 * 用来处理逻辑的控制层
 */

public class  MvcController {
    private LoginView view;
    public MvcController(LoginView view) {
        this.view = view;
    }
    public void login(String userName, String pwd) {
           /*模拟登录*/
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                /*模拟登录操作*/

                LoginResultBean resultBean = new LoginResultBean();//这里我可以看做是Model层

                if (resultBean.getCode() == 0) {
                    /*登录成功*/
                    view.onSuccess();
                }
            }
        }, 3000);

    }
}

这样一来我们的controller就和activity解耦了。

MvcActivity中的代码:


/*
*
* 将Activity当做是View层
*
* */
public class MvcActivity extends AppCompatActivity implements View.OnClickListener,LoginView {

    private EditText mUserNameEt;
    private EditText mPwdEt;
    private Button mLoginBtn;
    private MvcController controller;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvc);
        initView();
    }
    private void initView() {
        mUserNameEt = (EditText) findViewById(R.id.userNameEt);
        mPwdEt = (EditText) findViewById(R.id.pwdEt);
        mLoginBtn = (Button) findViewById(R.id.loginBtn);
        mLoginBtn.setOnClickListener(this);

        /*新建一个类作为controller*/
        controller = new MvcController(this);//这里的this仍然是当前类,只不过传到controller里向上转型了,我们就是通过这种方式来实现多态的
    }
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            default:
                break;
            case R.id.loginBtn:
                String userName = mUserNameEt.getText().toString().trim();
                String pwd = mPwdEt.getText().toString().trim();
                if (TextUtils.isEmpty(userName) || TextUtils.isEmpty(pwd)) {
                    Toast.makeText(this, "用户名或密码不能为空", Toast.LENGTH_SHORT).show();
                    return;
                }

                controller.login(userName,pwd);
                break;
        }
    }


    @Override
    public void onSuccess() {
        Toast.makeText(this, "登录成功", Toast.LENGTH_SHORT).show();
    }
}

我们也可以在其他Activity中这样使用

/*
* 启动界面
* 用来测试复用登录逻辑代码
* */
public class StartupActivity extends AppCompatActivity implements LoginView  {

    private MvcController controller;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_startup);
        controller = new MvcController(this);

        /*这里一般是从本地拿到之前登录时存储的数据*/
        String userName = "";
        String pwd = "";
        /*自动登录*/
        controller.login(userName, pwd);

    }

    @Override
    public void onSuccess() {
        Toast.makeText(this, "登录成功", Toast.LENGTH_SHORT).show();
    }
}

这样一来,上面代码复用的问题就解决了。

上面我也说了,架构是一种思想,表现出来的形式可以是多种。所以你可能会看到各种各样形式的mvc,其实他们的思想是一样的,也许只是起的名字不一样。
关于上面的代码,你完全可以接着改良,改到你觉得合适为止。比如,把登录逻辑也抽离出来,这个时候就有点mvp的意思了,所以下面直接介绍mvp


MVP

我们再来说说MVP,为什么有了MVC还要有MVP呢?
在传统的mvc中,model只是一个数据模型,能做的东西很少,大多数业务是在controller中处理的。
而在mvp中,model的概念不仅仅是数据模型了,model接管了之前controller的工作,主要是用来处理业务逻辑。
而presenter则充当一个协调者,用来协调view和model之间的交互。

这样的好处是**,解耦的更加彻底,层次更加分明。但同样带来的问题就是类文件会增加很多。**

网上MVP的介绍也很多。这里我还是做个简单的介绍

Model:用来处理业务逻辑
View:跟mvc一样,用来处理view相关的逻辑
Presenter:协调者,主要是用来协调model和view之间的联系

实际上改良过后的mvc已经有那么点mvp的味道了,下面我们来看代码就知道了。

我们先来看看结构

这里写图片描述

是的,你没看错,文件就是这么多。

先看代码:

view接口

/**
 * Created by yzq on 2018/1/11.
 * 登录的view接口
 * 用来定义LoginActivity视图层的处理逻辑   比如显示弹窗  登录成功
 */

public interface LoginView {

    void onSuccess();

    void onFailed();

}

Activity代码;


/*
*
* 以登录为例
* LoginActivity持有LoginPresenter的引用,并实现了LoginView接口
*
* */
public class LoginActivity extends AppCompatActivity implements View.OnClickListener, LoginView {
    private Button mLoginBtn;
    /*登录presenter*/
    private LoginPresenter presenter;
    private EditText mUserNameEt;
    private EditText mPwdEt;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        initView();
        presenter = new LoginPresenter(this);
    }

    private void initView() {
        mLoginBtn = (Button) findViewById(R.id.loginBtn);
        mLoginBtn.setOnClickListener(this);
        mUserNameEt = (EditText) findViewById(R.id.userNameEt);
        mPwdEt = (EditText) findViewById(R.id.pwdEt);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            default:
                break;
            case R.id.loginBtn:
                String userName = mUserNameEt.getText().toString();
                String pwd = mPwdEt.getText().toString();
                presenter.login(userName, pwd);
                break;
        }
    }

    @Override
    public void onSuccess() {
        Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_LONG).show();
    }

    @Override
    public void onFailed() {
        Toast.makeText(LoginActivity.this, "登录失败", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onBackPressed() {
        super.onBackPressed();
        finish();
    }

}

presenter接口


/**
 * Created by yzq on 2018/1/11.
 * 登录presenter接口
 */

public interface LoginPresenterInterface {

    void login(String userName,String pwd);

    void loginResult(boolean result);
}

presenter实现类

/**
 * Created by yzq on 2018/1/11.
 * 登录presenter实现类
 */

public class LoginPresenter implements LoginPresenterInterface {

    LoginView loginView;
    LoginModelImp model;

    public LoginPresenter(LoginView view) {
        this.loginView = view;
        model = new LoginModelImp(this);
    }


    @Override
    public void login(String userName, String pwd) {
        /*调用model的请求登录*/
        model.requestLogin(userName, pwd);
    }


    /*登录结果回调*/
    @Override
    public void loginResult(boolean result) {
        if (result) {
            loginView.onSuccess();
        } else {
            loginView.onFailed();
        }
    }
}

model接口

/**
 * Created by yzq on 2018/1/16.
 * 登录Model接口
 * 这里可以定义loginModel需要有哪些功能
 * 一般来说肯定会有个登录
 *
 */

public interface LoginModelInterface {

    /*请求登录*/
    void requestLogin(String userName,String pwd);
}

model实现类

/**
 * Created by yzq on 2018/1/11.
 * 登录model实现类
 */

public class LoginModelImp implements LoginModelInterface {

    private LoginPresenterInterface callback;
    public LoginModelImp(LoginPresenterInterface login) {
        this.callback = login;
    }


    @Override
    public void requestLogin(String userName, String pwd) {
        /*模拟登录操作*/
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                /*presenter*/
                callback.loginResult(true);

            }
        }, 3000);

    }


}

通过上面的代码可以看到,我们定义了一些接口,为什么要定义这些接口呢,其实是跟上面mvc一样的,是为了解耦和。
model层完全跟view层是分离的,他们之间是通过presenter来进行协调的。这样一来,如果我们需要在其他地方使用到登录的话,直接拿到presenter或model的实例就可以了。

这个时候你可能会想了,我写个登录有必要搞这么麻烦么。实话说,我第一次看的时候也是这样想的。
但是,注意了,但是!
这样做在某些场景下是值得的。比如,项目很大,业务逻辑比较复杂的时候。
多人开发,可以通过接口把数据类型约束好,然后分给不同的人开发不同的模块。

由于我做的项目都不是什么大项目,而且都是我一个人搞定的。所以也没办法说mvp有多么适合大项目,多么的适合多人开发。但是,通过mvc到mvp,确实可以发现mvp的结构更加清晰,分离的更加彻底。

而且我们可以通过其他技术来配合mvp使用,比如rxjava,Retrofit,dagger等,可以减少很多代码。

好了,本篇博客就是这样,希望能帮到你。

mvp过渡到mvvm(Android 架构组件)


下面是demo

CSDNDemo
githubDemo


如果你觉得本文对你有帮助,麻烦动动手指顶一下,算是对本文的一个认可,如果文中有什么错误的地方,还望指正,转载请注明转自喻志强的博客 ,谢谢!

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

喻志强(Xeon)

码字不易,鼓励随意。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值