Loading...

"The solution often turns out more beautiful than the puzzle."

Arushi Pant

Always eager to learn. Determined to make the best of the time I've got on Earth.

Bug fix: Livedata observer keeps observing even after calling removeObserver

Posted on July 21, 2018

In Project #3 of the Google Udacity Android Developer Nanodegree[1], I was learning how to work with LiveData and ViewModel.

 

While implementing LiveData, I faced a problem. 

I had only 1 Activity & Adapter for the Main screen - movie list. 

There are 3 sort options in the Main Activity. I was updating Adapter contents according to the sort option selected from the menu - Top-rated, Popular, Favourites.

LiveData is lifecycle aware. Therefore, in most cases, we do not need to manually remove an observer after we set it up. But, in this case, I needed to remove my Favourites observable because - If I did not, it updated my movie list when I was viewing Top-rated or Popular movies, and added/removed my favourites. The entire list data would get corrupted.

I had set up my observer as -

/* MainActivity.java */
mViewModel.getFavourites().observe(this, new Observer<List<Movie>>() {

    @Override public void onChanged(@Nullable List<Movie> movies) { 
                          ... code ... 
      }

 }

Then, I was removing observers using the code -

/* MainActivity.java */
mViewModel.getFavourites().removeObservers(this);

 

My View Model getFavourites() code was as below -

/* MainViewModel.java */
public LiveData<List<Movie>> getFavourites(){
        return movieRepository.getFavourites();
    }

This method was in turn calling the repository method to get the LiveData.

/* MovieRepository.java */
public LiveData<List<Movie>> getFavourites(){
        return favouriteDao.loadAllFavourites();
    }

 

I was assuming that any observers present would be removed when I called removeObservers(this) in the activity.

But, the onChanged method was still getting triggered. Do you know how we could solve this? I will detail below how I solved it.

 

I saved the observable in a variable as -

/* MainActivity.java */
LiveData<List<Movie>> mFavObservable;

if(mFavObservable==null) {
     mFavObservable = mViewModel.getFavourites();
}

 

and updated setup and removal code -

mFavObservable.observe(this, new Observer<List<Movie>>() { 
   ...

}

mFavObservable.removeObservers(this);

 

My issue got resolved. It looked like the code had opened separate streams of data each time I called mViewModel.getFavourites(), and storing it into a variable for use helped prevent the issue.

 

But, I was not satisfied yet. I did not know why this code change had solved the issue. I tried searching online, but did not find a clue. So, I turned to my Udacity mentor.

 

By the way, Udacity Nanodegree mentors are awesome. They are very helpful and supportive, always helping us solve our issues and clear our concepts.

I asked my mentor Peter - "Why was this happening? I seem to be missing some concept."

 

He went through my code and replied 

"So, I'm noticing that in your old set of code for favourites, that when you are calling hasObservers, it is always false. This means that whenever you are calling mViewModel.getFavourites, that, likely, this is a different livedata. You can fix it by doing what you did, and getting a hold on the livedata instance and making it a variable. But, this is a code smell and is indicative of another issue."

"I don't think you should be remaking your livedata at every getFavourites call... it should automatically give you back a live snapshot of your data... see here: Stackoverflow link"

"You can see an example here under the section Bye bye Presenter. Hello ViewModel. https://android.jlelse.eu/android-architecture-components-now-with-100-more-mvvm-11629a630125"

"Check out this code:

public LiveData<List<BusEntity>> getBusList() {

       if (busList == null) {

           busList = new MutableLiveData<>();

           loadBuses();

       }

       return busList;

   }

 

In the viewmodel, If the livedata is already created (from the repository), then it just returns the locally stored livedata. Else, it will fetch it from the repository. LiveData already has an observable hold on your data, and thus, you do not have to instantiate the list every time."

"Everytime you were calling .getFavourites, it was returning you a new copy of the LiveData object, which you only need to instantiate once. The reason why your fix worked in the activity is that you were handling all of that in the activity, which data should be kept in the view model"

 

Now, this totally made sense. So, instead of checking if(mFavObservable==null) in Activity, I could store the value in ViewModel and add the check there.

So, my final code now looks like this - 

/* MainViewModel.java */
public class MainViewModel extends ViewModel {
     private LiveData<List<Movie>> favouritesList = null;
     .
     .
     .
     private void initFavourites() {
        favouritesList = movieRepository.getFavourites();
     }
    
     public LiveData<List<Movie>> getFavourites(){
        if (favouritesList == null) {
            initFavourites();
        }
        return favouritesList;
     }

     .
     .
     .
}

 

/* MainActivity.java */

public class MainActivity extends AppCompatActivity
    implements View.OnClickListener{
            .
            .
            .
            private void setupFavouritesObserver() {
               if (mViewModel.getFavourites().hasObservers()) {
                     return;
                }
                  
               mViewModel.getFavourites().observe(this, new Observer<List<Movie>>() {
                  
                  @Override
                  public void onChanged(@Nullable List<Movie> movies) {
                       ... code ...
                   }
                });
             }

            private void removeObservers(){
                 final LiveData<List<Movie>> observable = mViewModel.getFavourites();
                 if(observable!=null
                            && observable.hasObservers()) {
                     observable.removeObservers(this);
                  }
             }
            .
            .
            .
}

 

I hope this post helps others who are facing a similar issue.

 

[1] Referral link - Will give you a Rs. 1000 cashback on your first Nanodegree enrollment


Liked the post?

Show your appreciation, and share with others.