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

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

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

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

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

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

Что я ожидаю, так это, пункт (6)не должно случиться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 дважды, для одних и тех же данных?


Demo

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

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

enter image description here

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

Ответы [ 8 ]

0 голосов
/ 25 июня 2019

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

0 голосов
/ 22 мая 2018

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

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

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

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

Теперь, почему onChanged () вызывается только один раз в самый первый раз, когда представление активируется после запуска приложения?Это происходит потому, что в первый раз, когда код проверки версии LiveData выполняется в результате перехода STOPPED-> STARTED, данные в реальном времени никогда не были установлены на что-либо, и, таким образом, LiveData пропускает информирование наблюдателя.Последующие вызовы через этот путь к коду (см. AllowNotify () в LiveData.java) выполняются после того, как данные были установлены хотя бы один раз.

LiveData определяет, имеет ли наблюдатель устаревшие данные, сохраняя номер версии, который указывает, сколькораз данные были установлены.Он также записывает номер версии, последний раз отправленный клиенту.Когда новые данные установлены, LiveData может сравнивать эти версии, чтобы определить, оправдан ли вызов onChange ().

Ниже приведены номера версий во время вызовов кода проверки версии 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 (наблюдатель находится во фрагменте, который был уничтожен, когда пользователь вернулся к основному фрагменту).

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

0 голосов
/ 22 ноября 2018

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

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

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

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

0 голосов
/ 22 мая 2018

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

ViewModelProviders.of(getActivity())

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

Когда вы впервые открываете свою комнату TrashFragment, вы запрашиваете базу данных, и ваши 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);
            }

        }
    });
0 голосов
/ 21 мая 2018

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

Метод наблюдателей void onChanged(@Nullable T t) вызывается дважды. Все в порядке.

Первый раз, когда он вызывается при запуске. Второй раз он вызывается, как только Room загружает данные. Следовательно, при первом вызове объект LiveData все еще пуст. Он спроектирован таким образом по веским причинам.

Второй звонок

Давайте начнем со второго вызова, вашего пункта 7. Документация Room гласит:

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

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

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

Bottomline

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

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

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

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

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

Приложение

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

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

0 голосов
/ 18 мая 2018

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

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

Закрыть. Способ, которым LiveData<List<T>> предоставляет доступ к Room, заключается в том, что он создает 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 active .

// 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 - это недавно оцененный набор результатов из вашей базы данных, где вычисления были вызваны активацией активных данных из Room.


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

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

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

0 голосов
/ 18 мая 2018

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

Чтобы упростить воспроизведение и исследование, я немного отредактировал ваш проект. Вы можете найти обновленный проект здесь: 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, так и из-за новых Подписка наблюдателя. Может быть связано с тем, как ComputableLiveData переводит вычисления onActive () в Executor.

0 голосов
/ 17 мая 2018

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

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.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...