What is the difference between map() and switchMap() methods?
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
.