繁体   English   中英

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

[英]Why LiveData observer is being triggered twice for a newly attached observer

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

目前,我有一个MainFragment ,它执行Room写操作,将non-trashed data更改为 trashed data

我还有另一个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按钮)后,当我切换到MainFragment时,我希望TrashFragment中的onChanged TrashFragment被调用一次。 但是,它被调用了两次。

在此处输入图像描述

Demo工程可以在https://github.com/yccheok/live-data-problem下载

我只在您的代码中引入了一项更改:

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

代替:

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

FragmentonCreate(Bundle)方法中。 现在它可以无缝地工作。

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

所以我想结论可能是,你应该从ViewModelProviders获取ViewModel

  • Fragment中的Fragment
  • Activity中的Activity

顺便提一句。

noteViewModel.getTrashedNotesLiveData().removeObservers(this);

在 Fragments 中不是必需的,但是我建议将它放在onStop

我分叉了你的项目并对其进行了一些测试。 据我所知,您发现了一个严重的错误。

为了使复制和调查更容易,我对您的项目进行了一些编辑。 你可以在这里找到更新的项目: https : //github.com/techyourchance/live-data-problem 我还向您的 repo 发起了拉取请求。

为了确保这不会被忽视,我还在 Google 的问题跟踪器中打开了一个问题

重现步骤:

  1. 确保在 MainFragment 中将 REPRODUCE_BUG 设置为 true
  2. 安装应用
  3. 点击“添加垃圾笔记”按钮
  4. 切换到 TrashFragment
  5. 请注意,只有一个具有正确值的通知表单 LiveData
  6. 切换到 MainFragment
  7. 点击“添加垃圾笔记”按钮
  8. 切换到 TrashFragment
  9. 请注意,LiveData 有两个通知,第一个值不正确

请注意,如果您将 REPRODUCE_BUG 设置为 false,则该错误不会重现。 它表明在 MainFragment 中订阅 LiveData 改变了 TrashFragment 中的行为。

预期结果:在任何情况下都只有一个具有正确值的通知。 由于以前的订阅,行为没有变化。

更多信息:我查看了一些来源,看起来由于 LiveData 激活和新的 Observer 订阅而触发了通知。 可能与 ComputableLiveData 将 onActive() 计算卸载到 Executor 的方式有关。

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

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

这不是一个错误,这是一个功能。 阅读为什么!

观察者方法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()钩子方法中设置您的观察者。 在这一点之后,生命周期将其状态更改为两次, on starton resume可见。 内部类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对象,这就是为什么第二个对象不是从第一个片段预加载的。

还要考虑旋转的情况。 视图模型的数据不会改变。 它不会触发通知。 生命周期的状态变化单独触发新视图的通知。

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

可能与 ComputableLiveData 将 onActive() 计算卸载到 Executor 的方式有关。

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

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

因此,当note表被写入时,绑定到 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 是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”调用是之前在 Activity 的 ViewModel 中保持活动状态的 LiveData 的保留值

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


所以从技术上讲,这是设计使然。 如果您想确保只获得“最新和最大”的值,那么您应该使用片段范围的 ViewModel。

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

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

这是幕后发生的事情:

ViewModelProviders.of(getActivity())

当您使用getActivity() 时,这会保留您的 NoteViewModel,而 MainActivity 的范围是活动的,因此您的trashedNotesLiveData 也是活动的。

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

然后你来到主片段添加一些垃圾笔记并再次转到 TrashFragment。 这一次,当 room 进行异步查询时,您首先会使用trashedNotesLiveData 中的缓存值。 查询完成后,您将获得最新值。 这就是为什么您会收到两个 onChange() 调用。

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

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

或者你可以使用这样的SingleLiveEvent

或者您可以使用 MediatorLiveData 拦截 Room 生成的一个并仅返回不同的值。

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

        }
    });

我的回答不是这个问题描述的解决方案,而是问题标题的解决方案。 只是标题。

如果您的 LiveData<*> 观察者被多次调用,则意味着您多次调用 livedata.observe(...) 。 这发生在我身上,因为我在一个方法中执行 livedata.observe(...) 并且在用户执行某些操作时调用此方法,从而再次观察 liveData。 为了解决这个问题,我将 livedata.observe(...) 移动到 onCreate() 生命周期方法。

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

永远不要将观察者放在循环中/任何注册两次的地方 观察者应该放在 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 
            }
 );
}

我特别发现了它为什么会这样。 观察到的行为是垃圾片段中的 onChanged() 在删除笔记后第一次激活片段时调用一次(在新的应用程序启动时),并在删除笔记后激活片段时调用两次。

双重调用的发生是因为:

调用 #1:片段在其生命周期中在 STOPPED 和 STARTED 之间转换,这会导致将通知设置为 LiveData 对象(毕竟它是生命周期观察者!)。 LiveData 代码调用 onChanged() 处理程序,因为它认为需要更新观察者的数据版本(稍后会详细介绍)。 注意:此时对数据的实际更新可能仍处于挂起状态,导致 onChange() 使用陈旧数据被调用。

调用 #2:作为查询设置 LiveData(正常路径)的结果而发生。 LiveData 对象再次认为观察者的数据版本是陈旧的。

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

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() 处理程序中检查该标志是否为 true。 这样你就可以确定你正在响应发生的事件,因为数据是真正设置的。

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

我的解决方案是在 Fragment 生命周期的 onCreate() 处观察 LiveData,生命周期所有者为 Activity,并在 Fragment 生命周期的 onDestroy() 处移除观察者

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

但主要的肇事者是片段生命周期所有者内部的一个错误,该错误在视图被销毁时未清除。

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

如果您至少使用 API 28 进行定位和编译,则情况不再如此

我使用了SingleLiveEvent并且有效。 当片段/活动被恢复或重新创建时 SingleLiveEvent 不会抛出事件,只有在显式更改时

以下是在 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 之后的流程 rest 保持不变。

这样做的原因:

Flow 与 liveData 不同,它不了解生命周期。 因此,即使未创建片段,流的值也将是最新的。

我的解决方案只是在需要时开始观察数据,并在检索到数据后立即删除观察者。 您不会以这种方式获得双重触发。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM