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