Почему LiveData observer запускается дважды для вновь присоединенного наблюдателя
мое понимание на LiveData заключается в том, что это вызовет наблюдателя на текущее изменение состояния данных, а не серию изменений состояния истории данных.
В настоящее время у меня есть MainFragment, которые выполняют Room операцию записи, чтобы изменить нерасшитые данные, на разбитые данные.
Я также другой TrashFragment, который наблюдает за разгромленными данными .
Рассмотрим следующий сценарий.
- в настоящее время существует 0 разгромленных данные .
MainFragment- текущий активный фрагмент.TrashFragmentеще не создан.
MainFragmentдобавлено 1 уничтоженные данные.- теперь есть 1 уничтоженные данные
- мы используем навигационный ящик, чтобы заменить
MainFragmentнаTrashFragment.
НаблюдательTrashFragmentсначала получитonChanged, С 0 уничтоженными данными
- опять же, наблюдатель
TrashFragmentво-вторых получитonChanged, С 1 разбитыми данными
Что из моего ожидание состоит в том, что пункт (6) не должен произойти. TrashFragment должны получать только последниеуничтоженные данные , что равно 1.
Вот мои коды
Мусор.java
public class TrashFragment extends Fragment {
@Override
public void onCreate(Bundle savedInstanceState) {
noteViewModel = ViewModelProviders.of(getActivity()).get(NoteViewModel.class);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
...
noteViewModel.getTrashedNotesLiveData().removeObservers(this);
noteViewModel.getTrashedNotesLiveData().observe(this, notesObserver);
MainFragment.java
public class MainFragment extends Fragment {
@Override
public void onCreate(Bundle savedInstanceState) {
noteViewModel = ViewModelProviders.of(getActivity()).get(NoteViewModel.class);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
...
noteViewModel.getNotesLiveData().removeObservers(this);
noteViewModel.getNotesLiveData().observe(this, notesObserver);
NoteViewModel .java
public class NoteViewModel extends ViewModel {
private final LiveData<List<Note>> notesLiveData;
private final LiveData<List<Note>> trashedNotesLiveData;
public LiveData<List<Note>> getNotesLiveData() {
return notesLiveData;
}
public LiveData<List<Note>> getTrashedNotesLiveData() {
return trashedNotesLiveData;
}
public NoteViewModel() {
notesLiveData = NoteplusRoomDatabase.instance().noteDao().getNotes();
trashedNotesLiveData = NoteplusRoomDatabase.instance().noteDao().getTrashedNotes();
}
}
Код, который имеет дело с комнатой
public enum NoteRepository {
INSTANCE;
public LiveData<List<Note>> getTrashedNotes() {
NoteDao noteDao = NoteplusRoomDatabase.instance().noteDao();
return noteDao.getTrashedNotes();
}
public LiveData<List<Note>> getNotes() {
NoteDao noteDao = NoteplusRoomDatabase.instance().noteDao();
return noteDao.getNotes();
}
}
@Dao
public abstract class NoteDao {
@Transaction
@Query("SELECT * FROM note where trashed = 0")
public abstract LiveData<List<Note>> getNotes();
@Transaction
@Query("SELECT * FROM note where trashed = 1")
public abstract LiveData<List<Note>> getTrashedNotes();
@Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract long insert(Note note);
}
@Database(
entities = {Note.class},
version = 1
)
public abstract class NoteplusRoomDatabase extends RoomDatabase {
private volatile static NoteplusRoomDatabase INSTANCE;
private static final String NAME = "noteplus";
public abstract NoteDao noteDao();
public static NoteplusRoomDatabase instance() {
if (INSTANCE == null) {
synchronized (NoteplusRoomDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
NoteplusApplication.instance(),
NoteplusRoomDatabase.class,
NAME
).build();
}
}
}
return INSTANCE;
}
}
Есть идеи, как я могу предотвратить получение onChanged дважды, для одних и тех же данных?
Демо
Я создал демонстрационный проект, чтобы продемонстрировать это проблема.
Как вы можете видеть, после того, как я выполнить операцию записи (нажать на добавить разгромили Примечание кнопка) в MainFragment, Когда я переключаюсь на TrashFragment, я ожидаю onChanged в TrashFragment будет вызываться только один раз. Однако его вызывают уже дважды.
Демонстрационный проект можно загрузить с сайта https://github.com/yccheok/live-data-problem
6 ответов:
Я раздвоил ваш проект и немного протестировал его. Из всего, что я могу сказать, вы обнаружили серьезную ошибку.
Чтобы облегчить воспроизведение и исследование, я немного отредактировал ваш проект. Вы можете найти обновленный проект здесь: https://github.com/techyourchance/live-data-problem . Я также открыл запрос на возврат в ваше РЕПО.
Чтобы убедиться, что это не останется незамеченным, я также открыл проблему в Google issue tracker:
Шаги к воспроизвести:
- убедитесь, что REPRODUCE_BUG имеет значение true в MainFragment
- установите приложение
- нажмите на кнопку "Добавить разбитую заметку"
- переключиться на TrashFragment
- обратите внимание, что была только одна форма уведомления LiveData с правильным значением
- переключиться на MainFragment
- нажмите на кнопку "Добавить разбитую заметку"
- переключиться на TrashFragment
- обратите внимание, что было два уведомления от LiveData, первое один с неправильным значением
Обратите внимание, что если вы установите REPRODUCE_BUG в false, то ошибка не будет воспроизводить. Это демонстрирует, что подписка на LiveData в MainFragment изменил поведение в TrashFragment.
Ожидаемый результат: только одно уведомление с правильным значением в любом случае. Никаких изменений в поведении из-за предыдущих подписок.
Дополнительная информация: я немного посмотрел на источники, и это выглядит так уведомления срабатывают из-за обоих LiveData активация и новое Подписка наблюдателя. Может быть связано с тем, как ComputableLiveData разгружает вычисления onActive () исполнителю.
Я внес только одно изменение в ваш код:
noteViewModel = ViewModelProviders.of(this).get(NoteViewModel.class);Вместо:
noteViewModel = ViewModelProviders.of(getActivity()).get(NoteViewModel.class);В методах
FragmentonCreate(Bundle). И теперь это работает без проблем.В вашей версии вы получили ссылку на
NoteViewModel, общую для обоих фрагментов (из действия).ViewModelбылObserverзарегистрирован в предыдущем фрагменте, я думаю. ПоэтомуLiveDataсохранил ссылку на обаObserver's (вMainFragmentиTrashFragment) и назвал оба значения.Таким образом, я предполагаю, что вывод может заключаться в том, что вы должны получить
ViewModelизViewModelProvidersиз:
FragmentвFragmentActivityвActivityКстати.
noteViewModel.getTrashedNotesLiveData().removeObservers(this);Не обязательно во фрагментах, однако я бы посоветовал поместить его в
onStop.
Я выхватил вилку Василия из вашей вилки вилки и сделал фактическую отладку, чтобы посмотреть, что произойдет.
Может быть связано с тем, как ComputableLiveData разгружает вычисления onActive () исполнителю.
Близко. Способ работы комнаты
LiveData<List<T>>expose заключается в том, что она создаетComputableLiveData, который отслеживает, был ли ваш набор данных недействителен внизу в комнате.trashedNotesLiveData = NoteplusRoomDatabase.instance().noteDao().getTrashedNotes();Поэтому, когда таблица
noteзаписывается, то InvalidationTracker привязывается к LiveData вызоветinvalidate(), когда произойдет запись.Теперь нам нужно знать, что@Override public LiveData<List<Note>> getNotes() { final String _sql = "SELECT * FROM note where trashed = 0"; final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0); return new ComputableLiveData<List<Note>>() { private Observer _observer; @Override protected List<Note> compute() { if (_observer == null) { _observer = new Observer("note") { @Override public void onInvalidated(@NonNull Set<String> tables) { invalidate(); } }; __db.getInvalidationTracker().addWeakObserver(_observer); }ComputableLiveData'Sinvalidate()будет фактически обновлять набор данных, если LiveData является активным.// invalidation check always happens on the main thread @VisibleForTesting final Runnable mInvalidationRunnable = new Runnable() { @MainThread @Override public void run() { boolean isActive = mLiveData.hasActiveObservers(); if (mInvalid.compareAndSet(false, true)) { if (isActive) { // <-- this check here is what's causing you headaches mExecutor.execute(mRefreshRunnable); } } } };Где
liveData.hasActiveObservers()- это:public boolean hasActiveObservers() { return mActiveCount > 0; }Таким образом,
refreshRunnableфактически выполняется только при наличии активного наблюдателя (afaik означает, что жизненный цикл, по крайней мере, запущен и наблюдает за живыми данными).
Это означает, что когда вы подписываетесь на TrashFragment, то происходит следующее ваши LiveData хранятся в Activity, поэтому они сохраняются даже тогда, когда TrashFragment исчез, и сохраняют Предыдущее значение.
Однако, когда вы открываете TrashFragment, то TrashFragment подписывается, LiveData становится активным, ComputableLiveData проверяет недействительность (что верно, поскольку он никогда не был повторно вычислен, потому что живые данные не были активны), вычисляет его асинхронно в фоновом потоке, и когда он завершен, значение публикуется.
Таким образом, вы получаете два обратного вызова потому что:
1.) первый вызов "onChanged" - это ранее сохраненное значение LiveData, сохраненное в ViewModel действия
2.) второй вызов "onChanged" - это вновь оцененный результирующий набор из вашей базы данных, где вычисление было инициировано тем, что живые данные из комнаты стали активными.
Так что технически это по замыслу. Если вы хотите убедиться, что вы получаете только" самое новое и самое большое " значение, то вы должны использовать фрагмент-область модель представления.
Вы также можете начать наблюдение в
onCreateView()и использоватьviewLifecycleдля жизненного цикла ваших LiveData (это новое дополнение, так что вам не нужно удалять наблюдателей вonDestroyView().Если важно, чтобы фрагмент видел последнее значение, даже если фрагмент не активен и не наблюдает его, то, поскольку ViewModel имеет область действия, вы можете зарегистрировать наблюдателя в действии, чтобы убедиться, что активный наблюдатель есть на вашем компьютере. LiveData.
Это не ошибка, это функция. Читайте почему!
Метод наблюдателей
void onChanged(@Nullable T t)вызывается дважды. Это прекрасно.Первый раз он вызывается при запуске. Второй раз он вызывается, как только комната загрузила данные. Следовательно, при первом вызове объект
LiveDataвсе еще пуст. Он разработан таким образом по уважительным причинам.Второй звонок
Давайте начнем со второго звонка, вашего пункта 7. В документацииRoomговорится:Сгенерированный код является объектом классаКомната генерирует весь необходимый код для обновления объекта LiveData когда база данных обновляется. Сгенерированный код выполняет запрос асинхронно в фоновом потоке, когда это необходимо.
ComputableLiveData, упомянутого в других публикациях. Он управляет объектомMutableLiveData. На этотLiveDataобъект он вызываетLiveData::postValue(T value), который затем вызываетLiveData::setValue(T value).
LiveData::setValue(T value)звонкиLiveData::dispatchingValue(@Nullable ObserverWrapper initiator). Это вызываетLiveData::considerNotify(ObserverWrapper observer)с оболочкой наблюдателя в качестве параметра. Это, наконец, призываетonChanged()к наблюдатель с загруженными данными в качестве параметра.Первый звонок
Теперь для первого звонка, ваша точка 6.Вы устанавливаете своих наблюдателей в рамках метода
onCreateView()hook. После этой точки жизненный цикл дважды меняет свое состояние, чтобы стать видимым,on startиon resume. Внутренний классLiveData::LifecycleBoundObserverуведомляется о таких изменениях состояния, поскольку он реализует интерфейсGenericLifecycleObserver, который содержит один метод с именемvoid onStateChanged(LifecycleOwner source, Lifecycle.Event event);.Этот метод вызывает
Все это происходит при определенных условиях. Я признаю, что не исследовал все условия внутри цепочки методов. Есть два изменения состояния, ноObserverWrapper::activeStateChanged(boolean newActive)какLifecycleBoundObserverрасширяетObserverWrapper. МетодactiveStateChangedвызываетdispatchingValue(), который в свою очередь вызываетLiveData::considerNotify(ObserverWrapper observer)с оболочкой наблюдателя в качестве параметра. Это, наконец, взывает к наблюдателю.onChanged()срабатывает только один раз, потому что условия проверяют такие вещи. Суть здесь в том, что существует цепочка методов, которая срабатывает при изменении жизненный цикл. Это отвечает за первый звонок.Нижняя линия
Я думаю, что с вашим кодом все в порядке. Это просто прекрасно, что наблюдатель призван к творению. Таким образом, он может заполнить себя исходными данными модели представления. Это то, что должен делать наблюдатель, даже если часть базы данных модели представления все еще пуста при первом уведомлении.Использование
Первое уведомление в основном сообщает, что модель представления готова к отображению, несмотря на это, он все еще не загружен данными из базовых баз данных. Второе уведомление сообщает, что эти данные готовы.
Когда вы думаете о медленных соединениях БД, это разумный подход. Вы можете получить и отобразить другие данные из модели представления, вызванной уведомлением, которое не приходит из базы данных.
У Android есть руководство, как справиться с медленной загрузкой базы данных. Они предлагают использовать заполнители. В этом примере разрыв настолько короток, что нет никаких причин идти на такое расширение.Приложение
Оба фрагмента используют там собственные объекты
Также подумайте о случае вращения. Данные модели представления не изменяются. Он не вызывает уведомления. Изменения состояния жизненного цикла сами по себе вызывают уведомление о новом новом представлении.ComputableLiveData, поэтому второй объект не предустановлен из первого фрагмента.
Вот что происходит под капотом:
ViewModelProviders.of(getActivity())Поскольку вы используете getActivity(), это сохраняет вашу NoteViewModel, пока область MainActivity жива, так же как и ваша trashedNotesLiveData.
При первом открытии TrashFragment room запрашивает БД, и ваш trashedNotesLiveData заполняется значением trashed (при первом открытии есть только один вызов onChange ()). Таким образом, это значение кэшируется в trashedNotesLiveData.
Затем вы переходите к главному фрагменту добавьте несколько испорченных заметок и снова перейдите к мусорному ящику. На этот раз вы сначала обслуживаетесь с кэшированным значением в trashedNotesLiveData в то время как номер делает асинхронный запрос. Когда запрос заканчивается вы привезли последнюю ценность. Вот почему вы получаете два вызова onChange ().
Таким образом, решение заключается в том, что вам нужно очистить trashedNotesLiveData перед открытием Мусорное ведро. Это можно сделать либо в вашем методе getTrashedNotesLiveData ().
public LiveData<List<Note>> getTrashedNotesLiveData() { return NoteplusRoomDatabase.instance().noteDao().getTrashedNotes(); }Или вы можете использовать что-то вроде этого SingleLiveEvent
Или вы можете использовать MediatorLiveData, который перехватывает номер, сгенерированный один и возвращает только отдельные значения.
final MediatorLiveData<T> distinctLiveData = new MediatorLiveData<>(); distinctLiveData.addSource(liveData, new Observer<T>() { private boolean initialized = false; private T lastObject = null; @Override public void onChanged(@Nullable T t) { if (!initialized) { initialized = true; lastObject = t; distinctLiveData.postValue(lastObject); } else if (t != null && !t.equals(lastObject)) { lastObject = t; distinctLiveData.postValue(lastObject); } } });
Я специально выяснил, почему он так себя ведет. Наблюдаемое поведение было onChanged () в мусорном фрагменте вызывается один раз при первой активации фрагмента после удаления заметки (при новом запуске приложения) и вызывается дважды, когда фрагмент активируется после удаления заметки.
Двойные вызовы происходят потому, что:
Вызов #1: фрагмент переходит между остановленным и запущенным в своем жизненном цикле, и это приводит к установке уведомления на Объект LiveData (в конце концов, это наблюдатель жизненного цикла!). Код LiveData вызывает обработчик onChanged (), поскольку он считает, что версия данных наблюдателя должна быть обновлена (подробнее об этом позже). Примечание: фактическое обновление данных все еще может быть отложено в этот момент, вызывая onChange() с устаревшими данными.
Вызов #2: следует в результате задания запроса LiveData (нормальный путь). Опять же объект LiveData думает, что версия данных наблюдателя является несвежий.
Теперь почему onChanged() вызывается только один раз в самый первый раз, когда представление активируется после запуска приложения? Дело в том, что при первом запуске кода проверки версии LiveData в результате перехода Stop- > STARTED текущие данные никогда не были заданы ни на что, и таким образом LiveData пропускает информирование наблюдателя. Последующие вызовы через этот путь кода (см. considerNotify () в LiveData.java) выполнить после того, как данные были установлены по крайней мере однажды.
LiveData определяет, есть ли у наблюдателя устаревшие данные, сохраняя номер версии, который указывает, сколько раз данные были установлены. Он также записывает номер версии, последний раз отправленный клиенту. При установке новых данных LiveData может сравнить эти версии, чтобы определить, оправдан ли вызов onChange ().Вот версия #s во время вызовов кода проверки версии LiveData для 4 вызовов:
Ver. Last Seen Ver. of the OnChanged() by Observer LiveData Called? -------------- --------------- ----------- 1 -1 (never set) -1 (never set) N 2 -1 0 Y 3 -1 0 Y 4 0 1 YЕсли вам интересно, почему версию в последний раз видел наблюдатель в вызове 3 это -1, хотя onChanged () был вызван 2-й раз, потому что наблюдатель в вызовах 1/2 отличается от наблюдателя в вызовах 3/4 (наблюдатель находится во фрагменте, который был уничтожен, когда пользователь вернулся к основному фрагменту).
Простой способ избежать путаницы в отношении ложных вызовов, которые происходят в результате переходов жизненного цикла, - это сохранить флаг во фрагменте intialized в false, который указывает, был ли фрагмент полностью возобновлен. Установить, что установите флажок true в обработчике onResume (), а затем проверьте, является ли этот флаг true в вашем обработчике onChanged (). Таким образом, вы можете быть уверены, что реагируете на события, которые произошли, потому что данные были действительно установлены.

Comments