Kotlin Flow上手指南(一)基础使用


在Kotlin协程出现之前,RxJava应该是在Android开发领域最火热的异步编程方案。

其中对于数据流的链式处理,相信用过RxJava的都很熟悉。

那么在Kotlin协程中又该如何进行数据流操作呢?

这是Kotlin协程系列的第二篇文章。

作为RxJava曾经的忠实拥护者,本篇尝试从RxJava使用者的角度,围绕Kotlin协程中的异步数据流操作展开。

Kotlin版本 :1.5.31

Coroutine 版本 : 1.5.2

RxJava版本 : 3.0.10

Kotlin协程系列相关文章导航

扒一扒Kotlin协程

Kotlin Flow上手指南(一)基础使用 (本篇)

Kotlin Flow上手指南(二)ChannelFlow

Kotlin Flow上手指南(三)SharedFlow与StateFlow

以下正文。


在Kotlin标准库中虽然也提供了Sequence与集合操作符,同样都能进行链式数据流操作。

但依然是阻塞线程同步数据流操作,那么有没有与RxJava类似的异步数据流操作呢?

在Kotlin协程中提供的响应式编程方式就是Kotlin Flow

Flow定义.png

Flow的定义很简单,就只是表示这个类能够被订阅收集。

而其中FlowCollector则定义发送数据的功能。

FlowCollector定义.png

在基础Kotlin协程中,

  • launch启动的协程,没有返回值,运行后就结束,类似于RxJavaCompletable
  • async启动的协程,能通过await函数获取协程返回值,但只能返回单个值,且无法进行数据流操作。

Kotlin Flow的出现,恰好弥补了Kotlin协程对于多个值异步运算的不足,并且允许进行复杂的数据流操作。

相当于RxJavaObservableFlowable类型,Maybe类型自然也可以兼容。

创建

首先来看看Flow数据流是如何创建的。

Flow的创建方法有很多种,最简单的是使用顶层函数flow

flow函数源码.png

其参数是suspend修饰,以FlowCollector为接收者的挂起函数,能直接调用emit发送数据。

 val flow = flow<String>{
     for(i in 1..5){
         emit("result $i") 
     }
 }
复制代码

其他还有诸如asFlowflowOf等创建方式。

Kotlin Flow其他构造方式

官方提供了很多类型转换到Flow的拓展函数,这里就不多列举了,有兴趣可以看源码。

  • 由于flow函数的代码块同样是由suspend修饰,内部可以调用挂起函数。

  • flow代码块的emit函数是线程不安全的,所以flow函数的不能修改协程上下文,无法调用如withContext等函数,避免下游collect被调度到其他线程。

  • 如果要修改数据流的协程调度,只能调用flowOn函数。

末端操作符

Flow创建的数据流,是冷流,也就是必须要在调用末端操作符后才会执行数据流生产操作。

  • 数据流的创建与数据流的消费是成对出现
  • 多个数据流订阅消费,也会同样有多个数据源生产创建

RxJava中需要调用subscribe订阅消费数据流,在Flow中自然就是调用collect

Flow类中声明的collect函数是以@InternalCoroutinesApi注解的,表示外部不能直接调用该函数。

好在官方提供了同名的Flow类型拓展函数供外部调用。

collect源码.png

collect挂起函数,限制只能在Kotlin协程(或另一个挂起函数)中调用。

flow及其他操作符都是普通函数,可以在协程外部创建。

也就是说可以做到数据流创建与数据流消费的分离

 fun testFlow(){
     val scope = CoroutineScope(SupervisorJob())
     val flow = flow{
          //创建数据流
          (0..2).forEach {
              println("emit $it")
              emit(it)
          }
     }
     scope.launch{
          flow.collect {
              //消费数据流
              println("last collect result :$it")
          }
     }.join()
 }
复制代码

除此之外,官方还提供了其他末端操作符

  • 集合转换类型,如toListtoSet
  • 获取数据流特定元素,如firstlast
  • 末端的运算符(累加器),如reducefold

这几种末端操作符都是挂起函数数据流的消费被限制只能在Kotlin协程(挂起函数)中运行

另外,还提供了一个便捷的拓展函数launchIn,指定运行在特定的协程作用域,但会忽略末端的数据流,通常和其他操作符配合使用。

launchIn源码.png

中间操作符

Flow中提供了丰富的中间操作符,让数据流能够产生更多玩法。

这些中间操作符会通过拦截(消费)上游的数据流,进行处理后,再返回新的数据流转发给下游数据流。如此便能使不同中间操作符(消费者),链式调用的串联在一起。

中间消费者并不会触发数据流被发射

目前Flow的中间操作符数量相对RxJava而言还比较少,但也足够满足日常使用了,甚至功能更加强大,可能有小部分较少使用场景的生僻操作符还需要自定义。

变换

先从熟悉的老朋友,数据流的变换操作符开始

  • transform

transform源码解析.png

接收一个FlowCollector作为接收者的函数类型,数据流上游传递的值作为函数参数。

transform操作符实现了拦截收集上游数据并转发的功能。

在订阅拦截了上游数据流后,通过以FlowCollector作为接收者的函数类型,也就具备向下游发送数据的能力。

  • transform能多次调用emit函数发送数据

作为最基础的实现,其他数据流变换操作符都是在transform基础上拓展而来。

  • map

    将上游数据流的每个值,通过指定变换函数的结果传递给下游。

     flow{
         (0..2).forEach {
             println("emit $it")
             emit(it) 
         }
     }.map{
         println("running map $it")
         it * 2 + 1
     }.collect{
         println("last collect result $it")
     }
     ​
     emit 0
     running map 0
     last collect result 1
     emit 1
     running map 1
     last collect result 3
     emit 2
     running map 2
     last collect result 5
    复制代码

当需要将上游数据流每个值都转化为另一个数据流的使用场景,在RxJava会用到flatMap

但在Flow并没有同名操作符,不过有类似的存在。

  • flatMapConcat

    将上游数据流的每个值,转化为另一个数据流。

    将新Flow数据流展开,等待新的数据流将每个值传递给下游,随后才继续从上游获取下一个值,顺序执行。

     @FlowPreview
     fun test() = runBlocking{
         flow{
             (0..2).forEach {
                 println("emit $it")
                 emit(it)
             }
         }.flatMapConcat {index->
            flow<String> {
               println("running flatMapConcat first $index")
               emit("flatMapConcat first $index")
               println("running flatMapConcat second $index")
               emit("flatMapConcat second $index")
               println("running flatMapConcat end $index")
            }
         }.collect {
             println("last collect result :$it")
         }
     }
     ​
     emit 0
     running flatMapConcat first 0
     last collect result :flatMapConcat first 0
     running flatMapConcat second 0
     last collect result :flatMapConcat second 0
     running flatMapConcat end 0
     emit 1
     running flatMapConcat first 1
     last collect result :flatMapConcat first 1
     running flatMapConcat second 1
     last collect result :flatMapConcat second 1
     running flatMapConcat end 1
     emit 2
     running flatMapConcat first 2
     last collect result :flatMapConcat first 2
     running flatMapConcat second 2
     last collect result :flatMapConcat second 2
     running flatMapConcat end 2
     after flow
    复制代码

    这等效于RxJavaflatMap操作符。

  • flatMapMerge

    flatMapConcat相似,但其内部是允许并发操作的,flatMapMerge接收上游数据流后,会尽可能收集上游数据值,直到达到最大并发数,然后一次性将新的数据流并发的传递到下游。

    由于flatMapMerge是并发的,所以不保证发送顺序

     @FlowPreview
     fun test() = runBlocking{
         flow{
             (0..2).forEach {
                 println("emit $it")
                 emit(it)
             }
         }.flatMapMerge {index->
            flow<String> {
               emit("flatMapConcat $index ,1")
               emit("flatMapConcat $index ,2")
            }
         }.collect {
             println("last collect result :$it")
         }
     }
     ​
     emit 0
     emit 1
     emit 2
     last collect result :flatMapConcat 0 ,1
     last collect result :flatMapConcat 0 ,2
     last collect result :flatMapConcat 1 ,1
     last collect result :flatMapConcat 1 ,2
     last collect result :flatMapConcat 2 ,1
     last collect result :flatMapConcat 2 ,2
    复制代码

    flatMapMerge函数接受一个concurrency参数,表示允许接收上游数据的并发数,默认为16。

    当concurrency = 1时,就相当于flatMapConcat

但官方其实并不太推荐使用这两个操作符。从操作符注释上看,官方似乎更希望我们保持数据流的线性操作,提高数据流操作的可读性。

毕竟flatMap比起map,逻辑相对没有那么清晰,通常情况下使用map也就足够了。而且官方也提供了FlowCollector拓展函数emitAll用于发送数据流。

flatMapConcat内部实际就是调用了emitAll来发送新的Flow数据流。

flatMapConcat源码.png

并且flatMapConcatflatMapMerge都是以@FlowPreview注解的,属于预览性质,并不保证后续版本的向下兼容。

如果有需要转化为新数据流的需求,在map中调用emitAll可能是更好的选择。

线程调度

还要记得RxJava的线程调度操作符subscribeOnobserveOn吗?

它们分别用于指定数据流上游数据流下游的运行线程。

Flow是基于Kotlin协程的,所以线程调度必然是修改CoroutineContext调度器。

但对于允许发送数据的操作符来说,会被限制在不允许内部调用诸如withContext修改协程上下文的挂起函数,仅允许在数据流中使用flowOn操作符修改协程上下文

flowOn操作符指定的调度器,只作用于该操作符上游的数据流

区别于定位相近的subscribeOnflowOn可以多次调用

指定的CoroutineContext作用范围是从当前flowOn操作符到前一个flowOn(或数据源)。

下游数据流未指定时,则继承外部协程上下文的调度器。

 fun test() = runBlocking{
     val myDispatcher = Executors.newSingleThreadExecutor()
             .asCoroutineDispatcher()
     flow {
         println("emit on ${Thread.currentThread().name}")
         emit("data")
     }.map {
         println("run first map on${Thread.currentThread().name}")
         "$it map"
     }
     //作用于前面flow创建与第一个map
     .flowOn(Dispatchers.IO)
     .map {
         println("run second map on ${Thread.currentThread().name}")
         "${it},${it.length}"
     }
     //作用于第二个map
     .flowOn(myDispatcher)
     .collect {
         println("result $it on ${Thread.currentThread().name}")
     }
 }
 ​
 emit on DefaultDispatcher-worker-2 @coroutine#3
 run first map on DefaultDispatcher-worker-2 @coroutine#3
 run second map on pool-1-thread-1 @coroutine#2
 result data map,8 on Test worker @coroutine#1
复制代码

相比subscribeOnobserveOnflowOn在使用上更加灵活。

如果想要达到observeOn的效果,可以尝试利用外部协程调用withContext切换调度器。

事件

Flow中还提供了数据流的hook操作符。

  • onStart上游数据流开始之前执行操作

    参数代码块是以FlowCollector作为接收者,具有发送数据功能。

    如果onStart内发送数据,会先将onStart数据发送传递到下游消费完成后,才开始将上游数据流传递到下游。

    onStart操作符声明.png

    从功能上看,其相当于RxJava中的startWithdoOnSubscribe操作符的结合替代品,并且功能更加强大。

    其参数代码块是以suspend修改的挂起函数,使其内部也能调用挂起函数,如delay,能轻松实现延迟发送数据的操作。

  • onEach

    上游数据流的每个值发送前执行操作。

    onEach操作符声明.png

    等效于RxJava中的doOnNextdoOnSuccess,并且同样有suspend关键字修饰代码块。

  • onCompletion

    上游数据流完成、取消、异常时,执行的操作。

    不同于RxJavaonCompletedoFinally的结束时兜底,onCompletion接收的参数同样是以FlowCollector作为接收者的函数类型,具有发送数据功能。

    onCompletion操作符声明.png

    而且函数类型的参数提供了对于异常抛出的收集,如果正常结束则为null。

    注意,onCompletion并不能捕获异常出现异常时无法发送数据

    需要捕获异常并发送数据应使用catch操作符。

    onCompletion在数据流中的位置需要注意,详见下文中的重试操作符。

    该操作符可以等价为命令式代码:

     try {
         flow.collect { value ->
             ...
         }
     } finally {
         //onCompletion代码块
         ...
     }
    复制代码

    但既然是使用响应式编程的Flow,就尽量把所有操作都放到数据流中吧。

  • onEmpty

    上游数据流完成时,没有任何值传递消费,则触发执行操作,可发送额外的元素

    onEmpty操作符声明.png

    上游数据流的数据也包括诸如onStartonCompletion等操作符发送的数据。

异常处理

RxJava时代,对于异常的捕获相信是最好用的功能之一,在onError中可以捕获到数据流中所抛出的异常。这在Flow里自然也有同样的替代品。

虽说可以直接try-catch代码块就把异常捕获了,但在数据流操作中,我们最好还是把内部异常透明化,重新抛出到外部来统一处理。

  • catch

    捕获上游数据流中所抛出的异常,并允许发送新的数据

    catch操作符声明.png

    catch所捕获的异常,不会传递到下游。

    catch操作符最好放在数据流的最下游,便于捕获所有上游抛出的异常。

    类似于RxJava中的onErrorRetrun操作符与onError的结合体。

    但如果在下游collect中抛出异常,是无法捕获到的。

    此时可以尝试将collect的部分操作,前移到上游的onEach中,然后由其下游的catch捕获并发送异常时数据。

    最后可在外部协程中设置CoroutineExceptionHandler兜底捕获异常。

重试

RxJava相同,当数据流中出现异常整个数据流将会结束,导致后续数据无法继续发送

但有些场景我们并不希望数据流就此结束,此时就需要使用重试机制。

  • retry

    上游数据流出现异常时,如果满足条件时,会捕获异常,并重新发出上游数据流,直到超出重试次数后,继续向下游抛出异常

    retry操作符声明.png

  • retryWhen

    retry内部就是调用的retryWhen,其会在捕获到异常后,如果满足重试条件,将在其上游数据流,从重新执行一次上游数据流,直到再次捕获到异常。

    不满足重试条件,会继续向下游抛出异常

    retryWhen操作符声明.png

  • 如果catch操作符在上游数据流,会直接由catch捕获, 而不会进入retryWhen。通常需要catch在下游捕获由retryWhen抛出的异常。

这等效于在RxJava中把onErrorReturn放到retryWhen上游。

  • onCompletion操作符在上游数据流,异常会先进入onCompletion执行,但不会发出数据,然后才进入retryWhen,重试后也是如此。

如果需要使用retryWhenretry操作符,尽可能靠近需要重试的数据流操作,避免出现不符合预期的情况。

合并

某些场景可能需要我们将数据流合并,在RxJava中可以使用concatzip等合并操作符,在Flow中也提供了类似功能。

  • combine

    将两个数据源中的最新值通过函数合并,并直接传递给下游。

    combine操作符源码.png

    类似于RxJavacombineLatest操作符,但在flow中可以是任意类型的数据流合并。

     //每个值延迟50ms发送
     val flow1 = flowOf("first","second","third").onEach { delay(50) }
     //每个值延迟100ms发送
     val flow2 = flowOf(1,2,3).onEach { delay(100) } 
     ​
     flow1.combine(flow2){first,second->
       "$first $second" 
     }
     .collect { println("result $it") }
     ​
     //flow2元素的1元素发送时,flow1数据源的second元素是最新值,
     //所以first会被忽略
     result second 1
     result third 1
     result third 2
     result third 3
    复制代码
  • combineTransform

    效果上与combine相同。

    唯一区别只是,combine在合并后立即发送数据,而combineTransform会经过转换由用户控制是否发送合并后的值

    combineTransform操作符源码.png

  • zip

    两个数据源中的每个值都合并,发送给下游,只要其中一个数据源结束,则数据流完成结束。

    zip操作符声明.png

    zip操作符与在RxJava中的表现一致,但函数类型支持挂起函数。

     //每个值延迟50ms发送
     val flow1 = flowOf("first","second","third").onEach { delay(50) }
     //每个值延迟100ms发送
     val flow2 = flowOf(1,2,3,4).onEach { delay(100) } 
     ​
     flow1.combine(flow2){first,second->
       "$first $second" 
     }
     .collect { println("result $it") }
     ​
     //由于flow1只有3个元素,所以flow2的第4个元素被抛弃了。
     result first 1
     result second 2
     result third 3
    复制代码

combinecombineTransform操作符支持多个数据源合并,

zip目前仅支持两个数据源合并。

常用操作符

除此之外,还有一些很多的操作符,这里只列举一些常用的,基本都是等效于RxJava对应的同名操作符。

  • first

    末端操作符,只取上游数据流的第一个元素,或满足条件的第一个元素,后续元素抛弃。

    first操作符声明.png

  • last

    末端操作符,只取上游数据流的最后一个元素,如果元素为null则抛出异常

    可使用lastOrNull获取最后一个可空元素。

  • filter

    中间操作符,过滤上游数据流的值,仅允许满足条件的值继续传递。基于transform

  • take

    中间操作符,只从上游数据流获取指定个数的元素,传递到下游,后续元素抛弃

  • decode

    中间操作符,在指定时间内,只允许最新的值传递到下游,多用于过滤上游高频率的生产者数据。

    debounce操作符声明.png

    RxJava时代,利用RxBinding库可以将View事件转化为数据流,然后利用decode操作符来实现对于EditText的输入防抖过滤,以及View的点击防抖操作。

    decode操作符在Flow中目前是不保证后续版本兼容的预览版本。

  • distinctUntilChanged

    中间操作符,过滤上游数据流中的重复元素,等价于RxJava内的distinct操作符。

    distinctUntilChanged操作符声明.png

操作符复用

RxJava中,对于一系列通用的数据流操作,我们可能会想在多个数据流中复用,此时会先将这一系列操作利用Transformer封装起来,然后利用compose操作符进行统一调用。

比如对于网络请求返回数据的一些通用处理。

 class MyTransformer<T>(...) : MaybeTransformer<T,T>{
     override fun apply(upstream: Maybe<T>): MaybeSource<T> {
      return upstream
             .subscribeOn(Schedulers.io())    
             .map { ... } //预处理请求返回
             .observeOn(AndroidSchedulers.mainThread())
             .onErrorReturn{...} //在异常时发送表示错误的数据 
     }
 }
 ​
 api.compose(MyTransformer(...)).subscribe(...)
复制代码

Flow的设计中,看得出来设计者在早期其实也有相同的想法,确实是有一个compose操作符。不过已经是废弃状态,在Kotlin里也确实不需要再整这么麻烦的事情了。

回想一下Flow里操作符是如何定义的呢?利用Kotlin的拓展函数,我们完全可以自定义一个操作符,内部调用这一系列操作。

 fun <T> Flow<T>.preHandleHttpResponse(
     dispatcher: CoroutineDispatcher = Dispatchers.IO,
 ){
     map {response->
         if (it.code == 0) it
         else throw MyHttpException(...) //抛出自定义异常,表示业务执行失败
     }
         .flowOn(dispatcher)
         .catch{error->
             //全局处理网络请求异常
             emit(...) //在异常时发送表示错误的数据 
         }
 }
 ​
 flow{...}.preHandleHttpResponse().collect{...}
复制代码

取消

RxJavasubscribe会返回Disposable用于控制数据流取消,或者利用AutoDispose绑定Lifecycle来自动管理数据流关闭。

Flow本身并没有提供取消功能,不过Flow的消费是挂起函数,必须运行在Kotlin协程,直接利用外部协程启动时的Job对象就能管理Flow的取消。

背压

在响应式编程中,数据背压是必然会存在的问题。

在生产者-消费者模型中,如果生产者的生产速率远大于消费者的消费速率,也就产生了背压问题

仅出现在生产者与消费者分别在不同线程的异步场景。

限制生产端

RxJava中,从RxJava2开始就专门推出了Flowable来解决数据背压问题。

并为其设置有个默认固定为128的异步缓存池以及Backpressure缓存策略:

  • MISSING :不做缓存处理
  • ERROR :缓存队列满后,抛出MissingBackpressureException异常
  • BUFFER :缓存队列满后,继续扩大缓存容量,知道OOM
  • DROP :缓存队列满后,直接抛弃后续新的值。
  • LAEST : 永远会将最新的一个值放入缓存队列,不论缓存池状态如何。

Flow同样也提供了类似的背压缓存策略

  • buffer

    基于Channel开启一个缓存区。

    Channel可以理解为Kotlin协程的BlockingQueue,接收和发送都是挂起函数。

     public fun <T> Flow<T>.buffer(
         capacity: Int = BUFFERED,
         onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
     ): Flow<T>
    复制代码

    参数capacity,表示缓存区容量,默认为BUFFERED,是Channel的常量。

    • RENDEZVOUS ,无缓存区,生产一个消费一个。
    • BUFFERED,创建个默认缓存容量为64的缓存区
    • CONFLATED ,相当于把缓存容量设置为1,且缓存策略强制为DROP_OLDEST
    • 自定义缓存区容量大小

    参数onBufferOverflow,缓存策略方式,默认使用SUSPEND

    • SUSPEND :当缓存区满时,将生产者挂起,不继续发送后续值,直到缓冲区有空缺位置。
    • DROP_OLDEST :缓存区满时,移除缓存区中最旧的值,并插入最新的值
    • DROP_LATEST :缓存区满时,抛弃最新的值。
  • conflate

    专门提供的快捷使用buffer(CONFLATED)的操作符

  • flowOn

    实际上flowOn修改了协程上下文后,内部也会开启默认缓冲区。

    相当于默认的buffer(),天然支持背压。

     flow{
         (0..100).forEach {
             delay(50)
             val currTime = System.currentTimeMillis()
             println("emit $it on ${currTime - startTime} ms")
             emit(it)
         }
     }
     .flowOn(Dispatchers.IO) //等价于buffer(),并且指定调度器
     .onEach { delay(300) } //消费端延迟
     .collect {
         val endTime = System.currentTimeMillis()
         println("result : $it on ${endTime - startTime} ms")
     }
     ​
     emit 0 on 93 ms
     emit 1 on 156 ms
     emit 2 on 220 ms
     emit 3 on 282 ms
     emit 4 on 345 ms
     emit 5 on 409 ms
     result : 0 on 409 ms
     emit 6 on 471 ms
     ...
     emit 9 on 657 ms
     result : 1 on 719 ms
     emit 10 on 719 ms
     ...
     //超出默认64位缓存区,生产端就被挂起暂停了,速度减缓
     //消费一个,从缓存队列中移除,然后生产一个填充队列。
     emit 79 on 5050 ms
     result : 15 on 5081 ms
     emit 80 on 5112 ms
     emit 81 on 5174 ms
     result : 16 on 5390 ms
     emit 82 on 5451 ms
     result : 17 on 5700 ms
     ...
     //直到生产端完成任务
     result : 34 on 11026 ms
     emit 100 on 11090 ms
     result : 35 on 11338 ms
     result : 36 on 11651 ms
     result : 37 on 11965 ms
     ... 
     //直到消费结束
    复制代码

    注意上面emit运行的时间,bufferflowOn操作符还有将上游数据快速并发的附带功能。

    • 其实可以利用这一特性快速并发运行多个任务,但buffer无法指定调度器

    如果将上面flowOn替换为conflate,程序运行结果:

     emit 0 on 99 ms
     emit 1 on 162 ms
     emit 2 on 224 ms
     emit 3 on 287 ms
     emit 4 on 351 ms
     emit 5 on 413 ms
     result : 0 on 413 ms
     emit 6 on 474 ms
     ...
     emit 9 on 660 ms
     result : 5 on 720 ms
     emit 10 on 720 ms
     ...
     emit 14 on 970 ms
     result : 9 on 1034 ms
     emit 15 on 1034 ms
     ...
     emit 19 on 1286 ms
     result : 14 on 1350 ms
     emit 20 on 1350 ms
     ...
    复制代码

    此时,都只能接收上一个最新的值,毕竟缓存区容量只有一个,其他新值都被丢弃。

    类似于RxJavaLAEST策略。

限制消费端

在消费端也提供了背压数据取舍的功能

  • collectLatest

    按顺序收集上游数据流传递的每一个元素,如果当前元素还未处理完,又新元素传递过来后,取消当前元素的收集操作。

    collectLast操作符声明.png

    collectLatest操作符对于上游数据流的耗时操作是不在意的,只有在collectLatest函数内部的处理时间过长才会被算作还未完成处理。

    将上面的程序稍作修改:

     //生产端不变
     flow.collectLatest{
         delay(300)  //延迟移到消费端
         ...
     }
     ​
     emit 0 on DefaultDispatcher-worker-1 @coroutine#3 (115 ms)
     emit 1 on DefaultDispatcher-worker-1 @coroutine#3 (177 ms)
     emit 2 on DefaultDispatcher-worker-1 @coroutine#3 (240 ms)
     emit 3 on DefaultDispatcher-worker-1 @coroutine#3 (304 ms)
     emit 4 on DefaultDispatcher-worker-1 @coroutine#3 (369 ms)
     emit 5 on DefaultDispatcher-worker-1 @coroutine#3 (433 ms)
     emit 6 on DefaultDispatcher-worker-1 @coroutine#3 (496 ms)
     ...
     //由于消费端一直在取消收集,只获取到最后一个值。
     result : 100 on Test worker @coroutine#104 (6721 ms)
    复制代码

总结

RxJava虽说很好用,但相对复杂的实现,繁杂的操作符,导致上手难度偏大,甚至使用多年的老手也很少敢说完全了解的。

相较于RxJava基于Java的复杂实现,背靠着Kotlin与Kotlin协程优秀基础设施的Flow,简直就是亲儿子,本身API实现能做到相当精简,经过多个版本的迭代,Kotlin Flow目前已经比较成熟。

虽然仍有部分处于实验阶段,可能在后续版本被修改,不过通常也只是废弃警告,替换成推荐等价函数即可。

对于日常使用而言,上述列举的基本都已经是正式操作符,基本能覆盖常用范围。相信Flow的设计过程中也参考了RxJava的设计,其在操作符的功能与命名方面都非常相似,对于用过RxJava的人来说,非常容易上手。

总体而言,在Kotlin协程中,自然优先使用Flow来进行异步数据操作。

原本使用RxJava进行异步编程的项目也可以尝试逐步使用Kotlin协程+Flow的方案替代。

限于篇幅有限,更多Flow的进阶功能,将在后续文章中继续探索。

参考资料

Android 上的 Kotlin 数据流

kotlin 协程官方文档(5)-异步流(Asynchronous Flow)

kotlin 协程 Flow:给 RxJava 使用者的介绍

抽丝剥茧Kotlin - 协程中绕不过的Flow

猜你喜欢

转载自juejin.im/post/7034379406730592269