LiveData remove Observer after first callback

假如想象 提交于 2019-11-29 05:26:06

Your first one will not work, because observeForever() is not tied to any LifecycleOwner.

Your second one will not work, because you are not passing the existing registered observer to removeObserver().

You first need to settle on whether you are using LiveData with a LifecycleOwner (your activity) or not. My assumption is that you should be using a LifecycleOwner. In that case, use:

Observer observer = new Observer<DownloadItem>() {
    @Override
    public void onChanged(@Nullable DownloadItem downloadItem) {
        if(downloadItem!= null) {
            DownloadManager.this.downloadManagerListener.onDownloadManagerFailed(null, "this item already exists");
            return;
        }
        startDownload();
        model.getDownloadByContentId(contentId).removeObservers((AppCompatActivity)context);
    }
};

model.getDownloadByContentId(contentId).observe((AppCompatActivity)context, observer);

Following on CommonsWare answer, instead of calling removeObservers() which will remove all the observers attached to the LiveData, you can simply call removeObserver(this) to only remove this observer:

Observer observer = new Observer<DownloadItem>() {
    @Override
    public void onChanged(@Nullable DownloadItem downloadItem) {
        if(downloadItem!= null) {
            DownloadManager.this.downloadManagerListener.onDownloadManagerFailed(null, "this item already exists");
            return;
        }
        startDownload();
        model.getDownloadByContentId(contentId).removeObserver(this);
    }
};

model.getDownloadByContentId(contentId).observe((AppCompatActivity)context, observer);

Note: in removeObserver(this), this refers to the observer instance and this works only in the case of an anonymous inner class. If you use a lambda, then this will refer to the activity instance.

There is a more convenient solution for Kotlin with extensions:

fun <T> LiveData<T>.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observer<T>) {
    observe(lifecycleOwner, object : Observer<T> {
        override fun onChanged(t: T?) {
            observer.onChanged(t)
            removeObserver(this)
        }
    })
}

This extension permit us to do that:

liveData.observeOnce(this, Observer<Password> {
    if (it != null) {
        // do something
    }
})

So to answer your original question, we can do that:

val livedata = model.getDownloadByContentId(contentId)
livedata.observeOnce((AppCompatActivity) context, Observer<T> {
    if (it != null) {
        DownloadManager.this.downloadManagerListener.onDownloadManagerFailed(null, "this item already exists");
    }
    startDownload();
})

The original source is here: https://code.luasoftware.com/tutorials/android/android-livedata-observe-once-only-kotlin/

Update: @Hakem-Zaied is right, we need to use observe instead of observeForever.

I agree with @vince above, but I believe that we either skip passing lifecycleOwner and use observerForever as below:

fun <T> LiveData<T>.observeOnce(observer: Observer<T>) {
    observeForever(object : Observer<T> {
        override fun onChanged(t: T?) {
            observer.onChanged(t)
            removeObserver(this)
        }
    })
}

or using the lifecycleOwner with observe as below:

fun <T> LiveData<T>.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observer<T>) {
    observe(lifecycleOwner, object : Observer<T> {
        override fun onChanged(t: T?) {
            observer.onChanged(t)
            removeObserver(this)
        }
    })
}

I love the generic solutions by @Vince and @Hakem Zaied, but to me the lambda version seems even better:

fun <T> LiveData<T>.observeOnce(observer: (T) -> Unit) {
    observeForever(object: Observer<T> {
        override fun onChanged(value: T) {
            removeObserver(this)
            observer(value)
        }
    })
}

fun <T> LiveData<T>.observeOnce(owner: LifecycleOwner, observer: (T) -> Unit) {
    observe(owner, object: Observer<T> {
        override fun onChanged(value: T) {
            removeObserver(this)
            observer(value)
        }
    })
}

So you end up with:

    val livedata = model.getDownloadByContentId(contentId)
    livedata.observeOnce((AppCompatActivity) context) {
        if (it != null) {
            DownloadManager.this.downloadManagerListener.onDownloadManagerFailed(null, "this item already exists")
        }
        startDownload();
    }

Which I find cleaner.

Also, removeObserver() is called first-thing as the observer is dispatched, which makes it safer (i.e. copes with potential runtime error throws from within the user's observer code).

  1. LiveData class has 2 similar methods to remove Observers. First is named,

removeObserver(@NonNull final Observer<T> observer) (see carefully the name of the method, it's singular) which takes in the observer you want to be removed from the list of Observers of the same LifecycleOwner.

  1. Second method is

removeObservers(@NonNull final LifecycleOwner owner) (see the plural method name). This method takes in the LifecycleOwner itself and removes all the Observers of the specified LifecycleOwner.

Now in your case, you can remove your Observer by 2 ways (there might be many ways), one is told by @ToniJoe in the previous answer.

Another way is just have a MutableLiveData of boolean in your ViewModel which stores true when it's been Observed the first time and just observe that Livedata as well. So whenever it turns to true, you'll be notified and there you can remove your observer by passing that particular observer.

The solution proposed by @CommonsWare and @Toni Joe didn't solve the issue for me when I needed to remove the observers after receiving the first result from a DAO query in my ViewModel. However, the following solution found at Livedata keeps observer after calling removeObserer did the trick for me with a little of my own intuition.

The process is as follows, create a variable in your ViewModel where the LiveData is stored upon request, retrieve it in a create observer function call in the activity after doing a null check, and call a remove observers function before calling the flushToDB routine in an imported class. That is, the code in my ViewModel looks as follows:

public class GameDataModel extends AndroidViewModel {
   private LiveData<Integer> lastMatchNum = null;
   .
   .
   .
   private void initLastMatchNum(Integer player1ID, Integer player2ID) {
       List<Integer> playerIDs = new ArrayList<>();
       playerIDs.add(player1ID);
       playerIDs.add(player2ID);

       lastMatchNum = mRepository.getLastMatchNum(playerIDs);
   }

 public LiveData<Integer> getLastMatchNum(Integer player1ID, Integer player2ID) {
       if (lastMatchNum == null) { initLastMatchNum(player1ID, player2ID); }
       return lastMatchNum;
   }

In the above, if there is no data in the LiveData variable in the ViewModel, I call initLastMatchNum() to retrieve the data from a function within the view model. The function to be called from the activity is getLastMatchNum(). This routine retrieves the data in the variable in the ViewModel (which is retrieved via the repository via the DAO).

The following code I have in my Activity

public class SomeActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
         .
         .
         .
        setupLastMatchNumObserver(); 
         .
         .
         .
    }

    private void setupLastMatchNumObserver() {
        if (mGameDataViewModel.getLastMatchNum(Player1ID, Player2ID).hasObservers()) {
            Log.v("Observers", "setupLastMatchNumObserver has observers...returning");
            return;
        }
        Log.v("Setting up Observers", "running mGameDataViewModel.get...observer()");
        mGameDataViewModel.getLastMatchNum(Player1ID, Player2ID).observe(this, new Observer<Integer>() {
            @Override
            public void onChanged(Integer MatchNumber) {
                if (MatchNumber == null ) {
                    matchNumber = 1;
                    Log.v( "null MatchNumber", "matchNumber: " + matchNumber.toString());
                }
                else {
                    matchNumber = MatchNumber; matchNumber++;
                    Log.v( "matchNumber", "Incrementing match number: " + matchNumber.toString());
                }
                MatchNumberText.setText(matchNumber.toString());
            }
        });
    }

    private void removeObservers() {
        final LiveData<Integer> observable = mGameDataViewModel.getLastMatchNum(Player1ID, Player2ID);
        if (observable != null && observable.hasObservers()) {
            Log.v("removeObserver", "Removing Observers");
            observable.removeObservers(this);
        }
    }

What's going on in the above, is 1.) I call the setupLastMatchNumObserver() routine in the onCreate method of the activity, to update the class's variable matchNum. This keeps track of the match numbers between players in my game which is stored in a database. Every set of players will have a different match number in the database based upon how often they play new matches with each other. The first solutions in this thread seemed a little weary to me as calling remove observers in the onChanged seems strange to me and would constantly change the TextView object after every database flush of each move of the players. So matchNumber was getting incremented after every move because there was a new value in the database after the first move (namely the one matchNumber++ value) and onChanged kept being called because removeObservers was not working as intended. setupLastMatchNumObserver() checks to see if there are observers of the live data and if so does not instantiate a new call each round. As you can see I am setting a TextView object to reflect the current matchnumber of the players.

The next part is a little trick on when to call removeObservers(). At first I thought if I called it directly after setupLastMatchNumObserver() in the onCreate override of the activity that all would be fine. But it removed the observer before the observer could grab the data. I found out that if I called removeObservers() directly prior to the call to flush the new data collected in the activity to the database (in separate routines throughout the activity) it worked like a charm. i.e.,

 public void addListenerOnButton() {
        .
        .
        .
            @Override
            public void onClick(View v) {
               .
               .
               .
               removeObservers();
               updateMatchData(data); 
            }
   }

I also call removeObservers(); and updateMatchData(data) in other places in my activity in the above fashion. The beauty is removeObservers() can be called as many times as needed since there is a check to return if there are no observers present.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!