map() 和 switchMap() 方法有什么区别?

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

LiveData 的这两种方法有什么区别class?官方文档和教程对此非常模糊。在 map() 方法中,第一个参数称为 source 但在 switchMap() 中它称为 触发器。这背后的原理是什么?

根据文档

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.

换句话说,我可能不是 100% 正确,但如果你熟悉 RxJava; Transformations#map 有点类似于 Observable#map & Transformations#switchMap 类似于 Observable#switchMap

举个例子,有一个 LiveData 发出一个字符串,我们想用大写字母显示该字符串。

一种方法如下;在 activity 或片段

Transformations.map(stringsLiveData, String::toUpperCase)
    .observe(this, textView::setText);

传递给 map return 的函数只是一个字符串,但 Transformation#map 最终 return 是一个 LiveData.

第二种方法;在 activity 或片段

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

如果你看到Transformations#switchMap实际上已经切换了LiveData。因此,再次根据文档 传递给 switchMap() 的函数必须 return 一个 LiveData 对象 .

所以,在 map 的情况下,它是 source LiveData 你正在转换,在 switchMap 的情况下,传递的 LiveData 将充当 触发器 ,在解包并向下游发送结果后,它将切换到另一个 LiveData

我的观察是,如果你的转换过程很快(不涉及数据库操作,或网络activity),那么你可以选择使用map

但是,如果你的改造过程比较慢(涉及数据库操作,或者网络activity),你需要使用switchMap

switchMap在执行time-consuming操作时使用

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不适合time-consuming操作

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() 在概念上与 RXJava 中的用法相同,基本上您是在更改另一个 LiveData 的参数

SwitchMap() 相反,您将用另一个替代 LiveData 本身!典型的情况是当你从一个存储库中检索一些数据时,例如 "eliminate" 以前的 LiveData(垃圾收集,通常是为了提高内存效率)你传递一个 new 执行相同操作的 LiveData(例如获取查询)

首先,map()switchMap()方法都在主线程上调用。而且它们与用于快速或慢速任务无关。但是,如果您在这些方法中执行复杂的计算或耗时任务而不是工作线程,它可能会导致 UI 延迟,例如解析或转换长 and/or 复杂 json 响应,因为它们是在 UI 线程上执行的。

  • 地图()

map() 方法的代码是

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

它所做的是,它使用源 LiveData,I 是输入类型,并在 LiveData 上调用 setValue(O),其中 O 是输出类型。

为了清楚起见,让我举个例子。您希望在用户更改时将用户名和姓氏写入 textView。

  /**
     * 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;

现在让我们在 mUserLiveData 更改时触发 mUserNameLiveData 的字符串更改。

   /*
     * 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();
        }
    });

让我们对 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());
        }
    });

如果您在 Activity 或 Fragment 上观察 MediatorLiveData,您会得到与观察 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 每次 SourceLiveData 更改时,相同的 MediatorLiveData 不是 new LiveData。

它的源代码是

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

基本上它所做的是,它创建了一个最终的 MediatorLiveData 并将其设置为 Result 就像 map does() 但这次函数 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;
    }

所以 map() 接受 LiveData<User> 并将其转换为 String,如果 User 对象更改,例如名称字段更改。

switchMap() 获取一个字符串并使用它获取 LiveData<User>。使用 String 从 web 或 db 查询用户并得到 LiveData<User> 作为结果。

上面已经有一些很好的答案了,但我还是费了好大的劲才弄明白,所以我会尝试用一个具体的例子来解释我的思维方式,而不涉及技术细节和代码。

mapswitchMap中都有一个source(或触发器)实时数据,在这两种情况下,您都希望将其转换 为另一个实时数据。您将使用哪一个 - 取决于您的转换正在执行的任务。

map

考虑随处使用的同一个简单示例 - 您的 source 实时数据包含一个 User 对象 - LiveData<User>,它指向当前记录的在用户。您想要在 UI 中显示一段文字 Current user: <USERNAME>。在这种情况下,来自源的每个变化信号都应该恰好触发结果 "mapped" LiveData 的一个信号。例如,当前 User 对象是 "Bob" 那么 UI 文本显示 Current user: Bob。一旦您的 LiveData<User> 触发更改,您的 UI 将观察它并将文本更新为 Current user: Alice。很简单,线性的,一对一的变化。

switchMap

考虑以下示例 - 您想要创建一个 UI 来显示名称与给定搜索词匹配的用户。我们可以对此非常聪明,并将搜索词保存为 LiveData!所以它将是一个 LiveData<String> 并且每次用户输入一个新的查询字符串时我们的 Fragment/Activity 将简单地将文本输入值设置为 ViewModel 中的这个实时数据.结果,此实时数据将触发更改信号。一旦我们得到这个信号,我们就开始搜索用户。现在让我们考虑一下我们的搜索速度如此之快以至于它立即 return 了一个值。此时您认为您可以只使用 map 和 return 匹配的用户,这将更新 UI。好吧,你现在有一个错误 - 假设你定期更新数据库,并且在下一次更新之后出现更多用户匹配搜索词!如您所见,在这种情况下,源触发器(搜索词)不一定会导致映射实时数据的单个触发,给定 UI 的映射实时数据可能仍需要继续触发新值后的值用户被添加到数据库中。此时你可能会说,我们可以 return 一个 "smarter" 实时数据,它不仅会等待源触发器,还会监视数据库中与给定术语匹配的用户(你将能够使用开箱即用的 Room DB 来做到这一点)。但随之而来的是另一个问题——如果搜索词发生变化怎么办?所以你的术语是 x,它触发了查询用户并关注数据库的实时数据,它 returns userx, userxx 然后五分钟后它 return s userx, userxxx 等等。然后该术语更改为y。现在我们需要以某种方式停止监听智能实时数据,为我们提供 x 的用户,并 切换 它与新的智能实时数据,它将监控并为我们提供用户他们的名字中有 y。而这正是 switchMap 正在做的!并且注意,这个转换需要以这样的方式完成,在你的 UI 中你只写 switchMap(...).observe 一次,这意味着 switchMap 必须 return 一个包装器 LiveData 将在整个执行过程中保持不变,但将为我们 切换 实时数据源。

结论

虽然乍一看似乎一样,但 mapswitchMap[=72= 的用例] 是不同的,一旦你开始实施你的案例,你就会感觉到使用哪一个,主要是当你意识到在你的映射函数中你必须从你的其他模块(比如 Repositories)调用一些代码时 return LiveData.

开关地图: 假设我们正在寻找用户名 Alice。存储库正在创建该用户 LiveData class 的新实例,然后我们显示用户。一段时间后,我们需要查找用户名 Bob,存储库创建了一个新的 LiveData 实例,我们的 UI 订阅了该 LiveData。所以此刻,我们的 UI 订阅了两个 LiveData 实例,因为我们永远不会删除前一个。因此,这意味着每当我们的存储库更改用户数据时,它都会发送两次订阅。现在,我们如何解决这个问题......?

我们真正需要的是一种机制,允许我们在想要观察新来源时停止从以前的来源观察。为此,我们将使用 switchMap。在幕后,switchMap 使用 MediatorLiveData 每当添加新源时都会删除初始源。简而言之,它为我们完成了删除和添加新观察者的所有机制。

但地图是静态的,当您不必每次都强制获取新的实时数据时使用它

  • 使用 map 你最终有 相同的源 livedata 但它的数据(值)在发出之前随提供的函数变化

  • 使用 switchMap,您仅使用源实时数据 作为触发器 以返回独立的实时数据(当然您可以在函数输入中使用触发器数据)

  • 触发器:导致 livedata 的观察者 onChanged() 调用的所有东西

简而言之,命名类似于rx map/switchMap。

Map是一对一映射,简单易懂

另一方面,SwitchMap 一次只映射最新值以减少不必要的计算。

希望这个简短的回答能轻松解决大家的问题

Transformation.map()

fun <X, Y> map(trigger: LiveData<X>, mapFunction: Function<X, Y> ): LiveData<Y>?

trigger - 一旦改变触发 mapFunction 执行的 LiveData 变量。

mapFunction - trigger LiveData 发生变化时调用的函数。参数 X 是对 trigger 的引用(通过 it)。函数 return 是指定类型 Y 的结果,最终由 map() 作为 LiveData 对象 return 编辑。

如果您想在 trigger LiveData 变量更改时执行操作(通过 mapFunction),请使用 map()map() 将 return 一个 LiveData 对象,应该观察到 mapFunction 被调用。

示例:

假设一个简单的投球手名单,他们的平均分和他们的平均分:

data class Bowler(val name:String, val average:Int, var avgWHDCP:Int)
var bowlers = listOf<Bowler>(Bowler("Steve", 150,150), Bowler ("Tom", 210, 210))

假设一个 MutableLiveData Int 变量保存让分增量值。当此值更改时,列表中所有投球手的 avgWHDCP 需要 re-computed。最初它被设置为零。

var newHDCP:MutableLiveData<Int> = MutableLiveData(0)

创建调用 Tranformation.map() 的变量。它的第一个参数是 newHDCP。它的第二个参数是 newHDCP 改变时调用的函数。在此示例中,该函数将遍历所有投球手对象,为投球手列表中的每个投球手计算新的 avgWHDCP,并将结果 return 作为 LiveData 投球手对象的可观察列表。请注意,在此示例中,原始 non-LiveData 保龄球手列表和 returned 保龄球手列表将反映相同的值,因为它们引用相同的数据存储。但是,函数的结果是可以观察到的。投球手的原始列表不是因为它没有设置为 LiveData。

var updatedBowlers: LiveData<List<Bowler>> = Transformations.map(newHDCP) {
    bowlers.forEach { bowler ->
        bowler.avgWHDCP  = bowler.average +  it
    }
    return@map bowlers
}

在您的代码中的某处,添加一个方法来更新 newHDCP。在我的示例中,单击单选按钮时,newHDCP 将发生变化,并且该过程将触发调用 Transformations.map()

中指定的函数
rbUpdateBy20.setOnCheckedChangeListener { _, isChecked ->
        viewModel.bowlingBallObject.newHDCP.value = 20
}

最后,所有这些只有在观察到 updatedBowlers 时才会起作用。这将以 OnViewCreated()

等方法放置在您的 Activity 或片段中
viewModel.updatedBowlers.observe(viewLifecycleOwner, Observer { bowler ->
    if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
        refreshRecycler()
    }
})

如果你想变得更简洁一点并且你真的不需要对 updatedBowlers 的实时引用,这里是你可以将 updateBowlers 与观察者结合起来的方法:

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

基本上就是这样。任何时候您更改 newHDCP 的值,都会调用 Transformation.map() 中指定的函数,它将使用新计算的 avgWHDCP 和 return 的 LiveData 对象转换 bowler 对象List<Bowler>

Transformation.switchMap()

fun <X, Y> switchMap(source: LiveData<X>, switchMapFunction: Function<X, LiveData<Y>!>): LiveData<Y>

source - 一旦变化触发switchMapFunction执行的LiveData变量。

switchMapFunction - 源 LiveData 发生更改时调用的函数。参数 X 是对同一源对象的引用(通过 it)。 switchMapFunction 函数必须 return 是一个 LiveData 结果,它通过 Transformation.switchMap() 有效地得到 returned。从本质上讲,这允许您将 LiveData 容器对象的一个​​引用换成另一个。

当您有一个引用 LiveData 对象的变量,并且您想要将该变量切换到另一个变量,或者换句话说,您想要刷新现有 LiveData 容器时,请使用 switchMap()。这很有用,例如,如果您的 LiveData 变量正在引用数据库数据存储并且您想要使用不同的参数重新查询。 switchMap 允许您 re-execute 查询并替换为新的 LiveData 结果。

示例

假设数据库存储库包含来自 BowlingBall DAO 的一堆保龄球查询 table:

private val repository  = BowlingBallRepository(application)

我想执行一个查询,根据用户指定的内容获取活动或非活动的保龄球。通过 UI,用户可以 select 激活或不激活,所以我的查询需要同时处理这两者。所以我创建了一个 MutableLiveData 变量来保存活动或非活动状态。在此示例中,我默认为“A”表示活动。

var activeFlag:MutableLiveData<String> = MutableLiveData(“A”)

现在,我们需要一个 LiveData 变量来保存我的查询结果,以获取特定状态的所有保龄球。因此,我创建了一个类型为 LiveData<List<BowlingBallTable>>? 的名为 allBowlingBalls 的变量,并将其分配给 Transformation.switchMap。我将 activeFlag 变量以及将接收相同 activeFlag 变量(通过 it)的 lambda 函数传递给 switchMap 函数,该函数调用在数据库存储库中查询 re-fetch 所有具有通过状态的保龄球。 lambda函数的LiveData结果通过switchMap方法传回,为re-assigned到allBowlingBalls.

private var allBowlingBalls: LiveData<List<BowlingBallTable>>? = Transformations.switchMap(activeFlag) {repository.getAllBalls(it)}

我需要一种方法来触发 allBowlibgBalls 的刷新。同样,这将在 activeFlag 更改时完成。在代码的某处,添加一个函数来更新 activeFlag。在我的示例中,单击单选按钮时,activeFlag 将发生变化,并且该过程将触发调用 Transformations.switchMap()

中指定的函数
rbActive.setOnCheckedChangeListener { _, isChecked ->
    if (isChecked) {
        viewModel.activeFlag.value = ActiveInactive.ACTIVE.flag
        refreshRecycler()
    }
}

最后,所有这些只有在观察到 allBowlingBalls 时才会起作用。所以首先创建一个函数来获取所有保龄球:

fun getAllBowlingBalls():LiveData<List<BowlingBallTable>>? {
    return  allBowlingBalls
}

然后在getAllBowlingBalls()上放置一个观察者:

viewModel.getAllBowlingBalls()?.observe(viewLifecycleOwner, Observer { balls ->
    if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
        refreshRecycler()
    }
})

就是这样。每次 activeFlag 更改时, allBowlingBalls 将通过调用 t 刷新e 存储库和 allBowlingBalls 上观察者的 onChange 事件将触发。一种从根本上构建动态搜索引擎的简单技术。

让我用一个例子来解释我的理解。考虑一个学生数据class

data class Student(val name: String, val marks: Int)

Transformation.map()

将 LiveData 的值转换为另一个值。它获取值,将 Function 应用于该值,并将 Function 的输出设置为 LiveData it returns 上的值。这是如何将其用于上述数据的示例 class:

 val student: LiveData<Student> = (get liveData<Student> from DB or network call)
 val studentName: LiveData<String> = Transformations.map(student) {it.name}

这里我们从网络或数据库中获取学生 LiveData,然后我们从作为 Student 对象的 LiveData 中获取值,只获取学生的姓名并将其映射到另一个 LiveData。

Transformation.switchMap()

将一个 LiveData 的值转换为另一个 LiveData。考虑我们要为学生实现搜索功能。每次搜索文本更改时,我们都希望更新搜索结果。下面的代码展示了它是如何工作的。

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)

所以这里每当 searchQuery 中有一个新值时,都会使用新的搜索查询调用 getSearchResults 并更新 searchResults。

这是一个简介

如果您希望结果值反复更改,请使用 swithMap() 如果只是一次操作,请使用 map() 代替 .

示例:如果您想显示现场比赛的比分,请使用 swithMap() 。 如果你想显示一个团队的球员名单使用 map()

根据我的经验,两者都是建立 桥梁 与你更新的内容(实时数据 #1)和你真正 care/observe(实时数据 #2)在 return。这个桥梁是必要的,这样您就可以将观察者(即您的片段)的生命周期向下传递到视图模型,然后他们可以自动删除 中涉及的所有 LiveData 的订阅.这是 LiveData 从一开始就做出的主要承诺之一。所以,这将信守诺言。

switchMap 的情况下,桥是 动态的 意味着总是有一个 new LiveData return 从函数(lambda)编辑 - 所以你切换到这个新的 LiveData。使用 map 它是静态的。

希望对您有所帮助。

他们有不同的用例:

如果您有一个源 LiveData,而您只是想将该 LiveData 中的值更改为其他数据类型,请使用 map

如果您有一个源 LiveData 和一个 return 一个 LiveData 的函数,并且您想要创建一个 LiveData 来根据该函数 return 编辑的 LiveData 更新值。使用 switchMap

分析源代码,我们看到 switchmapmap return 一个新的 MediatorLiveData 实例。

map 接受一个函数 return MediatorLiveData 的新值,而 switchmap 接受一个return 一个新的 LiveData 实例 的函数(然后如果 LiveData 的那个新实例的值发生变化,使用它来更新 MediatorLiveData 的值)

换句话说,如果该输入函数的 LiveData 值发生变化,switchmap 的 LiveData 值也会发生变化,switchmap 还有一个额外的好处,即从该输入注销之前的 LiveData return功能。