为什么 LiveData 观察者被新连接的观察者触发两次

Why LiveData observer is being triggered twice for a newly attached observer

我对LiveData的理解是,它会触发观察者对数据的当前状态变化,而不是数据的一系列历史状态变化。

目前,我有一个 MainFragment,它执行 Room 写操作,将 非垃圾数据 更改为 垃圾数据数据.

我还有另一个 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 两次吗?


演示

我创建了一个演示项目来演示这个问题。

如你所见,我在 MainFragment 中执行写入操作(点击 ADD TRASHED NOTE 按钮)后,当我切换到 TrashFragment 时,我希望 TrashFragment 中的 onChanged 只会被调用一次。但是,它被调用了两次。

演示项目可以从https://github.com/yccheok/live-data-problem

下载

我只对您的代码进行了一项更改:

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

而不是:

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

FragmentonCreate(Bundle)方法中。现在它可以无缝运行。

在您的版本中,您获得了对两个片段(来自 Activity)共有的 NoteViewModel 的引用。我想 ViewModel 在之前的 Fragment 中注册了 Observer。因此 LiveData 保留对 Observer 的引用(在 MainFragmentTrashFragment 中)并调用这两个值。

所以我想结论可能是,你应该从 ViewModelProviders 获得 ViewModel 来自:

  • FragmentFragment
  • ActivityActivity

顺便说一句。

noteViewModel.getTrashedNotesLiveData().removeObservers(this);

在片段中不是必需的,但我建议将其放在 onStop.

我 fork 你的项目并测试了一下。据我所知,您发现了一个严重的错误。

为了便于复制和调查,我对您的项目进行了一些编辑。您可以在此处找到更新的项目:https://github.com/techyourchance/live-data-problem。我还向您的回购打开了一个拉取请求。

为了确保这不会被忽视,我还在 Google 的问题跟踪器中 opened an issue

Steps to reproduce:

  1. Ensure that REPRODUCE_BUG is set to true in MainFragment
  2. Install the app
  3. Click on "add trashed note" button
  4. Switch to TrashFragment
  5. Note that there was just one notification form LiveData with correct value
  6. Switch to MainFragment
  7. Click on "add trashed note" button
  8. Switch to TrashFragment
  9. Note that there were two notifications from LiveData, the first one with incorrect value

Note that if you set REPRODUCE_BUG to false then the bug doesn't reproduce. It demonstrates that subscription to LiveData in MainFragment changed the behavior in TrashFragment.

Expected result: Just one notification with correct value in any case. No change in behavior due to previous subscriptions.

More info: I looked at the sources a bit, and it looks like notifications being triggered due to both LiveData activation and new Observer subscription. Might be related to the way ComputableLiveData offloads onActive() computation to Executor.

我从你的叉子中抢走了 Vasiliy 的叉子,并进行了一些实际调试,看看会发生什么。

Might be related to the way ComputableLiveData offloads onActive() computation to Executor.

关闭。 Room 的 LiveData<List<T>> 公开工作方式是它创建一个 ComputableLiveData,它跟踪您的数据集是否已在 Room 下失效。

trashedNotesLiveData = NoteplusRoomDatabase.instance().noteDao().getTrashedNotes();

因此,当 note table 被写入时,绑定到 LiveData 的 InvalidationTracker 将在写入发生时调用 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);
        }

现在我们需要知道的是ComputableLiveDatainvalidate()实际上刷新数据集,如果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" 调用是先前保留在 Activity 的 ViewModel

中的 LiveData 的值

2.) 第二个 "onChanged" 调用是来自您的数据库的新评估结果集,其中计算是由来自 Room 的实时数据变为活动状态触发的。


从技术上讲,这是设计使然。如果您想确保只获得 "newest and greatest" 值,那么您应该使用片段范围的 ViewModel。

您可能还想在 onCreateView() 中开始观察,并在 LiveData 的生命周期中使用 viewLifecycle(这是一个新增功能,因此您无需在 onDestroyView().

如果片段看到最新值很重要,即使片段未处于活动状态且未观察到它,那么由于 ViewModel 是 Activity-scoped,您可能希望在Activity 以及确保您的 LiveData 上有一个活跃的观察者。

这不是错误,而是一项功能。阅读原因!

观察者方法 void onChanged(@Nullable T t) 被调用了两次。没关系。

第一次启动时调用。第二次是在 Room 加载数据后立即调用。因此,在第一次调用时 LiveData 对象仍然是空的。这样做是有充分理由的。

第二次通话

让我们从第二个调用开始,您的第 7 点。Room 的文档说:

Room generates all the necessary code to update the LiveData object when a database is updated. The generated code runs the query asynchronously on a background thread when needed.

生成的代码是其他帖子中提到的class 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() 挂钩方法中设置观察者。在这一点之后,生命周期将其状态更改两次以变得可见,on starton resume。内部 class LiveData::LifecycleBoundObserver 会在此类状态更改时收到通知,因为它实现了 GenericLifecycleObserver 接口,该接口包含一个名为 void onStateChanged(LifecycleOwner source, Lifecycle.Event event);.

的方法

此方法调用 ObserverWrapper::activeStateChanged(boolean newActive) 作为 LifecycleBoundObserver 扩展 ObserverWrapper。方法 activeStateChanged 调用 dispatchingValue(),后者又以观察者包装器作为参数调用 LiveData::considerNotify(ObserverWrapper observer)。这最终调用了 onChanged() 观察者。

所有这些都是在特定条件下发生的。我承认我没有调查方法链中的所有条件。有两次状态变化,但是 onChanged() 只触发一次,因为条件会检查这样的事情。

这里的底线是,有一个方法链,在生命周期发生变化时触发。这是负责第一次调用。

底线

我认为您的代码没有任何问题。很好,观察者在创建时被调用。所以它可以用视图模型的初始数据填充自己。这就是观察者应该做的,即使视图模型的数据库部分在第一次通知时仍然是空的。

用法

第一个通知基本上告诉视图模型已准备好显示,尽管它仍然没有加载来自底层数据库的数据。第二个通知告诉我们,此数据已准备就绪。

当您想到慢速数据库连接时,这是一种合理的方法。您可能希望从通知触发的视图模型中检索和显示其他数据,这些数据不是来自数据库。

Android 有一个如何处理数据库加载缓慢的指南。他们建议使用占位符。在这个例子中,差距很短,没有理由进行这样的扩展。

附录

两个片段都使用自己的 ComputableLiveData 个对象,这就是为什么没有从第一个片段预加载第二个对象的原因。

又想到轮换的情况。视图模型的数据不会改变。它不会触发通知。生命周期的状态变化单独触发新视图的通知。

这是幕后发生的事情:

ViewModelProviders.of(getActivity())

当您使用 getActivity() 时,这会保留您的 NoteViewModel,而 MainActivity 的范围仍然存在,您的 trashedNotesLiveData 也是如此。

当你第一次打开你的 TrashFragment 房间时,查询数据库并且你的 trashedNotesLiveData 填充了垃圾值(在第一次打开时只有一个 onChange() 调用)。所以这个值缓存在 trashedNotesLiveData.

然后你来到主片段添加一些垃圾笔记并再次转到TrashFragment。这次首先为您提供缓存值 trashedNotesLiveData while room 进行异步查询。当查询完成时你是 带来了最新的价值。这就是您收到两次 onChange() 调用的原因。

所以解决方案是您需要在打开之前清理 trashedNotesLiveData 垃圾片段。这可以在您的 getTrashedNotesLiveData() 方法中完成。

public LiveData<List<Note>> getTrashedNotesLiveData() {
    return NoteplusRoomDatabase.instance().noteDao().getTrashedNotes();
}

或者你可以使用类似这样的东西SingleLiveEvent

或者您可以使用 MediatorLiveData 来拦截 Room 生成的一个并且 returns 仅截取不同的值。

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 之间转换,这导致通知设置到 LiveData 对象(毕竟它是生命周期观察者!)。 LiveData 代码调用 onChanged() 处理程序,因为它认为观察者的数据版本需要更新(稍后会详细介绍)。注意:此时对数据的实际更新可能仍未决,导致使用陈旧数据调用 onChange()。

调用 #2:查询设置 LiveData(正常路径)后发生。 LiveData 对象再次认为观察者的数据版本已过时。

现在,为什么 onChanged() 仅在应用程序启动后第一次激活视图时被调用 一次?这是因为第一次执行 LiveData 版本检查代码是 STOPPED->STARTED 转换的结果,实时数据从未设置为任何值,因此 LiveData 跳过通知观察者。通过此代码路径的后续调用(请参阅 LiveData.java 中的 considerNotify())在数据至少设置一次后执行。

LiveData 通过保留指示数据设置次数的版本号来确定观察者是否有过时数据。它还记录了最后发送给客户端的版本号。设置新数据后,LiveData 可以比较这些版本以确定是否需要调用 onChange()。

这是 4 次调用的 LiveData 版本检查代码调用期间的版本 #s:

   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 中(观察者位于当用户返回主片段时被销毁的片段中)。

避免混淆由于生命周期转换而发生的虚假调用的一种简单方法是在片段中保留一个标志,该标志初始化为 false,指示片段是否已完全恢复。在 onResume() 处理程序中将该标志设置为 true,然后检查该标志在您的 onChanged() 处理程序中是否为真。这样你就可以确定你正在响应发生的事件,因为数据是真实设置的。

我不确定这个问题是否仍然存在。

但罪魁祸首是片段生命周期所有者内部的一个错误,用于在销毁视图时未清除片段的片段。

以前,您必须实施自己的 lyfecycle 所有者,以便在调用 onDestroyView 时将状态移动到 destroyed

如果您至少使用 API 28

进行定位和编译,则情况将不再如此

我的解决方案是在需要时开始观察数据,并在它检索到数据后立即删除观察者。这样你就不会得到双重触发。

我用过 SingleLiveEvent 并且有效。当fragment/activity 恢复或重新创建 SingleLiveEvent 时不抛出事件,仅当显式更改时

原因是在您的 .observe() 方法中,您传递了一个片段作为生命周期所有者。应该传的是fragment

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

我的回答不是针对此问题描述的解决方案,而是针对问题标题的。只是标题。

如果您的 LiveData<*> 观察者被多次调用,则意味着您正在多次调用 livedata.observe(...)。 发生了这种情况对我来说,就像我在一个方法中做 livedata.observe(...) 并且每当用户执行某些操作时调用此方法从而再次观察 liveData 一样。为了解决这个问题,我将 livedata.observe(...) 移至 onCreate() 生命周期方法。

场景是什么? 该应用程序有一个色样。当用户选择一种颜色时,我必须调用 API 来获取该颜色的产品图片。进行 API 调用并观察 onColorChanged() 中的实时数据也是如此。当用户选择新颜色时,将再次调用 onColorChanged() 从而再次观察实时数据变化。

编辑:另一个问题可能是传递 this 而不是 viewLifecycleOwner 在注册 LiveData Observer 时如以下另一个答案中所指出的那样。在片段中观察 LiveData 时始终使用 viewLifecycleOwner

如果您正在寻找一种解决方案来避免弹出从目标片段到原始片段的返回堆栈上的多个触发器

My solution is to observe the LiveData at onCreate() of the Fragment lifecycle with lifecycle owner as Activity and remove the observer at onDestroy() of the Fragment lifecycle

永远不要将观察者放在 loops/any 被注册两次的地方 。观察者应该放在 onViewCreated / onCreate / 任何只被调用一次的地方。只观察一次 !

下面是一个错误方式的例子:

for(int i=0;i<5;i++){
//THIS IS WRONG, DONT PUT IT INSIDE A LOOP / FUNCTION CALL
    yourviewModel.getYourLiveData().observe(getViewLifecycleOwner(), new Observer<Boolean>() {
            @Override
            public void onChanged(Boolean sBoolean) {
                 //SOME CODE 
            }
 );
}

将它放在某些被多次调用的函数下是错误的,例如:

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
observeMyViewModel();
observeMyViewModel();//THIS IS WRONG, CALLING IT MORE THAN ONCE
}

private void observeMyViewModel(){
  yourviewModel.getYourLiveData().observe(getViewLifecycleOwner(), new Observer<Boolean>() {
            @Override
            public void onChanged(Boolean sBoolean) {
                 //SOME CODE 
            }
 );
}

以下是在 kotlin 中解决此问题的方法:

在房间 DAO 中,使用 Flow<List<T>> 而不是 LiveData<List<T>>

因此,在 OP 的示例中,我们可以使用:

@Query("SELECT * FROM note where trashed = 1")
fun getTrashedNotes(): Flow<List<Note>>

而不是

@Query("SELECT * FROM note where trashed = 1")
fun getTrashedNotes(): LiveData<List<Note>>

然后在viewModel中,我们可以使用val list = dao.getTrashedNotes().asLiveData().

所以 OP 的 viewModel 将是:

val trashedNotesLiveData = NoteplusRoomDatabase.instance().noteDao().getTrashedNotes().asLiveData()

viewModel 之后的其余流程保持不变。

这有效的原因:

Flow 与 liveData 不同,它不了解生命周期。因此,即使未创建片段,流的值也将为 up-to 日期。