Search code examples
javaandroidmvvmandroid-architecture-componentsandroid-livedata

MVVM MediatorLiveData observer onchanged called multiple times


I am using MVVM + LiveData + Dagger 2.11 on my app.On SignInFragment click on a textview send request to server and show respopnse on snackbar. It works fine on first time click of textview.If I click again,it sends request(inbetween shows snackbar response message here) and ViewModel MediatorLiveData observer onChanged method called muliple times.Is it default behaviour of MediatorLiveData?

SignInViewModel.java

public class SignInViewModel extends AndroidViewModel {

    @Inject
    MediatorLiveData mediatorLiveData;

    @Inject
    SnackbarMessage mSnackbarTextLiveData = new SnackbarMessage();

    @Inject
    public SignInViewModel(Application application,SignInRepository signInRepository) {
        super(application);
        this.signInRepository = signInRepository;
    }

    public MediatorLiveData<ResendActivationCodeResponse> resendActivationCode(final String phoneNumber, final String countryCode) {
        final MutableLiveData<NetworkResponse> connectViaPhoneResponseMutableLiveData = signInRepository.resendActivationCode(phoneNumber, countryCode);

        mediatorLiveData.addSource(connectViaPhoneResponseMutableLiveData, new NetworkResponseObserver() {
            @Override
            public void onSuccess(Object data) {
                mediatorLiveData.setValue(data);
            }

            @Override
            public void onBadRequest(Object data, String errorMessage) {
                mSnackbarTextLiveData.setValue(errorMessage);
            }

            @Override
            public void onUnAuthorisedError(Object data) {
                mSnackbarTextLiveData.setValue(data.toString());
            }

            @Override
            public void onFailure(Object data, String errorMessage) {
                mSnackbarTextLiveData.setValue(errorMessage);
            }

            @Override
            public void onNoNetworkAvailable(Object data, String errorMessage) {
                mSnackbarTextLiveData.setValue(data.toString());
            }

            @Override
            public void onLoading(Object data) {

            }
        });
        return mediatorLiveData;
    }
}

SignInFragment.java

@Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        mSignInViewModel = ViewModelProviders.of(mActivity, mViewModelFactory).get(SignInViewModel.class);
        setupSnackbar();
    }

    private void setupSnackbar() {
        mSignInViewModel.getSnackbarMessage().observe(this, new SnackbarMessage.SnackbarObserver() {
            @Override
            public void onNewMessage(String snackbarMessage) {
                ActivityUtils.showSnackbar(getView(), snackbarMessage);
            }
        });
    }

    @OnClick(R.id.resend_activation_code_textview)
    public void reSendActivationCode() {
        showProgress(true);

        final MediatorLiveData<ResendActivationCodeResponse> resendActivationCodeResponseMediatorLiveData = mSignInViewModel.resendActivationCode(mPhoneNumber, mCountryCode);


        Observer<ResendActivationCodeResponse> resendActivationCodeResponseObserver = new Observer<ResendActivationCodeResponse>() {

            @Override
            public void onChanged(@Nullable ResendActivationCodeResponse resendActivationCodeResponse) {
                if (resendActivationCodeResponse != null) {
                    showProgress(false);
                    ActivityUtils.showSnackbar(getView(), activationCodeResentMessage);
                    //resendActivationCodeResponseMediatorLiveData.removeObserver(this);
                }
            }
        };


        resendActivationCodeResponseMediatorLiveData.observe(PhoneNumberActivationFragment.this, resendActivationCodeResponseObserver);
    }

Solution

  • It looks like you're calling addSource with different LiveData associated with different phone numbers every time your resend_activation_code_textview is clicked. These different LiveData sources are also all associated with different NetworkResponseObservers, that call setValue(). setValue() is what updates your listening fragments and what is what is called too many times.

    I believe the issue is because you call addSource every time resend_activation_code_textview is clicked and you never remove any sources.

    If you click resend_activation_code_textview 10 times, your mediatorLiveData will have 10 different sources, when you probably only wanted one.

    When a source is added, it does an initial trigger of your mediatorLiveData, so you will always trigger setValue() at least once. When any of the 10 added sources are updated, it will also update your mediatorLiveData, and call setValue(). Depending on what signInRepository.resendActivationCode does and if it updates any of the other 10 LiveData sources, this will trigger multiple setValue() calls for one click.


    There's a removeSource() method which you could call to make sure you never have more than one source at a time, this likely getting rid of the multiple onChanged calls. But there's a built in solution for what I think you're trying to do (which uses MediatorLiveData under the hood) -- it's the switchMap Transformation.

    switchMap allows you to change the underlying source that a LiveData is listening to, without updating the observers. So instead of clicking resend_activation_code_textview 10 times and adding 10 different sources, you can have it so that every time you click resend_activation_code_textview the previous source will be swapped for a new source.

    The example scenario for switchMap is that you have a method to look up a userById(). You make a normal LiveData to store the user id, and then use the switchMap transformation so that you have another LiveData for the current user. As the id changes, the current user is swapped out and updated:

    MutableLiveData userIdLiveData = ...;
     LiveData userLiveData = Transformations.switchMap(userIdLiveData, id ->
         repository.getUserById(id));
    
     void setUserId(String userId) {
          this.userIdLiveData.setValue(userId);
     }
    

    I think you're doing something similar with the phone number and country code. This is like your "id". You'll want to create an object which contains a phone number and country code, let's call it FullPhoneNumber. Then you'll make a LiveData<FullPhoneNumber> phoneNumberLiveData, which is similar to the userIdLiveData in the previous example. Then:

    LiveData<ResendActivationCodeResponse> reactivationLiveData = 
    Transformations.switchMap(phoneNumberLiveData, currentPhoneNumber ->
         signInRepository.resendActivationCode(currentPhoneNumber.getNumber(), currentPhoneNumber.getCountryCode());
    

    Hope that helps or at least points you in the right direction!