0

I am trying to set a background color for 1 item in my recyclerview when I click on a button. I get the index of the particular item and try to use a function that includes notifyItemChange in the onclick method of my button in Activity. But it keeps changing the color of the first item in the recyclerview even if I hard code an index number into the function. I know that I am getting the correct index of the selected item, but I'm missing something. Perhaps some code in the adapter? I cannot glean enough information from other answers to figure out how this should be done. Any help? Thanks!

Variable for selectedPosition in Activity:

private var newSelectedPosition: Int = 0

Button onClick in Activity:

R.id.btn_add_daily_report_submit -> {
             Log.e("tag", "$newSelectedPosition")
             updateSingleItem(newSelectedPosition)
            }

Function updateSingleItem() in Activity:

private fun updateSingleItem(index: Int) {
    Log.e("updateSingleItem", "$newSelectedPosition")
    card_view.setBackgroundColor(resources.getColor(R.color.green))
    adapter.notifyItemChanged(index)
}

setOnClickListener from adapter interface to record the selected item's index in Activity:

        adapter.setOnClickListener(object :
        DailyReportsAdapter.OnClickListener {
        override fun onClick(position: Int, user: User) {
            newSelectedPosition = position
            Log.e("tag", "$newSelectedPosition")

            et_daily_report_employee.text = getString(
                R.string.tv_name,
                user.firstName,
                user.lastName
            )
        }
    })

Adapter class:

open class DailyReportsAdapter(
private val context: Context,
private var list: ArrayList<User>

) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

// A global variable for OnClickListener interface.
private var onClickListener: OnClickListener? = null
private var selectedItemPosition: Int = 0

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    return MyViewHolder(
        LayoutInflater.from(context).inflate(
            R.layout.item_daily_report_layout,
            parent,
            false
        )
    )
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, @SuppressLint("RecyclerView") position: Int) {
    val model = list[position]
    if (holder is MyViewHolder) {
        holder.itemView.employee_name.text = "${model.firstName} ${model.lastName}"
      j

// Assign the on click event for item view and pass the required params in the on click function. holder.itemView.setOnClickListener { if (onClickListener != null) { onClickListener!!.onClick(position, model) } } } }

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

fun setOnClickListener(onClickListener: OnClickListener) {
    this.onClickListener = onClickListener
}

/**
 * An interface for onclick items.
 */
interface OnClickListener {
    fun onClick(position: Int, user: User)
}

class MyViewHolder(view: View) : RecyclerView.ViewHolder(view)

}

tommybomb
  • 53
  • 9
  • I can't see anywhere you're changing the colour, except for changing something called ``cardView`` in the Activity? You need to do this kind of thing in ``onBindViewHolder``, where you're displaying the data for a particular item - part of that data is whether the current item is the selected one or not, and you can set the background colour (or whatever) of the ``ViewHolder`` appropriately – cactustictacs May 07 '22 at 18:29
  • Thank you for the response. cardview is the top level view of the item row layout. And yes, I'm thinking it needs to be done in the adapter. The thing is, I don't want it to change when the item is selected, which I know how to do. I want the color to change when a button in Activity is clicked. And that's where I'm stuck. I can't figure out how to code this properly. – tommybomb May 07 '22 at 21:34
  • Can't you just put some ``updateSelected`` function in the adapter, and make the button call that? Let the adapter keep track of which position needs to be highlighted, which one needs to be highlighted next but not yet, etc. Then ``onBind`` can highlight if the right conditions are met. You can put your ``notifyItemChanged`` call in that adapter function too - it's better if the adapter handles that kind of thing, it has a better idea of what needs updating (and how, and when) than the activity does – cactustictacs May 07 '22 at 22:00
  • OK. so I tried putting an updateSelected function in the adapter. If I put it outside of the onBindViewHolder, I can't access the holder parameter and therefore am unable to apply setBackgroundColor to my layout cardView. But having it in onBindViewHolder I can't figure out how to access the function from my Activity. What am I missing? Thanks! – tommybomb May 08 '22 at 01:34
  • Just pass the selected position value to ``updateSelected``, and store it in the adapter. Then in ``onBindViewHolder``, where you have a ``position`` value for the item you're binding, you can just check if it matches the selected position you have stored, and set the ``ViewHolder``'s background appropriately (don't forget to set it to "normal" if that item *isn't* the selected one) – cactustictacs May 08 '22 at 02:04
  • And I call that function from my Activity correct? Where do I put notifyItemChanged? – tommybomb May 08 '22 at 03:28
  • I've added an answer to show you the basic idea, but looks like you beat me to it! – cactustictacs May 08 '22 at 18:45
  • Thanks so much cactustictacs! I really appreciate your guidance on this! – tommybomb May 08 '22 at 18:52

2 Answers2

1

You need to separate out the Adapter (a thing which takes data and controls how it's displayed) from the rest of the UI (e.g. the Activity hosting it). If there's a button in your UI that means "update a thing in the adapter", really all the Activity or Fragment should be doing is telling the adapter "hey, here's a thing, do what you need to do"

It's not a requirement, but separating the concerns like this means the adapter can be self-contained, and the things that interact with the adapter don't need to know how it works, don't need to poke around at it internally (e.g. calling notifyItemChanged) or anything like that. By declaring visible (non-private) methods on the adapter, you can define its interface, how other components are meant to interact with it. Then you can handle all the logic of how that's supposed to work inside the method and the adapter itself


So I don't know how your app works, but this is just an example so you can work out a way to adapt it for what you actually need. I'm gonna imagine you have a list, with items you can select by tapping on them, but there's a button in the activity you need to press to actually highlight the last one tapped

In the Activity, onCreate or whatever

button.setOnClickListener {
    adapter.highlightLastTapped()
}

In the Adapter

// making this nullable for a "nothing selected" value, and it's easy to work with
// using Kotlin's null-checking features
private var lastTappedPosition: Int? = null

// check this in ``onBindViewHolder`` to see if that item should be highlighted
private var currentlyHighlightedPosition: Int? = null

// internal function you can call from a click listener on an item
private fun setLastTappedPosition(position: Int) {
    lastTappedPosition = position

    // If something was previously highlighted, we can update it right here.
    // This is an example of the kind of logic you can do if you put the adapter
    // in charge of its own state, instead of calling notifyItemChanged etc from outside
    val previousHighlight = currentlyHighlightedPosition
    currentlyHighlightedPosition = null
    previousHighlight?.let { 
        notifyItemChanged(it)
    }
}

// externally visible function - this is just "highlight whatever you need to"
// but you could pass in a parameter if you want. Keep the logic in the adapter though!
fun highlightLastTapped() {
    lastTappedPosition?.let {
        currentlyHighlightedPosition = it
        notifyItemChanged(it)
    }
}

That's just a basic sketch of what you could do, but I hope you get the idea. Treat the adapter as its own, independent class that manages its own state and logic, and let other components talk to it in a limited way, giving it just the data and events it needs

cactustictacs
  • 17,935
  • 2
  • 14
  • 25
0

Here is my solution. I still need to persist the selected items with sharedPrefs, but it works as is! Thanks @cactustictacs

My Adapter class:

open class DailyReportsAdapter(private val context: Context, private var list: ArrayList<User>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

private var onClickListener: OnClickListener? = null
private var selectedItemPosition: Int? = null

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    return MyViewHolder(
        LayoutInflater.from(context).inflate(
            R.layout.item_daily_report_layout,
            parent,
            false
        )
    )
}


override fun onBindViewHolder(
    holder: RecyclerView.ViewHolder,
    @SuppressLint("RecyclerView") position: Int
) {
    val model = list[position]
    if (holder is MyViewHolder) {
        holder.itemView.employee_name.text = "${model.firstName} ${model.lastName}"


        if (selectedItemPosition == position) {
            holder.itemView.card_view.setBackgroundColor(Color.parseColor("#8BC34A"))

        } else {
            holder.itemView.card_view.setBackgroundColor(Color.parseColor("#ffffff"))

        }


        holder.itemView.setOnClickListener {
            if (onClickListener != null) {
                onClickListener!!.onClick(position, model)
            }
        }
    }
}

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

/**
 * A function for OnClickListener where the Interface is the expected parameter and assigned to the global variable.
 *
 * @param onClickListener
 */
fun setOnClickListener(onClickListener: OnClickListener) {
    this.onClickListener = onClickListener
}

/**
 * An interface for onclick items.
 */
interface OnClickListener {

    // Define a function to get the required params when user clicks on the item view in the interface.
    fun onClick(position: Int, user: User)
}

fun updateSelected(index: Int): Int {
    Log.e("adapter", "updateSelected() executed")
    selectedItemPosition = index
    return selectedItemPosition as Int
}

/**
 * A ViewHolder describes an item view and metadata about its place within the RecyclerView.
 */
class MyViewHolder(view: View) : RecyclerView.ViewHolder(view)

}

And in my Activity I used the following code:

Global variable:

var selectedItemPositionFromActivity: Int = 0

Button Click in Activity:

            R.id.btn_add_daily_report_submit -> {
                Log.e("btnclick", "$selectedItemPositionFromActivity")
                if (validateDailyReportDetails()) {
                    uploadDailyReportDetails()
                adapter.updateSelected(selectedItemPositionFromActivity)
                    adapter.notifyItemChanged(selectedItemPositionFromActivity)
                    
                }
            }

ItemClick from interface in adapter where I record the position of the selected item. This is also in Activity:

//Define the onclick listener here that is defined in the adapter class and handle the click on an item in the base class.
    adapter.setOnClickListener(object :
        DailyReportsAdapter.OnClickListener {
        override fun onClick(position: Int, user: User) {
            selectedItemPositionFromActivity = position
        }
    })
tommybomb
  • 53
  • 9