Как обрабатывать события, подобные onClick или onTouch, в ViewModel с привязкой данных в MVVM Android

Я просмотрел множество блогов, связанных с Модель 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>
8
0
15 145
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Прежде всего, код вашего прослушивателя кликов содержит логику приложения и должен быть не в представлении, а в модели представления (например, вы можете добавить публичный метод с именем 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}" />

Спасибо. Но как реализовать события прослушивателя onTouch и onFocus напрямую через XML? Я имею в виду, что в XML у нас нет такого параметра, как android:onClick для прослушивателя кликов?

Kavita_p 30.04.2019 19:47

Кроме того, вы предлагаете использовать привязку данных для всех типов слушателей, где они будут вызываться напрямую через XML? Вместо того, чтобы вызывать его через активность, как я сделал binding.buttonLogin.setOnClickListener(...)?

Kavita_p 30.04.2019 19:52

Необязательно привязывать все через XML, но в большинстве случаев это рекомендуемый подход, так как это лучше и требует меньше кода. Кроме того, вы уже привязываете свои свойства через XML, так почему бы не привязать и действия пользователя. Пожалуйста, взгляните на мое редактирование, я добавил объяснение о том, как реализовать привязку onTouch с помощью адаптера.

Julio E. Rodríguez Cabañas 30.04.2019 20:01

Кстати, не забудьте отметить ответ как действительный, если вы так считаете.

Julio E. Rodríguez Cabañas 01.05.2019 10:14

Конечно, я также хочу знать, что, как и в моем click listener, у меня есть код, в котором мне нужен объект FragmentManager, который невозможно получить в ViewModel, поэтому я использую binding.buttonLogin.setOnClickListener(..) в деятельность. Так что я могу легко получить объект fragmentManager в деятельность. Вот почему меня немного смущает сохранение всех слушателей в ViewModel. Можете ли вы предложить что-то для таких случаев?

Kavita_p 01.05.2019 20:16

Я думаю, что мой ответ действителен для вашего первоначального вопроса и может быть помечен как принятый. То, о чем вы спрашиваете сейчас, отличается и, вероятно, должно быть включено в отдельный новый вопрос. Тем не менее, если вы хотите что-то делать в представлении (например, использовать FragmentManager), когда в модели представления происходят определенные вещи, я предлагаю вам взглянуть на эта статья о SingleLiveEvent, который является разумным и безопасным способом общения с модель представления для представления.

Julio E. Rodríguez Cabañas 01.05.2019 20:27

Да, ваш ответ верен, но у меня здесь есть одна путаница: при таком подходе вы передаете представление для просмотра модели. пожалуйста, помогите мне понять здесь, можем ли мы реализовать прослушиватель onclick в модели представления. И где мы должны хранить часть проверки.

ummer akbar 26.12.2020 16:35

Другие вопросы по теме