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

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

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

я тоже другой 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 дважды, для одних и тех же данных?


демо

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

как вы можете видеть, после выполнения операции записи (нажмите на ДОБАВИТЬ TRASHED ПРИМЕЧАНИЕ) в MainFragment, когда я переключаюсь на TrashFragment, Я жду onChanged на TrashFragment будет вызываться только однажды. Однако он вызывается дважды.

enter image description here

демо-проект можно скачать с https://github.com/yccheok/live-data-problem

6 ответов


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

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

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

воспроизведение:

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

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

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

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


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

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

вместо:

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

на Fragment ' s onCreate(Bundle) методы. И теперь он работает без проблем.

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

так Я думаю, вывод может быть, что вы должны получить ViewModel С ViewModelProviders from:

  • Fragment на Fragment
  • Activity на Activity

кстати.

noteViewModel.getTrashedNotesLiveData().removeObservers(this);

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


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

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

закрыть. Путь номер LiveData<List<T>> expose works заключается в том, что он создает 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 ' s 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'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" - это недавно оцененный результирующий набор из вашей базы данных, где вычисление было инициировано тем, что живые данные из комнаты стали активными.


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

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

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


это не ошибка, это функция. Читайте почему!

наблюдатели метод void onChanged(@Nullable T t) вызывается дважды. Все нормально.

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

второй вызов

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

Room генерирует весь необходимый код для обновления объекта LiveData при обновлении базы данных. Созданный код выполняет запрос асинхронно в фоновом потоке, когда это необходимо.

сгенерированный код является объектом класса ComputableLiveData упоминается в других публикациях. Он управляет


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

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);
            }

        }
    });

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

двойной звонки так:

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

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

теперь почему onChanged () вызывается только после самый первый раз, когда представление активируется после запуска приложения? Это потому, что в первый раз, когда код проверки версии LiveData выполняется в результате перехода STOPPED->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 (). Таким образом, вы можете быть уверены, что реагируете на события, которые произошли, потому что данные были действительно установлены.