Firestore实时数据下RecyclerView过滤异常问题求助
Firestore动态数据下RecyclerView过滤异常问题
我尝试使用Filterable对RecyclerView进行过滤,静态数据下功能正常,但在处理Firestore动态数据时出现问题:从Firestore获取数据存入ArrayList后,将其过滤为新列表;但当Firestore数据发生更新时,原始ArrayList会被更新,RecyclerView会显示原始数据而非当前SearchView过滤后的结果。我希望无论Firestore数据是否更新,RecyclerView始终显示SearchView输入内容对应的过滤结果。
SearchAdapter代码
class SearchAdapter() : RecyclerView.Adapter<SearchAdapter.ListViewHolder>(), Filterable { private var listSearch = ArrayList<Dosen>() private var listSearchFull = ArrayList<Dosen>() fun setData(list: ArrayList<Dosen>): { this.listSearch = list listSearchFull = ArrayList(listSearch) } inner class ListViewHolder(itemView: UserListBinding) : RecyclerView.ViewHolder(itemView.root) { private val binding = itemView fun bind(dosen: Dosen) { with(binding){ val db = Firebase.firestore val collection = db.collection("alat") .whereEqualTo("id", dosen.alat_id) collection.get() .addOnSuccessListener { document -> try { val location = document.toObjects(Alat::class.java)[0].lokasi tvLocation.text = location } catch (e: Exception){ Log.d("rv", "system error $e") } } .addOnFailureListener { exception -> Log.d("rv", "get failed with ", exception) } tvUsername.text = dosen.nama val simpleDateFormat = SimpleDateFormat("EEEE, dd LLLL yyyy") val date = simpleDateFormat.format(dosen.datetime!!.toDate()) Log.d("rvTime", date) tvDate.text = date val simpleTimeFormat = SimpleDateFormat("KK:mm:ss aaa") val time = simpleTimeFormat.format(dosen.datetime.toDate()) Log.d("rvTime", time) tvTime.text = time } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder { return ListViewHolder(UserListBinding.inflate(LayoutInflater.from(parent.context), parent, false)) } override fun onBindViewHolder(holder: ListViewHolder, position: Int) { holder.bind(listSearch[position]) } override fun getItemCount(): Int = listSearch.size override fun getFilter(): Filter { return object : Filter(){ override fun performFiltering(query: CharSequence?): FilterResults { val filteredList: ArrayList<Dosen> = ArrayList() if (query == null || query.length == 0){ filteredList.addAll(listSearchFull) } else{ val searchQuery = query.toString().toLowerCase().trim() for (item in listSearchFull) { if (item.nama!!.lowercase(Locale.ROOT).contains(searchQuery)){ filteredList.add(item) } } } val filterResults = FilterResults() filterResults.values = filteredList return filterResults } override fun publishResults(query: CharSequence?, filteredResult: FilterResults?) { listSearch.clear() listSearch.addAll(filteredResult!!.values as ArrayList<Dosen>) notifyDataSetChanged() } } } }
SearchFragment代码
class SearchFragment : Fragment() { private var _binding: FragmentSearchBinding? = null private val binding get() = _binding!! private lateinit var searchAdapter: SearchAdapter private lateinit var searchArrayList : ArrayList<Dosen> override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { _binding = FragmentSearchBinding.inflate(inflater, container, false) val root: View = binding.root searchArrayList = arrayListOf() @Suppress("DEPRECATION") setHasOptionsMenu(true) eventChangeListener() return root } private fun eventChangeListener() { val db = Firebase.firestore db.collection("presensi") .orderBy("datetime", Query.Direction.DESCENDING) .addSnapshotListener(object : EventListener<QuerySnapshot>{ override fun onEvent(value: QuerySnapshot?, error: FirebaseFirestoreException?) { searchArrayList.clear() for (document in value!!){ searchArrayList.add(document.toObject(Dosen::class.java)) searchAdapter.setData(searchArrayList) Log.d("arraySearch", document.toObject<Dosen>().toString()) Log.d("arraySearchList", searchArrayList.toString()) } searchAdapter.notifyDataSetChanged() } }) searchAdapter = SearchAdapter() with(binding){ tvNoData.visibility = View.GONE rvSearch.layoutManager = LinearLayoutManager(activity) rvSearch.setHasFixedSize(true) rvSearch.adapter = searchAdapter } } @Deprecated("Deprecated in Java") override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @Suppress("DEPRECATION") super.onCreateOptionsMenu(menu, inflater) menu.clear() inflater.inflate(R.menu.main_menu, menu) val searchView = context?.let { SearchView(it) } menu.findItem(R.id.menu_search).apply { setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW or MenuItem.SHOW_AS_ACTION_IF_ROOM) actionView = searchView } searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { return false } override fun onQueryTextChange(newText: String): Boolean { searchAdapter.filter.filter(newText) return false } }) } override fun onStart() { super.onStart() Log.d("firebaseFirestoreListener", "onStart") } override fun onDestroyView() { super.onDestroyView() _binding = null Log.d("firebaseFirestoreListener", "onDestroyView") } }
解决方案
该需求完全可以实现,不需要依赖Algolia这类第三方付费库,问题核心在于数据更新逻辑与Adapter状态维护的冲突,以下是具体修复步骤:
1. 修正Adapter的状态维护逻辑
当前setData方法直接覆盖显示列表并重置全量数据,导致Firestore更新时丢失过滤状态。修改如下:
class SearchAdapter() : RecyclerView.Adapter<SearchAdapter.ListViewHolder>(), Filterable { private var listSearch = ArrayList<Dosen>() private var listSearchFull = ArrayList<Dosen>() // 新增变量保存当前搜索关键词 private var currentSearchQuery: String? = null fun setData(list: ArrayList<Dosen>) { // 只更新全量数据副本 listSearchFull.clear() listSearchFull.addAll(list) // 根据当前搜索关键词重新过滤,保留过滤状态 filter.filter(currentSearchQuery ?: "") } override fun getFilter(): Filter { return object : Filter(){ override fun performFiltering(query: CharSequence?): FilterResults { // 原有过滤逻辑不变 val filteredList: ArrayList<Dosen> = ArrayList() if (query == null || query.length == 0){ filteredList.addAll(listSearchFull) } else{ val searchQuery = query.toString().toLowerCase().trim() for (item in listSearchFull) { if (item.nama!!.lowercase(Locale.ROOT).contains(searchQuery)){ filteredList.add(item) } } } val filterResults = FilterResults() filterResults.values = filteredList return filterResults } override fun publishResults(query: CharSequence?, filteredResult: FilterResults?) { // 更新当前搜索关键词 currentSearchQuery = query?.toString() listSearch.clear() listSearch.addAll(filteredResult!!.values as ArrayList<Dosen>) notifyDataSetChanged() } } } // 其余代码不变 }
2. 修复Firestore快照监听的执行顺序
当前代码先设置监听再初始化Adapter,会导致空指针;同时循环内多次调用setData会重复触发更新,修正如下:
private fun eventChangeListener() { // 先初始化Adapter并绑定到RecyclerView searchAdapter = SearchAdapter() with(binding){ tvNoData.visibility = View.GONE rvSearch.layoutManager = LinearLayoutManager(activity) rvSearch.setHasFixedSize(true) rvSearch.adapter = searchAdapter } // 再设置Firestore快照监听 val db = Firebase.firestore db.collection("presensi") .orderBy("datetime", Query.Direction.DESCENDING) .addSnapshotListener(object : EventListener<QuerySnapshot>{ override fun onEvent(value: QuerySnapshot?, error: FirebaseFirestoreException?) { searchArrayList.clear() for (document in value!!){ searchArrayList.add(document.toObject(Dosen::class.java)) } // 循环结束后统一更新Adapter数据 searchAdapter.setData(searchArrayList) } }) }
3. 优化SearchView的监听逻辑
确保搜索输入能正确触发过滤,同时处理提交事件:
searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { searchAdapter.filter.filter(query) searchView?.clearFocus() return true } override fun onQueryTextChange(newText: String): Boolean { searchAdapter.filter.filter(newText) return true } })
额外优化建议
- 将ViewHolder中获取
alat数据的逻辑移到数据加载阶段,避免每次bind都发起Firestore请求,减少性能消耗 - 使用
DiffUtil替代notifyDataSetChanged(),提升RecyclerView的更新效率
内容的提问来源于stack exchange,提问作者I GustiRanda
相关产品推荐
相关产品推荐

