Android MVVM设计模式

时间:2020-02-23 14:29:05  来源:igfitidea点击:

在本教程中,我们将在Android应用程序中讨论和实现Android MVVM体系结构模式。
我们之前已经讨论了Android MVP模式。

为什么我们需要这些模式?在"单个活动"或者"片段"中添加所有内容会导致测试和重构代码时出现问题。
因此,建议使用代码分离和干净的体系结构。

Android MVVM

MVVM代表模型,视图,视图模型。

  • 模型:保存应用程序的数据。
    它不能直接与视图对话。
    通常,建议通过Observables将数据公开给ViewModel。

  • 视图:它表示没有任何应用程序逻辑的应用程序的UI。
    它观察ViewModel。

  • ViewModel:它充当模型和视图之间的链接。
    它负责转换模型中的数据。
    它向视图提供数据流。
    它还使用挂钩或者回调来更新视图。
    它将要求模型提供数据。

以下流程说明了核心MVVM模式。

这与MVP有何不同?

  • ViewModel替换中间层中的Presenter。

  • 演示者持有对视图的引用。
    ViewModel没有。

  • 演示者使用经典方式(触发方法)更新视图。

  • ViewModel发送数据流。

  • 演示者和视图处于一对一关系。

  • View和ViewModel是一对多的关系。

  • ViewModel不知道View正在监听它。

有两种在Android中实现MVVM的方法:

  • 数据绑定
  • 接收Java

在本教程中,我们将仅使用数据绑定。
Google引入了数据绑定库,以便直接在xml布局中绑定数据。
有关数据绑定的更多信息,请参阅本教程。

我们将创建一个简单的登录页面示例应用程序,要求用户输入。
我们将看到ViewModel如何在不显示View引用的情况下何时显示Toast消息时通知View。

在没有参考的情况下如何通知某个类?

可以通过三种不同的方式完成此操作:

  • 使用双向数据绑定
  • 使用实时数据
  • 使用RxJava

双向数据绑定

双向数据绑定是一种将对象绑定到XML布局的技术,这样对象和布局都可以相互发送数据。

在我们的例子中,ViewModel可以将数据发送到布局并观察更改。

为此,我们需要一个XML中定义的BindingAdapter和自定义属性。

绑定适配器将侦听attribute属性中的更改。

通过下面的示例,我们将详细了解双向数据绑定。

添加数据绑定库

将以下代码添加到您应用的build.gradle文件中:

android {

  dataBinding {
      enabled = true
  }
}

这将在您的应用程序中启用数据绑定。

添加依赖项

在您的build.gradle文件中添加以下依赖项:

implementation 'android.arch.lifecycle:extensions:1.1.0'

模型

模型将保留用户的电子邮件和密码。
以下User.java类可以实现此目的:

package com.theitroad.androidmvvmbasics.model;

public class User {
  private String email;
  private String password;

  public User(String email, String password) {
      this.email = email;
      this.password = password;
  }

  public void setEmail(String email) {
      this.email = email;
  }

  public String getEmail() {
      return email;
  }

  public void setPassword(String password) {
      this.password = password;
  }

  public String getPassword() {
      return password;
  }

}

双向数据绑定允许我们在XML布局中绑定对象,以便该对象可以将数据发送到布局,反之亦然。

双向数据绑定的语法为@ = {variable}

布局

下面给出了activity_main.xml的代码:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="https://schemas.android.com/apk/res/android"
  xmlns:bind="https://schemas.android.com/tools">

  <data>

      <variable
          name="viewModel"
          type="com.theitroad.androidmvvmbasics.viewmodels.LoginViewModel" 
  </data>

  <ScrollView
      android:layout_width="match_parent"
      android:layout_height="match_parent">

      <LinearLayout
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:layout_gravity="center"
          android:layout_margin="8dp"
          android:orientation="vertical">

          <EditText
              android:id="@+id/inEmail"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:hint="Email"
              android:inputType="textEmailAddress"
              android:padding="8dp"
              android:text="@={viewModel.userEmail}" 

          <EditText
              android:id="@+id/inPassword"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:hint="Password"
              android:inputType="textPassword"
              android:padding="8dp"
              android:text="@={viewModel.userPassword}" 

          <Button
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:layout_marginTop="8dp"
              android:onClick="@{()-> viewModel.onLoginClicked()}"
              android:text="LOGIN"
              bind:toastMessage="@{viewModel.toastMessage}" 

      </LinearLayout>

  </ScrollView>

</layout>

数据绑定要求我们在顶部设置布局标签。
其中我们的ViewModel将数据绑定到View。
()-> viewModel.onLoginClicked()调用在我们的ViewModel中定义的Button点击监听器lambda。
EditText更新模型中的值(通过View Model)。

Bind:toastMessage =" @ {viewModel.toastMessage}""是我们为双向数据绑定创建的自定义属性。
根据ViewModel中toastMessage中的更改,将在View中触发BindingAdapter。

视图模型

下面给出了LoginViewModel.java的代码:

package com.theitroad.androidmvvmbasics.viewmodels;

import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.text.TextUtils;
import android.util.Patterns;

import com.android.databinding.library.baseAdapters.BR;
import com.theitroad.androidmvvmbasics.model.User;

public class LoginViewModel extends BaseObservable {
  private User user;

  private String successMessage = "Login was successful";
  private String errorMessage = "Email or Password not valid";

  @Bindable
  private String toastMessage = null;

  public String getToastMessage() {
      return toastMessage;
  }

  private void setToastMessage(String toastMessage) {

      this.toastMessage = toastMessage;
      notifyPropertyChanged(BR.toastMessage);
  }

  public void setUserEmail(String email) {
      user.setEmail(email);
      notifyPropertyChanged(BR.userEmail);
  }

  @Bindable
  public String getUserEmail() {
      return user.getEmail();
  }

  @Bindable
  public String getUserPassword() {
      return user.getPassword();
  }

  public void setUserPassword(String password) {
      user.setPassword(password);
      notifyPropertyChanged(BR.userPassword);
  }

  public LoginViewModel() {
      user = new User("","");
  }

  public void onLoginClicked() {
      if (isInputDataValid())
          setToastMessage(successMessage);
      else
          setToastMessage(errorMessage);
  }

  public boolean isInputDataValid() {
      return !TextUtils.isEmpty(getUserEmail()) && Patterns.EMAIL_ADDRESS.matcher(getUserEmail()).matches() && getUserPassword().length() > 5;
  }
}

在布局中调用的方法在上述代码中以相同的签名实现。
如果该方法的XML对应项不存在,则需要将属性更改为app:

上面的类还可以扩展ViewModel。
但是我们需要BaseObservable,因为它可以将数据转换为流并在toastMessage属性更改时通知。

我们需要为XML中定义的toastMessage自定义属性定义getter和setter。
在设置器内部,我们通知观察者(在我们的应用程序中为View)数据已更改。
View(我们的活动)可以定义适当的操作。

重建项目时,BR类是根据数据绑定自动生成的

下面给出了MainActivity.java类的代码:

package com.theitroad.androidmvvmbasics.views;

import android.databinding.BindingAdapter;
import android.databinding.DataBindingUtil;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

import com.theitroad.androidmvvmbasics.R;
import com.theitroad.androidmvvmbasics.databinding.ActivityMainBinding;
import com.theitroad.androidmvvmbasics.viewmodels.LoginViewModel;

public class MainActivity extends AppCompatActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      ActivityMainBinding activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
      activityMainBinding.setViewModel(new LoginViewModel());
      activityMainBinding.executePendingBindings();

  }

  @BindingAdapter({"toastMessage"})
  public static void runMe(View view, String message) {
      if (message != null)
          Toast.makeText(view.getContext(), message, Toast.LENGTH_SHORT).show();
  }
}

多亏了DataBinding,ActivityMainBinding类是从布局自动生成的。
每当在Button上定义的toastMessage属性更改时,都会触发@BindingAdapter方法。
它必须使用与XML和ViewModel中定义的属性相同的属性。

因此,在上述应用程序中,ViewModel通过侦听View中的更改来更新Model。
而且,模型可以使用notifyPropertyChanged通过ViewModel更新视图。