How to verify no interactions on RxJava2 Observable using Mockito - android

Observable is final class, so we can't create mocks for it to verify or capture any interactions performed on it.
And Observable's test(), TestSubscriber also does not provide any such interaction assertion techinque
I have created a generic method for checking cached data before loading from network
/**
* General parametrized method for loading data from service while checking connection
* and respecting reload state
*/
private <T> Observable<T> getData(Observable<T> dataSource, String key, boolean reload) {
T cachedData = (T) cacheModel.getValue(key);
// Do not use cache if reloading
if(!reload && cachedData != null)
return Observable.just(cachedData);
if(!utilModel.isConnected()) {
return Observable.error(new Throwable(Constants.NO_NETWORK));
}
return dataSource
.doOnNext(data -> cacheModel.saveObject(key, data))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
And using it as follows:
#Override
public Observable<User> getOrganiser(boolean reload) {
return getData(eventService.getUser(authorization), ORGANIZER, reload);
}
Before, I was not even calling eventService.getUser(...) to get the observable, so I could test if it was never being called, but now, as I have to pass it to the template method, I need to call it but verify if cache is present, it is never interacted with.
This is my previous test, which is obviously failing now
#Test
public void shouldLoadOrganizerFromCache() {
// Clear cache
objectCache.clear();
User user = new User();
objectCache.saveObject(RetrofitEventRepository.ORGANIZER, user);
// No force reload ensures use of cache
Observable<User> userObservable = retrofitEventModel.getOrganiser(false);
userObservable.test().assertNoErrors();
userObservable.test().assertValue(user);
verify(eventService, never()).getUser(auth);
}

Assuming the Observable you want to test is something like this:
Observable<User> userObservable = getData(eventService.getUser(authorization));
while you can't use directly test() method or subscribing with TestObserver, as you hand the input Observable to a different entity. you can use Observable side effect methods to verify almost any interaction with the Observable.
for instance in your case, you can use doOnSubscribe and raise a flag if it was called (indicates the service method was called while it shouldn't as cache should be invoked):
final boolean[] serviceCalled = new boolean[1];
Observable<User> userServiceObservable = eventService.getUser(authorization)
.doOnSubscribe(disposable -> serviceCalled[0] = true);
Observable<User> userObservable = getData(userServiceObservable, ORGANIZER, reload);
userObservable.test()
.assertNoErrors()
.assertValue(user)
.awaitTerminalEvent();
Assert.assertEquals(false, serviceCalled[0]);
BTW, your cache method might not work as expected as your'e testing the cache at getting the Observable and not when subscribing, so cache state can be different at subscribe time. also multiple calls can happen simultaneously resulting with multiple calls to server and updating the cache, you can see here my suggestion of cache using Observable.defer().

Related

Problems returning LiveData in Activity that observes changes in database using MVVM model

In my application I return LiveData from Room database (SQLite) in repository, and observe the data on my application Activity.
The problem is: having LiveData in Activity that observes changes in database using MVVM model, and runs some code when data is changed (as this is how observe works).
The method looks like this in repository:
public LiveData<TourWithAllGeoPoints> getTourWithAllGeoPoints(long tourId, boolean mIsFirstTime) {
if (!mIsFirstTime) {
return tourWithAllGeoPoints;
}
MyTourAssistentDatabase.databaseWriteExecutor.execute(()-> {
tourWithAllGeoPoints = toursDAO.getTourWithAllGeoPoints(tourId); //this part finishes after reuturn
});
return tourWithAllGeoPoints; //this part returns
}
mIsFirstTime checks if the Activity (or Fragment) is loading first time or not (if Bundle is null or not).
databaseWriteExecutor.execute() is a ThreadPool executing the code in own thread.
toursDAO.getTourWithAllGeoPoints(tourId) is where I ask and get data from Room database. It returns a LiveData object.
In Activity code I do observe the LiveData:
activeTourViewModel.getTourWithAllGeoPoints(tourId, mIsFirstTime).observe(this, geoPointsPlanned -> {
//Some code here changing UI, other variables, etc.
}
But the problem is that the method returns 'tourWithAllGeoPoints' before the execute() part is finished. So this means it returns an empty LiveData. Or the LiveData we observe on MainActivity is not the same LiveData we get from toursDAO.
And so in Activity it observes the empty LiveData.
My attempted solutions are:
1) I can run the query in main thread like this:
public LiveData<TourWithAllGeoPoints> getTourWithAllGeoPoints(long tourId, boolean mIsFirstTime) {
if (!mIsFirstTime) {
return tourWithAllGeoPoints;
}
tourWithAllGeoPoints = toursDAO.getTourWithAllGeoPoints(tourId);
return tourWithAllGeoPoints;
}
But then it gives warning message about not to run queries to Room database on main thread as it may take long time.
2) Or I can make the toursDAO.getTourWithAllGeoPoints(tourId) return a TourWithAllGeoPoints object rather than a LiveData, and put it into a LiveDataobject, like this:
public LiveData<TourWithAllGeoPoints> getTourWithAllGeoPoints(long tourId, boolean mIsFirstTime) {
if (!mIsFirstTime) {
return tourWithAllGeoPoints;
}
MyTourAssistentDatabase.databaseWriteExecutor.execute(()-> {
TourWithAllGeoPoints twagp = toursDAO.getTourWithAllGeoPoints(tourId);
tourWithAllGeoPoints.postValue(twagp)
});
return tourWithAllGeoPoints;
}
So that it observes the changes in LiveData. But then I can't observe the changes made in database, since it just returns a List. This means I have to run the same method every time I make a change in the database.
3) Or I can put a LiveData inside a LiveData, also like this:
public LiveData<LiveData<TourWithAllGeoPoints>> getTourWithAllGeoPoints(long tourId, boolean mIsFirstTime) {
if (!mIsFirstTime) {
return tourWithAllGeoPoints;
}
MyTourAssistentDatabase.databaseWriteExecutor.execute(()-> {
LiveData<TourWithAllGeoPoints> twagp = toursDAO.getTourWithAllGeoPoints(tourId); //returns LiveData
tourWithAllGeoPoints.postValue(twagp)
});
return tourWithAllGeoPoints;
}
But I don't know if putting LiveData inside a LiveData is a good idea or not.
Or the are other solutions. But how can I solve this problem?
The problem is: having LiveData in Activity that observes changes in database using MVVM model, and runs some code when data is changed (as this is how observe works).
For the specific problem you described (i.e. returning the first TourWithAllGeoPoints and nothing else), it seems LiveData isn't the most appropriate data type you can use here. LiveData is meant to be used when, as the name says, the underlying data is live and it could change anytime, and you need to observe the data everytime it changes. If all you need is one value, it's better not to use LiveData at all. Just make your DAO method getTourWithAllGeoPoints return TourWithAllGeoPoints (without LiveData) and call it from a background thread. Take a look at this link for some ways to do that. It's much easier to use Kotlin coroutines in this case, but you'd need to be using Kotlin for that (which I recommend :) ).
But if the problem you described is generic (not exactly just for returning one value once), you can use a MediatorLiveData to observe a LiveData and post something different (or not) every time it emits a new value. Take a look at this code:
private MediatorLiveData<TourWithAllGeoPoints> mediator;
public YourRepositoryConstructor() {
mediator = new MediatorLiveData<>();
mediator.addSource(toursDAO.getTourWithAllGeoPoints(tourId), data -> {
if (mediator.getValue() != null) {
mediator.setValue(data);
}
});
return mediator;
}
public LiveData<TourWithAllGeoPoints> getTourWithAllGeoPoints(long tourId, boolean mIsFirstTime) {
return mediator;
}
A MediatorLiveData observes one (or many) other LiveData objects and emits a new value according to the changes of the other LiveData objects. It may emit a different data type (i.e. it doesn't have to be the same type of the underlying LiveData objects) or even not emit anything at all. It's all according to your MediatorLiveData code. In this case specifically, every time the result of getTourWithAllGeoPoints emits something new, you MediatorLiveData will react to that and only emit a new value in itself if it's the first time. It can do that by checking if it's value is null, it doesn't need the variable mIsFirstTime (unless null is a valid value for you in that case).
The MediatorLiveData is a more generic approach suitable for the type of scenario you described, but it may be too much effort if you only need one result for that query, which you might solve by not using LiveData at all.

How to avoid unnecessary re-rendering of RecyclerView when using Flowable (RxJava)?

Trying to implement MVVM with room flowable # rxjava + retrofit2.
When the activity starts, I request a list of items from the repository. The repository retrieves them from the database (room) and returns Flowable to the ViewModel. At the same moment, the repository requests the current list from the API, and as soon as the result is returned, I delete the existing entries in the database and insert what arrived from the API.
The observer in activity, of course, updates recyclerview three times. Because of this, ui "blinks" when updating the list:
the list is not empty -> the list is empty -> the list is not empty.
After receiving the flowable from Room for the first time, the observer then receives an empty list (when deleting entries in the database) and then a new list after inserting it into the database from API.
How to avoid this behavior of RecyclerView? Is there any well-established best practice, true way, etc.?
PaymentRepository method for retrieving payment list:
private Flowable<List<Payment>> getPayments(Long accountId) {
// requesting actual data from API
paymentService.getPayments().flatMapCompletable(payments -> paymentDao
.deleteAllByAccountId(accountId)
.andThen(paymentDao.insert(payments))
.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
).subscribe();
// and return cached list from db
return paymentDao.findPaidByAccountId(accountId)
.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
}
ViewModel
private CompositeDisposable disposable = new CompositeDisposable();
public MutableLiveData<Resource<List<Payment>>> result = new MutableLiveData<>(Resource.loading());
public PaymentListViewModel() {
disposable.add(
paymentRepository.getPayments().subscribe(
payments -> result.setValue(Resource.success(payments)),
throwable -> result.setValue(paymentService.parseError(throwable).toResource())
)
);
}
Observer in Activity
viewModel.result.observe(this, resource -> {
switch (resource.status) {
case SUCCESS:
adapter.setItems(resource.data);
binding.preloader.setVisibility(View.GONE);
break;
case ERROR:
Toast.makeText(this, resource.message, Toast.LENGTH_LONG).show();
binding.preloader.setVisibility(View.GONE);
break;
case LOADING:
binding.preloader.setVisibility(View.VISIBLE);
break;
}
});
I'm not a Room expert, maybe there is so RX-way to update values without emitting it into listeners.
But here is the way that doesn't request it:
private Flowable<List<Payment>> getPayments(Long accountId) {
// requesting actual data from API
Flowable<List<Payment>> request = paymentService.getPayments()
.flatMapCompletable(payments -> paymentDao
.deleteAllByAccountId(accountId)
.andThen(paymentDao.insert(payments))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
);
return paymentDao.findPaidByAccountId(accountId)
.take(1)
.switchMap(dbList -> request.startWith(dbList))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.distinctUntilChanged();
}
in this scenario you take only first value from DB (take(1)) and then wait for the values from API.
switchMap (also could be flatMap here, no diff in this case) guaranty you that DB value will be first and only after that "listen" for API.
The only problem here if you suppose to update DB values somewhere else, while this Activity is running. In that case this updates will not be displayed.

Rxjava and work manager chained asychronous calls

Update: This was how my old insertIntoDb method looked like which didn't work :
private Completable insertIntoDb(List<ArticleEntity> articleItemEntities) {
return database.articleDao().insertArticles(articleItemEntities)
.observeOn(Schedulers.io());
}
I changed it to the following and now it works :
private void insertIntoDbNew(List<ArticleEntity> articleItemEntities) {
mCompositeDisposable.add(
database.articleDao().insertArticles(articleItemEntities)
.subscribeOn(Schedulers.io())
.subscribe());
}
I don't know why but now it works. Sure the worker completes before the database insert completes but that doesn't seem to be a problem which I believed before.
End of update.
I'm new to reactive programming. My goal is to schedule a work manager to do 4 Actions then return a result using RxJava2. Here are the tasks I want to perform
Do a web api call.
Structure the data we get from the API call.
Insert it into our local room database.
When everything is complete signal Result.success() back to the Job so it knows that everything went ok and can terminate.
So my preferred method would look something like this.
public Result doWork(){
return api.get("URL") responseData -> structureData(responseData) structuredData -> insertIntoDB(structuredData) -> Result.success()
}
I'm using RxJava2 and the RxWorker class.
Below is my current solution. Is this correct or am I doing something wrong?
public class DownloadWorker extends RxWorker {
#Override
public Single<Result> createWork() {
return apiService.download("URL")
.map(response -> processResponse(response))
.doOnSuccess(data -> insertIntoDb(data))
.flatMap(response ->
allComplete()
)
.observeOn(Schedulers.io());
}
Single<Result> allComplete() {
return Single.just(Result.success());
}
}
It behaves like I want it to. It downloads the data, structures it, then inserts it into the DB then returns Result.success(). But I have no idea what I am doing. Am I using RxJava as it was intended?
Also this part bothers me :
.flatMap(response -> allComplete())
the response part is superfluous can I remove it somehow?
I did some improvements to your code:
public class DownloadWorker extends RxWorker {
#Override
public Single<Result> createWork() {
return apiService.download("URL")
.map(response -> processResponse(response))
.flatMapCompletable(articleItemEntities -> database.articleDao().insertArticles(articleItemEntities))
.toSingleDefault(Result.success())
.onErrorReturnItem(Result.failure())
.observeOn(Schedulers.io());
}
}
In original code, you save data using doOnSuccess method which is a side effect. As you mentioned in your comment, insertIntoDb() method returns Completable. Therefore, I changed doOnSuccess(data -> insertIntoDb(data)) to flatMapCompletable(data -> insertIntoDb(data)) which will allow you make sure storing data succeeded and wait until it finishes. As insertIntoDb() method returns Completable and createWork() method has to return Result, we have to now change type from Completable to Single<Result>. Therefore, I used toSingleDefault which returns Result.success() by default. Also, I added onErrorReturnItem(Result.failure()) which will allow RxWorker to track errors. I hope my answer helps.

RxJava observe multiple observers in single subscriber

Code
Author author = baseRealm.where(Author.class).equalTo("id", mId).findFirst();
public boolean checkGlobalSyncStatus(Author author, List<Books> mBooks) {
final boolean[] isJobSynchronized = {false};
Observable.fromIterable(mBooks)
.filter(Books::isChanged)
.doOnNext(book -> isJobSynchronized[0] = true)
.just(author)
.flatMapIterable(Author::getAllBooks)
.filter(MyBook::isChanged)
.doOnNext(mBook -> isJobSynchronized[0] = true)
.just(author)
.flatMapIterable(Author::getAllWriters)
.filter(Writers::isChanged)
.doOnNext(jobPage -> isJobSynchronized[0] = true)
.subscribe();
return isJobSynchronized[0];
}
Problem
fromIterable(mBooks) is called from static-reference Observable BUT just(author) is called from instance-reference.
I only want to get this operation done in single query. I can make different observable for each and perform desired operation but that would be lengthy.
Why?
By doing so, SonarQube is giving me unsuccessful check and forcing me to remove instance-reference.
Any alternatives will be appreciated.
You are trying to use just() as an operator when it is really an observable. It looks like your intention is to use the passed in author to make a series of queries, and then check that any of the books associated with the author have "changed".
Additionally, you are trying to return a boolean value that likely has not been set by the time the return occurs. You may need to block and wait for the observer chain to finish if you want the value. More likely, you want the observer chain to finish if any book has changed.
Additionally, the series of steps where you set the flag to true come down to setting the flag to true the first time.
Instead of just(), use map() to rebind the original author into the observer chain. Use the toBlocking() operator to make the process synchronous.
Observable.fromIterable(mBooks)
.filter(Books::isChanged)
.toBlocking()
.subscribe( ignored -> isJobSynchronized[0] = true );
return isJobSynchronized[0];
Since the (presumably) asynchronous queries are no longer necessary to compute the value, remove RxJava:
return mBooks.stream()
.filter(Books::isChanged)
.anyMatch();
There is no reason to use RxJava here, however, the proper combination of operators would be as follows:
Author author = baseRealm.where(Author.class).equalTo("id", mId).findFirst();
public boolean checkGlobalSyncStatus(Author author, List<Books> mBooks) {
return Single.concat(
Observable.fromIterable(mBooks)
.any(Books::isChanged)
, // ---------------------------------------------
Observable.fromIterable(author.getAllBooks)
.any(MyBook::isChanged)
, // ---------------------------------------------
Observable.fromIterable(author.getAllWriters)
.any(Writers::isChanged)
)
.any(bool -> bool)
.blockingGet();
}

What is the difference between map() and switchMap() methods?

What is the difference between those 2 methods of the LiveData class? The official doc and tutorial are pretty vague on that. In the map() method the first parameter called source but in the switchMap() it called trigger. What's the rationale behind that?
As per the documentation
Transformations.map()
Applies a function on the value stored in the LiveData object, and propagates the result downstream.
Transformations.switchMap()
Similar to map, applies a function to the value stored in the LiveData object and unwraps and dispatches the result downstream. The function passed to switchMap() must return a LiveData object.
In other words, I may not be 100% correct but if you are familiar with RxJava; Transformations#map is kind of similar to Observable#map & Transformations#switchMap is similar to Observable#switchMap.
Let's take an example, there is a LiveData which emits a string and we want to display that string in capital letters.
One approach would be as follows; in an activity or fragment
Transformations.map(stringsLiveData, String::toUpperCase)
.observe(this, textView::setText);
the function passed to the map returns a string only, but it's the Transformation#map which ultimately returns a LiveData.
The second approach; in an activity or fragment
Transformations.switchMap(stringsLiveData, this::getUpperCaseStringLiveData)
.observe(this, textView::setText);
private LiveData<String> getUpperCaseStringLiveData(String str) {
MutableLiveData<String> liveData = new MutableLiveData<>();
liveData.setValue(str.toUpperCase());
return liveData;
}
If you see Transformations#switchMap has actually switched the LiveData. So, again as per the documentation The function passed to switchMap() must return a LiveData object.
So, in case of map it is the source LiveData you are transforming and in case of switchMap the passed LiveData will act as a trigger on which it will switch to another LiveData after unwrapping and dispatching the result downstream.
My observation is that, if your transformation process is fast (Doesn't involve database operation, or networking activity), then you can choose to use map.
However, if your transformation process is slow (Involving database operation, or networking activity), you need to use switchMap
switchMap is used when performing time-consuming operation
class MyViewModel extends ViewModel {
final MutableLiveData<String> mString = new MutableLiveData<>();
final LiveData<Integer> mCode;
public MyViewModel(String string) {
mCode = Transformations.switchMap(mString, input -> {
final MutableLiveData<Integer> result = new MutableLiveData<>();
new Thread(new Runnable() {
#Override
public void run() {
// Pretend we are busy
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int code = 0;
for (int i=0; i<input.length(); i++) {
code = code + (int)input.charAt(i);
}
result.postValue(code);
}
}).start();
return result;
});
if (string != null) {
mString.setValue(string);
}
}
public LiveData<Integer> getCode() {
return mCode;
}
public void search(String string) {
mString.setValue(string);
}
}
map is not suitable for time-consuming operation
class MyViewModel extends ViewModel {
final MutableLiveData<String> mString = new MutableLiveData<>();
final LiveData<Integer> mCode;
public MyViewModel(String string) {
mCode = Transformations.map(mString, input -> {
/*
Note: You can't launch a Thread, or sleep right here.
If you do so, the APP will crash with ANR.
*/
/*
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
*/
int code = 0;
for (int i=0; i<input.length(); i++) {
code = code + (int)input.charAt(i);
}
return code;
});
if (string != null) {
mString.setValue(string);
}
}
public LiveData<Integer> getCode() {
return mCode;
}
public void search(String string) {
mString.setValue(string);
}
}
First of all, map() and switchMap() methods are both invoked on the main thread. And they have nothing to do with being used for fast or slow tasks. However, it might cause lags on UI if you do complex computational or time consuming tasks inside these methods instead of a worker thread, parsing or converting a long and/or complex json response for instance, since they are executed on the UI thread.
map()
map() method's code is
#MainThread
public static <X, Y> LiveData<Y> map(#NonNull LiveData<X> source,
#NonNull final Function<X, Y> func) {
final MediatorLiveData<Y> result = new MediatorLiveData<>();
result.addSource(source, new Observer<X>() {
#Override
public void onChanged(#Nullable X x) {
result.setValue(func.apply(x));
}
});
return result;
}
What it does is, it uses a source LiveData, I is input type, and calls setValue(O) on LiveData where O is output type.
For it to be clear let me give an example. You wish to write user name and last name to textView whenever a user changes.
/**
* Changes on this user LiveData triggers function that sets mUserNameLiveData String value
*/
private MutableLiveData<User> mUserLiveData = new MutableLiveData<>();
/**
* This LiveData contains the data(String for this example) to be observed.
*/
public final LiveData<String> mUserNameLiveData;
now let's trigger changes on mUserNameLiveData's String when mUserLiveData changes.
/*
* map() method emits a value in type of destination data(String in this example) when the source LiveData is changed. In this example
* when a new User value is set to LiveData it trigger this function that returns a String type
*
* Input, Output
* new Function<User, String>
*
* public String apply(User input) { return output;}
*/
// Result<Output> Source<Input> Input, Output
mUserNameLiveData = Transformations.map(mUserLiveData, new Function<User, String>() {
#Override
public String apply(User input) {
// Output
return input.getFirstName() + ", " + input.getLastName();
}
});
And let's do the same thing with MediatorLiveData
/**
* MediatorLiveData is what {#link Transformations#map(LiveData, Function)} does behind the scenes
*/
public MediatorLiveData<String> mediatorLiveData = new MediatorLiveData<>();
/*
* map() function is actually does this
*/
mediatorLiveData.addSource(mUserLiveData, new Observer<User>() {
#Override
public void onChanged(#Nullable User user) {
mediatorLiveData.setValue(user.getFirstName() + ", " + user.getLastName());
}
});
And if you observe MediatorLiveData on Activity or Fragment you get the same result as observing LiveData<String> mUserNameLiveData
userViewModel.mediatorLiveData.observe(this, new Observer<String>() {
#Override
public void onChanged(#Nullable String s) {
TextView textView = findViewById(R.id.textView2);
textView.setText("User: " + s);
Toast.makeText(MainActivity.this, "User: " + s, Toast.LENGTH_SHORT).show();
}
});
switchMap()
switchMap() returns the same MediatorLiveData not a new LiveData every time the SourceLiveData changes.
It's source code is
#MainThread
public static <X, Y> LiveData<Y> switchMap(#NonNull LiveData<X> trigger,
#NonNull final Function<X, LiveData<Y>> func) {
final MediatorLiveData<Y> result = new MediatorLiveData<>();
result.addSource(trigger, new Observer<X>() {
LiveData<Y> mSource;
#Override
public void onChanged(#Nullable X x) {
LiveData<Y> newLiveData = func.apply(x);
if (mSource == newLiveData) {
return;
}
if (mSource != null) {
result.removeSource(mSource);
}
mSource = newLiveData;
if (mSource != null) {
result.addSource(mSource, new Observer<Y>() {
#Override
public void onChanged(#Nullable Y y) {
result.setValue(y);
}
});
}
}
});
return result;
}
Basically what it does is, it creates a final MediatorLiveData and it's set to the Result like map does() but this time function returns LiveData
public static <X, Y> LiveData<Y> map(#NonNull LiveData<X> source,
#NonNull final Function<X, **Y**> func) {
final MediatorLiveData<Y> result = new MediatorLiveData<>();
result.addSource(source, new Observer<X>() {
#Override
public void onChanged(#Nullable X x) {
result.setValue(func.apply(x));
}
});
return result;
}
#MainThread
public static <X, Y> LiveData<Y> switchMap(#NonNull LiveData<X> trigger,
#NonNull final Function<X, **LiveData<Y>**> func) {
final MediatorLiveData<Y> result = new MediatorLiveData<>();
result.addSource(trigger, new Observer<X>() {
LiveData<Y> mSource;
#Override
public void onChanged(#Nullable X x) {
LiveData<Y> newLiveData = func.apply(x);
if (mSource == newLiveData) {
return;
}
if (mSource != null) {
result.removeSource(mSource);
}
mSource = newLiveData;
if (mSource != null) {
result.addSource(mSource, new Observer<Y>() {
#Override
public void onChanged(#Nullable Y y) {
result.setValue(y);
}
});
}
}
});
return result;
}
So map() takes LiveData<User> and transforms it into a String, if User object changes name field changes for instance.
switchMap() takes a String and gets LiveData<User> using it. Query a user from web or db with a String and get a LiveData<User> as a result.
There are already some good answers above, but I still struggled with them till I understood it, so I will try to explain on a concrete example for people with my way of thinking, without going into technical details and code.
In both map and switchMap there is a source (or trigger) live data, and in both cases you want to transform it to another live data. Which one will you use - depends on the task that your transformation is doing.
map
Consider the same simple example that is used everywhere - your source live data contains a User object - LiveData<User>, which points to the currently logged in user. You want to display a text in your UI saying Current user: <USERNAME>. In this case each change signal from source should trigger exactly one signal of the resulting "mapped" LiveData. For example, the current User object is "Bob" then the UI text shows Current user: Bob. Once your LiveData<User> triggers a change your UI will observe it and update text to Current user: Alice. Very simple, linear, one to one change.
switchMap
Consider the following example - you want to create a UI which shows the users whose name matches the given search term. We can be quite smart about it and hold the search term as a LiveData! So it will be a LiveData<String> and every time the user inputs a new query string our Fragment/Activity will simply set the text input value to this live data in the ViewModel. As a result, this live data will fire a change signal. Once we get this signal we start searching for the users. Now let's consider our search is so fast that it immediately returns a value. At this point you think that you can just use a map and return the matching users which will update the UI. Well, you will have a bug now - imagine you update the database regularly and after next update more users appear matching the search term! As you can see, in this scenario the source trigger (search term) does not necessarily result in a single trigger of mapped live data, the mapped live data given to the UI might still need to continue triggering the values after new users are added to the database. At this point you might say, that we could return a "smarter" live data, which will not only wait for source triggers, but will also monitor the database for users matching the given term (you will be able to do that with Room DB out of the box). But then comes another question - what if the search term changes? So your term was x, it triggered a live data which queries the users and keeps an eye on the database, it returns userx, userxx and then after five minutes it returns userx, userxxx and so on. Then the term was changed to y. Now we need to somehow stop listening to the smart live data giving us users with x in it, and switch it with the new smart live data which will monitor and give us users with y in their names. And that is exactly what switchMap is doing! And notice, this switch needs to be done in such a way, that in your UI you just write switchMap(...).observe once, that means that switchMap must return a wrapper LiveData which will stay the same throughout the execution, but will switch the live data sources under the hood for us.
Conclusion
Although they seem to look the same at first glance, the use cases for map and switchMap are different, you will get the feeling of which one to use once you start implementing your case, mostly when you realize that in you mapping function you have to call some code from your other modules (like Repositories) which return LiveData.
Map() is conceptually identical to the use in RXJava, basically you are changing a parameter of LiveData in another one
SwitchMap() instead you are going to substitute the LiveData itself with another one! Typical case is when you retrieve some data from a Repository for instance and to "eliminate" the previous LiveData (to garbage collect, to make it more efficient the memory usually) you pass a new LiveData that execute the same action( getting a query for instance)
switchMap :
Let’s say we’re looking for the username Alice. The repository is creating a new instance of that User LiveData class and after that, we display the users. After some time we need to look for the username Bob there’s the repository creates a new instance of LiveData and our UI subscribes to that LiveData. So at this moment, our UI subscribes to two instances of LiveData because we never remove the previous one. So it means whenever our repository changes the user’s data it sends two times subscription. Now, how do we solve this problem…?
What we actually need is a mechanism that allows us to stop observing from the previous source whenever we want to observe a new one. In order to this, we would use switchMap. Under the hood, switchMap uses the MediatorLiveData that removes the initial source whenever the new source is added. In short, it does all the mechanism removing and adding a new Observer for us.
but map is static it used when you don't forced to get new live data every time
With map you have same source livedata in the end but it's data (value) changes with provided function before emitting
With switchMap, you use source livedata just as a trigger for returning a standalone livedata (of course you can use triggers data in your function input)
Trigger: everything that causes livedata's observer's onChanged() invoking
Transformation.map()
fun <X, Y> map(trigger: LiveData<X>, mapFunction: Function<X, Y> ): LiveData<Y>?
trigger - the LiveData variable that once changed triggers mapFunction to execute.
mapFunction - the function to call when a change take place on the trigger LiveData. Parameter X is a reference to trigger (via it). The function returns a result of specified type Y, which ultimately is returned by map() as a LiveData object.
Use map() when you want to perform an operation (via mapFunction) when the trigger LiveData variable changes. map() will return a LiveData object that should be observed for the mapFunction to be called.
Example:
Assume a simple list of bowler names, their average and their average with handicap:
data class Bowler(val name:String, val average:Int, var avgWHDCP:Int)
var bowlers = listOf<Bowler>(Bowler("Steve", 150,150), Bowler ("Tom", 210, 210))
Assume a MutableLiveData Int variable that holds a handicap increment value. When this value changes, avgWHDCP for all bowlers in the list needs to be re-computed. Initially it is set to zero.
var newHDCP:MutableLiveData<Int> = MutableLiveData(0)
Create a variable that calls Tranformation.map(). It’s first argument is newHDCP. It’s second argument is the function to be called when newHDCP changes. In this example, the function will iterate through all the bowler objects, compute the new avgWHDCP for each bowler in the bowlers list, and return the result as an observable list of LiveData Bowler objects. Note that in this example, the original non-LiveData list of bowlers and the returned list of bowlers will reflect the same value, as they are referencing the same data store. However, the result of the function is observable. The original list of bowlers is not as it was not setup as a LiveData.
var updatedBowlers: LiveData<List<Bowler>> = Transformations.map(newHDCP) {
bowlers.forEach { bowler ->
bowler.avgWHDCP = bowler.average + it
}
return#map bowlers
}
Somewhere in your code, add a method to update newHDCP. In my example, when a radio button is clicked, newHDCP will get changed, and the process will trigger to call the function specified in Transformations.map()
rbUpdateBy20.setOnCheckedChangeListener { _, isChecked ->
viewModel.bowlingBallObject.newHDCP.value = 20
}
Finally, all this will only work if updatedBowlers is observed. This would be placed in your Activity or Fragment in a method such as OnViewCreated()
viewModel.updatedBowlers.observe(viewLifecycleOwner, Observer { bowler ->
if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
refreshRecycler()
}
})
If you wanted to get a little more concise and you really didn’t need a live reference to updatedBowlers, here’s how you can combine updateBowlers with the observer:
Transformations.map(viewModel.newHDCP) {
viewModel.bowlers.forEach { bowler ->
bowler.avgWHDCP = bowler.average + it
}
return#map viewModel.bowlers
}.observe(viewLifecycleOwner, Observer { bowler ->
if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
refreshRecycler()
}
})
And that’s basically it. Anytime you change the value of newHDCP, the function specified in Transformation.map() will get called, it will transform the bowler object with the newly computed avgWHDCP and return a LiveData object of List<Bowler>
Transformation.switchMap()
fun <X, Y> switchMap(source: LiveData<X>, switchMapFunction: Function<X, LiveData<Y>!>): LiveData<Y>
source - the LiveData variable that once changes triggers switchMapFunction to execute.
switchMapFunction - the function to call when a change take place on the source LiveData. Parameter X is reference to the same source object (via it). The switchMapFunction function MUST returns a LiveData result, which effectively gets returned via Transformation.switchMap(). In essence, this allows you to swap out one reference of a LiveData container object for another.
Use switchMap() when you have a variable referencing a LiveData object, and you want to switch that variable to another, or to say it a different way you want to refresh the existing LiveData container. This is useful, for example, if your LiveData variable is referencing a database data store and you want to requery with different parameters. switchMap allows you to re-execute the query and replace with a new LiveData results.
Example:
Assume a database repository with a bunch of bowling ball queries from a BowlingBall DAO table:
private val repository = BowlingBallRepository(application)
And I want to execute a query that fetches active or inactive bowling balls, depending on what the user specifies. Through the UI, the user can select active or inactive, so my query needs to handle both. So I create a MutableLiveData variable that hold an active or inactive status. In this example, I default to ‘A’ for active.
var activeFlag:MutableLiveData<String> = MutableLiveData(“A”)
Now, we need a LiveData variable that will hold the result of my query to get all the bowling balls of a specific status. So I create a variable called allBowlingBalls of type LiveData<List<BowlingBallTable>>? and assign it to Transformation.switchMap. I pass to the switchMap function the activeFlag variable as well as a lambda function that will receive that same activeFlag variable (via it) and the function makes a call to a query in the DB repository to re-fetch all bowling balls with the passed status. The LiveData result of the lambda function passes back through the switchMap method and is re-assigned to allBowlingBalls.
private var allBowlingBalls: LiveData<List<BowlingBallTable>>? = Transformations.switchMap(activeFlag) {repository.getAllBalls(it)}
I need a way to trigger a refresh of allBowlibgBalls. Again, this will be done when activeFlag changes. Somewhere in your code, add a function to update activeFlag. In my example, when a radio button is clicked, activeFlag will get changed, and the process will trigger to call the function specified in Transformations.switchMap()
rbActive.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
viewModel.activeFlag.value = ActiveInactive.ACTIVE.flag
refreshRecycler()
}
}
Finally, all this will only work if allBowlingBalls is observed. So first create a function to fetch allBowlingBalls:
fun getAllBowlingBalls():LiveData<List<BowlingBallTable>>? {
return allBowlingBalls
}
Then place an observer on getAllBowlingBalls():
viewModel.getAllBowlingBalls()?.observe(viewLifecycleOwner, Observer { balls ->
if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
refreshRecycler()
}
})
And that’s it’s it. Every time activeFlag changes, allBowlingBalls will be refreshed with a call to the repository and the onChange event of the observer on allBowlingBalls will trigger. A simple technique for essentially building a dynamic search engine.
Let me explain what I understood with an example. Consider a student data class
data class Student(val name: String, val marks: Int)
Transformation.map()
Transforms the value of LiveData into another. It takes the value, applies the Function on the value, and sets the Function’s output as a value on the LiveData it returns. Here’s a example of how this can be used for the above data class:
val student: LiveData<Student> = (get liveData<Student> from DB or network call)
val studentName: LiveData<String> = Transformations.map(student) {it.name}
Here we get a student LiveData from a network or DB and then we take the value from the LiveData which is the Student object and just get the name of the student and maps it to another LiveData.
Transformation.switchMap()
Transforms the value of a LiveData into another LiveData. Consider we want to implement a search feature for Students. Every time the search text changes we want to update search results. The following code shows how that works.
val searchQuery: LiveData<String> = ...
val searchResults: LiveData<List<Student>> =
Transformations.switchMap(searchQuery) { getSearchResults(it) }
fun getSearchResults(query: String): LiveData<List<Student>> = (get liveData<List<Student>> from DB or network call)
So here every time there is a new value in searchQuery, getSearchResults will be called with a new search query and searchResults will be updated.
In short, the naming is analogous to rx map/switchMap.
Map is 1 to 1 mapping which is easy to understand.
SwitchMap on the other hand only mapping the most recent value at a time to reduce unnecessary compute.
Hope this short version of answer can solve everyone's problem easily.
To my experience, both are to build a bridge with what you update (livedata #1) and what you really care/observe (livedata #2) in return. This bridge is necessary so that you can carry the lifecycle of the observer (i.e. your fragment) down to view models and they then can drop the subscription on all the LiveData involved in automatically. This is one of the main promises of LiveData from the beginning. So, this will keep that promise.
In case of switchMap the bridge is dynamic meaning there's always a new LiveData returned from the function (the lambda) - so you switch to this new LiveData. With map it's static.
I hope it helps a bit.
They have different Use case:
if you have a source LiveData and you just want to change the value inside that LiveData into some other data type, use map
If you have a source LiveData and a function that return a LiveData, and you want to create a LiveData that updates value base on the LiveData returned by that function. Use switchMap
Analyzing the source code, we see both switchmap and map return a new instance of MediatorLiveData.
map takes in a function that return a new value for that MediatorLiveData while switchmap takes in a function that return a new instance of LiveData (and then if the value of that new instance of LiveData change, use that to update MediatorLiveData's value)
in another word, switchmap's LiveData value change if that input function's LiveData value change, switchmap also have the added benefit of unregistering the previous LiveData return from that input function.
Here is a brief
If you are expecting the result value to change repeatedly use swithMap()
and if it is just one time operation use map() instead .
Example : If you want to show scores of a live game use swithMap() .
If you want to show list of player of a team use map()

Categories

Resources