Kotlin中ListAdapter如何结合SearchView通过Filterable实现过滤?
核心问题根因
你遇到的两个开发异常,本质是对ListAdapter运行机制和Filter跨线程执行逻辑的认知偏差导致的:
publishResults方法拿到空值的原因:直接依赖currentList作为过滤数据源是不可靠的。currentList是ListAdapter对外暴露的只读属性,如果你没有在数据提交阶段对全量数据做单独的持久化缓存,当子线程执行performFiltering读取该属性时,很容易因为内存可见性、列表未完成初始化拿到空值。- 过滤后界面无响应的原因有两个:一是过滤后的列表如果和原列表是同一个内存引用,ListAdapter内置的DiffUtil会直接判定列表无变化,跳过刷新流程;二是和LiveData的绑定逻辑存在冲突——LiveData每次回调都会触发
submitList(全量数据),直接覆盖你之前生成的过滤结果,让列表始终展示全量内容。
可直接复用的Kotlin实现方案
整套实现完全基于ListAdapter和Filterable的原生能力,不需要依赖第三方库:
Adapter内部独立维护全量数据源,替代不可靠的
currentList- 在adapter类中声明私有全量列表缓存,避免跨线程访问异常:
private var fullList: List<Articolo> = emptyList() - 重写父类的
submitList方法,在数据提交时同步更新全量缓存,不直接依赖父类的内部存储:override fun submitList(list: List<Articolo>?) { fullList = list ?: emptyList() super.submitList(fullList) } - 对外暴露单独的全量数据更新方法,给LiveData回调逻辑使用:
fun updateFullData(newData: List<Articolo>) { fullList = newData }
- 在adapter类中声明私有全量列表缓存,避免跨线程访问异常:
实现符合Kotlin空安全规则的自定义Filter
- 所有过滤逻辑基于独立维护的
fullList执行,从根源上避免空值问题:private val itemFilter = object : Filter() { // performFiltering在子线程执行,所有数据匹配逻辑都放在这里 override fun performFiltering(constraint: CharSequence?): FilterResults { val query = constraint?.toString()?.trim().orEmpty() // 关键词为空时直接返回全量列表 val filteredResult = if (query.isEmpty()) { fullList } else { // 匹配条码、描述字段,过滤时生成新的列表实例,不修改原列表 fullList.filter { it.barcode.contains(query, ignoreCase = true) || it.description.contains(query, ignoreCase = true) } } return FilterResults().apply { values = filteredResult count = filteredResult.size } } // publishResults切回主线程执行,处理UI刷新逻辑 override fun publishResults(constraint: CharSequence?, results: FilterResults?) { // 做空值兜底,避免强转类型异常 val finalList = (results?.values as? List<Articolo>) ?: emptyList() // 传入全新的ArrayList实例,解决引用校验导致的刷新失效问题 submitList(ArrayList(finalList)) } } - 重写
getFilter方法对外暴露这个Filter实例:override fun getFilter(): Filter = itemFilter
为什么一定要传全新的ArrayList实例?因为ListAdapter内部的DiffUtil校验逻辑,如果检测到传入的列表和当前adapter持有的列表是同一个内存引用,会直接判定内容无变化,跳过整个差异计算和刷新流程,这也是很多开发者过滤后列表毫无反应的核心原因。
- 所有过滤逻辑基于独立维护的
解决和LiveData绑定的逻辑冲突
- 不要在LiveData观察回调里直接把全量数据传给
submitList,而是先更新adapter里的全量缓存,再用当前的搜索关键词重新触发一次过滤,避免数据更新覆盖之前的过滤结果:// Fragment/Activity里的LiveData观察逻辑 viewModel.articoliList.observe(viewLifecycleOwner) { fullData -> adapter.updateFullData(fullData) // 读取当前SearchView的实时输入值,重新触发过滤 adapter.filter.filter(searchView.query) }
- 不要在LiveData观察回调里直接把全量数据传给
SearchView监听逻辑做最小化处理
- 不要在搜索监听里手动操作列表,直接把输入的关键词透传给adapter的filter即可:
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { adapter.filter.filter(query) return true } override fun onQueryTextChange(newText: String?): Boolean { adapter.filter.filter(newText) return true } })
- 不要在搜索监听里手动操作列表,直接把输入的关键词透传给adapter的filter即可:
高频踩坑提醒
- 不要在
performFiltering里调用任何和UI相关的方法,包括submitList,这个方法运行在子线程,所有列表更新操作必须放在主线程的publishResults里执行 - 永远不要修改
fullList的内容,这个列表的作用是存储从数据库拉取的原始全量数据,所有过滤结果都是基于它生成的临时列表,不要把过滤后的结果反向赋值给fullList - 不要为了强制刷新调用
notifyDataSetChanged,只要给submitList传入全新的列表实例,ListAdapter的DiffUtil会自动计算item差异,做高效的局部刷新
内容的提问来源于stack exchange,提问作者NiceToMytyuk
相关产品推荐
相关产品推荐

