从天地初开到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等,可以减少很多代码。
好了,本篇博客就是这样,希望能帮到你。
下面是demo
如果你觉得本文对你有帮助,麻烦动动手指顶一下,算是对本文的一个认可,如果文中有什么错误的地方,还望指正,转载请注明转自喻志强的博客 ,谢谢!