0

My LiveData object is called twice when my httprequest is succefull, but in case of an error, it's only called once, causing the UI to display an empty list since my Error verification was skipped since there was no alert fired.

My repository code

private LiveData<List<Card>> getCards() {
    return cardsDao.getCards();
}

// Network request, that deals with the success or error of the request

public LiveData<DataWrapper<List<Card>>> loadCards() {
    AtomicReference<LiveData<DataWrapper<List<Card>>>> atomicWrapper = new AtomicReference<>();
    if (getCards().getValue() == null) {
        webService.getCards(result -> {
            CardResponse res = (CardResponse) result;
            List<Card> cardList = res.getCardsList();
            getInsertAsyncTask.execute(cardList.toArray(new Card[cardList.size()]));
        }, error -> {
            atomicWrapper.set(asLiveData(null, error.getMessage()));
        }, TAG);
    }
    atomicWrapper.set(asLiveData(getCards(),null));
    return atomicWrapper.get();
}

My BaseRepository code

LiveData<DataWrapper<List<T>>> asLiveData(LiveData<List<T>> dataSource, String error) {
    MediatorLiveData<DataWrapper<List<T>>> mediatorLiveData = new MediatorLiveData<>();
    mediatorLiveData.addSource(dataSource, data -> mediatorLiveData.setValue(new DataWrapper<>(data, error)));
    return mediatorLiveData;
}

My Fragment code

private void subscribeToCards() {
    mViewModel.getCards().observe(this, listDataWrapper -> {
        if( listDataWrapper == null ) {
            return;
        }

        if( listDataWrapper.error != null) {
            // Show error on UI
            dismissProgress();
            Log.e(TAG, "Error - " + listDataWrapper.error);
            showError(getString(R.string.cards_error_get_list_message));
            EventBus.getDefault().post(new DialogMessageEvent(getString(R.string.cards_error_get_list_title),
                getString(R.string.cards_error_get_list_message), getString(R.string.cards_error_get_list_button)));
        }

        if( listDataWrapper.data != null ) {
            // Update Ui
            refreshCardsList(listDataWrapper.data);
            cardsViewVisibility(true);
        }
    });
}

And finally, my ViewModel code

public LiveData<DataWrapper<List<Card>>> getCards(){
    return repository.loadCards();
}

To summarize, in case of failure, why does the observer callback only gets called once ? Because I've debug it, and in both ( succefull and failure ) cases, the method asLiveData is called TWICE, but it's only in the succefull attempt that the callback is called TWICE aswell, on failure the observer callback it's only called ONCE.

Edit: Added asynctask code

@SuppressLint("StaticFieldLeak")
AsyncTask<T,Void,Void> getInsertAsyncTask = new AsyncTask<T, Void, Void>() {
    @Override
    protected Void doInBackground(T... ts) {
        daoObject.insertObjects(Arrays.asList(ts));
        return null;
    }
};
Greggz
  • 1,873
  • 1
  • 12
  • 31

1 Answers1

1

The reason you get 2 callbacks for the success case and only 1 call for the error scenario looks to be to do with your repository setup.

When calling loadCards you first emit the state of the database with this call:

atomicWrapper.set(asLiveData(getCards(),null));

Your database will be queried at this point and the current values will trigger the mediatorLiveData.setValue. This will be the first emission.

mediatorLiveData.addSource(dataSource, data -> mediatorLiveData.setValue(new DataWrapper<>(data, error)));

At the same time you have triggered a call to you webservice, which if successful will trigger your asynctask to update the database.

webService.getCards(result -> {
    CardResponse res = (CardResponse) result;
    List<Card> cardList = res.getCardsList();
    getInsertAsyncTask.execute(cardList.toArray(new Card[cardList.size()]));
}, error -> {
    atomicWrapper.set(asLiveData(null, error.getMessage()));
}, TAG);

Once the insert command completes the MediatorLiveData will trigger it's setValue call again - it's listening to changes in the database so on the insertion it will receive a callback. This is the second emission in the success case.

In the error scenario you pass through null as the dataSource. Its surprising this doesn't crash as the addSource method marks the source parameter as Non Null. As it is null you don't get a callback and the mediatorLiveData.setValue won't be called. This means for an error scenario you only receive the first emission.

It may be simpler if you did something like the following:

  • setup a listener on the database and emit your datavalue without the error when a database update occurs.

  • on receiving an error you could then just emit the DataWrapper with an error

e.g. something like:

    private final AtomicBoolean isLoading = new AtomicBoolean(false);
    private final MediatorLiveData<DataWrapper<List<Card>>> mediatorLiveData = new MediatorLiveData<>();

    private MyRepository() {
        // requires a cardsDao
        // listen for DB changes and emit on callback
        mediatorLiveData.addSource(cardsDao.getCards(), data -> mediatorLiveData.setValue(new DataWrapper<>(data, null)));
    }

    public LiveData<DataWrapper<List<Card>>> cardsData() {
        return mediatorLiveData;
    }

    public void loadCards() {
        if (!isLoading.get()) {
            isLoading.set(true);
            webService.getCards(result -> {
                CardResponse res = (CardResponse) result;
                List<Card> cardList = res.getCardsList();
                // Trigger update in db
                getInsertAsyncTask.execute(cardList.toArray(new Card[cardList.size()]));
                isLoading.set(false);
            }, error -> {
                // Emit the error
                mediatorLiveData.setValue(new DataWrapper<>(null, error.getMessage()));
                isLoading.set(false);
            }, TAG);
        }
    }
Chris
  • 2,332
  • 1
  • 14
  • 17
  • Yep my implementation was incorrect.. In the error case I was creating yet another `Mediator` instance.. I tried to mimic how google does in their sample, but I screwed up somewhere along the line – Greggz Sep 07 '18 at 15:50
  • In this example, is how you usually deal with these kind of implementations ? Thanks for your answer. Put me in the right track – Greggz Sep 07 '18 at 15:51
  • One of the best places to look at examples is the architecture components repo: Their repository pattern is the one Google seem to be pushing for. https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/RepoRepository.kt – Chris Sep 07 '18 at 16:00
  • One of the authors has a few blog posts which may also be useful - think it has some references on the repository pattern. https://medium.com/androiddevelopers/livedata-beyond-the-viewmodel-reactive-patterns-using-transformations-and-mediatorlivedata-fda520ba00b7 https://medium.com/androiddevelopers/viewmodels-and-livedata-patterns-antipatterns-21efaef74a54 – Chris Sep 07 '18 at 16:00
  • There is also the android-sunflower project which is the latest showcase from google with respect to the Architecture Components: https://github.com/googlesamples/android-sunflower – Chris Sep 07 '18 at 16:01
  • Yes that was the github link I was following. But I'm not Kotlin *fluent* – Greggz Sep 07 '18 at 16:02