Я просмотрел множество блогов, связанных с Модель MVVM с привязкой данных. Поскольку привязка данных с ViewModel упрощает написание тестовых случаев junit.
Я хочу знать, как я могу реализовать события прослушивателя, такие как OnTouchListener, OnClickListener, OnFocusChangeListener, с привязкой данных в ViewModel, что упростит написание тестовых случаев.
Я использовал библиотеку ножей для масла для привязки, и через нее я выполняю события OnTouch, мой вопрос: Это правильный способ реализации слушателей в Activity вместо прямой реализации в ViewModel?
Пожалуйста, обратитесь к следующему коду для LoginScreen со структурой MVVM:
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");
}
}
Логинвиевмодел.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>
стили.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>
Прежде всего, код вашего прослушивателя кликов содержит логику приложения и должен быть не в представлении, а в модели представления (например, вы можете добавить публичный метод с именем login() в свою модель представления и обрабатывать логику входа внутри него).
Во-вторых, чтобы привязать событие клика к методу, вы можете сделать это в 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 для OnTouchListener с помощью кода, показанного выше. Если вам это нужно, то вам придется реализовать свой адаптер по-другому:
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}" />
Кроме того, вы предлагаете использовать привязку данных для всех типов слушателей, где они будут вызываться напрямую через XML? Вместо того, чтобы вызывать его через активность, как я сделал binding.buttonLogin.setOnClickListener(...)?
Необязательно привязывать все через XML, но в большинстве случаев это рекомендуемый подход, так как это лучше и требует меньше кода. Кроме того, вы уже привязываете свои свойства через XML, так почему бы не привязать и действия пользователя. Пожалуйста, взгляните на мое редактирование, я добавил объяснение о том, как реализовать привязку onTouch с помощью адаптера.
Кстати, не забудьте отметить ответ как действительный, если вы так считаете.
Конечно, я также хочу знать, что, как и в моем click listener, у меня есть код, в котором мне нужен объект FragmentManager, который невозможно получить в ViewModel, поэтому я использую binding.buttonLogin.setOnClickListener(..) в деятельность. Так что я могу легко получить объект fragmentManager в деятельность. Вот почему меня немного смущает сохранение всех слушателей в ViewModel. Можете ли вы предложить что-то для таких случаев?
Я думаю, что мой ответ действителен для вашего первоначального вопроса и может быть помечен как принятый. То, о чем вы спрашиваете сейчас, отличается и, вероятно, должно быть включено в отдельный новый вопрос. Тем не менее, если вы хотите что-то делать в представлении (например, использовать FragmentManager), когда в модели представления происходят определенные вещи, я предлагаю вам взглянуть на эта статья о SingleLiveEvent, который является разумным и безопасным способом общения с модель представления для представления.
Да, ваш ответ верен, но у меня здесь есть одна путаница: при таком подходе вы передаете представление для просмотра модели. пожалуйста, помогите мне понять здесь, можем ли мы реализовать прослушиватель onclick в модели представления. И где мы должны хранить часть проверки.
Спасибо. Но как реализовать события прослушивателя onTouch и onFocus напрямую через XML? Я имею в виду, что в XML у нас нет такого параметра, как
android:onClickдля прослушивателя кликов?