LiveData的优点
1.LiveData可以用在Java层面上
2.LiveData使用简单,可以感知生命周期
3.LiveData可以自动取消订阅
LiveData的缺陷
1.LiveData只能在主线程更新数据
2.LiveData的操作符不够强大,在处理复杂数据流时有些捉襟见肘(虽然MediaorLiveData支持map、switchMap操作符,但也只能在主线程操作)
3.LiveData在子线程多次调用PostValue会导致数据丢失\
LiveData操作符的使用
mediatorData.addSource(likeNum) { t ->
Log.e(TAG, "mediator data receive likeNum=${t}")
mediatorData.value = t.toString()
}
mediatorData.addSource(flowNum) { t ->
Log.e(TAG, "mediator data receive flowNum=${t}")
mediatorData.value = t.toString()
}
mediatorData.observe(this, Observer<String> {
Log.e(TAG, "mediator data value=${it}")
})
transData = Transformations.map(
likeNum
) { return@map it.toString() + "map operator" }
switchData = Transformations.switchMap(
likeNum
) { input -> MutableLiveData<Double>(input?.plus(4)?.toDouble()) }
transData.observe(this, Observer<String> {
Log.e(TAG, "transData=${it}")
})
switchData.observe(this, Observer<Double> {
Log.e(TAG, "switchData=${it}")
})
findViewById<AppCompatTextView>(R.id.text).setOnClickListener {
likeNum.value = 2
flowNum.value = 3
}
为什么要引入Flow
liveData不支持切换线程,所有数据转换都在主线程完成;RxJava入门门槛较高,同时需要自己处理生命周期感知,在生命周期结束时取消订阅;Flow刚好介于两者之间,Flow操作符简单,也支持线程切换、背压,同时又属于Kotlin协程的一部分,可以跟协程的基础设施结合
flow的创建
val flow = flowOf{}val flow = listOf(1,2,3).asFlow()val flow = channelFlow{}
flow的操作符
中间操作符
- map
- onEach
- transform
- take
- buffer
- onCompletion
- catch 代码示例
runBlocking {
flowOf(1, 2)
.onEach { // 发送流之前做的事
println(it)
delay(100)
}
.map { value -> value + 1 } // 对流中的数据进行转换
.transform<Int, String> { // 重新创建流并改变流的数据类型
val result = it.toString() + "1"
emit(result)
}
.take(1) // 取流中的第一条数据
.onCompletion { cause -> // 处理流完成的回调
run {
if (cause != null) {
println("exception ${cause.message}")
} else {
println("on Complete")
}
}
}
.collect { // 触发流发送的操作
println(it)
}
}
末端流操作符
- collect
- toList/toMap
- reduce
val mainScope = MainScope()
mainScope.launch {
val ret = countdown(60_000, 2_000) { io(it) }
.onStart { Log.v("test", "countdown start") }
.onCompletion { Log.v("test", "countdown end") }
.reduce { acc, value -> acc + value } // 终端消费者:计算所有异步结果的和
// 因为 reduce() 是一个 suspend 方法,所以会挂起协程,直到倒计时完成才打印所有异步结果的和
Log.v("test", "coutdown acc ret = $ret")
}
Flow的冷流
Flow 是冷流,冷流不会发射数据,直到它被收集,所以冷流是“声明式的”,所有能触发收集数据动作的消费者称为终端消费者,所有终端消费的方法都是suspend方法,必须在协程执行
// 执行式的
suspend fun get(): List<String> =
listof("a", "b", "c").onEach {
delay(1000)
print(it)
}
// 声明式的
fun get(): Flow<String> =
flowOf("a", "b", "c").onEach
{ delay(1000)
print(it)
}
Flow的线程切换
已搜索关键字来展示结果的场景为例子
editText.textChangeFlow() // 构建输入框文字变化流
.filter { it.isNotEmpty() } // 过滤空内容,避免无效网络请求
.debounce(300) // 300ms防抖
.flatMapLatest { searchFlow(it.toString()) } // 新搜索覆盖旧搜索
.flowOn(Dispatchers.IO) // 让搜索在异步线程中执行
.onEach { updateUi(it) } // 获取搜索结果并更新界面
.launchIn(MainScope()) // 在主线程收集搜索结果
// 构建输入框文字变化流
fun EditText.textChangeFlow(): Flow<CharSequence> = callbackFlow {
// 构建输入框监听器
val watcher = object : TextWatcher {
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
// 在文本变化后向流发射数据
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
s?.let { offer(it) }
}
}
addTextChangedListener(watcher) // 设置输入框监听器
awaitClose { removeTextChangedListener(watcher) } // 阻塞以保证流一直运行
}
// 更新界面
fun updateUi(it: List<String>?) {}
// 访问网络进行搜索
fun search(key: String): List<String>? {
//TODO 网络请求
return null
}
// 将搜索关键词转换成搜索结果流
fun searchFlow(key: String) = flow { emit(search(key)) }
Flow的一种实现方式SharedFlow
由于flow是冷流,冷流与订阅者是一对一的关系当 热流有多个订阅者时,热流与订阅者们的关系是一对多的关系,可以与多个订阅者共享信息。
shareFlow的构造函数
public fun <T> MutableSharedFlow( replay: Int = 0,
extraBufferCapacity: Int = 0,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND ): MutableSharedFlow<T>
1.replay表示当新的订阅者Collect时,发送几个已经发送过的数据给它,默认为0,即默认新订阅者不会获取以前的数据
2.extraBufferCapacity表示减去replay,MutableSharedFlow还缓存多少数据,默认为0
3.onBufferOverflow表示缓存策略,即缓冲区满了之后Flow如何处理,默认为挂起
whilesubscribed策略
whileSubscribed 策略会在没有收集器的情况下取消上游数据流。通过 stateIn 运算符创建的 StateFlow 会把数据暴露给视图 (View),同时也会观察来自其他层级或者是上游应用的数据流。让这些流持续活跃可能会引起不必要的资源浪费,例如一直通过从数据库连接、硬件传感器中读取数据等等。当您的应用转而在后台运行时,您应当保持克制并中止这些协程。
whileSubscribed构造函数中两个重要的参数
-
1.
stopTimeoutMillis控制一个以毫秒为单位的延迟值,指的是最后一个订阅者结束订阅与停止上游流的时间差。默认值是 0 (立即停止).这个值非常有用,因为您可能并不想因为视图有几秒钟不再监听就结束上游流。这种情况非常常见——比如当用户旋转设备时,原来的视图会先被销毁,然后数秒钟内重建。 -
2.
replayExpirationMillis表示数据重播的过时时间,如果用户离开应用太久,此时您不想让用户看到陈旧的数据,你可以用到这个参数
在页面中观察StateFlow(LiveData)
观察StateFlow需要在协程中,因此我们需要协程构建器,一般我们会使用下面几种
lifecycleScope.launch: 立即启动协程,并且在本Activity或Fragment销毁时结束协程。LaunchWhenStarted和LaunchWhenResumed,它会在lifecycleOwner进入X状态之前一直等待,又在离开X状态时挂起协程
StateFlow 与 LiveData 有一些相同点:
- 提供「可读可写」和「仅可读」两个版本(
StateFlow,MutableStateFlow) - 它的值是唯一的
- 它允许被多个观察者共用 (因此是共享的数据流)
- 它永远只会把最新的值重现给订阅者,这与活跃观察者的数量是无关的
- 支持
DataBinding
它们也有些不同点:
- 必须配置初始值
- value 空安全
- 防抖
onCreateView(...) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
myViewModel.myUiState.collect { ... }
}
}
}
参考文献