MVVM_用户登录实例
目录
1.1 主要修改点(Databinding 、RxJava订阅)
(1) 当用户点击UI 时,View 直接传递给ViewModel 处理
(3) 当ViewModel 收到Model 数据后(RxJava订阅机制通知),ViemModel 通知View更新
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
下一篇: nginx的介绍与安装使用
推荐阅读