androidkotlinandroid-recyclerviewsearchview

Positions getting messed up after filtering RecyclerView


So here is my issue:

(Items that I display are houses)

HouseAdapter (recyclerview adapter) :

class HouseAdapter(private var recyclerViewInterface: RecyclerViewInterface) : RecyclerView.Adapter<HouseAdapter.HouseViewHolder>() {

    inner class HouseViewHolder(val binding: ItemHouseBinding) : RecyclerView.ViewHolder(binding.root){

        private val formatter : NumberFormat = DecimalFormat("$#,###")


        fun bind(house: House) {
            binding.tvPrice.text = formatter.format(house.price)
            "${house.zip} ${house.city}".also { binding.tvAddress.text = it }
            binding.tvBed.text = house.bedrooms.toString()
            binding.tvBath.text = house.bathrooms.toString()
            binding.tvLayers.text = house.size.toString()

            val houseImageUrl : String = BASE_URL + house.image
            Glide.with(binding.root.context).load(houseImageUrl).centerCrop().into(binding.ivHouse)
        }


    }

    private val diffCallBack = object : DiffUtil.ItemCallback<House>(){

        // Call to check whether two items represent the same item
        override fun areItemsTheSame(oldItem: House, newItem: House): Boolean {
            return oldItem.id == newItem.id
        }

        // Call to check whether two items have the same data
        override fun areContentsTheSame(oldItem: House, newItem: House): Boolean {
            return oldItem == newItem
        }

    }

    private val differ = AsyncListDiffer(this, diffCallBack)

    var houses: List<House>
        get() = differ.currentList
        set(value) {differ.submitList(value)}


    override fun getItemCount(): Int {
        return houses.size
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HouseViewHolder {
        return HouseViewHolder(ItemHouseBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        ))
    }


    override fun onBindViewHolder(holder: HouseViewHolder, position: Int) {
        println("OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO : $position")

        holder.bind(houses[position])

        holder.binding.vBody.setOnClickListener {
            recyclerViewInterface.onItemClick(position)
        }
    }


    fun setFilteredList(filteredList: List<House>){
        this.houses = filteredList

    }

}

HomeFragment (containing the recycler view) :

class HomeFragment : Fragment(), RecyclerViewInterface {

    private var _binding: FragmentHomeBinding? = null
    private val binding get() = _binding!!

    private lateinit var container : ViewGroup

    private lateinit var houseAdapter: HouseAdapter
    private lateinit var listHouses: ArrayList<House>
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentHomeBinding.inflate(inflater, container, false)
        if (container != null) {
            this.container = container
        }

        setupRecyclerView()

        lifecycleScope.launchWhenCreated {
            val response = try {
                RetrofitInstance.api.getHouses(API_KEY)
            } catch (e: IOException) {
                Log.e(TAG, "IOException, you might not have internet connection")
                return@launchWhenCreated
            } catch (e: HttpException){
                Log.e(TAG, "HttpException, unexpected response ")
                return@launchWhenCreated
            }
            if (response.isSuccessful && response.body() != null){
                listHouses = (response.body() as ArrayList<House>?)!!

                houseAdapter.houses = listHouses

                listHouses.sortWith(compareBy {it.price})
                listHouses.forEach {println(it.price) }


            }else{
                Log.e(TAG, "Response not successful")
            }
        }

        setupSearchView()

        return binding.root
    }
    // Called on fragment launch - Add OnQueryTextListener to the SearchView
    private fun setupSearchView(){
        binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener,
            androidx.appcompat.widget.SearchView.OnQueryTextListener {
            override fun onQueryTextSubmit(query: String?): Boolean {
                return false
            }

            override fun onQueryTextChange(newText: String): Boolean {
                filterList(newText)
                return true
            }

        })
    }

    // Called when text from SearchView is changed - Create new filtered list or display empty information
    private fun filterList(text: String) {
        val filteredList = arrayListOf<House>()

        listHouses.forEach { house : House ->
            if (house.city.contains(text) || house.zip.contains(text)){
                filteredList.add(house)
            }
        }

        if (filteredList.isEmpty()){
            binding.rvHouses.visibility = View.INVISIBLE
            binding.ivSearchEmpty.visibility = View.VISIBLE
            binding.tvSearchEmpty1.visibility = View.VISIBLE
            binding.tvSearchEmpty2.visibility = View.VISIBLE
        }else{
            houseAdapter.setFilteredList(filteredList)
            binding.rvHouses.visibility = View.VISIBLE
            binding.ivSearchEmpty.visibility = View.INVISIBLE
            binding.tvSearchEmpty1.visibility = View.INVISIBLE
            binding.tvSearchEmpty2.visibility = View.INVISIBLE
        }

    }

    // Called on fragment launch - Setup the RecyclerView with the HouseAdapter
    private fun setupRecyclerView() = binding.rvHouses.apply {
        houseAdapter = HouseAdapter(this@HomeFragment)
        adapter = houseAdapter
        layoutManager = LinearLayoutManager(context)
    }


    // Called when user click on House item - Pass properties from the chosen house to DetailActivity
    override fun onItemClick(position: Int) {
        val i = Intent(this.context, DetailActivity::class.java)
        println("OOOOOOOOOOOOOOOOOOOOOOOOOOOOO : $position")
        i.putExtra("PRICE", listHouses[position].price)
        i.putExtra("IMAGE_URL", listHouses[position].image)
        i.putExtra("BEDS", listHouses[position].bedrooms)
        i.putExtra("BATH", listHouses[position].bathrooms)
        i.putExtra("SIZE", listHouses[position].size)
        i.putExtra("LAT", listHouses[position].latitude)
        i.putExtra("LONG", listHouses[position].longitude)
        i.putExtra("DESC", listHouses[position].description)
        startActivity(i)
    }

}

(I used the println("OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO : $position") to keep track of the position of the items at the bindviewholder function of the recylcerview and when i click on an item to see if it was corresponding... The answer is : "it depends" sometimes yes sometimes no. I tried to find some logic to it but i am unsure of my theory : I think that when i filter the recyclerview without seeing all the items before doing so, the filtered list does not know what position give to the items that were "unseen".

If you have any other advice besides my issue I'd be happy to hear it (As I said I am currently learning kotlin)


Solution

  • Check out positions in RecyclerView.

    There are two kinds, layout and adapter. You want to use the adapter one. So change your onBindViewHolder function to use getAbsoluteAdapterPosition:

    holder.binding.vBody.setOnClickListener {
        recyclerViewInterface.onItemClick(holder.absoluteAdapterPosition) // not position
    }