You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 19:31:22