MVVM_用户登录实例

目录

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);
  }
}

 

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
WPF(Windows Presentation Foundation)是一种用于创建用户界面的微软技术,MVVM(Model-View-ViewModel)是一种设计模式,用于将界面逻辑与业务逻辑分离。下面是一个使用WPF MVVM设计模式的登录实例。 在MVVM架构中,登录界面通常由三个部分组成:视图(View)、视图模型(ViewModel)和模型(Model)。 首先,我们需要创建一个WPF窗口作为登录界面的视图。该窗口会包含用户名和密码的文本框以及登录按钮。在视图的代码中,我们会绑定这些控件的属性到相应的视图模型。 接下来,我们创建一个视图模型,它是视图和模型之间的中间层。视图模型会实现一个命令,该命令会在用户点击登录按钮时执行。在命令的执行逻辑中,我们会调用模型层来验证用户输入的用户名和密码是否正确。 最后,我们创建一个模型类,它包含用户信息和登录逻辑。模型类会提供一个方法,用于验证用户输入的用户名和密码是否与预先设定的值匹配。如果验证成功,模型类会返回一个布尔值。 整个过程中,视图和模型是完全解耦的。视图通过数据绑定将用户输入传递给视图模型,视图模型则处理逻辑并调用模型层进行验证。 通过使用WPF MVVM设计模式,我们可以有效地将界面逻辑和业务逻辑分离,提高代码的可维护性和重用性。同时,这种设计模式也可以帮助我们实现更好的单元测试,因为我们可以通过模拟视图模型和模型来进行测试,而无需依赖于实际的用户界面。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值