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

后端 未结 12 2056
北荒
北荒 2020-12-12 18:03

My understanding on LiveData is that, it will trigger observer on the current state change of data, and not a series of history state change of data.

相关标签:
12条回答
  • 2020-12-12 18:22

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

    0 讨论(0)
  • 2020-12-12 18:22

    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

    0 讨论(0)
  • 2020-12-12 18:23

    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);
                }
    
            }
        });
    
    0 讨论(0)
  • 2020-12-12 18:23

    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.

    0 讨论(0)
  • 2020-12-12 18:23

    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

    0 讨论(0)
  • 2020-12-12 18:25

    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.

    0 讨论(0)
提交回复
热议问题