MVC MVP和MVVM 学习摘要

文章目录

  • 1 MVC 模式
    • 1.1 实现
    • 1.2 步骤 1
    • 1.3 步骤 2
    • 1.4 步骤 3
    • 1.5 步骤 4
    • 1.6 步骤 5
    • 1.7 MVC的优缺点
      • 1.7.1 优点
      • 1.7.2 缺点
    • 1.8 框架和设计模式的区别
  • 2 MVP 模式
    • 2.1 实现步骤
    • 2.2 MVC和MVP
    • 2.3 优点
    • 2.4 缺点
  • 3 MVVM
    • 3.1 概述
    • 3.2 详谈MVVM
      • 3.2.1 关于Model
      • 3.2.2 关于ViewModel
      • 3.2.3 界面初始化
      • 3.2.4 触发登陆
      • 3.2.5 单向绑定与双向绑定
      • 3.2.6 在 ViewModel 中数据请求与处理
    • 3.3 实现步骤
      • 3.3.1 build.gradle 配置
      • 3.3.2 View布局
      • 3.3.3 ViewModel实现
      • 3.3.4 LoginActivity
    • 3.4 MVVM优点

1 MVC 模式

MVC 模式代表 Model-View-Controller(模型-视图-控制器) 模式。这种模式用于应用程序的分层开发。

MVC MVP和MVVM 学习摘要_第1张图片

  • Model(模型) - 模型代表一个存取数据的对象或 JAVA POJO,表示业务规则。它也可以带有逻辑,在数据变化时更新控制器。在MVC的三个部件中,模型拥有最多的处理任务。被模型返回的数据是中立的,模型与数据格式无关,这样一个模型能为多个视图提供数据,由于应用于模型的代码只需写一次就可以被多个视图重用,所以减少了代码的重复性
  • View(视图) - 视图代表模型包含的数据的可视化。
  • Controller(控制器) - 控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。控制器接受用户的输入并调用模型和视图去完成用户的需求,控制器本身不输出任何东西和做任何处理。它只是接收请求并决定调用哪个模型构件去处理请求,然后再确定用哪个视图来显示返回的数据。
    MVC MVP和MVVM 学习摘要_第2张图片

1.1 实现

我们将创建一个作为模型的 Student 对象。StudentView 是一个把学生详细信息输出到控制台的视图类,StudentController 是负责存储数据到 Student 对象中的控制器类,并相应地更新视图 StudentView

MVCPatternDemo,我们的演示类使用 StudentController 来演示 MVC 模式的用法。
MVC MVP和MVVM 学习摘要_第3张图片

1.2 步骤 1

创建模型。

Student.java

public class Student {
    private String rollNo;
    private String name;
    
    public String getRollNo() {
        return rollNo;
    }
    
    public void setRollNo(String rollNo) {
        this.rollNo = rollNo;
    }
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

1.3 步骤 2

创建视图。

StudentView.java

public class StudentView {
    public void printStudentDetails(String studentName, String studentRollNo){
        System.out.println("Student: ");
        System.out.println("Name: " + studentName);
        System.out.println("Roll No: " + studentRollNo);
    }
}

1.4 步骤 3

创建控制器。

StudentController.java

public class StudentController {
    private Student model;
    private StudentView view;
    public StudentController(Student model, StudentView view){
        this.model = model;
        this.view = view;
    }
    public void setStudentName(String name){
        model.setName(name);
    }
    public String getStudentName(){
        return model.getName();
    }
    public void setStudentRollNo(String rollNo){
        model.setRollNo(rollNo);
    }
    public String getStudentRollNo(){
        return model.getRollNo();
    }
    public void updateView(){
        view.printStudentDetails(model.getName(), model.getRollNo());
    }
}

1.5 步骤 4

使用 StudentController 方法来演示 MVC 设计模式的用法。

MVCPatternDemo.java

public class MVCPatternDemo {
    public static void main(String[] args) {
        //从数据库获取学生记录
        Student model  = retriveStudentFromDatabase();
        //创建一个视图:把学生详细信息输出到控制台
        StudentView view = new StudentView();
        StudentController controller = new StudentController(model, view);
        controller.updateView();
        //更新模型数据
        controller.setStudentName("John");
        controller.updateView();
    }
    
    private static Student retriveStudentFromDatabase(){
        Student student = new Student();
        student.setName("Robert");
        student.setRollNo("10");
        return student;
    }
}

1.6 步骤 5

执行程序,输出结果:

Student: 
Name: Robert
Roll No: 10
Student: 
Name: John
Roll No: 10

1.7 MVC的优缺点

1.7.1 优点

耦合性低

视图层和业务层分离,这样就允许更改视图层代码而不用重新编译模型和控制器代码,同样,一个应用的业务流程或者业务规则的改变只需要改动MVC的模型层即可。因为模型与控制器和视图相分离,所以很容易改变应用程序的数据层和业务规则。

重用性高

MVC允许使用各种不同样式的视图来访问同一个服务器端的代码,因为多个视图能共享一个模型,它包括任何WEB(HTTP)浏览器或者无线浏览器(wap),比如,用户可以通过电脑也可通过手机来订购某样产品,虽然订购的方式不一样,但处理订购产品的方式是一样的。由于模型返回的数据没有进行格式化,所以同样的构件能被不同的界面使用。

部署快,生命周期成本低

MVC使开发和维护用户接口的技术含量降低。使用MVC模式使开发时间得到相当大的缩减,它使程序员(Java开发人员)集中精力于业务逻辑,界面程序员(HTML和JSP开发人员)集中精力于表现形式上。

可维护性高

分离视图层和业务逻辑层也使得WEB应用更易于维护和修改。

1.7.2 缺点

不适合小型,中等规模的应用程序

在一个中小型的应用程序中,强制性的使用MVC进行开发,往往会花费大量时间,并且不能体现MVC的优势,同时会使开发变得繁琐。

增加系统结构和实现的复杂性

对于简单的界面,严格遵循MVC,使模型、视图与控制器分离,会增加结构的复杂性,并可能产生过多的更新操作,降低运行效率。

完全理解MVC并不是很容易。使用MVC需要精心的计划,由于它的内部原理比较复杂,所以需要花费一些时间去思考。同时由于模型和视图要严格的分离,这样也给调试应用程序带来了一定的困难。每个构件在使用之前都需要经过彻底的测试。

视图对模型数据的低效率访问

依据模型操作接口的不同,视图可能需要多次调用才能获得足够的显示数据。对未变化数据的不必要的频繁访问,也将损害操作性能。

一般高级的界面工具或构造器不支持模式

改造这些工具以适应MVC需要和建立分离的部件的代价是很高的,会造成MVC使用的困难

1.8 框架和设计模式的区别

有很多程序员往往把框架模式和设计模式混淆,认为MVC是一种设计模式。实际上它们完全是不同的概念

框架通常是代码重用,而设计模式是设计重用,架构则介于两者之间,部分代码重用,部分设计重用,有时分析也可重用。在软件生产中有三种级别的重用:内部重用,即在同一应用中能公共使用的抽象块;代码重用,即将通用模块组合成库或工具集,以便在多个应用和领域都能使用;应用框架的重用,即为专用领域提供通用的或现成的基础结构,以获得最高级别的重用性。

设计模式是对在某种环境中反复出现的问题以及解决该问题的方案的描述,它比框架更抽象;框架可以用代码表示,也能直接执行或复用,而对模式而言只有实例才能用代码表示;设计模式是比框架更小的元素,一个框架中往往含有一个或多个设计模式,框架总是针对某一特定应用领域,但同一模式却可适用于各种应用。可以说,框架是软件,而设计模式是软件的知识。

2 MVP 模式

mvp的全称为Model-View-Presenter,Model提供数据,View负责显示,Controller/Presenter负责逻辑的处理。MVP与MVC有着一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中View会直接从Model中读取数据而不是通过 Controller。

MVC MVP和MVVM 学习摘要_第4张图片

2.1 实现步骤

Model: 定义并实现获取数据操作(如数据库读取、网络加载)的接口

View: 定义并在Activity,Fragment等中实现用于界面处理(初始化,数据展示)的接口

Controller/Presenter: 负责逻辑的处理,定义用于调用Model中的数据请求方法的接口,实现此接口,并实现Model中定义的数据请求的回调接口

第一步: 编写Model逻辑

/**
 * Model层接口---实现该接口的类负责实际的获取数据操作,如数据库读取、网络加载
 */
public interface IModel {

    void getData(Model.LoadDataCallback callback);
}

数据请求接口的实现:

/**
 * 实现IModel接口,负责实际的数据获取操作(数据库读取,网络加载等),然后通过自己的接口(LoadDataCallback)反馈出去
 */
public class Model implements IModel {

    @Override
    public void getData(final LoadDataCallback callback) {
        //数据获取操作,如数据库查询、网络加载等
        new Thread() {
            @Override
            public void run() {
                try {
                    //模拟耗时操作
                    Thread.sleep(3000);
                    //获取到了数据
                    String data = "我是获取到的数据";
                    //将获取的数据通过接口反馈出去
                    callback.success(data);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    //获取数据失败的回调
                    callback.failure();
                }
            }
        }.start();
    }

    /**
     *
     * 用于回传请求的数据的回传
     */
    public interface LoadDataCallback {

        void success(String taskId);

        void failure();
    }
}

第二步:编写View逻辑

定义用于界面处理(初始化,数据展示)的接口

/**
 * View层接口---执行各种UI操作,定义的方法主要是给Presenter中来调用的
 */
public interface IView {

    void showLoadingProgress(String message);

    void showData(String text);
}

在Activity,Fragment等中对接口的实现:

/**
 * 实现IView接口并实现各种UI操作的方法(其他的业务逻辑在Presenter中进行操作)
 */
public class ViewActivity extends AppCompatActivity implements IView {

    private Button mBtnShowToast;
    private TextView mText;
    private MyHandler mHandler = new MyHandler(ViewActivity.this);
    private IPresenter mPresenter;

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

        //实例化Presenter,并将实现了IView接口的类传入进去
        mPresenter = new Presenter(ViewActivity.this);

        mBtnShowToast = findViewById(R.id.btn_show_toast);
        mText = findViewById(R.id.text);

        mBtnShowToast.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //通过Presenter来实现业务逻辑操作,View层只负责UI相关操作
                mPresenter.loadData();
            }
        });
    }

    @Override
    public void showLoadingProgress(final String message) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                mText.setText(message);
            }
        });
    }

    @Override
    public void showData(final String text) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                mText.setText(text);
            }
        });
    }

    private static class MyHandler extends Handler {

        //弱引用,防止内存泄露
        WeakReference<ViewActivity> weakReference;

        public MyHandler(ViewActivity activity) {
            this.weakReference = new WeakReference<ViewActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    weakReference.get().mText.setText(msg.what);
                    break;
            }
        }
    }
}

第三步:编写presenter逻辑

因为MVP模式中,View和Model是不直接交互的,而是通过presenter这个纽带来进行交互。

View通过presenter对象来调用Model中数据请求的接口,而Model中数据请求的结果会通过presenter中定义的接口回调给presenter,然后presenter在通知给View

定义用于调用Model中的数据请求方法的接口:
具体实现:

/**
 * Presenter层接口---控制Model层的数据操作及调用View层的UI操作来完成“中间人”工作
 */
public interface IPresenter {

    void loadData();

}

定义用于调用Model中的数据请求方法的接口,实现此接口,并实现M中定义的数据请求的回调接口

/**
 * Presenter层接口---控制Model层的数据操作及调用View层的UI操作来完成“中间人”工作.
 * 用于model和view的相关方法的调用
 */
public class Presenter implements IPresenter, Model.LoadDataCallback {

    private final IView mView;
    private final Model mModel;

    public Presenter(IView view) {
        mView = view;
        mModel = new Model();
    }

    @Override
    public void loadData() {
        mView.showLoadingProgress("加载数据中");
        mModel.getData(Presenter.this);
    }

    @Override
    public void success(String data) {
        mView.showData(data);
    }

    @Override
    public void failure() {

    }
}

2.2 MVC和MVP

MVP 是从经典的模式MVC演变而来,它们的基本思想有相通的地方:Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示。作为一种新的模式,MVP与MVC有着一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中View会从直接Model中读取数据而不是通过 Controller。

在MVC里,View是可以直接访问Model的!从而,View里会包含Model信息,不可避免的还要包括一些业务逻辑。 在MVC模型里,更关注的Model的不变,而同时有多个对Model的不同显示,及View。所以,在MVC模型里,Model不依赖于View,但是View是依赖于Model的。不仅如此,因为有一些业务逻辑在View里实现了,导致要更改View也是比较困难的,至少那些业务逻辑是无法重用的

2.3 优点

1、模型与视图完全分离,我们可以修改视图而不影响模型

2、可以更高效地使用模型,因为所有的交互都发生在一个地方——Presenter内部

3、我们可以将一个Presenter用于多个视图,而不需要改变Presenter的逻辑。这个特性非常的有用,因为视图的变化总是比模型的变化频繁。

4、如果我们把逻辑放在Presenter中,那么我们就可以脱离用户接口来测试这些逻辑(单元测试)

2.4 缺点

由于对视图的渲染放在了Presenter中,所以视图和Presenter的交互会过于频繁。还有一点需要明白,如果Presenter过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么Presenter也需要变更了。

2.4 Android MVP

MVP和MVC最大的区别是P层代替了以前的C层,控制的不再是具体的实现而是接口;

先看MVC模式,用登录的例子,android已经很好的把C层和V层分开了,layout中的xml布局相当于V层,Activity相当于C层

public interface ICallback{
  void receive(boolean success);
}
//Model
public class LoginModel{
  public void login(String name,String password,ICallback callback){
    WebApi.login(name,password,callback);
  }
}
//Controller
public class LoginActivity extends Activity{
  private LoginModel mLoginModel;
  private EditText mUserNameEt;
  private EditText mPasswordEt;
  private Button mSubmitBtn;
  public void onCreate(......){
    mLoginModel = new LoginModel(...);
    mSubmitBtn.setOnClickListener(new OnClickListener(View view){
        mLoginModel.login(mUserNameEt.get...,mPasswordEt.get...,new ICallback(){
          public void receive(boolean success){
            if(success){
              startActivity(new Intent(this,MainActivity.class));
              finish();
            } else {
              Toast.makeText(this,"登录失败",Toast.LENGTH_SHORT).show();
            }
          }
        });
    });
  }
}

用MVP模式:

//多接口集成在一个接口文件中,防止代码碎片化
public interface ILoginContract {

  public interface ILoginModel{
    public void login(String name,String password,ICallback callback);
  }

  public interface ILoginPresenter{
    public void login(String name,String password);
  }

  public interface ILoginView{
    public void showDialog();
    public void dismissDialog();
    public void showToast(String message);
    public void navigateToMain();
  }
}
//Presenter
public class PresenterImpl implements ILoginPresenter,ICallback{
  private ILoginView mLoginView;
  private ILoginModel mLoginModel;

  public PresenterImpl(ILoginView loginView){
    this.mLoginView = loginView;
    this.mLoginModel = new LoginModelImpl();
  }

  public void login(String name,String password){
    //处理View的显示
    if(isEmpty(name)||isEmpty(password)){
        this.mLoginView.showToast("用户名或密码不能为空");
        return;
    }
    this.mLoginModel.login(name,password,this);
  }

  public void receive(boolean success){
    if(success){
      this.mLoginView.navigateToMain();
    }else{
      this.mLoginView.showToast("登录失败");
    }
  }

  private boolean isEmpty(String text){
    return text==null||"".equals(text)?true:false;
  }
}
public class LoginActivity extends Activity implements ILoginView{
  private IPresenter mPresenter;
  private EditText mUserNameEt;
  private EditText mPasswordEt;
  private Button mSubmitBtn;
  public void onCreate(......){
    mPresenter = new PresenterImpl(this);
    mSubmitBtn.setOnClickListener(new OnClickListener(View view){
        mPresenter.login(mUserNameEt.getText().toString(),
                          mPasswordEt.getText().toString());
    });
  }

  public void showDialog(){
    //显示一个转圈的dialog;
  }

  public void dismissDialog(){
    //隐藏转圈的dialog;
  }

  public void showToast(String message){
    Toast.makeText(this,message,Toast.LENGTH_SHORT).show();
  }

  public void navigateToMain(){
    startActivity(new Intent(this,MainActivity.class));
    finish();
  }
}

代码逻辑要比MVC模式写起来更清晰,代码测试也更简单,甚至可以在没有页面效果图只有功能的时候完成功能,UI的改变也不会影响任何的业务代码。

Google MVP的代码地址:

https://github.com/googlesamples/android-architecture/tree/todo-mvp/

3 MVVM

3.1 概述

MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。

MVC MVP和MVVM 学习摘要_第5张图片

Model(模型)

  • 模型是指代表真实状态内容的领域模型(面向对象),或指代表内容的数据访问层(以数据为中心)。Model层就是职责数据的存储、读取网络数据、操作数据库数据以及I/O,一般会有ViewModel对象来调用获取这一部分的数据

View(视图)

  • 就像在MVC和MVP模式中一样,视图是用户在屏幕上看到的结构、布局和外观(UI)。View层做的仅仅和UI相关的工作,我们只在XML、Activity、Fragment写View层的代码,View层不做和业务相关事情,也就是我们的Activity 不写和业务逻辑相关代码,一般Activity不写更新UI的代码,如果非得要写,那更新的UI必须和业务逻辑和数据是没有关系的,只是单纯UI逻辑,来更新UI,比如:滑动时头部颜色渐变、editttext根据输入内容显示隐藏等

ViewModel (视图模型)

  • 视图模型是暴露公共属性和命令的视图的抽象。MVVM没有MVC模式的控制器,也没有MVP模式的presenter,有的是一个绑定器。在视图模型中,绑定器在视图和数据绑定器之间进行通信。ViewModel 只做和业务逻辑和业务数据相关的事,不做任何和UI、控件相关的事;ViewModel 层不会持有任何控件的引用,更不会在ViewModel中通过UI控件的引用去做更新UI的事情。ViewModel就是专注于业务的逻辑处理,操作的也都是对数据进行,这些个数据源绑定在相应的控件上会自动去更改UI,开发者不需要关心更新UI

绑定器

  • 声明性数据和命令绑定隐含在MVVM模式中。关于data-binding 参考 https://developer.android.com/topic/libraries/data-binding

MVC MVP和MVVM 学习摘要_第6张图片

View层的Activity通过DataBinding生成Binding实例,把这个实例传递给ViewModel,ViewModel层通过把自身与Binding实例绑定,从而实现View中layout与ViewModel的双向绑定。mvvm的缺点数据绑定使得 Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。

3.2 详谈MVVM

3.2.1 关于Model

对于一个 Model ,比如我们要存储和显示一个人的信息,这个人具有姓名、年龄、性别这三个属性。这个Model的伪代码如下

class Person {
    String  name;
    int     age;
    int     gender;
}

Model 的数据模型,和我们的业务需求或者说业务实体(Entity)是一一映射关系.

3.2.2 关于ViewModel

ViewModel 顾名思义,就是一个 Model of View,它是一个 View 信息的存储结构,ViewModel 和 View 上的信息是一一映射关系.

以一个软件的登陆场景为例子,假设这个登录界面上有如下逻辑:

用户名输入框
密码输入框
登陆按钮,点击登陆按钮按钮置灰,显示 loading 框
登陆成功,页面触发跳转
登陆失败,loading 框消失,在界面上显示错误信息
错误信息可以分为两种情况: 1、密码错误;2、没有网络

那么我们下面来定义这样一个 ViewModel:

class LoginViewModel {
    String  userId;
    String  password;
    bool    isLoading;
    bool    isShowErrorMessage;
    String  errorMessage;
}

3.2.3 界面初始化

由于 LoginView 和 LoginViewModel 是映射关系,也称为绑定关系,那么 LoginViewModel 是怎样的数据,View 就按照怎样的数据来进行显示。界面第一次打开时,整个 LoginViewModel 的初始值为:

{
    userId: '',
    password: '',
    isLoading: false,
    isShowErrorMessage: false,
    errorMessage: ''
}

那么此时界面为:
​ 用户名输入框显示为空白字符串
​ 密码输入框显示为空白字符串
​ loading 框因为 isLoading = false,所以不显示
​ 错误信息框 因为 isShowErrorMessage = false,所以不显示
​ 错误信息框里面的文字为空白字符串

3.2.4 触发登陆

接下来,用户输入用户名和密码,点击登录按钮,在登陆事件里面触发网络通信逻辑,同时设定 isLoading = true,伪代码如下:

function onLoginButtonClick() {
    request(url, ...);
    loginViewModel.isLoading = true;
}

此时 LoginViewModel 的值为:

{
    userId: 'this is user id',
    password: 'this is password',
    isLoading: true,
    isShowErrorMessage: false,
    errorMessage: ''
}

随着 isLoading 值的变化,因为 ViewModel 和 View 存在绑定关系,那么此时界面动态变化为:

  • 用户名输入框显示为刚刚输入的字符串
  • 密码输入框显示为刚刚输入的字符串
  • 因为isLoading = true,所以显示 loading 框
  • 因为isLoading = true,登陆按钮置灰,不可点击
  • 错误信息框 因为 isShowErrorMessage = false,所以不显示
  • 错误信息框里面的文字为空白字符串

“当任何外部事件发生时,永远只操作 ViewModel 中的数据”

这里外部事件主要指界面点击、文字输入、网络通信等等事件。因为绑定关系的存在,ViewModel 变成啥样,界面就会自动变成啥样。

3.2.5 单向绑定与双向绑定

所谓“单向绑定”就是 ViewModel 变化时,自动更新 View
所谓“双向绑定”就是在单向绑定的基础上 View 变化时,自动更新 ViewModel

单向绑定模式下的伪代码如下:

function onUserIdTextViewChanged(textView) {
    loginViewModel.userId = textView.text;
}

function onPasswordTextViewChanged(textView) {
    loginViewModel.password = textView.text;
}

function onLoginButtonClick() {
    loginViewModel.isLoading = true;
    loginViewModel.isShowErrorMessage: false,
    login(loginViewModel.userId, loginViewModel.password);
}

可以看到,我们需要非常明确的在 TexView 变化事件里面去重新设定 LoginViewModel 中的值,而双向绑定模式下,根据绑定关系,这一过程就隐藏性的自动完成了.

既然“双向绑定”那么智能、简单,为什么还需要“单向绑定”呢?因为在真实的“业务需求”下,实际情况是非常复杂的,虽然 ViewModel 可以和 View 形成映射关系,但是它们之间的值却不一定能直接划等号. 比如在界面上要填写性别,我们通常会提供一个下拉列表框,让用户选择。这个选择框里面至少有“未知”、“男”和“女”三种字符串值,而我们的 ViewModel 一般情况下并不直接存储这些字符串.因为 ViewModel 中的数据很大一部分情况下是来自于数据库、来自于服务器,而数据库和服务器中几乎是不可能直接把性别字符串存储在数据模型中的。一般会建立一个 int 类型的字段,用 0 表示未知;用 1 表示男人;用 2 表示女人.

那么问题来了,在 ViewModel 中一个 gender 属性类型为 int,值为 0 或者 1 或者 2 时,与其绑定的 View 怎么知道该如何来显示为“未知”、“男”或者“女”呢?

所以“属性转换器”应运而生,在给 View 绑定 ViewModel 时,发现属性值不匹配,那么就需要设定一个属性转换器。反之亦然,当性别选择下拉列表框被用户改变时,用户选择了“男”,在双向绑定模式下,那么 View 依然需要在一个属性转换器的帮助下,把“男”转换为 1,然后设定到 ViewModel .

因为绑定关系触发 ViewModel 和 View 的动态变化过程是隐藏不可见的,也给调试带来了极大的麻烦

3.2.6 在 ViewModel 中数据请求与处理

针对前面的登陆代码,我们再来做一次优化,得到一个更加合理的版本:

class LoginViewModel {
    String  userId;
    String  password;
    bool    isLoading;
    bool    loginStatus;
    String  errorMessage;

    Login() {
        request(url, this.userId, this.password, {
            success: function() {
                ...
            },
            failed: function() {
                this.isLoading = false;             //触发绑定关系,隐藏登陆 loading 框
                this.isShowErrorMessage = true;     //触发绑定关系,显示错误提示框
                this.errorMessage = '密码错误';      //触发绑定关系,设置错误提示文字内容
            }
        });
    }
}

可以看到,我们把整个登陆过程放在了 LoginViewModel 中,那么登陆按钮点击后这一套响应过程也相应的有所调整

function onUserIdTextViewChanged(textView) {
    loginViewModel.userId = textView.text;
}

function onPasswordTextViewChanged(textView) {
    loginViewModel.password = textView.text;
}

function onLoginButtonClick() {
    loginViewModel.isLoading = true;            //触发绑定关系,显示登陆 loading 框
    loginViewModel.isShowErrorMessage: false;   //触发绑定关系,隐藏错误提示框
    loginViewModel.login();                     //开始登陆
}

上面这段代码再也不处理任何的数据逻辑,不关心数据库、不关心网络调用,也完全不关心界面随着数据和逻辑的变化应该如何去设置控件属性状态等等.

MVVM 的核心原则:“当任何外部事件发生时,永远只操作 ViewModel 中的数据”

上面这段代码它不属于 Model,不属于 View,也不属于 ViewModel,那它应该写在哪里呢?

  • iOS 下依然写在 ViewController 中
  • Android 下依然写在 Activity 或者 Fragment 中
  • ReactNative 下依然写在 Component 中
  • 微信小程序 下依然写在 Page 中

所以 MVC 中的 C,其实一直都默默的存在着,只是变得弱化了,一定要完整的讲的话,那就是 Model-View-Controler-ViewModel 模式。只有在理想的双向绑定模式下,Controller 才会完全的消失。

参考:

https://www.jianshu.com/p/a898ef83f38c

3.3 实现步骤

使用MVVM实现如下需求:用户输入用户名,密码,点击登录按钮后调用接口进行检查,成功则跳转到下一个界面,失败则提示错误信息

3.3.1 build.gradle 配置

启用DataBinding

首先,要保证你的Gradle插件版本要大于 1.5.0-alpha1及以上(现在基本都比这个版本高了吧),然后在app下的build.gradle文件添加以下代码:

dataBinding {
    enabled = true
}

添加ViewModel and LiveData

implementation "android.arch.lifecycle:extensions:1.0.0"
annotationProcessor "android.arch.lifecycle:compiler:1.0.0"

3.3.2 View布局

登录页面activity_login.xml



<layout xmlns:android="http://schemas.android.com/apk/res/android">

​```
<data>

    <variable
        name="viewmodel"
        type="com.example.zq.mvvmdemo.user.LoginViewModel" />
data>

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical">
​```

​```
    <EditText
        android:layout_width="240dp"
        android:layout_height="40dp"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="100dp"
        android:hint="@string/login_name"
        android:text="@={viewmodel.loginName}" />

    <EditText
        android:layout_width="240dp"
        android:layout_height="40dp"
        android:layout_below="@+id/name"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="30dp"
        android:hint="@string/login_password"
        android:text="@={viewmodel.loginPass}" />

    <Button
        android:id="@+id/submit"
        android:layout_width="240dp"
        android:layout_height="40dp"
        android:layout_below="@+id/password"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="40dp"
        android:text="@string/login"
        android:textSize="16dp" />
LinearLayout>
​```

layout>

从这里开始,就会发现和我们之前的写法有很大的区别了,之前的xml文件根节点是LinearLayout或RelativeLayout等布局,但是在使用DataBinding后,我们的xml文件可以概括成这样:


<layout >
    <data>
    .......
    data>
    <LinearLayout>
        ........
        自己的布局
        ........
    LinearLayout>
layout>

最外层以layout标签包裹,里边用data标签表示我们要绑定的数据的名字以及类型,然后就是我们自己的布局。

ViewModel是View与Model层交互的桥梁,所以具体用到的业务数据,比如这里用户名,密码等我这里全部放到了ViewModel中,然后将ViewModel与View进行绑定:

<data>
     <variable
         name="viewmodel"
         type="com.example.zq.mvvmdemo.user.LoginViewModel" />
data>

3.3.3 ViewModel实现

LoginViewModel文件

public class LoginViewModel extends AndroidViewModel {
    private static final String TAG = "LoginViewModel";

    private final SingleLiveEvent<String> mOpenUserList = new SingleLiveEvent<>();
    private final SnackbarMessage mSnackbarText = new SnackbarMessage();
    // To avoid leaks, this must be an Application Context.
    private final Context mContext; 

    public final ObservableField<String> loginName = new ObservableField<>();
    public final ObservableField<String> loginPass = new ObservableField<>();

    public LoginViewModel(Application mContext) {
        super(mContext);
        this.mContext = mContext.getApplicationContext();
    }

    public SingleLiveEvent<String> getmOpenUserList() {
        return mOpenUserList;
    }

    SnackbarMessage getSnackbarMessage() {
        return mSnackbarText;
    }

    private void login(String loginName, String loginPass) {

        if (TextUtils.isEmpty(loginName)) {
            mSnackbarText.setValue(mContext.getString(R.string.login_name_not_input));
            return;
        }
        if (TextUtils.isEmpty(loginPass)) {
            mSnackbarText.setValue(mContext.getString(R.string.login_pass_not_input));
            return;
        }

        mOpenUserList.setValue("123456");

    }

    public void login() {
        login(loginName.get(), loginPass.get());
    }
}

ObservableField

首先我们来看loginName和loginPass这两个变量,ObservableField为DataBinding中提供的一个类,它使我们的对象变得可观测,即修改界面上的值,对应的loginName和loginPass的值就会改变,反之亦然。

LiveData

再看mOpenUserList与mSnackbarText,它们是LiveData类型的,LiveData是一个数据持有类,并且在给定的生命周期中其变化是可观测的,这里用来处理ViewModels与 UI views (activities and fragments)的一些交互。
login()方法由点击登录按钮后触发,这里注意,因为loginName和loginPass已经与我们的视图文件绑定在一起了,所以就不用在调用的时候从EditText获取文本内容再传进来了。

getmOpenUserList()与getSnackbarMessage()将mOpenUserList与mSnackbarText公布给 UI views (activities and fragments),来处理一些交互,在这个例子里,主要是弹出提示以及跳转页面。

3.3.4 LoginActivity

LoginActivity文件

public class LoginActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ViewModelFactory factory = ViewModelFactory.getInstance(getApplication());
        final LoginViewModel loginViewModel = ViewModelProviders.of(this, factory).get(LoginViewModel.class);

        final ActivityLoginBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_login);
        binding.setViewmodel(loginViewModel);

        loginViewModel.getmOpenUserList().observe(this, new Observer<String>() {
            @Override
            public void onChanged(@Nullable String s) {
                Intent intent = new Intent(LoginActivity.this, UserListActivity.class);
                intent.putExtra("token", s);
                startActivity(intent);
                finish();
            }
        });

        loginViewModel.getSnackbarMessage().observe(this, new SnackbarMessage.SnackbarObserver() {
            @Override
            public void onNewMessage(String message) {
                SnackbarUtils.showSnackbar(binding.getRoot(), message);
            }
        });

        findViewById(R.id.submit).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                loginViewModel.login();
            }
        });
    }

}

ActivityLoginBinding

这里就相当于View层,按照前面说的,这里应该只有一些界面的设置,不应该有任何的逻辑处理。ActivityLoginBinding是自动生成的,注意,在写完xml文件后要Build-Make Project一下,才会生成这个文件。

梳理一下逻辑:
点击登录按钮后,会调用LoginViewModel中的login()方法,进行参数的检查,如果参数不合法,为mSnackbarText设置对应的文案提示,因为我们在Activity已经监测了mSnackbarText的变化,当它的值发生变化后,会通过回调通知回来,我们可以进行提示:

SnackbarUtils.showSnackbar(binding.getRoot(), message);

当参数全部合法后,改变mOpenUserList,同样会触发回调,进而跳转到下一个界面:

Intent intent = new Intent(LoginActivity.this, UserListActivity.class);
                intent.putExtra("token", s);
                startActivity(intent);
                finish();

以上就是用这种新的开发模式来完成这个登录需求的一个记录,在检查参数这里写的比较简单,其实应该用接口来检验,这又牵扯到一层封装,这里暂时先不写。欢迎大佬给出建议和指正错误。

3.4 MVVM优点

MVVM模式和MVC模式一样,主要目的是分离视图(View)和模型(Model),有几大优点

1. 低耦合。视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的"View"上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。

2. 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。

3. 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计,使用Expression Blend可以很容易设计界面并生成xaml代码。

4. 可测试。界面素来是比较难于测试的,而现在测试可以针对ViewModel来写。

你可能感兴趣的:(Android,App,其他)