Конфликт с EditText: наблюдатель к представлению + наблюдатель к MutableLiveData

У меня возникли проблемы с пониманием того, как парадигма Fragment + ViewModel работает с представлением, подобным EditText.

Это EditText, очевидно, он будет изменен в представлении (фрагменте). Но я также хочу иметь возможность изменять его в ViewModel: например. чтобы стереть его текст.

Вот код в классе Fragment:

public void onActivityCreated(@Nullable Bundle savedInstanceState) {
...
        comment = mViewModel.getComment();
        comment.observe(getViewLifecycleOwner(), new Observer<String>() {
            @Override
            public void onChanged(String s) {
                commentView.setText(s);
            }
        });
...
        commentView.addTextChangedListener(new TextWatcher() {
            @Override
            public void afterTextChanged(Editable s) {
                mViewModel.setComment(String.valueOf(s));
            }
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) { }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) { }
        });

Как видите, я установил наблюдатель, поэтому, когда я изменяю значение MutableLiveData, представление изменяется. И я устанавливаю наблюдатель, поэтому, когда я (при использовании приложения) изменяю значение представления, MutableLiveData изменяется.

Вот код класса ModelView:

public void addRegister() {
...
String comment = this.comment.getValue();
...
this.comment.setValue("");

Когда я запускаю приложение, ошибка не появляется, но оно зависает. Я думаю, из-за бесконечного цикла. Как мне подходить к EditTexts с этой парадигмой View + ViewModel? Что я не понимаю?

Заранее большое спасибо!


person DidacC    schedule 23.01.2020    source источник
comment
Где реализация getComment() ? Можете ли вы показать весь свой класс ViewModel   -  person Rafsanjani    schedule 23.01.2020
comment
@Rafsanjani getComment() просто возвращает комментарий. Вот фрагмент: paste.debian.net/1127266 и вот ViewModel: paste.debian.net/1127265   -  person DidacC    schedule 23.01.2020
comment
Не могли бы вы предоставить код класса ViewModel?   -  person Samir Spahic    schedule 23.01.2020
comment
@SamirSpahic Да, вот оно: paste.debian.net/1127265   -  person DidacC    schedule 23.01.2020
comment
Попробуйте изменить поля геттера на тип LiveData вместо MutableLiveData   -  person Samir Spahic    schedule 23.01.2020


Ответы (3)


в вашем наблюдателе для комментария liveData просто сначала отмените регистрацию TextWatcher, затем после setText из комментария liveData перерегистрируйте TextWatcher, все должно быть хорошо :)

person Devara    schedule 30.01.2020
comment
Что вы имеете в виду, отменив регистрацию TextWatcher? - person DidacC; 31.01.2020
comment
в настоящее время ваш код commentView.addTextChangedListener(anonymouseTextWatcherClass) . Вы можете сделать это, сначала инициализируйте объект textWatcher, например textWatcher = TextWatcher(), и сохраните его как переменную поля. Затем, когда вы собираетесь установить значение для своего EditText, сначала отмените регистрацию textWatcher, вызвав эту функцию commentView.removeTextChangedListener(textWatcher);, после чего вы можете безопасно установить значение, не запуская textWatcher. После того, как вы установили значение, снова зарегистрируйте свой textWatcher - person Devara; 03.02.2020
comment
Приятно слышать :) может быть, вы можете установить это как принятый ответ - person Devara; 06.02.2020

Для этого вы можете использовать двустороннюю привязку данных:

  • Когда пользователь вводит текст: текущие данные будут обновлены
  • Если вы программно установите значение текущих данных, содержимое EditText будет обновлено.

Вы должны иметь возможность удалить обоих прослушивателей в своей деятельности, так как привязка данных сделает это за вас.

построить.градле:

android {
    dataBinding {
        enabled = true
    }
}

макет:

  • добавить элемент <layout> на верхний уровень
  • определить переменную для вашей модели представления
  • подключите свой EditText к модели представления
<layout>
    <data>
        <variable
            name="viewModel"
            type="com.mycompany.AddRegisterViewModel" />
    </data>
    <EditText
                android:id="..."
                android:layout_width="..."
                android:layout_height="..."
                android:text="@={viewModel.getComment()}" />
</layout>

Фрагмент (извините, пример котлина):

  • Подключите поле viewModel в xml с помощью вашего объекта viewmodel:
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val binding: MyFragmentBinding = DataBindingUtil.inflate(inflater, R.layout.my_fragment, container, false)
        binding.setViewModel(myViewModel)

Обратите внимание, что вам нужен знак равенства, @=, чтобы иметь двустороннюю привязку данных. Если вы просто используете @{viewModel.getComment()}, то текст редактирования будет обновлен, если вы программно установите значение живых данных, но по-другому не получится.

Заметки:

  • Вы можете использовать ObservableField вместо MutableLiveData для привязки данных, если хотите.
  • Возможно, вы можете ссылаться на текущие данные в xml со ссылкой на поле вместо ссылки на метод, например @={viewModel.comment}

Ссылка: документация Android для двусторонней привязки данных: https://developer.android.com/topic/libraries/data-binding/two-way

person Carmen    schedule 23.01.2020
comment
Большое спасибо за ответ, я обязательно посмотрю на это. Хотя я читал несколько негативных отзывов о библиотеке привязки данных, в которых говорилось, что размещение логики или функций в статических файлах xml является плохой практикой. Что вы думаете? Является ли привязка данных Android стандартом в настоящее время? - person DidacC; 23.01.2020
comment
Вы должны избегать размещения какой-либо значительной логики в файлах xml. Но здесь, как типичный базовый пример привязки данных, на самом деле нет никакой логики: это просто сопоставление представления с полем модели представления (обычно я выставляю MutableLiveData как поля (kotlin vals), поэтому я обычно использую android:text="@={viewModel.comment}" вместо функции getComment()) - person Carmen; 23.01.2020

Поскольку принятый ответ не работал у меня во всех случаях (когда текст был изменен в ViewModel другими средствами, кроме самого EditText), и я также не хотел использовать привязку данных, я придумал следующее решение, где флаг отслеживает обновления, инициированные TextWatcher, и прерывает цикл при вызове наблюдателя:

Вот мой код на Котлине. Для деятельности:

class SecondActivity : AppCompatActivity() {

    /** Flag avoids endless loops from TextWatcher and observer */
    private var textChangedByListener = true
    private val viewModel by viewModels<SecondViewModel>()
    private lateinit var binding:SecondActivityBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = SecondActivityBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.editText.addTextChangedListener(object: TextWatcher {
            override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3:     Int) {            }
            override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {                }

            override fun afterTextChanged(editable: Editable?) {
                textChangedByListener = true
                viewModel.editText = editable.toString()
            }

        })
        viewModel.editTextLiveData.observe(this) { text -> setEditTextFromViewModel(text) }
    }

    private fun setEditTextFromViewModel(text: String?) {
        if (!textChangedByListener) {
            //text change was not initiated by the EditText itself, and
            //therefore EditText does not yet contain the new text.
            binding.editText.setText(text)
        } else {
            //Don't move that outside of else, because it would then
            //immediately overwrite the value set by TextWatcher
            //which is triggered by the above setText() call.
            textChangedByListener = false
        }
    }

}

И для полноты также ViewModel:

class SecondViewModel() : ViewModel()
{
    var editText: String
        get() {
            return editTextLiveData.value ?: "InitialLiveData"
        }
        set(value) {
            editTextLiveData.value = value
        }

    var editTextLiveData = MutableLiveData<String>()
}

На всякий случай, вы не знакомы с привязкой представления: вы можете заменить

binding.editText

с

findViewById(R.id.editTextId) as EditText.

person user2808624    schedule 13.03.2021