How to handle onClick or onTouch like events in ViewModel with data binding in MVVM Android
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}" />
Related videos on Youtube
Kavita Patil
Updated on June 04, 2022Comments
-
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 about 5 yearsthank 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 about 5 yearsAlso, 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 about 5 yearsIt 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 about 5 yearsBTW, please remember to mark the answer as valid if you think it is.
-
Kavita Patil about 5 yearsSure, I also want to know that as in my
click listener
I have code where I need the object ofFragmentManager
which is not possible to get in the ViewModel, that's why I am usingbinding.buttonLogin.setOnClickListener(..)
in the activity. So that I can easily get thefragmentManager
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 about 5 yearsI 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 theSingleLiveEvent
, which is a sensible, safe way to communicate from the viewmodel to the view. -
ummer akbar over 3 yearsYes 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.