Kotlin协程并发实战,仿微信实时显示搜索结果

·  阅读 1559
Kotlin协程并发实战,仿微信实时显示搜索结果

(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是更好的选择。。


分类:
Android
标签:
分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改