Kotlin Flow 基础概念

更新日期: 2023-06-05 阅读次数: 1050 字数: 1247 分类: Android

本以为我的 Android 开发技术已经天下无敌了😅,没想到连 Flow / Channel 我都第一次见。。。不得不说 Android 的新概念层出不穷。

周末看了一堆文档,有了一个大概的了解。我发现微信公众号的文章质量还真是高,不少高手都做了详细的对比说明。 但是最终我一个也没有关注,从这些人的近期推文看都堕落了,不是贩卖焦炉,就是无脑卖其他骗子的课。扯远了,开始正题。

Flow 为何而生

一个挂起函数 (suspending function) 可以异步地返回一个值,但是没法多次返回。于是 Flow 就诞生了。 这也是为什么 Flow 获取 select 结果不需要加上 suspend 关键词,因为本身就不是挂起函数。

Flow 是 Kotlin 语言层的实现,所以解决了 LiveData 无法跨平台的问题。 同时 Flow 也解决了 LiveData 只能在 UI 主线程更新值的缺点。

cold / hot stream

Flow 是基于 Kotlin Coroutines 实现的 cold stream,即冷流。

注意:虽然 flow 是 code stream,但是新引入的 flow 子类, StateFlow 和 SharedFlow 是 hot stream。

  • hot stream: 热流,即便没有消费端订阅数据,生产端也会一直 push 数据。而 channel 则是 hot stream。
  • cold stream: 热流,只有当消费端开始 collect 数据的时候,生产端才会 push 数据。

flow 的几种构建方式

// 第一种
val namesFlow = flowOf("Jody", "Steve", "Lance", "Joe")

// 第二种
val namesFlow = listOf("Jody", "Steve", "Lance", "Joe").asFlow()

// 第三种
val namesFlow = flow {
  val names = listOf("Jody", "Steve", "Lance", "Joe")
  for (name in names) {
    delay(100)
    emit(name)
  }
}

emit 是向消费端发送数据;flow 后面的大括号代表一个 lambda block。

flow operator

fun main() = runBlocking {
  namesFlow
      .map { name -> name.length }
      .filter { length -> length < 5 }
      .collect { println(it) }

  println()
}

上面代码中,有两种 flow operator:

  • Intermediate Operator: map 和 filter。map 类似于 list 的 map 操作,将当前 list 转换一下;filter 过滤元素。注意:区分开 Intermediate(居中的,中间的) 和 Immediate(立即,马上)。所以,Intermediate Operator 作用于 flow 时,相关操作并不会立即执行。即其返回的依旧是 cold stream。
  • Terminal Operator: collect。flow 作为 cold stream, 其并不会产生数据,直到调用了 terminal operator。collect 是一个 suspending function, 所以需要在 coroutine 或者另一个 suspending function 中调用。

Room 对 flow 的支持

Room 2.2 版本开始支持 Flow。

即 select 操作可以被监听,当 insert 或者 remove 数据到数据库时,可以被监听到。

例如:

@Query("SELECT * FROM forecasts_table")
fun getForecasts(): Flow<List<DbForecast>>

这里用到了前面提到的一种将 list 转换为 flow 的做法。

Flow 与 LiveData

Android 中有三种常见的 observer pattern:LiveData, Flow, RxJava:

  • LiveData: 用法最简单,上手容易。非常适合 View 与 ViewModel 间同步状态。但是处理不了更复杂的场景。LiveData 是不防抖的。LiveData 的 transformation 工作在主线程
  • RxJava: 功能最强大,但上手最难。
  • Flow:鉴于两者之间。flow 相对 livedata 的优势是,设置数据,不需要切换到主线程。而 LiveData 的 setValue() 发生在主线程(非主线程调用会抛异常,postValue() 内部会切换到主线程调用 setValue())。且 Flow 是 koltin 语言层支持,支持跨平台;而 LiveData 不支持跨平台。

LiveData 相对于 Flow 的优势:内置了生命周期的管理,所以非常适合 View 与 ViewModel 间的通信。

为何如此看重生命周期管理? 假设我们的程序已经不在前台了,flow 流还在持续更新,则UI更新依然在持续进行当中。这是非常危险的事情,因为在非前台的情况下更新UI,某些场景下是会导致程序崩溃的。

但是我感觉 90% 的场景都适用于 LiveData,即基本都是用在界面状态同步上。偶尔才会有类似手机传感器数据这种复杂的处理场景。

Flow 转换为 LiveData

.asLiveData()

例如:

//1
val forecasts: LiveData<List<ForecastViewState>> = weatherRepository
    //2
    .getForecasts()
    //3
    .map {
      homeViewStateMapper.mapForecastsToViewState(it)
    }
    //4
    .asLiveData()

flow 适合做搜索的地方

private val _locations = queryChannel
    //1
    .asFlow()
    //2
    .debounce(SEARCH_DELAY_MILLIS)
    //3
    .mapLatest {
      if (it.length >= MIN_QUERY_LENGTH) {
        getLocations(it)
      } else {
        emptyList()
      }
    }
    //4
    .catch {
      // Log Error
    }

  • 将 channel 转换为 flow。实际这部分可以用 StateFlow 替代 Channel 尝试一下。
  • 防抖动:debounce 用于防抖动。例如,实时搜索功能,当用户输入一个单词的时候,不需要每个字母都触发网络搜索,希望能加上触发的时间间隔,以避免高频率访问网络接口。
  • mapLatest(): 处理最新的数据,并把之前没有完成的计算 cancel 掉。

StateFlow 替代 Channel

BroadcastChannel 未来会在 Kotlin 1.6.0 中弃用,在 Kotlin 1.7.0 中删除。它的替代者是 StateFlow 和 SharedFlow。

StateFlow 和 SharedFlow 是 hot stream。

例如,实时搜索词进行查询的场景: View 通过 channel 将查询词传递给 viewmodel 层,viewmodel 中将 channel 转换为 flow, 再 map 进行查询。这里的 channel 就可以用 flow 替代。

参考

  • https://www.kodeco.com/9799571-kotlin-flow-for-android-getting-started
  • https://kotlinlang.org/docs/flow.html
  • https://www.baeldung.com/kotlin/flows-vs-channels

tags: flow

关于作者 🌱

我是来自山东烟台的一名开发者,有敢兴趣的话题,或者软件开发需求,欢迎加微信 zhongwei 聊聊, 查看更多联系方式