MVVM_用户登录实例

2022-07-25,

目录

1. 导入

1.1 主要修改点(Databinding 、RxJava订阅) 

(1) 当用户点击UI 时,View 直接传递给ViewModel 处理

(2) ViewModel 向Model 发起数据请求 

(3) 当ViewModel 收到Model 数据后(RxJava订阅机制通知),ViemModel 通知View更新

2. 具体实现

2.1 Model (RxJava订阅)

2.2 ViewModel (Databinding)

2.3 XML (使用ViewModel 定义的函数)

2.4 Activity (创建ViewModel/ DatabindingUtils 加载布局文件并生成ViewDataBinding 子类对象,并为它设置ViewModel)

3. ViewDatabinding 子类 AcitivityLoginUsingVmBinding


1. 导入

在之前写了用户登录实例中,使用的是MVP 框架实现的: https://blog.csdn.net/whjk20/article/details/112511365

可以发现Activity 与 Model 解耦了,但是Activity 中仍有一些对View 的更新操作。

这些更新操作也可以通过MVVM 框架中的 ViewModel 去实现,即View(XML 文件) 与ViewModel 绑定, ViewModel 替代了Presenter。

MVVM 的简单入门使用可以参考: https://blog.csdn.net/whjk20/article/details/106903496

1.1 主要修改点(Databinding 、RxJava订阅) 

(1) 当用户点击UI 时,View 直接传递给ViewModel 处理

(2) ViewModel 向Model 发起数据请求 

(3) 当ViewModel 收到Model 数据后(RxJava订阅机制通知),ViemModel 通知View更新

2. 具体实现

2.1 Model (RxJava订阅)

package com.example.mvplogindemo.model;

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

import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;

public class LoginIteratorForViewModel {

    //被观察者
    private final PublishSubject<Boolean> mUsernameError = PublishSubject.create();
    private final PublishSubject<Boolean> mPasswordError = PublishSubject.create();
    private final PublishSubject<Boolean> mLoginSuccess = PublishSubject.create();

    //设置接口返回被观察者
    public Observable<Boolean> setUsernameError(){
        return mUsernameError.subscribeOn(Schedulers.computation())
                .observeOn(AndroidSchedulers.mainThread());
    }

    //回调,设置状态改变,通知观察者
    public void setUsernameError(Boolean isError) {
        mUsernameError.onNext(isError);
    }

    public Observable<Boolean> setPasswordError(){
        return mPasswordError.subscribeOn(Schedulers.computation())
                .observeOn(AndroidSchedulers.mainThread());
    }

    public void setPasswordError(Boolean isError) {
        mPasswordError.onNext(isError);
    }

    public Observable<Boolean> setLoginSuccess(){
        return mLoginSuccess.subscribeOn(Schedulers.computation())
                .observeOn(AndroidSchedulers.mainThread());
    }

    public void setLoginSuccess(Boolean isError) {
        mLoginSuccess.onNext(isError);
    }

    public void login(final String username, final String password){
        new Handler().postDelayed(()-> {
            final boolean userNameEmpty = TextUtils.isEmpty(username);
            final boolean passwordEmpty = TextUtils.isEmpty(password);
            setUsernameError(userNameEmpty);
            setPasswordError(passwordEmpty);
            setLoginSuccess(!userNameEmpty && !passwordEmpty);
        }, 2000);
    }
}

其中,需要在app 目录的build.gradle 文件中 添加RxJava 依赖

dependencies {
    //rxjava
    implementation 'io.reactivex.rxjava2:rxjava:2.1.4'
    implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'

}

使用了 PublishSubject 做为订阅事件, 可以由我们 手动调用onNext 去触发事件,  以 mUsernameError 订阅事件为例

    public Observable<Boolean> setUsernameError(){
        return mUsernameError.subscribeOn(Schedulers.computation())
                .observeOn(AndroidSchedulers.mainThread());
    }

subscribeOn/observeOn 会返回被观察者的对象,  并且指定了被观察者 和 观察者所在的线程 (观察者在主线程响应)

 

   public void setUsernameError(Boolean isError) {
        mUsernameError.onNext(isError);
    }

手动调用onNext()触发回调 。 这里主要是由 login() 函数 触发。

其它的操作类似。

2.2 ViewModel (Databinding)

需要在app 的 build.gradle 中开启

android {
    dataBinding {
        enabled = true
    }
}
package com.example.mvplogindemo.viewmodel;

import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;
import androidx.databinding.library.baseAdapters.BR;

import com.example.mvplogindemo.model.LoginIteratorForViewModel;

import io.reactivex.disposables.CompositeDisposable;

public class LoginViewModel extends BaseObservable {

    private LoginIteratorForViewModel mLoginIterator;
    private final CompositeDisposable mCompositeDisposable = new CompositeDisposable();
    private boolean mUsernameError = false;
    private boolean mPasswordError = false;
    private boolean mLoginSuccess = false;
    private boolean mLogining = false;

    public LoginViewModel(){
        mLoginIterator = new LoginIteratorForViewModel();
        subscribeEvents();
    }

    private void subscribeEvents() {
        //订阅
        mCompositeDisposable.add(mLoginIterator.setUsernameError().subscribe(this::updateUsernameError)); //onNext()函数
        mCompositeDisposable.add(mLoginIterator.setPasswordError().subscribe(this::updatePasswordError));
        mCompositeDisposable.add(mLoginIterator.setLoginSuccess().subscribe(this::updateLoginSuccess));
    }

    private void updateUsernameError(Boolean isError) {
        mUsernameError = isError;
        notifyPropertyChanged(BR.isUseNameError); //@Binding 的方法, 去掉get

        updateLoginingState(false);
    }

    private void updatePasswordError(Boolean isError) {
        mPasswordError = isError;
        notifyPropertyChanged(BR.isPasswordError); //@Binding 的方法, 去掉get

        updateLoginingState(false);
    }

    private void updateLoginSuccess(Boolean isSuccess) {
        mLoginSuccess = isSuccess;

        updateLoginingState(false);
        //TODO - 调用view, 还是才用监听???
    }

    @Bindable
    public boolean getIsUseNameError(){
         return mUsernameError;
    }

    @Bindable
    public boolean getIsPasswordError(){
        return mPasswordError;
    }

    @Bindable
    public boolean getLoginSuccess(){
        return mLoginSuccess;
    }

    @Bindable
    public boolean getIsLogining(){
        return mLogining;
    }


    public void verify(String username, String password) {
        updateLoginingState(true);

        mLoginIterator.login(username, password);
    }

    private void updateLoginingState(boolean isLogining) {
        mLogining = isLogining;
        notifyPropertyChanged(BR.isLogining);
    }

    public void dispose(){
        mCompositeDisposable.clear();
    }
}

其中:

(1) ViewModel 需要继承自BaseObservable , 才能定义@Bindable 方法。 如:

    @Bindable
    public boolean getIsUseNameError(){
         return mUsernameError;
    }

在编译之后,会生成对应的BR 类,可以在xml 中使用ViewModel类的对象 例如 viewmodel.isUseNameError ,实际上是调用 ViewModel 类中定义的方法 getIsUseNameError (省略了get)

也可以在ViewModel 类中, 通过 notifyPropertyChanged(BR.isUseNameError), 触发xml中所有用到这个方法的地方,进行重新获取 getIsUseNameError  的值去更新界面, 实现了数据(Model) 到View 的更新 (其实还是通过ViewModel)

package androidx.databinding.library.baseAdapters;

public class BR {
  public static final int _all = 0;

  public static final int isLogining = 1;

  public static final int isPasswordError = 2;

  public static final int isUseNameError = 3;

  public static final int loginSuccess = 4;

  public static final int loginViewModel = 5;
}

(2) 订阅事件,并且实现回调onNext后的操作,例如

mCompositeDisposable.add(mLoginIterator.setUsernameError().subscribe(this::updateUsernameError)); //onNext()函数

相当于为被观察者 添加 观察者,并且指定响应操作。

(3) 回调函数

    private void updateUsernameError(Boolean isError) {
        mUsernameError = isError;
        notifyPropertyChanged(BR.isUseNameError); //@Binding 的方法, 去掉get

        updateLoginingState(false);
    }

先更新成员变量,再触发xml的更新操作 。  此外updateLoginingState 表示显示或者隐藏进度条,类似。

2.3 XML (使用ViewModel 定义的函数)

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <import type="android.view.View"/>
        <variable
            name="loginViewModel"
            type="com.example.mvplogindemo.viewmodel.LoginViewModel" />
    </data>

    <LinearLayout
        android:layout_width="400dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="16dp"
        android:gravity="center"
        android:orientation="vertical">

        <LinearLayout
            android:id="@+id/name_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal">
            <EditText
                android:id="@+id/username"
                android:layout_width="0dp"
                android:layout_weight="4"
                android:layout_height="wrap_content"
                android:drawablePadding="8dp"
                android:drawableStart="@drawable/ic_username"
                android:gravity="center_vertical"
                android:hint="@string/user_name"
                android:inputType="text"/>
            <TextView
                android:id="@+id/name_error"
                android:layout_width="0dp"
                android:layout_weight="2"
                android:layout_height="wrap_content"
                android:text="@string/username_error"
                android:textSize="10sp"
                android:gravity="center"
                android:visibility="@{loginViewModel.isUseNameError ? View.VISIBLE : View.GONE}"/>
        </LinearLayout>

        <LinearLayout
            android:id="@+id/password_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal">

            <EditText
                android:id="@+id/password"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="4"
                android:drawableStart="@drawable/ic_password"
                android:drawablePadding="8dp"
                android:gravity="center_vertical"
                android:hint="@string/password"
                android:inputType="text" />

            <TextView
                android:id="@+id/password_error"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="2"
                android:gravity="center"
                android:text="@string/password_error"
                android:textSize="10sp"
                android:visibility="@{loginViewModel.isPasswordError ? View.VISIBLE : View.GONE}"/>
        </LinearLayout>

        <Button
            android:id="@+id/login_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/login"
            android:onClick="@{() -> loginViewModel.verify(username.getText().toString(), password.getText().toString())}"
            android:layout_marginTop="8dp"/>

        <ProgressBar
            android:id="@+id/login_progress"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:visibility="@{loginViewModel.isLogining ? View.VISIBLE: View.GONE}" />
    </LinearLayout>
</layout>

其中, 

(1) 根标签为 <layout> </layout>  , 它的内部再嵌套布局

(2)  <data> </data> 标签里, 定义用到的ViewModel 类型 和变量名, 并且导入了View 类(为了使用View.VISIBLE /View.GONE)

    <data>
        <import type="android.view.View"/>
        <variable
            name="loginViewModel"
            type="com.example.mvplogindemo.viewmodel.LoginViewModel" />
    </data>

(3) name_error /password_error 表示输入空名字或空密码的提示, 使用TextView,  方便控制显示或隐藏,  通过ViewModel 控制: android:visibility="@{loginViewModel.isPasswordError ? View.VISIBLE : View.GONE}"/>

 (TextView.setError() 是TextView的方法,似乎在xml 中无法调用)

(4) 注意Button 的onClick 响应中, 引用了xml 中其它控件的数据, 使用方法为 username.getText()。  注意是getText(), 如果使用Kotlin 语言 写 username.text, 则编译时会无法生成对应的Databinding 类。 

 android:onClick="@{() -> loginViewModel.verify(username.getText().toString(), password.getText().toString())}"

例如报错:

* What went wrong:
Execution failed for task ':app:compileDebugJavaWithJavac'.
> android.databinding.tool.util.LoggedErrorException: Found data binding error(s):
  [databinding] {"msg":"if getId is called on an expression, it should have an id: password.text","file":"app\\src\\main\\res\\layout\\acitivity_login_using_vm.xml","pos":[]}

 

2.4 Activity (创建ViewModel/ DatabindingUtils 加载布局文件并生成ViewDataBinding 子类对象,并为它设置ViewModel)

package com.example.mvplogindemo;

import android.os.Bundle;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;

import com.example.mvplogindemo.databinding.AcitivityLoginUsingVmBinding;
import com.example.mvplogindemo.viewmodel.LoginViewModel;


public class LoginActivityForVM extends AppCompatActivity {
    LoginViewModel mLoginViewModel;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        AcitivityLoginUsingVmBinding binding = DataBindingUtil.setContentView(this, R.layout.acitivity_login_using_vm);
        mLoginViewModel = new LoginViewModel();
        binding.setLoginViewModel(mLoginViewModel);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mLoginViewModel.dispose();
        mLoginViewModel = null;
    }
}

(1) 使用了 DataBindingUtil.setContentView() 代替了setContentView(), 并获得 ViewDataBinding对象。

其中AcitivityLoginUsingVmBinding 是编译后自动生成的类(ViewDataBinding的子类),命名是根据它的layout文件即 acitivity_login_using_vm (每个词的首字母大小) + Binding , 路径如:

MVPLoginDemo\app\build\generated\data_binding_base_class_source_out\debug\out\com\example\mvplogindemo\databinding\AcitivityLoginUsingVmBinding.java

(2) 创建ViewModel 对象

(3)为得到的ViewDataBinding 对象设置ViewModel

(4) 最好是在适当时机(如onDestory()),清除ViewModel 里的订阅事件。

 

此外,目前对登录成功,还没逻辑处理跳转到主页内容界面,仅需在ViewModel 中新加订阅事件订阅接口,然后在登录界面的Activity中订阅 并且实现回调处理逻辑即可。

例如在  LoginViewModel 中增加一下代码:

    private final PublishSubject<Boolean> mLoginSuccessSubject = PublishSubject.create();

    //订阅接口
    public Observable<Boolean> setLoginSuccessSubject(){
        return mLoginSuccessSubject.subscribeOn(Schedulers.computation()).observeOn(AndroidSchedulers.mainThread());
    }

    public void setLoginSuccessSubject(boolean isSuccess) {
        mLoginSuccessSubject.onNext(isSuccess);
    }

    private void updateLoginSuccess(Boolean isSuccess) {
        // 省略其它处理逻辑
        // 采用触发事件
        setLoginSuccessSubject(isSuccess);
    }

然后在 LoginActivityForVM 增加:

    private final CompositeDisposable mDisposable = new CompositeDisposable();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {

// 省略其它逻辑       
  
 mDisposable.add(mLoginViewModel.setLoginSuccessSubject().subscribe(this::navigateToHome));
    }

    private void navigateToHome(Boolean isSuccess) {
        if(isSuccess) {
            startActivity(new Intent(this, SimpleContentActivity.class));
        }
    }

3. ViewDatabinding 子类 AcitivityLoginUsingVmBinding

最后,附上编译生成的ViewDatabinding 子类AcitivityLoginUsingVmBinding, 可以看到它包含了xml 定义的所有控件(有id),并且定义了设置ViewModel 对象setLoginViewModel方法, 实际上是由 AcitivityLoginUsingVmBindingImpl 实现

// Generated by data binding compiler. Do not edit!
package com.example.mvplogindemo.databinding;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.databinding.Bindable;
import androidx.databinding.DataBindingUtil;
import androidx.databinding.ViewDataBinding;
import com.example.mvplogindemo.R;
import com.example.mvplogindemo.viewmodel.LoginViewModel;
import java.lang.Deprecated;
import java.lang.Object;

public abstract class AcitivityLoginUsingVmBinding extends ViewDataBinding {
  @NonNull
  public final Button loginButton;

  @NonNull
  public final ProgressBar loginProgress;

  @NonNull
  public final TextView nameError;

  @NonNull
  public final LinearLayout nameLayout;

  @NonNull
  public final EditText password;

  @NonNull
  public final TextView passwordError;

  @NonNull
  public final LinearLayout passwordLayout;

  @NonNull
  public final EditText username;

  @Bindable
  protected LoginViewModel mLoginViewModel;

  protected AcitivityLoginUsingVmBinding(Object _bindingComponent, View _root, int _localFieldCount,
      Button loginButton, ProgressBar loginProgress, TextView nameError, LinearLayout nameLayout,
      EditText password, TextView passwordError, LinearLayout passwordLayout, EditText username) {
    super(_bindingComponent, _root, _localFieldCount);
    this.loginButton = loginButton;
    this.loginProgress = loginProgress;
    this.nameError = nameError;
    this.nameLayout = nameLayout;
    this.password = password;
    this.passwordError = passwordError;
    this.passwordLayout = passwordLayout;
    this.username = username;
  }

  public abstract void setLoginViewModel(@Nullable LoginViewModel loginViewModel);

  @Nullable
  public LoginViewModel getLoginViewModel() {
    return mLoginViewModel;
  }

  @NonNull
  public static AcitivityLoginUsingVmBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable ViewGroup root, boolean attachToRoot) {
    return inflate(inflater, root, attachToRoot, DataBindingUtil.getDefaultComponent());
  }

  /**
   * This method receives DataBindingComponent instance as type Object instead of
   * type DataBindingComponent to avoid causing too many compilation errors if
   * compilation fails for another reason.
   * https://issuetracker.google.com/issues/116541301
   * @Deprecated Use DataBindingUtil.inflate(inflater, R.layout.acitivity_login_using_vm, root, attachToRoot, component)
   */
  @NonNull
  @Deprecated
  public static AcitivityLoginUsingVmBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable ViewGroup root, boolean attachToRoot, @Nullable Object component) {
    return ViewDataBinding.<AcitivityLoginUsingVmBinding>inflateInternal(inflater, R.layout.acitivity_login_using_vm, root, attachToRoot, component);
  }

  @NonNull
  public static AcitivityLoginUsingVmBinding inflate(@NonNull LayoutInflater inflater) {
    return inflate(inflater, DataBindingUtil.getDefaultComponent());
  }

  /**
   * This method receives DataBindingComponent instance as type Object instead of
   * type DataBindingComponent to avoid causing too many compilation errors if
   * compilation fails for another reason.
   * https://issuetracker.google.com/issues/116541301
   * @Deprecated Use DataBindingUtil.inflate(inflater, R.layout.acitivity_login_using_vm, null, false, component)
   */
  @NonNull
  @Deprecated
  public static AcitivityLoginUsingVmBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable Object component) {
    return ViewDataBinding.<AcitivityLoginUsingVmBinding>inflateInternal(inflater, R.layout.acitivity_login_using_vm, null, false, component);
  }

  public static AcitivityLoginUsingVmBinding bind(@NonNull View view) {
    return bind(view, DataBindingUtil.getDefaultComponent());
  }

  /**
   * This method receives DataBindingComponent instance as type Object instead of
   * type DataBindingComponent to avoid causing too many compilation errors if
   * compilation fails for another reason.
   * https://issuetracker.google.com/issues/116541301
   * @Deprecated Use DataBindingUtil.bind(view, component)
   */
  @Deprecated
  public static AcitivityLoginUsingVmBinding bind(@NonNull View view, @Nullable Object component) {
    return (AcitivityLoginUsingVmBinding)bind(component, view, R.layout.acitivity_login_using_vm);
  }
}

 

本文地址:https://blog.csdn.net/whjk20/article/details/112602368

《MVVM_用户登录实例.doc》

下载本文的Word格式文档,以方便收藏与打印。