本以为我的 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
微信关注我哦 👍
我是来自山东烟台的一名开发者,有感兴趣的话题,或者软件开发需求,欢迎加微信 zhongwei 聊聊, 查看更多联系方式