本文独家发布到公众号:Android技术杂货铺
近两年来,RxJava可以说是异常的火爆,受到众多开发者的追捧与青睐,虽然后入门的门槛较高,学习成本较大,但是还是掀起一场学习Rxjava的狂潮。为什么呢?因为RxJava的特性:轻松的线程切换、流式的API写法和强大的操作符。这使得我们做异步操作变得很简单,不用像以前一样写各种Handler来回调主线程,只需要一个操作符一行代码就搞定。流式的API使我们的逻辑变得非常清晰,可读性很强。因此,RxJava也是我们项目重构的利器。
说到RxJava强大的操作符,那就不得不提flatMap
了,那么篇文章就简单谈谈flatMap
的使用场景和它与另一个操作符concatMap
的区别。
由于现在RxJava已经发布2.x版本了,因此本文我们使用Rxjava 的 2.x 版本写所有的示例。
build.gradle
中依赖最新版本:
compile 'io.reactivex.rxjava2:rxjava:2.1.0'
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
一、操作符 flatMap
详解
flatMap将一个发射数据的Observable变换为多个Observables,然后将它们发射的数据合并后放进一个单独的Observable。
flatMap使用一个指定的函数对原始Observable发射的每一项数据之行相应的变换操作,这个函数返回一个本身也发射数据的Observable,然后FlatMap合并这些Observables发射的数据,最后将合并后的结果当做它自己的数据序列发射。
这个方法是很有用的,例如,当你有一个这样的Observable:它发射一个数据序列,这些数据本身包含Observable成员或者可以变换为Observable,因此你可以创建一个新的Observable发射这些次级Observable发射的数据的完整集合。
上面是官方关于flatMap的解释,如果只看这一段话,是不是有点难以理解呢?是的,可能是有点蒙.没关系,我们先来看一个它的使用场景,然后我们倒回来看一下解释,可能你就清楚它是干什么的了。
我们假设有这个一场景:每个学校都有成绩统计系统,有这样一个需求,我们要抽取一个班,打印该班的每个同学的每一门课程成绩。
首先根据需求,抽取2个实体:学生实体和课程实体,如下:
/**
* Created by zhouwei on 17/6/19.
*/
public class Student {
public String name;//学生名字
public int id;
public List<Source> mSources;//每个学生的所有课程
public Student(String name, int id, List<Source> sources) {
this.name = name;
this.id = id;
mSources = sources;
}
}
课程实体:
public class Source {
public int sourceId;//id
public String name;//课程名
public int score;//成绩
public Source(int sourceId, String name, int score) {
this.sourceId = sourceId;
this.name = name;
this.score = score;
}
}
按照传统的方式:
//开启一个线程
new Thread(new Runnable() {
@Override
public void run() {
// 从服务器获取班级所有同学信息
List<Student> students = MockData.getAllStudentInfoById(0);
for(int i=0;i<students.size();i++){
List<Source> sources = students.get(i).mSources;
for (int index=0;index<sources.size();index++){
final Source source = sources.get(index);
runOnUiThread(new Runnable() {
@Override
public void run() {
//主线程更改UI
String content = "sourceName:"+source.name +" source score:"+source.score;
mTextView.setText(content);
Log.i(TAG,content);
}
});
}
}
}
}).start();
以上就是传统的实现方式,可以看到整个代码的结构是非常难看的,如果在加一些其他的过滤条件,要打印指定某一个人的某一门课程的成绩?那么我们又会嵌套if-else
判断,这种多层的循环嵌套使得我们的代码可读性变得非常差。怎么解决呢?flatMap操作符就是专门为这个而生的,我们来看一下用flatMap来改造一下:
Flowable.fromIterable(MockData.getAllStudentInfoById(0))
.flatMap(new Function<Student, Publisher<Source>>() {
@Override
public Publisher<Source> apply(@NonNull Student student) throws Exception {
return Flowable.fromIterable(student.mSources);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Source>() {
@Override
public void accept(@NonNull Source source) throws Exception {
String content = "sourceName:"+source.name +" source score:"+source.score;
mTextView.setText(content);
Log.i(TAG,content);
}
});
这样是不是看起来就舒服多了呢?每一步做了什么我们都看得很清楚,整个流程非常的清晰。
打印的结果是这样如下(3位同学):
那么我们结合这个示例和上面的图我们在回过头来理解一下官方对flatMap
的解释
解释:上图中的圆表示的就是原始数据,中间是
flatMap
操作符执行的变换(将圆变换为一个菱形和正方形),函数返回的是一个可以发射数据(菱形和正方形)的Observable,最合后并这些发送数据的Observable。那么映射到上面的这个示例就是:List< Student > 代表圆,然后`Flowable.fromIterable(student.mSources)`就是flatMap执行的变换操作,返回的就是可以发射Source数据的Observable(本文用的是Flowable),最后合并后的结果就是自己的数据序列( 也就是上图中打印的3个同学的各门成绩)。
这样应该是说清楚了flatMap的作用了吧?如果还没理解,多看几遍图,看图百遍,其义自现。
注意:如果任何一个通过这个flatMap操作产生的单独的Observable调用onError异常终止了,这个Observable自身会立即调用onError并终止。
flatMap的特点:其实从上面的图就可以看出,经过flatMap操作变换后,最后输出的序列有可能是交错的,因为flatMap最后合并结果采用的是merge操作符。如果要想经过变换后,最终输出的序列和原序列一致,那就会用到另外一个操作符,concatMap
。
二、操作符 concatMap介绍以及与 flatMap的区别
concatMap
操作符的功能和flatMap
是非常相似的,只是有一点,concatMap 最终输出的数据序列和原数据序列是一致,它是按顺序链接Observables,而不是合并(flatMap用的是合并)。
我们来看一个例子:Observable 发射5个数据(1,2,3,4,5),然后分别用flatMap
和concatMap
对它执行一个变换( *10),然后再输出结果序列。
flatMap:
Observable.fromArray(1,2,3,4,5)
.flatMap(new Function<Integer, ObservableSource<Integer>>() {
@Override
public ObservableSource<Integer> apply(@NonNull Integer integer) throws Exception {
int delay = 0;
if(integer == 3){
delay = 500;//延迟500ms发射
}
return Observable.just(integer *10).delay(delay, TimeUnit.MILLISECONDS);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Integer>() {
@Override
public void accept(@NonNull Integer integer) throws Exception {
Log.e("zhouwei","accept:"+integer);
}
});
输出结果序列如下图:
为了更真实的模拟,我们将第三个数据延迟500ms发射,我们看到,最终的结果出现了交错。(如果与原序列一致的话应该是:10,20,30,40,50)。
那么我们用同样的代码,将flatMap
换成concatMap
看一下:
Observable.fromArray(1,2,3,4,5)
.concatMap(new Function<Integer, ObservableSource<Integer>>() {
@Override
public ObservableSource<Integer> apply(@NonNull Integer integer) throws Exception {
int delay = 0;
if(integer == 3){
delay = 500;//延迟500ms发射
}
return Observable.just(integer *10).delay(delay, TimeUnit.MILLISECONDS);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Integer>() {
@Override
public void accept(@NonNull Integer integer) throws Exception {
Log.e("zhouwei","accept:"+integer);
}
});
看一下concatMap
的结果序列:
可以看到经过
concatMap
变换后的数据序列 与 原数据序列的顺序是保持一致的。
小结:
concatMap
和flatMap
的功能是一样的, 将一个发射数据的Observable变换为多个Observables,然后将它们发射的数据放进一个单独的Observable。只不过最后合并ObservablesflatMap
采用的merge,而concatMap
采用的是连接(concat)。总之一句一话,他们的区别在于:concatMap是有序的,flatMap是无序的,concatMap最终输出的顺序与原序列保持一致,而flatMap则不一定,有可能出现交错。
三、总结
flatMap
是Rxjava中一个强大的操作符,在实际项目中,应用的场景很多,比如开始列举的化解循环嵌套,还有一种场景在我们实际项目中是非常多的,那就是连续请求两个接口,第一个接口的返回值是第二个接口的请求参数,在这种情况下,以前我们会在一个请求完成后,在onResponse中获取结果再请求另一个接口。这种接口嵌套,代码看起来是非常丑陋的,运用flatMap就能很好的解决这个问题。代码看起来非常优雅而且逻辑清晰。 如果需要保证顺序的话,请使用concatMap
。
以上就是concatMap
和flatMap
使用场景介绍及区别与联系,如有什么问题,欢迎指正。
参考
ReactiveX