如何使用 MVVM 中的数据绑定处理 ViewModel 中的 onClick 或 onTouch 类事件 Android

How to handle onClick or onTouch like events in ViewModel with data binding in MVVM Android

我浏览了许多与 带数据绑定的 MVVM 模型相关的博客。 由于与 ViewModel 的数据绑定使得编写 junit 测试用例变得容易。

我想知道,如何在 ViewModel 中使用数据绑定实现 OnTouchListenerOnClickListenerOnFocusChangeListener 等侦听器事件,这将使写入单元测试用例简单。

我使用黄油刀库进行绑定,并通过它执行 OnTouch 事件,我的问题是,它是在 Activity 中实现监听器的正确方法吗在 ViewModel 中直接实现它? MVVM结构的LoginScreen请参考以下代码:

LoginActivityNew.java

public class LoginActivityNew extends AppCompatActivity {

@BindView(R.id.et_password)
AppCompatEditText etPassword;

private LoginViewModel loginViewModel;

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

        binding = DataBindingUtil.setContentView(this, R.layout.activity_login);
        loginViewModel = ViewModelProviders.of(this).get(LoginViewModel.class);
        binding.setViewModel(loginViewModel);
        binding.setLifecycleOwner(this);

        ButterKnife.bind(this);

        binding.buttonLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Common common = new Common(getApplicationContext());
                common.isInternetAvailable(LoginActivityNew.this, new Common.InternetStateListener() {
                    @Override
                    public void onNetworkStateObtain(boolean isAvailable) {
                        loginViewModel.getAuthenticateTokenData().observe(LoginActivityNew.this, new Observer<TokenResponse>() {
                            @Override
                            public void onChanged(@Nullable TokenResponse tokenResponse) {
                                if (tokenResponse != null) {
                                    loginResponseHandler(tokenResponse, tokenResponse.getUserName(), tokenResponse.getPassword());
                                } else {
                                    Log.d("jdhadd","TokenResponse == null");
                                }
                            }
                        });
                    }
                });
            }
        });

}


private void loginResponseHandler(final TokenResponse tokenResponse, final String username, final String password) {
    switch (tokenResponse.getState()) {
        case ApiState.LOADING:
            Log.d("testData","Loading");
            break;
        case ApiState.COMPLETED:

            Log.d("testData","COMPLETED");
            break;
        case ApiState.FAILURE:
            Log.d("testData","FAILURE");

            break;
        default:
    }
}

@OnClick(R.id.et_user_name)
void onTouchUserName() {
    loginViewModel.resetEditTextField("username");
}

@OnClick(R.id.et_password)
void onTouchPassword() {
    loginViewModel.resetEditTextField("password");
}
}

LoginViewModel.java

public class LoginViewModel extends AndroidViewModel {


public final MutableLiveData<String> userName = new MutableLiveData<>();
public final MutableLiveData<String> password = new MutableLiveData<>();
public final MutableLiveData<String> userNameError = new MutableLiveData<>();
public final MutableLiveData<String> passwordError = new MutableLiveData<>();
public final MutableLiveData<Boolean> userNameErrorVisibility = new MutableLiveData<>();
public final MutableLiveData<Boolean> passwordErrorVisibility = new MutableLiveData<>();
public final MutableLiveData<Boolean> isViewPasswordIconVisible = new MutableLiveData<>();

private MutableLiveData<TokenResponse> tokenResponse;
private Application application;

public LoginViewModel(@NonNull Application application) {
    super(application);
    this.application = application;
}

public boolean isValidData() {
    boolean isValid = true;

    Log.d("fekjfnew","email = "+userName.getValue()+",, pass = "+password.getValue());

    if (userName.getValue() == null || userName.getValue().equals("")) {

        userNameError.setValue("Invalid Email");
        isValid = false;
        userNameErrorVisibility.setValue(true);

    } else {
        userNameError.setValue(null);
        userNameErrorVisibility.setValue(false);
    }

    if (password.getValue() == null || password.getValue().equals("")) {
        passwordError.setValue("Password too short");
        passwordErrorVisibility.setValue(true);
        isValid = false;

    } else {
        passwordError.setValue(null);
        passwordErrorVisibility.setValue(false);
    }

    return isValid;
}


public MutableLiveData<TokenResponse> getAuthenticateTokenData() {
    tokenResponse = new MutableLiveData<>();
    if(isValidData()) {
    // Call Repository to Perform API operation
    }
    return tokenResponse;
}





public void setPasswordIcon(boolean isVisible) {
    isViewPasswordIconVisible.setValue(isVisible);
}

public void resetEditTextField(String filedName) {

    if(filedName.equals("username"))
        userNameErrorVisibility.setValue(false);
    else if(filedName.equals("password"))
        passwordErrorVisibility.setValue(false);
}
}

activity_login_new.xml

<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="com.test.views.activities.LoginActivityNew">

<data>
    <import type="android.view.View"/>
    <variable name="viewModel" type="com.test.viewModels.LoginViewModel"/>

</data>

<LinearLayout
    android:padding="40dp"
    android:orientation="vertical"
    android:id="@+id/cl_login"
    android:gravity="center_horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#4">


    <android.support.v7.widget.AppCompatTextView
        android:id="@+id/tv_sign_in"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/text_sign_in"
        android:textColor="@color/colorWhite"
        android:textSize="@dimen/login_header_text_size"
        android:layout_marginTop="50dp"
        />

    <android.support.v7.widget.AppCompatEditText
        android:id="@+id/et_user_name"
        android:layout_width="match_parent"
        style="@style/LoginEditTextViewStyle"
        android:layout_marginTop="10dp"
        android:background="@{viewModel.userNameErrorVisibility ? @drawable/bg_error_edit_text : @drawable/bg_edit_text}"
        android:ems="10"
        android:hint="@string/hint_username_email"
        android:imeOptions="actionNext"
        android:transitionName=""
        android:inputType="textPersonName"
        android:paddingStart="20dp"
        android:paddingTop="10dp"
        android:paddingEnd="20dp"
        android:text="@={viewModel.userName}"
        android:paddingBottom="10dp"
        android:layout_height="@dimen/login_height_of_edit_text" />

    <android.support.v7.widget.AppCompatTextView
        android:id="@+id/tv_incorrect_username"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="20dp"
        android:layout_marginTop="10dp"
        android:text="@={viewModel.userNameError}"
        android:textColor="@color/colorErrorText"
        android:textSize="@dimen/wrong_entries_text_size"
        android:visibility="@{viewModel.userNameErrorVisibility ? View.VISIBLE : View.GONE}"
      />

    <android.support.design.widget.TextInputEditText
        android:id="@+id/et_password"
        android:layout_width="match_parent"
        style="@style/LoginEditTextViewStyle"
        android:layout_marginTop="30dp"
        android:background="@{viewModel.passwordErrorVisibility ? @drawable/bg_error_edit_text : @drawable/bg_edit_text}"
        android:ems="10"
        android:text="@={viewModel.password}"
        android:hint="@string/hint_password"
        android:imeOptions="actionDone"
        android:inputType="text"
        android:paddingStart="20dp"
        android:paddingTop="10dp"
        android:paddingEnd="20dp"
        android:paddingBottom="10dp"
        android:layout_height="@dimen/login_height_of_edit_text" />


    <android.support.v7.widget.AppCompatTextView
        android:id="@+id/tv_incorrect_password"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="20dp"
        android:layout_marginTop="10dp"
        android:text="@={viewModel.passwordError}"
        android:textColor="@color/colorErrorText"
        android:textSize="@dimen/wrong_entries_text_size"
        android:visibility="@{viewModel.passwordErrorVisibility ? View.VISIBLE : View.GONE}"
        app:layout_constraintStart_toEndOf="@id/guideline_v1"
        app:layout_constraintTop_toBottomOf="@id/et_password" />

    <android.support.v7.widget.AppCompatButton
        android:id="@+id/button_login"
        android:layout_width="match_parent"
        android:layout_marginBottom="20dp"
        android:background="#FF077DB2"
        android:text="@string/label_sign_in"
        android:textAllCaps="false"
        android:layout_height="@dimen/login_height_of_edit_text"
        android:textColor="#ffffff" />

    <LinearLayout
        android:id="@+id/ll_finger_print"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:gravity="center"
        android:orientation="horizontal"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        android:visibility="gone"
        app:layout_constraintTop_toBottomOf="@id/button_login">

        <android.support.v7.widget.AppCompatImageView
            android:layout_width="24dp"
            android:layout_height="24dp"
            android:src="@drawable/ic_fingerprint" />

        <android.support.v7.widget.AppCompatTextView
            android:id="@+id/text_fingerprint"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="10dp"
            android:text="@string/text_fingerprint_id"
            android:textColor="@color/colorWhite"
            android:textSize="@dimen/fingerprint_id_text_size"
            app:layout_constraintStart_toEndOf="@id/guideline_v7"
            app:layout_constraintTop_toBottomOf="@id/button_login" />
    </LinearLayout>
</LinearLayout>

styles.xml

<style name="LoginEditTextViewStyle" parent="android:Theme">
    <item name="android:paddingStart">20dp</item>
    <item name="android:paddingEnd">20dp</item>
    <item name="android:paddingTop">10dp</item>
    <item name="android:paddingBottom">10dp</item>
    <item name="android:textColor">@color/colorWhite</item>
    <item name="android:textColorHint">@color/colorWhiteWithThirtyTransparency</item>
    <item name="android:background">@drawable/bg_edit_text</item>
    <item name="android:textSize">@dimen/login_edit_text_size</item>
</style>

首先,您的点击监听器的代码包含应用程序逻辑,不应在视图中,而应在视图模型中(例如,您可以添加一个名为 login() 的 public 方法到您的视图模型并处理其中的登录逻辑)。

其次,为了将点击事件绑定到该方法,您可以在布局的 XML 文件中进行:

<android.support.v7.widget.AppCompatButton
    android:id="@+id/button_login"
    ...
    android:onClick="@{() -> viewModel.login()}" />

然后,在单元测试中您可以调用方法 login() 来测试它。

另一方面,要绑定在 XML 中不直接可用的回调,例如 OnTouch,您可以创建适配器以使其可用:

object MyAdapters {

    ...

    @JvmStatic
    @BindingAdapter("onTouch")
    fun setTouchListener(view: View, callback: () -> Boolean) {
        view.setOnTouchListener { v, event -> callback() }
    }
}
<android.support.v7.widget.AppCompatButton
    android:id="@+id/button_login"
    ...
    app:onTouch="@{() -> viewModel.methodThatReturnsABoolean()}" />

请注意,您无法使用上面显示的代码获取 OnTouchListenerMotionEvent 值。如果你需要它,那么你将不得不以不同的方式实现你的适配器:

object MyAdapters {

    ...

    @JvmStatic
    @BindingAdapter("onTouchListener")
    fun setTouchListener(view: View, listener: OnTouchListener) {
        view.setOnTouchListener(listener)
    }
}
<android.support.v7.widget.AppCompatButton
    android:id="@+id/button_login"
    ...
    app:onTouchListener="@{viewModel.onTouchListener}" />