AndroidViewModel - Making duplicate calls doesn't return data in observe function
I

3

8

My question is related to ViewModel second time returns null wherein I am not getting callback inobserve function if I make a repeated call to server. Following is the code I am using -

@Singleton
public class NetworkInformationViewModel extends AndroidViewModel {
  private LiveData<Resource<NetworkInformation>> networkInfoObservable;
  private final APIClient apiClient;

  @Inject
  NetworkInformationViewModel(@NonNull APIClient apiClient, @NonNull Application application) {
    super(application);
    this.apiClient = apiClient;
    getNetworkInformation();
  }

  public LiveData<Resource<NetworkInformation>> getNetworkInfoObservable() {
    return networkInfoObservable;
  }

  // making API calls and adding it to Observable
  public void getNetworkInformation() {
    networkInfoObservable = apiClient.getNetworkInformation();
  }
}

In Activity, the ViewModel is defined as followed -

final NetworkInformationViewModel networkInformationViewModel =
      ViewModelProviders.of(this, viewModelFactory).get(NetworkInformationViewModel.class);
    observeViewModel(networkInformationViewModel);

The observeViewModel function is used to add observable on ViewModel.

public void observeViewModel(final NetworkInformationViewModel networkInformationViewModel) {
    networkInformationViewModel.getNetworkInfoObservable()
      .observe(this, networkInformationResource -> {
        if (networkInformationResource != null) {
          if (networkInformationResource.status == APIClientStatus.Status.SUCCESS) {
            Timber.d("Got network information data");
          } else {
            final Throwable throwable = networkInformationResource.throwable;
            if (throwable instanceof SocketTimeoutException) {
              final NetworkInformation networkInformation = networkInformationResource.data;
              String error = null;
              if (networkInformation != null) {
                error = TextUtils.isEmpty(networkInformation.error) ? networkInformation.reply : networkInformation.error;
              }
              Timber.e("Timeout error occurred %s %s", networkInformationResource.message, error);

            } else {
              Timber.e("Error occurred %s", networkInformationResource.message);
            }
            if (count != 4) {
              networkInformationViewModel.getNetworkInformation();
              count++;
              // Uncommenting following line enables callback to be received every time 
              //observeViewModel(networkInformationViewModel);
            }
          }
        }
      });
  }

Uncommenting the following line in above function allows the callback to come everytime, but there has to be a proper way of doing this.

//observeViewModel(networkInformationViewModel);

Please note:- I don't need RxJava implementation for implementing this.

Ink answered 28/11, 2017 at 7:7 Comment(9)
Have you checked the answer: #45890104 ?Muskeg
@Muskeg Tried that, doesn't help.Ink
can you add code how you are adding it to observable ?Diaconate
@Diaconate Didn't understand, already added ViewModel and piece of code where I am adding observable on it, let me know what else you need.Ink
got it.. didn't saw that..so if i am right, if you uncomment the line as mentioned you get the callback and if you comment it out you are not getting the callback in observer function ?Diaconate
i could not understand your problem. do you want to observe network info only 4 times?Marriageable
@Marriageable The problem was that if I try to retry, or make secondary call, I was not getting the callback on Observable unless I attach the observable again.Ink
it's bacause your network state is not changing or you can not post a value to your live data when a network state change occurred.Marriageable
@Marriageable The issue is that if we use networkInfoObservable = apiClient.getNetworkInformation(); again for secondary call, a new object is created hence observable is lost.Ink
T
1

Right now in getNetworkInformation() you are:

  1. Creating a new LiveData
  2. Updating the the LiveData using setValue

Instead, you should have a single LiveData for APIClient created as a member variable, then in getNetworkInformation() just update that member LiveData.

More generally, your APIClient is a data source. For data sources, you can have them contain member LiveData objects that update when the data changes. You can provide getters to those LiveData objects to make them accessible in ViewModels, and ultimately listen to them in your Activities/Fragments. This is similar how you might take another data source, such as Room, and listen to a LiveData returned by Room.

So the code in this case would look like:

@Singleton
public class APIClient {
    private final MutableLiveData<Resource<NetworkInformation>> mNetworkData = new MutableLiveData<>(); // Note this needs to be MutableLiveData so that you can call setValue

    // This is basically the same code as the original getNetworkInformation, instead this returns nothing and just updates the LiveData
    public void fetchNetworkInformation() {
        apiInterface.getNetworkInformation().enqueue(new Callback<NetworkInformation>() {
          @Override
          public void onResponse(
            @NonNull Call<NetworkInformation> call, @NonNull Response<NetworkInformation> response
          ) {
            if (response.body() != null && response.isSuccessful()) {
              mNetworkData.setValue(new Resource<>(APIClientStatus.Status.SUCCESS, response.body(), null));
            } else {
              mNetworkData.setValue(new Resource<>(APIClientStatus.Status.ERROR, null, response.message()));
            }
          }

          @Override
          public void onFailure(@NonNull Call<NetworkInformation> call, @NonNull Throwable throwable) {
            mNetworkData.setValue(
              new Resource<>(APIClientStatus.Status.ERROR, null, throwable.getMessage(), throwable));
          }
        });
    }

    // Use a getter method so that you can return immutable LiveData since nothing outside of this class will change the value in mNetworkData
    public LiveData<Resource<NetworkInformation>> getNetworkData(){
        return mNetworkData;
    }

}

Then in your ViewModel...

// I don't think this should be a Singleton; ViewModelProviders will keep more than one from being instantiate for the same Activity/Fragment lifecycle
public class SplashScreenViewModel extends AndroidViewModel {

private LiveData<Resource<NetworkInformation>> networkInformationLiveData;

  @Inject
  SplashScreenViewModel(@NonNull APIClient apiClient, @NonNull Application application) {
    super(application);
    this.apiClient = apiClient;

    // Initializing the observable with empty data
    networkInfoObservable = apiClient.getNetworkData()

  }

  public LiveData<Resource<NetworkInformation>> getNetworkInfoObservable() {
    return networkInformationLiveData;
  }

}

Your activity can be the same as you originally coded it; it will just get and observe the LiveData from the ViewModel.

So what is Transformations.switchMap for?

switchMap isn't necessary here because you don't need to change the underlying LiveData instance in APIClient. This is because there's really only one piece of changing data. Let's say instead your APIClient needed 4 different LiveData for some reason, and you wanted to change which LiveData you observed:

public class APIClient {
    private MutableLiveData<Resource<NetworkInformation>> mNetData1, mNetData2, mNetData3, mNetData4;

    ...
}

Then let's say that your fetchNetworkInformation would refer to different LiveData to observe depending on the situation. It might look like this:

public  LiveData<Resource<NetworkInformation>> getNetworkInformation(int keyRepresentingWhichLiveDataToObserve) {
    LiveData<Resource<NetworkInformation>> currentLiveData = null;
    switch (keyRepresentingWhichLiveDataToObserve) {
        case 1:
            currentLiveData = mNetData1; 
            break;
        case 2:
            currentLiveData = mNetData2; 
            break;
        //.. so on
    }

    // Code that actually changes the LiveData value if needed here

    return currentLiveData;
}

In this case the actual LiveData coming from getNetworkInformation is changes, and you're also using some sort of parameter to determine which LiveData you want. In this case, you'd use a switchMap, because you want to make sure that the observe statement you called in your Activity/Fragment observes the LiveData returned from your APIClient, even if you change the underlying LiveData instance. And you don't want to call observe again.

Now this is a bit of an abstract example, but it's basically what your calls to a Room Dao do -- if you have a Dao method that queries your RoomDatabase based on an id and returns a LiveData, it will return a different LiveData instance based on the id.

Trelliswork answered 7/12, 2017 at 21:59 Comment(4)
Final question, you have made mNetworkData a private global variable in the APIInterface class which kind of make sense since I will use it in other places as weill. However, the app might have 30 more observables for different APIs in different screens, in such cases, making them all global doesn't make sense, how to tackle such scenario.Ink
You can have different repositories for the different data types in your app as shown here in the Github sample - that can help you seperate the data and only use the repositories that represent the data needed for a particular ViewModel.Trelliswork
FYI if you're wondering why the github sample seems to be returning LiveData, it's because it's returning a MediatorLiveData object called NetworkResource. The usage of MediatorLiveData in this way is described here.Trelliswork
Retry logic in case of call failure in this particular sample uses Transformation.switchMap() in similar fashion as I was using before. The only difference was that since the API didn't need any input I was using Void. As per your ViewModel how we can make a retry call?Ink
I
0

I have already updated the linked question's answer. Re-posting here since I have placed a bounty on the question and hopefully someone will verify that this is the proper way to handle the issue.

Following is the updated working solution -

@Singleton
public class SplashScreenViewModel extends AndroidViewModel {
  private final APIClient apiClient;
  // This is the observable which listens for the changes
  // Using 'Void' since the get method doesn't need any parameters. If you need to pass any String, or class
  // you can add that here
  private MutableLiveData<Void> networkInfoObservable;
  // This LiveData contains the information required to populate the UI
  private LiveData<Resource<NetworkInformation>> networkInformationLiveData;

  @Inject
  SplashScreenViewModel(@NonNull APIClient apiClient, @NonNull Application application) {
    super(application);
    this.apiClient = apiClient;

    // Initializing the observable with empty data
    networkInfoObservable = new MutableLiveData<Void>();
    // Using the Transformation switchMap to listen when the data changes happen, whenever data 
    // changes happen, we update the LiveData object which we are observing in the MainActivity.
    networkInformationLiveData = Transformations.switchMap(networkInfoObservable, input -> apiClient.getNetworkInformation());
  }

  /**
   * Function to get LiveData Observable for NetworkInformation class
   * @return LiveData<Resource<NetworkInformation>> 
   */
  public LiveData<Resource<NetworkInformation>> getNetworkInfoObservable() {
    return networkInformationLiveData;
  }

  /**
   * Whenever we want to reload the networkInformationLiveData, we update the mutable LiveData's value
   * which in turn calls the `Transformations.switchMap()` function and updates the data and we get
   * call back
   */
  public void setNetworkInformation() {
    networkInfoObservable.setValue(null);
  }
}

The Activity's code will be updated as -

final SplashScreenViewModel splashScreenViewModel =
  ViewModelProviders.of(this, viewModelFactory).get(SplashScreenViewModel.class);
observeViewModel(splashScreenViewModel);
// This function will ensure that Transformation.switchMap() function is called
splashScreenViewModel.setNetworkInformation();

Watch her droidCon NYC video for more information on LiveData. The official Google repository for LiveData is https://github.com/googlesamples/android-architecture-components/ look for GithubBrowserSample project.

The apiClient.getNetworkInformation() call doesn't need it any parameters to get additional information. Hence, the 'Void' added in MutableLiveData.

public LiveData<Resource<NetworkInformation>> getNetworkInformation() {
    final MutableLiveData<Resource<NetworkInformation>> data = new MutableLiveData<>();

    apiInterface.getNetworkInformation().enqueue(new Callback<NetworkInformation>() {
      @Override
      public void onResponse(
        @NonNull Call<NetworkInformation> call, @NonNull Response<NetworkInformation> response
      ) {
        if (response.body() != null && response.isSuccessful()) {
          data.setValue(new Resource<>(APIClientStatus.Status.SUCCESS, response.body(), null));
        } else {
          data.setValue(new Resource<>(APIClientStatus.Status.ERROR, null, response.message()));
        }
      }

      @Override
      public void onFailure(@NonNull Call<NetworkInformation> call, @NonNull Throwable throwable) {
        data.setValue(
          new Resource<>(APIClientStatus.Status.ERROR, null, throwable.getMessage(), throwable));
      }
    });
    return data;
  }
Ink answered 5/12, 2017 at 6:39 Comment(4)
Usually you'd use the input as an id or something to the getNetworkInformation function, which would necessitate returning a different LiveData tied to that id. Is it possible to share what the getNetworkInformation function is doing? That could be where your root issue lies. I'm guessing it is constructing a new LiveData - it would be helpful to determine if it needs to construct a new LiveData or if it can simply update a preexisting LiveData with setValue/postValue.Trelliswork
For an example of what updating a LiveData that retrieves network data looks like (as opposed to making a new LiveData), see this class.Trelliswork
Here a LiveData is created and then updated when the network request finishes.Trelliswork
@Trelliswork Thanks for taking out time, I have updated the answer, the API call doesn't need any input, hence Void added as an input. But whereever required, I am changing the Void to relevant class or object. Hope this is correct implementation.Ink
D
0

I didn't met the same issue, but i came across a similar thing where the number of observers were increasing each time i was saving the data in db. The way i debugged was how many instances or different instances of observers were getting invoked and i came to know that when you are fetching the live data from view model it needs to be checked for non null or you can say only 1 instance is being returned -

private LiveData<T> data;
    public LiveData<T> getLiveData(){
        if(data ==null){
            data = //api call or fetch from db
        }
        return data;
    }

Before i was simply returning the data object and then after checking the source i came to the conclusion that livedata automatically updates your object and everytime without the null check new instance was getting created and new observers were getting attached. Someone can correct me if my understanding regarding livedata is wrong.

Diaconate answered 5/12, 2017 at 7:4 Comment(2)
Did you see the video of droidCon? That helped.Ink
Meeting with her helped ! She recommended it.Ink

© 2022 - 2024 — McMap. All rights reserved.