4. StateFlow 代码实战
说了这么多 Flow 的东西,最后以一个实际的例子结束这一章节的学习笔记吧!
下面我将用一个应用实例来讲解 StateFlow 的实际应用。这个例子将会用到 debounce
、distinctUnitChanged
、flatMapLatest
等操作符,用这些操作符去实现一个文本输入中实时查询的例子。
假设有个需求,要实现一个浏览器搜索的功能,根据用户不断输入的字符去查询相关的内容。如果不做任何处理,用户对键入的字符串做的任何修改,都会去请求一次接口,那后端服务器肯定是吃不消的;对于用户而言,在不断输入的过程中返回的结果用户并不会很关心,他只会关心最终输入完成之后请求的数据。那么,如何减少后端的接口请求次数是关键所在。
先来看看核心的代码:
// code 8 ViewModel.kt 文件 val queryStateFlow = MutableStateFlow("") fun getQueryResult(): Flow<String> { return queryStateFlow .debounce(300L) .distinctUntilChanged() .flatMapLatest { if (it.isNullOrBlank()) { flow { emit("") } } else { dataFromNetwork(it).catch { emitAll( flow { emit("") } ) } } } .flowOn(Dispatchers.IO) } // 模拟网络请求的耗时操作 private fun dataFromNetwork(query: String): Flow<String> { return flow { delay(2000) emit(query) // 返回请求的结果 } }
首先可以直观地感受到,使用 Flow 去处理这一逻辑较为简单,代码量较少,这也是 Flow 的魅力所在。我们按顺序介绍一下所使用到的 Flow 操作符:
debounce 操作符
具体的操作符方法声明:
// code 9 public fun <T> Flow<T>.debounce(timeoutMillis: Long): Flow<T>
用于过滤掉最新的发射值之前 timeoutMillis 时间内发射的值,返回一个过滤后的 Flow。官方栗子非常清楚:
// code 10 flow { emit(1) delay(90) emit(2) delay(90) emit(3) delay(1010) emit(4) delay(1010) emit(5) }.debounce(1000) 最终会发射出下面的三个值: 3, 4, 5
发射 1 之后不到 1000ms 又发射了 2,所以 1 就会被过滤掉不会发射了,以此类推。所以最后发射的值是一定可以发射成功的。通过这个操作符,我们就可以有效减少频繁请求接口的问题,这里设置的 timeout 为 300ms,即在用户连续输入过程中每间隔 300ms 才去请求一次数据。
distinctUntilChanged 操作符
具体操作符声明为:
// code 11 public fun <T> Flow<T>.distinctUntilChanged(): Flow<T>
用于过滤掉重复的发射值。虽然 StateFlow 本身就可过滤掉没有变化的发射值,但在这里还是需要的,因为用户可能会删除刚输入的字符,这一操作符可进一步减少不必要的接口请求。
flatMapLatest 操作符
我看的代码版本这个操作符还是实验性api,后续可能被移除。具体操作符声明为:
// code 12 @ExperimentalCoroutinesApi public inline fun <T, R> Flow<T>.flatMapLatest(@BuilderInference crossinline transform: suspend (value: T) -> Flow<R>): Flow<R>
这个操作符可以在原流的基础上生成一个新流,当原流依次发出 a、b 两值时,新流都会接收,但如果新流 a 值的相关操作还未结束,则会取消 a 值的相关操作,并用 b 值进行操作。简单说就是,丢弃旧值操作,换用新值操作。下面是一个例子:
// code 13 fun flatMapLatestDemo() { val testFlow = flow { emit("a") delay(100) emit("b") }.flatMapLatest { flow { emit("receive $it") delay(200) emit("send $it") } } lifecycleScope.launch { testFlow.collect { println("----$it") } } }
通过打印的 log 可以看出,a,b 都被 flatMapLatest
操作符接收到了,只有 b 最终通过。这是因为 a 先到达,等待了 100ms 后新的值 b 也到了,但 a 还在等待中,这时 flatMapLatest
就会取消掉 a 后续的操作。如果把 delay(200)
改成 delay(50)
,那最终 a,b 都能被打印出来。
所以这个操作符在 code 8 中的作用就是进一步减少接口请求的次数。当输入的新字符串到来时,就会将之前旧字符串还未结束的请求操作取消掉,用新的字符串去请求数据。
ViewModel.kt
的代码终于说完了,其他的代码就比较常规了,直接上码:
// code 14 MainActivity.kt binding.editText.addTextChangedListener(object : TextWatcher{ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { } override fun onTextChanged(input: CharSequence?, start: Int, before: Int, count: Int) { viewModel.queryStateFlow.value = input.toString() } override fun afterTextChanged(s: Editable?) { } }) lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.getQueryResult() .collect { binding.tvText.text = it } } }
有关 Flow 的相关知识就到此结束了,来个简单总结吧~
总结
1)shareIn
和 stateIn
都可将冷流转化为热流,将数据共享给多个消费者,无需为每个消费者创建同一个数据流的新实例。两者通常用于提升性能,在没有消费者时缓存数据;
2)SharingStarted
启动方式有 Eagerly
、Lazily
、WhileSubscribed
三种,最常用的还是 WhileSubscribed
,有消费者就启动,没有就停止,还能设置停止延时时长和缓存过期时长;
3)注意 shareIn
、stateIn
都会新建一个 Flow,不要用于方法的返回值,建议赋值给属性;
4)shareIn
、stateIn
与 onStart
、onCompletion
等搭配可监听转成的热流的状态;
5)distinctUntilChanged
操作符可过滤重复数据,一般用于 SharedFlow;debounce
可用于在某一时间段内防抖;flatMapLatest
操作符可以用最新值替换旧值发送给下游,旧值直接被取消作废。
更多内容,欢迎关注公众号:修之竹 或者查看 修之竹的 Android 专辑
赞人玫瑰,手留余香!欢迎点赞、转发~ 转发请注明出处~