8

I have gone through many blogs related to MVVM model with Data Binding. As data binding with ViewModel makes it easy to write junit test cases.

I want to know, how can I implement listener events like OnTouchListener, OnClickListener, OnFocusChangeListener with data binding in the ViewModel which will make writing unit test cases easy.

I have used butter knife library for binding and through that I am performing OnTouch events, my question is, Is it a proper way to implement listeners in Activity instead of directly implementing that in ViewModel? Please refer the following code for LoginScreen with MVVM structure:

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>
Kavita Patil
  • 1,784
  • 1
  • 17
  • 30

1 Answers1

14

First of all, the code of your click listener contains application logic and should not be in the view, but in the viewmodel (for example, you could add a public method called login() to your viewmodel and handle the login logic inside it).

Second, in order to bind the click event to the method, you can do it in the XML file of your layout:

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

Then, in the unit tests you can invoke the method login() in order to test it.

On the other hand, to bind callbacks that are not directly available in XML, such as OnTouch, you can create adapters to make them available:

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()}" />

Please note that you cannot get the MotionEvent value of the OnTouchListener with the code shown above. If you need it, then you will have to implement your adapter differently:

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}" />
  • thank you. But how to implement onTouch and onFocus listener events directly through XML? I mean in XML we don't have any parameter like `android:onClick` for click listener? – Kavita Patil Apr 30 '19 at 17:47
  • Also, are you suggesting to use data binding for all type of listeners, where they will be called directly through XML? Instead of calling it through the activity , like I have done `binding.buttonLogin.setOnClickListener(...)`? – Kavita Patil Apr 30 '19 at 17:52
  • It is not mandatory to bind everything via XML, but for most cases that's the [recommended approach](https://developer.android.com/topic/libraries/data-binding/expressions#listener_bindings), as it is nicer and requires less code. Besides, you are already binding your properties via XML, so why not to bind the user actions as well. Please take a look at my edit, I have added an explanation about how to implement the `onTouch` binding using an adapter. – Julio E. Rodríguez Cabañas Apr 30 '19 at 18:01
  • BTW, please remember to mark the answer as valid if you think it is. – Julio E. Rodríguez Cabañas May 01 '19 at 08:14
  • Sure, I also want to know that as in my `click listener` I have code where I need the object of `FragmentManager` which is not possible to get in the ViewModel, that's why I am using `binding.buttonLogin.setOnClickListener(..)` in the **activity**. So that I can easily get the `fragmentManager` object in an **activity**. That's why I am abit confused about keeping all listeners in ViewModel. Can you suggets something for such cases? – Kavita Patil May 01 '19 at 18:16
  • I think my answer is valid for your original question and could be marked as accepted. What you are asking about now is different and should probably be included in a separate, new question. That said, if you want to do stuff in the view (e.g., using the `FragmentManager`) when certain things happen in the viewmodel, I suggest you take a look at [this article](https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150) about the `SingleLiveEvent`, which is a sensible, safe way to communicate from the viewmodel to the view. – Julio E. Rodríguez Cabañas May 01 '19 at 18:27
  • Yes your answer is valid, but I have one confusion here, with this approach u are passing view to view model. please help me to understand here, can we implement onclick listener in viewmodel. And where should we keep validation part. – ummer akbar Dec 26 '20 at 15:35