Почему наблюдатель LiveData запускается дважды для вновь прикрепленного наблюдателя

Мое понимание LiveData заключается в том, что оно вызывает наблюдателя при текущем изменении состояния данных, а не в изменении состояния данных в истории.

В настоящее время у меня есть MainFragment, который выполняет операцию записи в Room, чтобы изменить не поврежденные данные, чтобы обрезать данные.

Я также еще один TrashFragment, который наблюдает за разбитыми данными.

Рассмотрим следующий сценарий.

  1. В настоящее время имеются данные с обрывом.
  2. MainFragment - текущий активный фрагмент. TrashFragment еще не создан.
  3. MainFragment добавлено 1 MainFragment данные.
  4. Теперь есть 1 поврежденные данные
  5. Мы используем навигационный ящик, чтобы заменить MainFragment на TrashFragment.
  6. Наблюдатель TrashFragment сначала получит onChanged, с 0 поврежденными данными
  7. Опять же, наблюдатель TrashFragment во второй раз получит onChanged, с 1 поврежденными данными

То, что из моего ожидания, состоит в том, что пункт (6) не должен происходить. TrashFragment должен получать только самые последние данные об TrashFragment 1.

Здесь мои коды


TrashFragment.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 дважды, для одних и тех же данных?


демонстрация

Я создал демонстрационный проект, чтобы продемонстрировать эту проблему.

Как вы можете видеть, после выполнения операции записи (нажмите кнопку ADD TRASHED NOTE) в MainFragment, когда я переключусь на TrashFragment, я ожидаю, что onChanged в TrashFragment будет TrashFragment только один раз. Однако он называется дважды.

enter image description here

Демо-проект можно загрузить с https://github.com/yccheok/live-data-problem

Ответы

Ответ 1

Я разветкил ваш проект и немного его протестировал. Из всего, что я могу сказать, вы обнаружили серьезную ошибку.

Чтобы облегчить воспроизведение и исследование, я немного изменил ваш проект. Вы можете найти обновленный проект здесь: https://github.com/techyourchance/live-data-problem. Я также открыл запрос на возврат к вашему репо.

Чтобы убедиться, что это не остается незамеченным, я также открыл проблему в трекерах Google:

Действия по воспроизведению:

  1. Убедитесь, что для параметра REPRODUCE_BUG установлено значение true в MainFragment
  2. Установка приложения
  3. Нажмите кнопку "добавить обработанную записку"
  4. Переключиться на TrashFragment
  5. Обратите внимание, что существует только одна форма уведомления LiveData с правильным значением
  6. Переключиться на MainFragment
  7. Нажмите кнопку "добавить обработанную записку"
  8. Переключиться на TrashFragment
  9. Обратите внимание, что из LiveData было два уведомления, первое из которых с неправильным значением

Обратите внимание: если вы установите значение REPRODUCE_BUG на false, ошибка не будет воспроизводиться. Он демонстрирует, что подписка на LiveData в MainFragment изменила поведение в TrashFragment.

Ожидаемый результат: только одно уведомление с правильным значением в любом случае. Никаких изменений в поведении из-за предыдущих подписей.

Дополнительная информация: я немного посмотрел на источники, и похоже, что уведомления активируются из-за активации LiveData и новой подписки Observer. Может быть связано с тем, как ComputableLiveData выгружает onActive() вычисление для Executor.

Ответ 2

Я внес только одно изменение в ваш код:

noteViewModel = ViewModelProviders.of(this).get(NoteViewModel.class);

вместо:

noteViewModel = ViewModelProviders.of(getActivity()).get(NoteViewModel.class);

в методах Fragment onCreate(Bundle). И теперь это работает без проблем.

В вашей версии вы получили ссылку на NoteViewModel, общую для обоих фрагментов (из Activity). ViewModel зарегистрировал Observer в предыдущем фрагменте, я думаю. Поэтому LiveData сохранил ссылку на ObserverMainFragment и TrashFragment) и назвал оба значения.

Поэтому я могу сделать вывод, что вы должны получить ViewModel из ViewModelProviders из:

  • Fragment в Fragment
  • Activity в Activity

Btw.

noteViewModel.getTrashedNotesLiveData().removeObservers(this);

нет необходимости во фрагментах, однако я бы посоветовал поместить его в onStop.

Ответ 3

Я выхватил Василию вилку вилки вилки и сделал некоторую фактическую отладку, чтобы посмотреть, что произойдет.

Может быть связано с тем, как ComputableLiveData выгружает onActive() вычисление для Executor.

Близко. Способ Room LiveData<List<T>> показывает, что он создает ComputableLiveData, который отслеживает, был ли ваш набор данных недействительным под номером Room.

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 invalidate() фактически обновит набор данных, если активна 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 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, которое было сохранено в Activity ViewModel

2.) второй вызов "onChanged" - это недавно оцененный результирующий набор из вашей базы данных, где вычисление было вызвано тем, что активные данные из комнаты стали активными.


Так технически это по дизайну. Если вы хотите, чтобы вы получили только "новейшее и наибольшее" значение, вы должны использовать ViewModel с областью с фрагментами.

Вы также можете начать наблюдать в onCreateView() и использовать viewLifecycle для жизненного цикла вашей LiveData (это новое дополнение, так что вам не нужно удалять наблюдателей в onDestroyView().

Если важно, чтобы Фрагмент увидел последнее значение, даже если Фрагмент НЕ активен и НЕ наблюдает его, то, поскольку ViewModel является областью действия, вы можете захотеть зарегистрировать наблюдателя в Управлении, а также убедиться, что есть активного наблюдателя в вашей LiveData.

Ответ 4

Это не ошибка, это особенность. Читайте, почему!

Метод наблюдателей 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) с LiveData::considerNotify(ObserverWrapper observer) в качестве параметра. Это, наконец, вызывает onChanged() для наблюдателя с загруженными данными в качестве параметра.

Первый звонок

Теперь для первого вызова, ваш пункт 6.

Вы устанавливаете своих наблюдателей в onCreateView() hook onCreateView(). После этого момента жизненный цикл изменяет его состояние в два раза, чтобы оно стало видимым, 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) с LiveData::considerNotify(ObserverWrapper observer) в качестве параметра. Это, наконец, вызывает onChanged() для наблюдателя.

Все это происходит при определенных условиях. Я признаю, что не исследовал все условия в цепочке методов. Есть два изменения состояния, но onChanged() запускается только один раз, потому что условия проверяют такие вещи.

Суть здесь заключается в том, что существует цепочка методов, которая запускается при изменении жизненного цикла. Это отвечает за первый звонок.

Нижняя линия

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

использование

Первое уведомление в основном говорит о том, что модель представления готова для отображения, несмотря на то, что она еще не загружена данными из базовых баз данных. Второе уведомление сообщает, что эти данные готовы.

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

Android имеет руководство, как бороться с медленной загрузкой базы данных. Они предлагают использовать заполнители. В этом примере разрыв является настолько коротким, что нет никаких оснований идти в таком направлении.

аппендикс

Оба фрагмента используют собственные объекты ComputableLiveData, поэтому второй объект не предварительно загружается из первого фрагмента.

Также подумайте о случае вращения. Данные модели просмотра не меняются. Он не вызывает уведомления. Изменения состояния всего жизненного цикла запускают уведомление о новом новом представлении.

Ответ 5

Это то, что происходит под капотом:

ViewModelProviders.of(getActivity())

Поскольку вы используете getActivity(), это сохраняет ваш NoteViewModel, пока область MainActivity не активна, поэтому ваш trashedNotesLiveData.

Когда вы сначала открываете свои запросы в комнате TrashFragment, db и ваш trashedNotesLiveData заполняется значением trashed (при первом открытии есть только один вызов onChange()). Таким образом, это значение кэшируется в trashedNotesLiveData.

Затем вы переходите к основному фрагменту, добавляете несколько разбросанных нот и снова отправляетесь в TrashFragment. На этот раз вам сначала подают кешированное значение в trashedNotesLiveData, тогда как комната делает асинхронный запрос. Когда запрос заканчивается, вы получаете последнее значение. Вот почему вы получаете два вызова onChange().

Таким образом, решение - вам нужно очистить trashedNotesLiveData, прежде чем открывать TrashFragment. Это можно сделать либо в методе 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);
            }

        }
    });

Ответ 6

Я узнал, почему он действует так, как есть. Наблюдаемое поведение было onChanged() в фрагменте мусора вызывается один раз при первом включении фрагмента после извлечения заметки (при запуске нового приложения) и дважды вызывается, когда фрагмент активируется после того, как записка будет стерта.

Двойные вызовы происходят потому, что:

Вызов # 1: фрагмент переходит из STOPPED и STARTED в его lifecyle, и это приводит к тому, что уведомление должно быть установлено в объект LiveData (это наблюдатель жизненного цикла после всех!). Код LiveData вызывает обработчик onChanged(), поскольку он думает, что версия данных наблюдателя нуждается в обновлении (подробнее об этом позже). Примечание: фактическое обновление данных все еще может быть отложено в этой точке, в результате чего onChange() будет вызван со старыми данными.

Вызов # 2: Обеспечивает в результате запроса значение LiveData (обычный путь). Опять же объект LiveData считает, что версия данных наблюдателя устарела.

Теперь почему OnChanged() только один раз дозвонились в первый раз вид активируется после запуска приложения? Это связано с тем, что первый раз, когда код проверки версии LiveData выполняется в результате перехода STOPPED-> STARTED, данные в реальном времени никогда не были установлены ни на что, и, таким образом, LiveData пропускает информацию о наблюдателе. Последующие вызовы через этот путь кода (см. ОписаниеNotify() в 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() был вызван во второй раз вокруг нее, потому что наблюдатель в вызовах 1/2 является другим наблюдателем, чем тот, который в вызовах 3/4 (наблюдатель находится во фрагменте, который был разрушен, когда пользователь вернулся к основному фрагменту).

Легкий способ избежать путаницы в отношении ложных вызовов, которые происходят в результате переходов жизненного цикла, заключается в том, чтобы сохранить флаг в фрагменте intialized до false, который указывает, был ли полностью восстановлен фрагмент. Установите этот флаг в true в обработчике onResume(), затем проверьте, действительно ли этот флаг является истинным в вашем обработчике onChanged(). Таким образом, вы можете быть уверены, что реагируете на события, которые произошли, потому что данные действительно были установлены.

Ответ 7

Я не уверен, что эта проблема все еще активна.

Но основным виновником была ошибка в владельце фрагмента жизненного цикла для фрагментов, которая не была очищена при уничтожении представления.

Ранее вам приходилось реализовывать своего собственного владельца lyfecycle, который переводил бы состояние в destroyed при onDestroyView.

Это больше не должно иметь место, если вы нацеливаетесь и компилируете хотя бы с API 28

Ответ 8

Причина в том, что в вашем методе .observe() вы передали фрагмент как владелец жизненного цикла. То, что должно было быть передано, является объектом viewLifecycleOwner фрагмента

viewModel.livedata.observe(viewLifecycleOwner, Observer {
        // Do your routine here
    })

Ответ 9

Я использовал SingleLiveEvent и работает. Когда фрагмент/действие возобновляется или воссоздается, SingleLiveEvent не генерирует событие, только когда явно изменяется

Ответ 10

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