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

15,032

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}" />
Share:
15,032

Related videos on Youtube

Kavita Patil
Author by

Kavita Patil

Updated on June 04, 2022

Comments

  • Kavita Patil
    Kavita Patil almost 2 years

    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
    Kavita Patil about 5 years
    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
    Kavita Patil about 5 years
    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(...)?
  • Julio E. Rodríguez Cabañas
    Julio E. Rodríguez Cabañas about 5 years
    It is not mandatory to bind everything via XML, but for most cases that's the recommended approach, 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
    Julio E. Rodríguez Cabañas about 5 years
    BTW, please remember to mark the answer as valid if you think it is.
  • Kavita Patil
    Kavita Patil about 5 years
    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?
  • Julio E. Rodríguez Cabañas
    Julio E. Rodríguez Cabañas about 5 years
    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 about the SingleLiveEvent, which is a sensible, safe way to communicate from the viewmodel to the view.
  • ummer akbar
    ummer akbar over 3 years
    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.