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

Che*_*eng 41 android android-livedata android-architecture-components

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

目前,我有一个MainFragment执行Room写操作的程序,用于将非删除数据更改为已删除的数据.

我也是另一个TrashFragment观察到破坏数据的人.

请考虑以下情形.

  1. 目前有0个已删除的数据.
  2. MainFragment是当前活动的片段.TrashFragment尚未创建.
  3. MainFragment添加了1个已删除的数据.
  4. 现在,有1个已删除的数据
  5. 我们采用抽屉式导航来代替MainFragmentTrashFragment.
  6. TrashFragment的观察者将首先收到onChanged0个已删除的数据
  7. 再次,TrashFragment观察者将接下来接收onChanged1个已删除的数据

我的期望是,第(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);
Run Code Online (Sandbox Code Playgroud)

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);
Run Code Online (Sandbox Code Playgroud)

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

处理房间的代码

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

onChanged对于同样的数据,我知道如何防止两次接收?


演示

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

正如你所看到的,我执行写操作后(点击ADD丢弃注按钮)中MainFragment,当我切换到TrashFragment,我希望onChangedTrashFragment将只被调用一次.然而,它被称为两次.

在此输入图像描述

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

R. *_*ski 34

我在您的代码中只介绍了一个更改:

noteViewModel = ViewModelProviders.of(this).get(NoteViewModel.class);
Run Code Online (Sandbox Code Playgroud)

代替:

noteViewModel = ViewModelProviders.of(getActivity()).get(NoteViewModel.class);
Run Code Online (Sandbox Code Playgroud)

FragmentonCreate(Bundle)方法.现在它无缝地工作.

在您的版本中,您获得了NoteViewModel两个片段(来自Activity)的共同参考.我认为,ViewModel已经Observer在之前的片段中注册过了.因此,LiveData保持引用两个Observer(in MainFragmentTrashFragment)并调用两个值.

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

  • FragmentFragment
  • ActivityActivity

顺便说一句.

noteViewModel.getTrashedNotesLiveData().removeObservers(this);

在片段中没有必要,但我建议把它放进去onStop.

  • 但是,如果您看官方教程https://developer.android.com/topic/libraries/architecture/viewmodel#sharing,他们建议对ViewModelProviders.of使用活动,对observe使用片段。 (2认同)
  • Android官方文档提供了理想的案例,有时候也是错误的.我不是说这是一个错误,但帮助防止了这个案子. (2认同)
  • 文档Cheok Yan Cheng指出,实际上建议在片段之间共享视图模型的选项。但是目的是不同的。正如我在回答中指出的那样,我认为两步加载不是要解决的错误,而是解决数据库连接速度慢和轮换等问题的功能。 (2认同)

Vas*_*liy 16

我分叉你的项目并测试了一下.我可以告诉你发现了一个严重的错误.

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

为了确保不会忽视这一点,我还在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的方式有关.

  • 一年过去了,我发现使用 Oreo 仍然存在这个问题。 (2认同)

小智 16

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

viewModel.livedata.observe(viewLifecycleOwner, Observer {
        // Do your routine here
    })
Run Code Online (Sandbox Code Playgroud)


Ram*_*shi 9

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

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

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

编辑:另一个问题可能是在注册 LiveData Observer 时传递此参数而不是viewLifecycleOwner,如下面另一个答案中所指出的。观察 Fragments 中的 LiveData 时,始终使用viewLifecycleOwner 。


Epi*_*rce 8

我抓住了Vasiliy的叉子叉子并做了一些实际的调试,看看会发生什么.

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

关.Room的LiveData<List<T>>曝光工作方式是它创建一个ComputableLiveData,它可以跟踪你的数据集是否在Room下面无效.

trashedNotesLiveData = NoteplusRoomDatabase.instance().noteDao().getTrashedNotes();
Run Code Online (Sandbox Code Playgroud)

因此,当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);
        }
Run Code Online (Sandbox Code Playgroud)

现在,我们需要知道的是,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);
            }
        }
    }
};
Run Code Online (Sandbox Code Playgroud)

在哪里liveData.hasActiveObservers():

public boolean hasActiveObservers() {
    return mActiveCount > 0;
}
Run Code Online (Sandbox Code Playgroud)

所以refreshRunnable实际上只有在有活跃的观察者时才会运行(afaik意味着生命周期至少开始,并观察实时数据).



这意味着当您在TrashFragment中订阅时,会发生的情况是您的LiveData存储在Activity中,因此即使TrashFragment消失,它也会保持活动状态,并保留以前的值.

但是,当您打开TrashFragment,然后TrashFragment订阅,LiveData变为活动状态,ComputableLiveData检查失效(由于实时数据未处于活动状态而从未重新计算,这是真的),在后台线程上异步计算它,当它是完成后,该值已过帐.

所以你得到两个回调因为:

1.)首先"onChanged"调用是Activity的ViewModel中保存的LiveData的先前保留值

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


从技术上讲,这是设计的.如果您想确保只获得"最新且最好"的值,那么您应该使用片段范围的ViewModel.

您可能还想开始观察onCreateView()并使用viewLifecycleLiveData的生命周期(这是一个新的添加,因此您不需要删除观察者onDestroyView().

如果片段即使在片段未处于活动状态并且未观察到片段时看到最新值也很重要,那么当ViewModel是活动范围时,您可能希望在Activity中注册观察者以确保存在LiveData上的活跃观察者.

  • 我们没有必要争论它是否是错误.我没有看到错误状态的虚假通知如何只是一个严重的错误,但让我们让googlers彻底检查这一点并让社区做出反应. (2认同)
  • ^ 创建一个额外的 LiveData,由 MutableLiveData 支持,用于跟踪操作的状态。将其值设置为“done”(但是您想要表示该状态),然后将其状态设置为“irrelevant”(但是您想要表示该状态,我通常使用 null 并且观察者简单地忽略 null),然后在订阅观察者时初始值将是“无关紧要的”。 (2认同)

小智 8

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

  • 你是最棒的。我快死了。 (3认同)

Ree*_* PK 8

切勿将观察者放入循环/任何被注册两次的地方。观察者应该放在 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 
            }
 );
}
Run Code Online (Sandbox Code Playgroud)

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

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

Run Code Online (Sandbox Code Playgroud)


Blc*_*knx 6

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

观察者方法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)称为LifecycleBoundObserverextends ObserverWrapper。该方法activeStateChanged调用dispatchingValue(),然后LiveData::considerNotify(ObserverWrapper observer)以观察者包装器作为参数进行调用。最后onChanged(),这要求观察者。

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

这里的底线是,存在一系列方法,这些方法是在生命周期更改时触发的。这负责第一次通话。

底线

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

用法

第一个通知基本上告诉我们视图模型已准备好显示,尽管该视图模型尚未从底层数据库中加载数据。第二个通知表明此数据已准备就绪。

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

Android提供了有关如何处理缓慢的数据库加载的指南。他们建议使用占位符。在此示例中,差距是如此之短,因此没有理由进行这种扩展。

附录

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

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

  • `很好,观察者被称为创造。所以它可以用视图模型的初始数据填充自己。这就是观察者应该做的` Imo,如果我订阅了一个数据源,我应该获得它的最新状态。你的陈述告诉你,可以得到一些垃圾(实际上不符合现实的东西),忽略那些垃圾。我认为这应该被视为故障。 (3认同)

kat*_*moi 5

这是幕后发生的事情:

ViewModelProviders.of(getActivity())
Run Code Online (Sandbox Code Playgroud)

当您使用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();
}
Run Code Online (Sandbox Code Playgroud)

或者你可以使用这样的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);
            }

        }
    });
Run Code Online (Sandbox Code Playgroud)