Search code examples
androidkotlinandroid-recyclerviewimageviewandroid-databinding

Android Kotlin ImageView inside RecyclerView.Adapter doesn't update as expected


After finally implementing icons for device types in my server, I replaced the temporary drawables I previously used with the icons from the server and load them using coil. The ImageView for them is inside an adapter btw. On first draw, everything is "aligned" properly, the labels and images are the right pairing. But if I do some sort of sorting, the images in the ImageViews doesn't align with the labels anymore or they're completely gone. I have a behavior in my app that first I'll get and display data from the server and if the local data is equal to the server's, then I'd just use the local data. But I don't think that should affect anything as I'm saving every field of the server data on the database. I also added some logging to see what URL should be retrieved by coil, and they are the correct URL for each icon. I call notifyDatasetChanged() to redraw the adapters because the sorting might return a different length of items than the default list, which is all the items I have.

Here's my adapter's code:

class ActuatorDeviceInfoAdapter(
    val ctx: FragmentActivity,
    var itemLst: MutableList<ActuatorDeviceInfo>,
    val showToggle: Boolean
) : RecyclerView.Adapter<ActuatorDeviceInfoAdapter.ViewHolder>() {
    private lateinit var binding: AdapterActuatorBinding
    var onItemClick: ((ActuatorDeviceInfo) -> Unit)? = null
    var onToggleClick: ((ActuatorDeviceInfo, Boolean, AdapterActuatorBinding) -> Unit)? = null

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): ActuatorDeviceInfoAdapter.ViewHolder {
        binding = AdapterActuatorBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ActuatorDeviceInfoAdapter.ViewHolder, position: Int) {
        val pic = itemLst[position]
        holder.bind(pic)
    }

    override fun getItemCount(): Int = itemLst.size

    inner class ViewHolder(private val binding: AdapterActuatorBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(actuatorDeviceInfo: ActuatorDeviceInfo) {
            binding.actuatorDevInfo = actuatorDeviceInfo

            if (showToggle) {
                binding.tglStatus.visibility = View.VISIBLE
                binding.tglStatus.isChecked = actuatorDeviceInfo.status
            } else {
                binding.tglStatus.visibility = View.GONE
            }

            setIcon(actuatorDeviceInfo.type!!)
        }

        init {
            itemView.setOnClickListener {
                onItemClick?.invoke(itemLst[adapterPosition])
            }
            binding.tglStatus.setOnClickListener {
                onToggleClick?.invoke(itemLst[adapterPosition], binding.tglStatus.isChecked, binding)
            }
        }
    }

    private fun setIcon(type: ActuatorType) {
        val iconPath = "${Utils.instance.getServerHost(ctx)}${type.iconPath}"
        binding.imgIcon.load(iconPath)
    }

    companion object{
        const val TAG = "ActuatorDeviceInfoAdapter"
    }
}

Solution

  • The problem is in the private function setIcon. You need to update each Item's view with each ActuatorDeviceInfo. You need to use the Binding of Item's view for setting the data and image.

    For statements inside bind() uses the each item's binding. But setIcon uses some other binding that has the last initialized view which it should not as per your requirement.

    Solution

    1. Remove the variable private lateinit var binding: AdapterActuatorBinding.
    2. Declare the local variable binding in onCreateViewHolder function as val binding = .
    3. Move the private setIcon function to ViewHolder class.

    Above 1,2 steps are optional.

    Result

    class ActuatorDeviceInfoAdapter(
        val ctx: FragmentActivity,
        var itemLst: MutableList<ActuatorDeviceInfo>,
        val showToggle: Boolean
    ) : RecyclerView.Adapter<ActuatorDeviceInfoAdapter.ViewHolder>() {
        // 1. Removed
        // private lateinit var binding: AdapterActuatorBinding
        var onItemClick: ((ActuatorDeviceInfo) -> Unit)? = null
        var onToggleClick: ((ActuatorDeviceInfo, Boolean, AdapterActuatorBinding) -> Unit)? = null
    
        override fun onCreateViewHolder(
            parent: ViewGroup,
            viewType: Int
        ): ActuatorDeviceInfoAdapter.ViewHolder {
            // 2. Declared
            val binding = AdapterActuatorBinding.inflate(LayoutInflater.from(parent.context), parent, false)
            return ViewHolder(binding)
        }
    
        override fun onBindViewHolder(holder: ActuatorDeviceInfoAdapter.ViewHolder, position: Int) {
            val pic = itemLst[position]
            holder.bind(pic)
        }
    
        override fun getItemCount(): Int = itemLst.size
    
        inner class ViewHolder(private val binding: AdapterActuatorBinding) :
            RecyclerView.ViewHolder(binding.root) {
    
            fun bind(actuatorDeviceInfo: ActuatorDeviceInfo) {
                binding.actuatorDevInfo = actuatorDeviceInfo
    
                if (showToggle) {
                    binding.tglStatus.visibility = View.VISIBLE
                    binding.tglStatus.isChecked = actuatorDeviceInfo.status
                } else {
                    binding.tglStatus.visibility = View.GONE
                }
    
                setIcon(actuatorDeviceInfo.type!!)
            }
            // 3. Moved
            private fun setIcon(type: ActuatorType) {
                val iconPath = "${Utils.instance.getServerHost(ctx)}${type.iconPath}"
                binding.imgIcon.load(iconPath)
            }
    
            init {
                itemView.setOnClickListener {
                    onItemClick?.invoke(itemLst[adapterPosition])
                }
                binding.tglStatus.setOnClickListener {
                    onToggleClick?.invoke(itemLst[adapterPosition], binding.tglStatus.isChecked, binding)
                }
            }
        }
    
        
    
        companion object{
            const val TAG = "ActuatorDeviceInfoAdapter"
        }
    }
    

    You can avoid these kinds of issues by following best practices.

    • Don't declare the same variable name in nested blocks or inner classes (Variable shadowing)