LiveData with two sources?

Viewed 298

I'd like to use the same LiveData with different sources. One from an API call which is an observable and one from a database which is a LiveData. I'd like to be able to do something like this:

private LiveData<List<Items>> items = new MutableLiveData<>();

// this one comes from an API and it's an observable
public void onApiItemsSelected(String name) {
 Disposable disposable = repository.getItemsFromApi(name)
 .observeOn(AndroidSchedulers.mainThread())
 .subscribe(itemsList -> {
     items.setValue(itemsList);
 });

 compositeDisposable.add(disposable);
}

// this one comes from the database and it's a livedata that I want to transform first
public void onDatabaseItemsSelected() {
 items = Transformations.map(repository.getItemsFromdatabase(), itemsList -> {
    List<Items> finalItemsList = new ArrayList<>();

    for (Item item : itemsList) {
      finalItemsList.add(itemsList.toSomething());
    }

    return finalItemsList;
 });
}

The problem is that a Transformation.map always returns a LiveData and in order to make a items.setValue(itemsList) items need to be a MutableLiveData. I tried with MediatorLiveData but it's calling the two sources and mixing everything. That's not what I need here. I need one source OR the other. Is it possible?

2 Answers

I think you could delegate this responsibility to your repository.

See for example this open source project and related article with more info.

Basically, the repository handles all the complexity of deciding which source to get the data from (see the JobRepository). It exposes a rx Observable to the ViewModel (see the JobsViewModel). Then all the ViewModel has to do is update the LiveData:

private val presentation: MutableLiveData<Presentation> = MutableLiveData()

fun bind() {
        jobRepository.getJobs(true)
            .subscribeOn(Schedulers.io())
            .subscribe { resource: Resource<List<JobWithRelations>> ->
                when (resource) {
                    // commenting some parts for brevity ...
                    is Resource.ResourceFound -> {
                        presentation.postValue(Presentation(getApplication(), resource.data!!))
                    }
                    // ...
                }
            }.addTo(compositeDisposable)
    }

Managing multiple sources of data is complex. Among the many aspects involved in architecting this, things to keep in mind include:

  • In-memory caching expiration strategies
  • Policies for db caching, for example, when users are offline
  • Throttling and retry of API calls
  • Policies for updating the in-memory cache or db when new data comes from the network

One library that could help you is the Dropbox Store. With it, you could build your data sources like in the example below from their documentation:

StoreBuilder
    .from(
        fetcher = nonFlowValueFetcher { api.fetchSubreddit(it, "10").data.children.map(::toPosts) },
        sourceOfTruth = SourceOfTrue.from(
            reader = db.postDao()::loadPosts,
            writer = db.postDao()::insertPosts,
            delete = db.postDao()::clearFeed,
            deleteAll = db.postDao()::clearAllFeeds
        )
    ).build()

Then to get the data in your ViewModel:

private val presentation: MutableLiveData<Presentation> = MutableLiveData()

lifecycleScope.launchWhenStarted {
  store.stream(StoreRequest.cached(key = key, refresh=true)).collect { response ->
    when(response) {
        // commenting some parts for brevity ...
        is StoreResponse.Data -> {
            presentation.postValue(response.value)
        }
        // ...
    }
  }
}

I am not sure it is the best practice but I think will do the trick.


public void onDatabaseItemsSelected() {
items.value = repository.getItemsFromdatabase().value;
};

The syntax may not be accurate as I am know little about java, I do mainly kotlin. But the thing is: when the user clicks Api, make your onApiItemsSelected() run, that will set the value of items, when the user clicks database, make onDatabaseItemsSelected() run, so it will replace items value for the result of repository.getItemsFromDatabase()

Related