Преобразование односторонней привязки данных в двустороннюю с помощью компонентов архитектуры Android.

Я реорганизую свое приложение Android для университетского проекта, чтобы использовать компоненты архитектуры, и мне трудно реализовать двустороннюю привязку данных к SwitchCompat. Приложение имеет простой пользовательский интерфейс с TextView, отображающим статус обновлений местоположения, и вышеупомянутым SwitchCompat, который включает и выключает обновления местоположения.
Сейчас я использую одностороннюю привязку данных к атрибуту checked SwitchCompat, но хотел бы использовать двустороннюю привязку данных.
Текущая реализация с использованием архитектуры Model-View-ViewModel выглядит следующим образом:
MainViewModel.java:

public class MainViewModel extends ViewModel {

    private LiveData<Resource<Location>> mLocationResource;

    public MainViewModel() {
        mLocationResource = Repository.getInstance().getLocationResource();
    }

    public LiveData<Resource<Location>> getLocationResource() {
        return mLocationResource;
    }

    public void onCheckedChanged (Context context, boolean isChecked) {
        if (isChecked) {
            Repository.getInstance().requestLocationUpdates(context);
        } else {
            Repository.getInstance().removeLocationUpdates(context);
        }
    }
}

Resource‹Location> (увидел идею здесь) — это класс, содержащий данные, допускающие значение NULL. (местоположение) и ненулевое состояние, которое может обрабатывать TextView:
State.java

public enum State {
    LOADING,
    UPDATE,
    UNKNOWN,
    STOPPED
}

А теперь реализация android:onCheckedChanged в fragment_main.xml:

android:onCheckedChanged="@{(buttonView, isChecked) -> viewModel.onCheckedChanged(context, isChecked)}"

И, наконец, настраиваемый адаптер привязки для преобразования из состояния в логическое проверенное состояние:

@BindingAdapter({"android:checked"})
public static void setChecked(CompoundButton view, Resource<Location> locationResource) {
    State state = locationResource.getState();
    boolean checked;
    if (state == State.STOPPED) {
        checked = false;
    } else {
        checked = true;
    }
    if (view.isChecked() != checked) {
        view.setChecked(checked);
    }
}

и реализация атрибута android:checked в fragment_main.xml:

android:checked="@{viewModel.getLocationResource}"

Как сказано в руководстве для разработчиков Android, на которое я ссылался выше, как я могу выполнять всю работу внутри android:checked вместо использования android:checked и android:onCheckedChanged (односторонняя привязка данных к двусторонней привязке данных)?
Кроме того, дайте мне знать, если вы думаю, что архитектуру/логику моего приложения можно улучшить :)




Ответы (2)


Вот как я бы это сделал (извините за код Kotlin):

Сначала я бы реорганизовал класс Resource<T> и сделал переменную state объектом MutableLiveData<State>:

enum class State {
    LOADING,
    UPDATE,
    UNKNOWN,
    STOPPED
}

class Resource<T>() {
    var state = MutableLiveData<State>().apply { 
        value = State.STOPPED //Setting the initial value to State.STOPPED
    }
}

Затем я бы создал следующую ViewModel:

class MainViewModel: ViewModel() {

     val locationResource = Resource<Location>()

}

В макете привязки данных я бы написал следующее:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="viewModel"
            type="MainViewModel" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <androidx.appcompat.widget.SwitchCompat
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:resourceState="@={viewModel.locationResource.state}" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{String.valueOf(viewModel.locationResource.state)}" />

    </LinearLayout>

</layout>

Обратите внимание на двустороннее выражение привязки данных @= в представлении SwitchCompat.

А теперь к BindingAdapter и InverseBindingAdapter:

@BindingAdapter("resourceState")
fun setResourceState(compoundButton: CompoundButton, resourceState: State) {
    compoundButton.isChecked = when (resourceState) {
        // You can decide yourself how the mapping should look like:
        State.LOADING -> true 
        State.UPDATE -> true
        State.UNKNOWN -> true
        State.STOPPED -> false
    }
}

@InverseBindingAdapter(attribute = "resourceState", event = "resourceStateAttrChanged")
fun getResourceStateAttrChanged(compoundButton: CompoundButton): State =
    // You can decide yourself how the mapping should look like:
    if (compoundButton.isChecked) State.UPDATE else State.STOPPED

@BindingAdapter("resourceStateAttrChanged")
fun setResourceStateAttrChanged(
    compoundButton: CompoundButton,
    attrChange: InverseBindingListener
) {
    compoundButton.setOnCheckedChangeListener { _, isChecked -> 
        attrChange.onChange()

        // Not the best place to put this, but it will work for now:
        if (isChecked) {
            Repository.getInstance().requestLocationUpdates(context);
        } else {
            Repository.getInstance().removeLocationUpdates(context);
        }
    }
}

Вот и все. В настоящее время:

  • Всякий раз, когда locationResource.state изменяется на State.STOPPED, кнопка SwitchCompat переходит в неотмеченное состояние.
  • Всякий раз, когда locationResource.state изменяется с State.STOPPED на другое состояние, кнопка SwitchCompat переходит в отмеченное состояние.
  • Всякий раз, когда кнопка SwitchCompat нажимается и переходит в отмеченное состояние, значение locationResource.state изменяется на State.UPDATE.
  • Всякий раз, когда кнопка SwitchCompat нажимается и переходит в неотмеченное состояние, значение locationResource.state изменяется на State.STOPPED.

Не стесняйтесь задавать мне любые вопросы, если что-то не ясно.

person janosch    schedule 04.02.2019
comment
Привет, Янош, спасибо за ответ. Я не понимаю, где логика вызывать requestLocationUpdates или removeLocationUpdates на основе проверенного состояния: разве это не должно быть в BindingAdapter из resourceStateAttrChanged? Я преобразовал ваш фрагмент в Java и отредактировал в соответствии со своими потребностями и предположениями, но получил следующее ошибка. (ссылки на пастебин). Спасибо за помощь. - person Oliver; 05.02.2019
comment
Хм, разве параметр состояния setResourceState(CompoundButton view, int state) не должен быть типа LocationState, является ли LocationState перечислением? То же самое относится и к возвращаемому типу public static int getResourceStateAttrChanged(CompoundButton view). Ошибка выглядит так, как будто вы все еще используете функцию getLocationResource(), а ваш mLocationResource все еще является объектом LiveData, я предлагаю вам удалить getLocationResource() сделать mLocationResource общедоступным и изменить его на Resource‹Location› с объектом MutableLiveData state в качестве переменной-члена, как в первый шаг моего ответа. - person janosch; 05.02.2019
comment
Также вы не вызываете attrChange.onChange() в своем методе public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) . Он уведомляет библиотеку привязки данных об изменении значения переключателя. Что касается того, где вызывать requestLocationUpdates или removeLocationUpdates, это другой вопрос, но пока ваше решение будет работать. - person janosch; 05.02.2019
comment
Привет снова. LocationState — это класс, содержащий public static final ints, и я внес это изменение после просмотра это видео. Да, я все еще использую getLocationResource(), потому что я не хотел бы раскрывать mLocationResource, но я бы предпочел использовать геттер. Кроме того, я хотел бы сохранить структуру Resource<T> и не превращать поле State в MutableLiveData. Что касается отсутствующего attrChange.onChange(), да, он отсутствует, потому что мне нужно запрашивать/удалять на основе проверенного состояния, и поскольку я не знаю двусторонней привязки данных, как следует из заголовка, я бросил его туда... - person Oliver; 05.02.2019
comment
... если бы вы могли отредактировать свой ответ, включая логику onCheckedChanged в MainViewModel.java в исходном вопросе, это было бы очень полезно ???? - person Oliver; 05.02.2019
comment
Да вы правы, не надо выставлять переменную. Но ваша текущая структура MutableLiveData‹Resource‹T›› не будет работать, потому что вы не можете инкапсулировать объекты LiveData внутри объектов LiveData и предоставлять внутренний объект LiveData макету привязки данных. Ваш текущий BindingAdapter в настоящее время будет запускаться только при изменении всего объекта Resource‹T›, но не при изменении состояния вашего объекта Resource‹T›, поэтому я предлагаю вам обернуть сам статус в объект MutableLiveData. Вы должны просто добавить attrChange.onChange() в свой метод onCheckedChanged. - person janosch; 05.02.2019
comment
У меня нет проблем с тем, что Resource<T> не меняется, так как я завернул его в MutableLiveData, где я могу использовать (все это в репозитории) mLocationResource.setValue(locationResource), все работает (если я использую хак, сообщите мне!). Что касается attrChange.onChange(), спасибо за редактирование вашего ответа, теперь я понял. Однако вы против того, чтобы помещать туда логику запроса/удаления: где я должен ее разместить? Каков ваш совет? Спасибо! - person Oliver; 05.02.2019

В конце концов я отказался от попыток преобразовать одностороннюю привязку данных в двустороннюю, но мне удалось немного упростить атрибут android:checked:
я заменил значение

"@{viewModel.getLocationResource}"

с

"@{viewModel.locationResource.state != State.STOPPED}"

и полностью удалил android:checked @BindingAdapter.

person Oliver    schedule 08.02.2019