(5.22更新:打扰了,我膨胀了,我都敢谈结构化并发了,标题已修改。)
前言:本文遵循jetpack最佳实践,抽象出数据层,使用依赖注入,易于升级,维护,拓展,测试。使用kotlin 协程和缓存。
免责声明:本文的做法有点浪费资源,实际上因该借鉴 BackPressure的处理方式,因此使用协程的 ConflatedBroadcastChannel+flow 是最好的选择。
1.平时开发多线程的场景
学习协程蛮久了,但是平时开发过程中并发的场景不多,总感觉手里有力量使不出的感觉。哈哈,我现在终于找到一个业务场景,很适合练习协程的结构化并发。
我们平时用的各种app都有搜索框,我发现京东,淘宝,微信这些app,搜索框都是根据输入框的关键字实时展示搜索结果,这肯定涉及到实时的网络请求,而且用户如果输入的内容变化快的话,要实时展示,app要丝滑,难度估计也有一点。
这个实时展示用来练习协程真的是难度刚刚好,妙啊。我来展示一下我做的一个’高’性能,顺滑的 搜索 功能。 github链接:github.com/JJJsn/struc…
首先是微信的效果:(十分流畅,猜测是实时网络请求 + 缓存,那么我也整一个?)
接下来是我实现的效果:
2.直接开发
开发之前,我们先技术选型,这个简单的界面,我们就用遵循官方的 jetpack best practice好了,kotlin+jetpack 全家桶。
2.1数据层接口
数据层,我们先定义数据层的接口吧:
interface SearchSource{
suspend fun search(keyWord:String): List<String> //根据搜索框内的关键字 获取 搜索结果
}复制代码
ok,就一个方法,非常简单。
2.2监听输入框
在activity中监听 输入框的 内容:
et_keyWord.addTextChangedListener { text ->
with(viewModel) {
keyWord.value = text.toString()
}
}//每次输入框里面的内容变化,我们都会开启一个协程,是并发场景,用来练习协程,难度刚刚好复制代码
2.3viewModel层:
class SearchViewModel(val repository: SearchSource):ViewModel(){
val keyWord=MutableLiveData<String>()
val searchResult: LiveData<Result<List<String>>> = keyWord.switchMap { keyWord ->
liveData(Dispatchers.Default) { //切换线程,默认是ui线程
emit(Result.Loading)
emit(try {
Result.Success(repository.search(keyWord))
}catch (e:Exception){
Result.Error(e)
})
}
}复制代码
这串代码很短,但是一点也不简单:
1.首先 liveData(小写l)可以看作是一个协程的builder,里面可以直接调用suspend 函数
2.直接说结论:每次keyWord(输入框的内容)变化的时候,都会开启一个新的协程。
然后,searchReault这个LiveData的值就会从 Loading -> 新的keyWord的值对应的 搜索 结果。之前的用来查询旧的keyword的搜索结果的 协程 要是没有完成,会直接被取消。这 是结构化并发给我们带来的好处。
3.在liveData这个builder里面更新LiveData的值 不用管是在哪个线程,直接emit就完事儿了。
4.建议去看switchMap和liveData这个buider的源码。既然开启协程,那么每次开启的协程的scope是啥?emmm,是这样的,liveData这个buidler开启的协程用是一个supervisorJob(不是viewModelscope的supervisorJob),所以开启的协程的生命周期不是由viewModel来管理的。liveData源码里面有个 CoroutineLiveData,当这个LiveData没有活跃的观察者,就会自己取消。然后再结合下switchMap的源码,就明白了。
2.4数据层具体实现
现在来看下数据层的申明哈:
class SearchRepository(val searchRemoteSource: SearchSource, //remote(比如后端)
val searchLocalSource: SearchSource ) //本地数据层(比如数据库)
:SearchSource {}复制代码
2.4.1 本地数据源实现
class SearchLocalSource private constructor(): SearchSource{
//简单起见:直接返回一个空列表吧,本地没有有效的缓存数据
override suspend fun search(keyWord: String): List<String> {
return if(isDirty()) emptyList() else throw RuntimeException("not implemented yet")
}
fun isDirty()=true
companion object{
val instance by lazy {
SearchLocalSource()
}
}
}复制代码
2.4.2 remote数据源
class SearchRemoteSource private constructor() :SearchSource{
//这是一个假后端实现类
override suspend fun search(keyWord: String): List<String> {
delay(200)
if(keyWord.isEmpty()){ return emptyList()}
return fakeRemoteData.filter { candidate ->
candidate.contains(keyWord,true) }
}
/*伪造后端数据*/
val fakeRemoteData= mutableListOf<String>().apply {
//测试数据
add("123我爱你");add("17岁");add("123木头人")
add("11111");add("1234");add("1990");add("1111");add("111");add("119")
add("1121"); add("112");add("1111")
add("111 Summer Classics");add("111111");add("111 (Centoundici)");
add("11:11 (Amended)");add("11111101");add("11112")
add("1111111");add("11111101");add("11111 (feat. Jake Candieux, Dan Monic..)");
add("11111111") }
companion object{
val instance by lazy {
SearchRemoteSource()
}
}}
复制代码
2.4.3 把我们的本地数据源和remote数据源塞到repository里面去,并实现在 repository建立一个LruCache防止不必要的网络请求。
class SearchRepository(val searchRemoteSource: SearchSource,
val searchLocalSource: SearchSource )
:SearchSource {
@Volatile var searchResultCache=LruCache<String,List<String>?>(20)
val pending =AtomicBoolean(true)
override suspend fun search(keyWord:String): List<String> {
val getFromCache = searchResultCache.atomicallyGet(keyWord, pending)
if(getFromCache!=null) return getFromCache //如果从缓存中找到了,直接返回
val localResult = searchLocalSource.search(keyWord)
if(!localResult.isEmpty()) //如果从本地数据源(比如数据库)找到了有效数据,直接返回,并把数据存入 lruCache
return localResult.also { searchResultCache.atomicallyPut(keyWord,it,pending) }
//从后端获取数据,并存入 lruchche, todo :实现一个本地数据库,存入数据库。
return searchRemoteSource.search(keyWord).also { searchResultCache.atomicallyPut(keyWord,it,pending) }
}
}复制代码
因为是多线程环境,写两个扩展函数来实现LruCache的get和put操作的原子性:
suspend fun <K,V> LruCache<K,V>.atomicallyPut(key:K, value:V, pending:AtomicBoolean ){
while (true){
if(pending.compareAndSet(true,false)){
put(key,value)
break
}
delay(50)
}
pending.set(true)
}
suspend fun <K,V> LruCache<K,V?>.atomicallyGet(key:K, pending:AtomicBoolean ): V? {
var retryInterval=50L
while (true){
if(pending.compareAndSet(true,false)){
val get = get(key)
pending.set(true)
return get
}
retryInterval+=50
delay(min(retryInterval,3_000L))
}
}复制代码
2.5把复杂的任务交给viewModel之后,我的Activty就变得很轻松~
class SearchActivity : AppCompatActivity() {
val searchAdapter = SearchAdapter()
//注入我们的viewModelFactory,在Factory注入viewModel所需的数据层实现类,这里是SearchRepository
val viewModelFactory by lazy {
InjectorUtil.provideSearchViewModelFactory()
}
val viewModel: SearchViewModel by lazy {
ViewModelProvider(
this,
viewModelFactory)
.get(SearchViewModel::class.java) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search)
restoreStateFromViewModel() //activity旋转之后重建,恢复搜索框里面的文字内容
et_keyWord.addTextChangedListener { text ->
with(viewModel) {
keyWord.value = text.toString() //每次输入框里面的内容变化,我们都会开启一个协程,是并发场景,用来练习协程,难度刚刚好
}
}
observeState()
//显示搜索结果的recyclerview,设置一下。
rv_search_result.adapter = searchAdapter
rv_search_result.layoutManager = LinearLayoutManager(this)
}
private fun observeState() {
viewModel.searchResult.observe(this) { result ->
progress_bar.visibility = (result is Result.Loading).toVisibility() //只有在loading的时候我们才能看见 progress bar
when (result) {
is Result.Loading -> { tv_empty_search_result.visibility = View.GONE }
is Result.Error -> { showMessage(result.toString()) } //出错了,直接toast提示
is Result.Success -> {
searchAdapter.submitList(result.data) //把搜索结果给 recyclerview 展示
tv_empty_search_result.visibility = (result.data.size == 0).toVisibility() //如果返回的数据是空的列表,提示用户无相关内容
}
}
}
}
private fun restoreStateFromViewModel() {
viewModel.keyWord.value?.also { keyWord ->
et_keyWord.setText(keyWord)
}
}
}
@Suppress("UNCHECKED_CAST")
class SearchViewModelFactory(val repository: SearchSource) //在构造函数中注入 数据层接口 的 实现类
:ViewModelProvider.Factory{
override fun <T : ViewModel?> create(viewModel: Class<T>): T {
return SearchViewModel(repository) as T
}
}
复制代码
最后:
这篇文章很多地方有待优化,然后其实不用livedata,用 channel 和 flow是更好的选择。。