1. 需求场景
几乎大部分人都使用过下面的场景:使用百度等搜索引擎时,输入一个字符后会自动联想出相关的搜索关键字;在淘宝、京东等APP搜索商品时,会自动帮你联想出相关的商品。这在很大程度上提高了用户体验,快速引导用户到达搜索结果页。
所以我们总结出同类型的需求,有一个搜索输入框,用户可以随意输入任何字符,然后前端根据用户的输入给出联想提示信息。
2. 实现方案
一般前端实现起来也不复杂,前端监听输入框文本的变化,然后根据输入内容从本地或者调用接口从服务端查询相关联数据,然后在前端展示结果即可。虽然看起来很简单,但是要比较完美地实现,还是有很多需要考虑的:
- 用户输入文本的速度是随机的,如果监听文本变化之后立马去查询,可能会过多地发起查询,浪费系统资源。例如用户本意要输入"abcde",那么每输入一个字符都会触发查询,会触发 5 次,其实用户可能只想得到 "abcde" 的查询结果;
- 查询是异步的,异步返回的时机是不确定的,可能造成当前返回的异步查询结果与当前输入框里的文本不一致。例如用户输入"ab",先后触发关键字 "a"、"ab" 的查询,结果 "a" 的查询结果后返回,最后导致显示的是 "a" 的查询结果;
所以一个优雅的方案我觉得应该包含:
- 防抖限流;
- 自动取消无效查询;
以前要实现这些效果,监听输入框变化时,会通过计时以及延时任务等,在限定时间段内只触发一次,写起来也很麻烦。
3. 使用 Kotlin callbackFlow
Kotlin Flow 支持防抖、背压等特性,利用 callbackFlow 可以将输入框的文本变化包装成一个 Flow,然后来实现这个需求。
将 EditText 的输入文本变化包装成 Flow:
private fun textChangeFlow(editText: EditText): Flow<String> {
return callbackFlow {
val watcher = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
override fun afterTextChanged(s: Editable?) {
s?.let {
trySend(s.toString())
}
}
}
editText.addTextChangedListener(watcher)
//在 flow 被 close 时调用,可以清理资源,一般必须要有
awaitClose {
editText.removeTextChangedListener(watcher)
}
}
}
根据输入框文本内容,我们去查询相关信息:
fun testCallbackFlow(editText: EditText) {
viewModelScope.launch {
textChangeFlow(editText)
.debounce(300) //防抖处理,间隔 300ms 响应一次
.flatMapLatest { keyword -> //flatMapLatest 操作符,只处理最新的关键字,并且老的查询如果没有完成会自动 cancel 掉
println("keyword = $keyword")
flow {
//根据输入关键字查询信息
var result = queryByKeyword(keyword)
emit(result)
}
}.catch {
println("exception: $it")
}
.collect {
//查询到相关结果
}
}
}
如上代码所示,可以很优雅地实现我们想要的效果。