为什么对于一个新连接的观察者,LiveData观察者会被触发两次?

94 人关注

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

目前,我有一个 MainFragment ,它执行 Room 的写操作,来改变 非废弃的数据 , to 废弃的数据 .

我也是另一个 TrashFragment ,它观察到的是 废弃的数据 .

请考虑以下情况。

  • There are currently 0 废弃的数据 .
  • MainFragment is the current active fragment. TrashFragment is not created yet.
  • MainFragment added 1 废弃的数据 .
  • Now, there are 1 废弃的数据
  • We use navigation drawer, to replace MainFragment with TrashFragment .
  • TrashFragment 's observer will first receive onChanged , with 0 废弃的数据
  • Again, TrashFragment 's observer will secondly receive onChanged , with 1 废弃的数据
  • 出乎我意料的是,第(6)项不应该发生。 TrashFragment 应该只收到最新的 废弃的数据 , which is 1.

    Here's my code:

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

    Code which deals with Room

    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();
    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,
                        ).build();
            return INSTANCE;
    

    有什么办法可以防止我对同一个数据收到onChanged两次?

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

    正如你所看到的,在我执行写操作后(点击添加废弃的笔记MainFragment中,当我切换到TrashFragment时,我期望TrashFragment中的onChanged将只被调用一次。然而,它却被调用了两次。

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

    5 个评论
    LiveData 立即传递最后一个值(如果之前发布了一个值),加上未来的变化。 getTrashedNotesLiveData() 的实现是什么?你是否从Room返回 LiveData ?你是否使用RxJava和 LiveDataReactiveStreams ?这是一个自定义的 LiveData 实现吗?还是别的什么?
    只是一个来自Room的直接的LiveData。我已经更新了我的问题,以提供更多信息。因为在我们切换到 TrashFragment 之前,数据库应该写有11个废弃的数据。我不知道为什么TrashFragment中的观察者会先收到10个废弃的数据(旧快照),然后是11个废弃的数据(最新快照)。
    我们可以通过修改我们的 ViewModel 来 "防止 "这种情况的发生。 gist.github.com/yccheok/4bb6539c93fa39cf7dc7f08f0752d232 .每当在 TrashFragment MainFragment 之间切换之前,我们会在 ViewModel 中调用 init 。然而,我们还不想走这条路,直到我们理解这个问题。
    你好 @CommonsWare ,我已经创建了一个演示项目来演示这个问题。不知道你是否愿意指出,代码的哪一部分出了问题?谢谢你。
    你可以把这个样本应用程序的问题归档,作为可能出现问题的证据。我会去掉 removeObservers() 的调用,因为它们在现实世界中应该是不需要的,而且绝对不是再现问题所需要的。也就是说,把演示缩减到最低限度来说明问题。感觉你一开始得到的是一个缓存的值,然后才是真实的值,虽然我不知道为什么。
    android
    android-architecture-components
    android-livedata
    Cheok Yan Cheng
    Cheok Yan Cheng
    发布于 2018-05-08
    14 个回答
    R. Zagórski
    R. Zagórski
    发布于 2019-06-25
    已采纳
    0 人赞同

    我只对你的代码做了一个改动。

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

    FragmentonCreate(Bundle)方法。而现在,它的工作是无缝的。

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

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

  • Fragment in Fragment
  • Activity in Activity
  • noteViewModel.getTrashedNotesLiveData().removeObservers(this);

    在Fragments中没有必要,但我建议把它放在onStop中。

    但如果你看一下官方的教程。 developer.android.com/topic/libraries/architecture/... 他们建议对 ViewModelProviders.of 使用活动,而对 observe 使用片段。
    R. Zagórski
    安卓官方文档提出了理想的案例,有时就是错误的。我不是说这是一个错误或不是,而是帮助防止这种情况。
    建议的解决方案是错误的,只要你会收到不想要的 onChanged() 事件。假设你已经在 TrashFragment 里面呆过一次,现在你已经浏览到了 HomeFragment 。只要你启动另一个数据库变化,那么 TrashFragment 中的订阅就会被调用,因为你提供了一个错误的 LifecycleOwner (活动仍然是活的,而片段不是),因此你违反了 LiveData 的整体目的- 在屏幕实际处于活动状态时获取数据。
    Cheok Yan Cheng指出的文档,事实上建议在片段之间共享视图模型的选项。但其目的是不同的。正如我在回答中指出的,我认为两步加载不是一个需要解决的bug,而是一个解决DB连接缓慢和旋转等问题的功能。
    谢谢你,我已经找了很多天了。
    Vasiliy
    Vasiliy
    发布于 2019-06-25
    0 人赞同

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

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

    为了确保这一点不被忽视,我还 开通了一个问题 在谷歌的问题跟踪器中。

    Steps to reproduce:

  • Ensure that REPRODUCE_BUG is set to true in MainFragment
  • Install the app
  • Click on "add trashed note" button
  • Switch to TrashFragment
  • Note that there was just one notification form LiveData with correct value
  • Switch to MainFragment
  • Click on "add trashed note" button
  • Switch to TrashFragment
  • Note that there were two notifications from LiveData, the first one with incorrect value
  • 注意,如果你把REPRODUCE_BUG设置为false,那么这个错误就不会 重现。它证明了在 的订阅改变了TrashFragment的行为。

    预期的结果。在任何情况下,只有一个具有正确值的通知。 没有因为以前的订阅而改变行为。

    更多信息。我稍微看了一下资料,看起来像是 通知被触发的原因是LiveData的激活和新的 观察者订阅。可能与ComputableLiveData的方式有关 将onActive()的计算卸载给了Executor。

    谢谢。你的修改使这个错误更加明显。注意第7步,如果我们把 Click on "add trashed note" button 改为 Click on "add trashed note" button 1 or multiple times ,它将变得更加明显。
    刚看到你对票据的评论。在提交另一个票据之前,我只审查了开放的问题,因为我从来没有想过,像这样严重的事情可以如此轻易地被驳回。你做得很好,你的项目清楚地证明了这个问题。我不知道为什么他们甚至懒得去检查。
    一年过去了,我看到使用奥利奥,这个问题仍然存在。
    JoM
    JoM
    发布于 2019-06-25
    0 人赞同

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

    viewModel.livedata.observe(viewLifecycleOwner, Observer {
            // Do your routine here
        
    murt
    这就是我的问题。当我回到上一个片段时,我又在观察视图,而这个片段并没有被破坏。 同样有效的解决方案是在onViewDestory中移除Observer。
    @murt viewLifecycleOwner让观察者/LiveData意识到生命周期,所以观察者将被自动取消注册,这避免了在onDestroyView方法中手动取消注册的需要。
    在片段中做了 flowWithLifecycle(viewLifecycleOwner.lifecycle) .launchIn(viewLifecycleOwner.lifecycleScope) 的流程后,我的问题得到了解决。在没有 viewLifecycleOwner 的情况下,当我来回走动时,它的观察结果越来越多。
    Blcknx
    Blcknx
    发布于 2019-06-25
    0 人赞同

    It's not a bug, it's a feature. Read why!

    观察员的方法 void onChanged(@Nullable T t) 被调用了两次。这很好。

    第一次是在启动时被调用。第二次则是在Room加载完数据后立即调用。因此,在第一次调用时, LiveData 对象仍然是空的。这样设计是有原因的。

    Second call

    让我们从第二个电话开始,即你的第7点。替换代码2】的文件说。

    当数据库更新时,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() ,并以加载的数据为参数。

    First call

    现在是第一个电话,你的第6点。

    你在 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) ,并将观察者包装器作为参数。最后在观察者身上调用 onChanged()

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

    这里的底线是,有一连串的方法,在生命周期的变化中被触发。这是对第一次调用的负责。

    Bottomline

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

    Usage

    第一个通知基本上告诉我们,视图模型已经准备好显示,尽管它还没有从底层数据库加载数据。第二个通知告诉我们,这些数据已经准备好了。

    当你考虑到缓慢的数据库连接时,这是一个合理的方法。你可能想从通知触发的视图模型中检索和显示其他数据,这些数据并不来自数据库。

    安卓系统有一个如何处理缓慢的数据库加载的指导原则。他们建议使用占位符。在这个例子中,差距很短,所以没有理由去做这样的事情。

    Appendix

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

    也要考虑到旋转的情况。视图模型的数据并没有改变。它不会触发一个通知。只有生命周期的状态变化才会触发新的新视图的通知。

    替换代码0我认为,如果我被订阅了一个数据源,我应该得到它的最新状态。你的陈述告诉我们,得到一些垃圾(实际上与现实不相符的东西)是可以的,只要忽略这些垃圾。我认为这应该被认为是一个故障。
    你问的是,对 onChanged() 的第一个通知的调用是否会产生一个空列表。 事实上,如果Rooms还没有被加载,可能会出现这种情况。那么视图模型在初始通知时仍然是空的,你会显示一个空列表。我不会说这是垃圾。这只是它的实现方式。 一旦房间完成了它的工作,它就会触发它的通知并更新列表。如果你看演示,更新的速度非常快,你甚至看不到空列表的通知。
    显然,LiveData背后的概念是,你会在两个场合得到通知,一是数据的变化,二是生命周期的状态变化。
    说实话?我认为可以用MediatorLiveDtaa来覆盖内置行为。我不知道如果我写了它,会不会得到500的声誉,笑。
    傻子。宽限期在7小时后结束。但坦率地说,我认为它做得很好,因为它是。对我来说,当数据库仍在加载时,提前收到通知并没有什么不妥。要检查它是否仍然是空的很容易。
    EpicPandaForce
    EpicPandaForce
    发布于 2019-06-25
    0 人赞同

    我把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是积极.

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

    Where liveData.hasActiveObservers() is:

    public boolean hasActiveObservers() {
        return mActiveCount > 0;
    

    So refreshRunnable 实际上 runs only if there is an 积极 observer (afaik means lifecycle is at least started, and observes the live data).

    这意味着,当你在TrashFragment中订阅时,那么发生的情况是,你的LiveData被存储在Activity中,所以即使TrashFragment消失了,它也会保持活力,并保留之前的值。

    However, when you open TrashFragment, then TrashFragment subscribes, LiveData becomes 积极, ComputableLiveData checks for invalidation (which is true as it was never re-computed because the live data was not 积极), computes it asynchronously on background thread, and when it is complete, the value is posted.

    所以你得到两个回调,因为。

    1.) 第一个 "onChanged "调用是先前保留的LiveData的值,在Activity的ViewModel中保持活力。

    2.) second "onChanged" call is the newly evaluated result set from your database, where the computation was triggered by that the live data from Room became 积极.

    所以从技术上讲,这是设计上的问题。如果你想确保你只得到 "最新和最大 "的价值,那么你应该使用一个片段范围的ViewModel。

    你可能还想在onCreateView()中开始观察,并在你的LiveData的生命周期中使用viewLifecycle(这是一个新增加的内容,这样你就不需要在onDestroyView()中删除观察者。

    If it is important that the Fragment sees the latest value even when the Fragment is NOT 积极 and NOT observing it, then as the ViewModel is Activity-scoped, you might want to register an observer in the Activity as well to ensure that there is an 积极 observer on your LiveData.

    我不认为SO是进行这种讨论的地方,但官方的问题追踪器也不是。所以,我就在这里写。我认为你的结论下得太快了。 1) by design 的事实并不意味着它不是一个bug 2) 它已经在 onCreateView 中观察到了(并不重要) 3)3) viewLifecycle 与此无关 4) 你的最后一段基本上是对一个bug的黑客解决方法 5) OP,我和@CommonsWare怀疑这是一个bug,所以我开了票让googlers来检查。 - 在这个具体案例中,你的意见是不成熟的,IMHO
    EpicPandaForce
    不, viewLifecycle 真的与此无关。只是,只有当至少有一个活跃的观察者时,Room的LiveData才会被重新评估。"如果一棵树在森林中倒下,而没有人在那里听到它,它会发出声音吗?"就ComputableLiveData而言,显然不会。你通常想在RecyclerView + ListAdapter中显示数据,所以它会用 submitList 来处理相应的变化。
    我们没有必要争论这是否是错误。我不明白一个错误状态的虚假通知怎么会是一个严重的错误,但让我们让googlers彻底检查这个问题,让社区作出反应。
    ^创建一个额外的LiveData,由MutableLiveData支持,目的是为了跟踪操作的状态。将它的值设置为 "已完成"(无论你想如何表示这种状态),然后将它的状态设置为 "不相关"(无论你想如何表示这种状态,我通常使用null,观察者会简单地忽略null),然后在订阅观察者时,初始值将是 "不相关"。
    这就是我所面临的100%的问题,它导致我从这里保存的数据与从活动中重新加载的onResume数据发生冲突。简而言之,这个 feature 导致了一个竞赛条件,我不得不忽略保存这个回调的结果,而只是让它运行两次。它设置了两次UI,但听起来它只是 the way it is ,现在是。
    Ramakrishna Joshi
    Ramakrishna Joshi
    发布于 2019-06-25
    0 人赞同

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

    If your observer for a LiveData<*> is getting called multiple times then it means you are calling livedata.observe(...) multiple times. 这发生在我身上,因为我在一个方法中做livedata.observe(...),每当用户做一些动作时就会调用这个方法,从而再次观察到liveData。为了解决这个问题,我把livedata.observe(...)移到了onCreate()生命周期方法中。

    那是什么情况? 该应用程序有一个颜色对照表。当用户选择一种颜色时,我必须调用API来获取该颜色的产品图片。因此,在进行API调用时,观察到 onColorChanged() 中的livedata。当用户选择一个新的颜色时, onColorChanged() 将被再次调用,从而再次观察livedata的变化。

    编辑:另一个问题可能是通过 this 而不是 viewLifecycleOwner 正如下面的另一个答案所指出的那样,在注册LiveData Observer的时候,必须使用 "S"。始终使用 viewLifecycleOwner 观察片段中的LiveData时。

    +1的解释。有没有参考一下为什么。"在观察片段中的LiveData时,总是使用viewLifecycleOwner"。?
    Ornolis Vázquez Thompson
    Ornolis Vázquez Thompson
    发布于 2019-06-25
    0 人赞同

    I used SingleLiveEvent 并发挥作用。当片段/活动恢复或重新创建时,SingleLiveEvent不抛出事件,只有在明确改变时才抛出。

    你是最棒的。我当时快死了。
    Reejesh PK
    Reejesh PK
    发布于 2019-06-25
    0 人赞同

    不要把观察者放在循环中/任何会被重复注册的地方。 .观察者应该被放在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 
        
    katharmoi
    katharmoi
    发布于 2019-06-25
    0 人赞同

    这就是引擎盖下发生的事情。

    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,它拦截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);
        
    SingleLiveEvent起到了作用。谢谢。
    Muhamed Riyas M
    Muhamed Riyas M
    发布于 2019-06-25
    0 人赞同

    如果你正在寻找一个解决方案,以避免在从目标片段到原始片段的弹出堆栈中出现多个触发器。

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

    是的,将实时数据扩大到活动范围帮助我解决了这个自动触发的问题
    Tim
    @Muhammed Riyas,对不起,先生,请问为什么把所有者改为活动,就能防止多次触发?
    JJF
    JJF
    发布于 2019-06-25
    0 人赞同

    我特别发现了它的行为方式。 观察到的行为是,垃圾片段中的onChanged()在你扔掉一个音符后第一次激活片段时被调用一次(在新的应用程序启动时),此后在一个音符被扔掉后激活片段时被调用两次。

    双重呼叫的发生是因为。

    调用#1:片段在其生命周期中的STOPPED和STARTED之间过渡,这导致通知被设置到LiveData对象(毕竟它是一个生命周期观察者!)。 LiveData代码调用onChanged()处理程序,因为它认为观察者的数据版本需要被更新(后面会有更多介绍)。 注意:对数据的实际更新在此时可能仍未完成,导致onChange()被调用时数据已经过期。

    调用#2:由于查询设置了LiveData(正常路径)而产生的结果。 LiveData对象再次认为观察者的数据版本是过时的。

    现在,为什么onChanged()只被调用? once the very first time the view is activated after app startup? It's because the first time the LiveData version checking code executes as a result of the STOPPED->STARTED transition the live data has never been set to anything and thus LiveData skips informing the observer. Subsequent calls through this code path (see considerNotify() in LiveData.java) execute after the data has been set at least once.

    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中的观察者是不同的(观察者在片段中,当用户回到主片段时被销毁)。

    避免因生命周期转换而发生的虚假调用的一个简单方法是,在片段中保持一个标记为false,表示该片段是否已经完全恢复。 在onResume()处理程序中设置该标志为真,然后在onChanged()处理程序中检查该标志是否为真。 这样你就可以确保你对发生的事件做出响应,因为数据是真正被设置的。

    mvbrenes
    mvbrenes
    发布于 2019-06-25
    0 人赞同

    我不确定这个问题是否仍然活跃。

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

    以前,你必须实现你自己的lyfecycle所有者,当 onDestroyView 被调用时,它会将状态移动到 destroyed

    如果你至少用API 28作为目标并进行编译,就不应该再出现这种情况。

    Jazib Khan
    Jazib Khan
    发布于 2019-06-25
    0 人赞同

    Here's how to fix this in kotlin:

    In room DAO, use 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()

    So OP's viewModel will be:

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