Map()和 switchMap()方法之间的区别是什么?

LiveData 类的这两个方法之间的区别是什么?官方的文档和教程对此含糊不清。在 地图()方法中,第一个参数称为 来源,但在 开关图()中,它称为 触发器。这背后的理由是什么?

39450 次浏览

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);
}
}

Map() is conceptually identical to the use in RXJava, basically you are changing a parameter of LiveData in another one enter image description here

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)

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 Fragment6 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 Fragment7 it with the new smart live data which will monitor and give us users with y in their names. And that is exactly what Fragment8 is doing! And notice, this switch needs to be done in such a way, that in your UI you just write Fragment3 once, that means that Fragment2 must return a wrapper Fragment5 which will stay the same throughout the execution, but will Fragment7 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.

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

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.

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.

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()

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.