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

时间:2019-04-30 17:30:01

标签: android mvvm android-databinding android-viewmodel android-mvvm

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

我想知道,如何通过ViewModel中的数据绑定实现OnTouchListenerOnClickListenerOnFocusChangeListener之类的侦听器事件,这将使编写单元测试用例变得容易。

我已经使用黄油刀库进行绑定,并且通过执行OnTouch事件,我的问题是,这是在Activity中实现侦听器而不是在ViewModel中直接实现侦听器的正确方法吗?< / strong> 对于具有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>

1 个答案:

答案 0 :(得分:1)

首先,单击侦听器的代码包含应用程序逻辑,不应包含在视图中,而应位于视图模型中(例如,可以在视图模型中添加名为login()的公共方法并处理其中的登录逻辑)。

第二,为了将click事件绑定到方法,您可以在布局的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()}" />

请注意,使用上面显示的代码无法获得MotionEvent的{​​{1}}值。如果需要,则必须以不同的方式实现适配器:

OnTouchListener
object MyAdapters {

    ...

    @JvmStatic
    @BindingAdapter("onTouchListener")
    fun setTouchListener(view: View, listener: OnTouchListener) {
        view.setOnTouchListener(listener)
    }
}