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

后端 未结 12 2076
北荒
北荒 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:39

    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> 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> getNotes() {
        final String _sql = "SELECT * FROM note where trashed = 0";
        final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
        return new ComputableLiveData>() {
          private Observer _observer;
    
          @Override
          protected List compute() {
            if (_observer == null) {
              _observer = new Observer("note") {
                @Override
                public void onInvalidated(@NonNull Set 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.

提交回复
热议问题