为什么 LiveData 观察器会为新加入的观察器触发两次

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

目前,我有一个 MainFragment,它执行 Room写操作,将 未经处理的数据改为 垃圾数据

我还有另一个 TrashFragment,它观察到 垃圾数据

考虑下面的场景。

  1. 目前有0个 垃圾数据
  2. MainFragment是当前活动的片段。 TrashFragment尚未创建。
  3. MainFragment增加了1个 垃圾数据
  4. 现在,有一个 垃圾数据
  5. 我们使用导航抽屉,用 TrashFragment代替 MainFragment
  6. TrashFragment的观察者将首先接收 onChanged,0 垃圾数据
  7. 同样,TrashFragment的观察者将第二次接收到 onChanged垃圾数据

出乎我意料的是,第(6)项不应该发生。TrashFragment应该只接收最新的 垃圾数据,即1。

这是我的密码


垃圾片段.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);

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

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中执行写操作(单击 添加废纸条按钮)之后,当我切换到 TrashFragment时,我预计 TrashFragment中的 onChanged只会被调用一次。然而,它被调用了两次。

enter image description here

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

88593 次浏览

I have introduced just one change in your code:

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

instead of:

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

in Fragment's onCreate(Bundle) methods. And now it works seamlessly.

In your version you obtained a reference of NoteViewModel common to both Fragments (from Activity). ViewModel had Observer registered in previous Fragment, I think. Therefore LiveData kept reference to both Observer's (in MainFragment and TrashFragment) and called both values.

So I guess the conclusion might be, that you should obtain ViewModel from ViewModelProviders from:

  • Fragment in Fragment
  • Activity in Activity

Btw.

noteViewModel.getTrashedNotesLiveData().removeObservers(this);

is not necessary in Fragments, however I would advise putting it in onStop.

I forked your project and tested it a bit. From all I can tell you discovered a serious bug.

To make the reproduction and the investigation easier, I edited your project a bit. You can find updated project here: https://github.com/techyourchance/live-data-problem . I also opened a pull request back to your repo.

To make sure that this doesn't go unnoticed, I also opened an issue in Google's issue tracker:

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.

I snatched Vasiliy's fork of your fork of the fork and did some actual debugging to see what happens.

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

Close. The way Room's LiveData<List<T>> expose works is that it creates a ComputableLiveData, which keeps track of whether your data set has been invalidated underneath in Room.

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

So when the note table is written to, then the InvalidationTracker bound to the LiveData will call invalidate() when a write happens.

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

Now what we need to know is that ComputableLiveData's invalidate() will actually refresh the data set, if the LiveData is 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);
}
}
}
};

Where liveData.hasActiveObservers() is:

public boolean hasActiveObservers() {
return mActiveCount > 0;
}

So refreshRunnable actually runs only if there is an active observer (afaik means lifecycle is at least started, and observes the live data).



This means that when you subscribe in TrashFragment, then what happens is that your LiveData is stored in Activity so it is kept alive even when TrashFragment is gone, and retains previous value.

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

So you get two callbacks because:

1.) first "onChanged" call is the previously retained value of the LiveData kept alive in the Activity's 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 active.


So technically this is by design. If you want to ensure you only get the "newest and greatest" value, then you should use a fragment-scoped ViewModel.

You might also want to start observing in onCreateView(), and use viewLifecycle for the lifecycle of your LiveData (this is a new addition so that you don't need to remove observers in onDestroyView().

If it is important that the Fragment sees the latest value even when the Fragment is NOT active 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 active observer on your LiveData.

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

The observers method void onChanged(@Nullable T t) is called twice. That's fine.

The first time it is called upon startup. The second time it is called as soon as Room has loaded the data. Hence, upon the first call the LiveData object is still empty. It is designed this way for good reasons.

Second call

Let's start with the second call, your point 7. The documentation of Room says:

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.

The generated code is an object of the class ComputableLiveData mentioned in other postings. It manages a MutableLiveData object. Upon this LiveData object it calls LiveData::postValue(T value) which then calls LiveData::setValue(T value).

LiveData::setValue(T value) calls LiveData::dispatchingValue(@Nullable ObserverWrapper initiator). This calls LiveData::considerNotify(ObserverWrapper observer) with the observer wrapper as parameter. This finally calls onChanged() upon the observer with the loaded data as parameter.

First call

Now for the first call, your point 6.

You set your observers within the onCreateView() hook method. After this point the lifecycle changes it state twice to come visible, on start and on resume. The internal class LiveData::LifecycleBoundObserver is notified upon such changes of state because it implements the GenericLifecycleObserver interface, which holds one method named void onStateChanged(LifecycleOwner source, Lifecycle.Event event);.

This method calls ObserverWrapper::activeStateChanged(boolean newActive) as LifecycleBoundObserver extends ObserverWrapper. The method activeStateChanged calls dispatchingValue() which in turn calls LiveData::considerNotify(ObserverWrapper observer) with the observer wrapper as parameter. This finally calls onChanged() upon the observer.

All this happens under certain conditions. I admit that I didn't investigated all conditions within the chain of methods. There are two changes of state, but onChanged() is only triggered once, because the conditions check for things like this.

The bottomline here is, that there is a chain of methods, that is triggered upon changes of the lifecycle. This is responsible for the first call.

Bottomline

I think nothing goes wrong with your code. It's just fine, that the observer is called upon creation. So it can fill itself with the initial data of the view model. That's what an observer should do, even if the database part of the view model is still empty upon the first notification.

Usage

The first notification basically tells that the view model is ready for to display, despite it still is not loaded with data from underlying databases. The second notification tells, that this data is ready.

When you think of slow db connections, this is a reasonable approach. You may want to retrieve and display other data from the view model triggered by the notification, that does not come from the database.

Android has a guideline how to deal with slow database loading. They suggest to use placeholders. In this example the gap is that short, that there is no reason to go to such an extend.

Appendix

Both Fragments use there own ComputableLiveData objects, that's why the second object is not preloaded from the first fragment.

Also think of the case of rotation. The data of the view model does not change. It does not trigger a notification. The state changes of the lifecycle alone trigger the notification of the new new view.

This is what happens under the hood:

ViewModelProviders.of(getActivity())

As you are using getActivity() this retains your NoteViewModel while the scope of MainActivity is alive so is your trashedNotesLiveData.

When you first open your TrashFragment room queries the db and your trashedNotesLiveData is populated with the trashed value (At the first opening there is only one onChange() call). So this value is cached in trashedNotesLiveData.

Then you come to the main fragment add a few trashed notes and go to the TrashFragment again. This time you are first served with the cached value in trashedNotesLiveData while room makes async query. When query finishes you are brought the latest value. This is why you get two onChange() calls.

So the solution is you need to clean the trashedNotesLiveData before opening TrashFragment. This can either be done in your getTrashedNotesLiveData() method.

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

Or you can use something like this SingleLiveEvent

Or you can use a MediatorLiveData which intercepts the Room generated one and returns only distinct values.

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


}
});

I found out specifically why it's acting the way it is. The observed behavior was onChanged() in the trash fragment is called once the first time you activate the fragment after trashing a note (on fresh app start) and gets called twice when fragment get activated thereafter after a note is trashed.

The double calls happen because:

Call #1: The fragment is transitioning between STOPPED and STARTED in its lifecyle and this causes a notification to be set to the LiveData object (it's a lifecycle observer after all!). The LiveData code calls the the onChanged() handler because it thinks the observer's version of the data needs to be updated (more on this later). Note: the actual update to the data could still be pending at this point causing the onChange() to get called with stale data.

Call #2: Ensues as a result of the query setting the LiveData (normal path). Again the LiveData object thinks the observer's version of the data is stale.

Now why does onChanged() only get called 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 determines if the observer has stale data by keeping a version number that indicates how many times the data has been set. It also records the version number last sent to the client. When new data is set LiveData can compare these versions to determine if an onChange() call is warranted.

Here's the version #s during the calls to the LiveData version checking code for the 4 calls:

   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

If you're wondering why version last seen by the observer in call 3 is -1 even though onChanged() was called the 2nd time around it's because the observer in calls 1/2 is a different observer than the one in calls 3/4 (the observer is in the fragment which was destroyed when the user went back to the main fragment).

An easy way to avoid confusion regarding the spurious calls that happen as a result of lifecycle transitions is to keep a flag in the fragment intialized to false that indicates if the fragment has been fully resumed. Set that flag to true in the onResume() handler then check to see if that flag is true in your onChanged() handler. That way you can be sure you're responding to events that happened becuase data was truly set.

I'm not sure if this issue is still active.

But the main perpetrator was a bug inside the fragment Lifecycle owner for fragments which was not cleared when the view was destroyed.

Previously you would have to implement your own lyfecycle owner that would move the state to destroyed when onDestroyView would be called.

This should no longer be the case if you target and compile with at least API 28

The solution I had was simply to start observing data when I need it and remove the observer as soon as it has retrieved the data. You won't get double triggering this way.

I used SingleLiveEvent and works. When fragment/activity is resumed or recreated SingleLiveEvent not throw the event, only when explicitly changes

The reason is that in your .observe() method, you passed a fragment as the lifecycle owner. What should have been passed is the viewLifecycleOwner object of the fragment

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

My answer is not a solution to this question description but rather to question title. Just title.

If your observer for a LiveData<*> is getting called multiple times then it means you are calling livedata.observe(...) multiple times. This happened to me as I was doing livedata.observe(...) in a method and was calling this method whenever user does some action thus observing liveData again. To solve this I moved livedata.observe(...) to onCreate() lifecycle method.

What was the scenario? The App has a color swatch. When user selects a color I had to make API call to fetch Product Images for that color. So was making API call and was observing livedata in onColorChanged() . When user selects a new color, onColorChanged() would be called again thus observing for livedata changes again.

Edit: The other issue could be passing this instead of viewLifecycleOwner while registering LiveData Observer as pointed out in another answer below. Always use viewLifecycleOwner when observing LiveData in Fragments.

If you are looking for a solution to avoid the multiple triggers on popUp the back stack from destination fragment to the original fragment

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

Never put an observer inside loops/any place where it gets registered twice. Observers should be put inside onViewCreated / onCreate / any place that gets called only once. OBSERVE ONLY ONCE !

Here is an example of the wrong way :

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

IT IS WRONG TO PUT IT UNDER SOME FUNCTION THAT GETS CALLED MORE THAN ONCE, like:

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


Here's how to fix this in kotlin:

In room DAO, use Flow<List<T>> instead of LiveData<List<T>>.

So, in the OP's example we can use:

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

instead of

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

Then in viewModel, we can use val list = dao.getTrashedNotes().asLiveData().

So OP's viewModel will be:

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

And rest of the flows after viewModel remains the same.

Reason why this works:

Flow, unlike liveData, is not lifecycle aware. So, even if the fragment is not created, flow's value will be up-to date.