Home > database >  Managing multiple timers/handlers in recyclerview
Managing multiple timers/handlers in recyclerview

Time:03-30

I have a recycler view where I list different jobs and each job has a progress bar, because you can do multiple jobs at a time. I'm currently using a handler to reduce my progress bar, but it only works for one job at a time. I want it to be tied to a position rather than the full recycler view so I can have multiple handlers running at a time. How do I update my adapter so that the handler (or another timer if it would work better) is specific to the [position] rather than the full recycler view?

class JobAdapter(
    private val context: Context,
    private val dataset: List<JobList>
) : RecyclerView.Adapter<JobAdapter.ItemViewHolder>() {

    class ItemViewHolder(private val view: View): RecyclerView.ViewHolder(view){
        val tvJobName = view.findViewById<TextView>(R.id.tvJobName)
        val tvJobTime = view.findViewById<TextView>(R.id.tvJobTime)
        val tvJobReward = view.findViewById<TextView>(R.id.tvJobReward)
        val pbJobTime = view.findViewById<ProgressBar>(R.id.pbJobTime)
        val btnStartJob = view.findViewById<Button>(R.id.btnStartJob)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        val jobLayout = LayoutInflater.from(parent.context)
            .inflate(R.layout.job_layout, parent, false)
        return ItemViewHolder(jobLayout)
    }

    var currentProgress: Int = 0
    var counter: Int=0
    var isTriggered: Boolean = false

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {

        val item = dataset[position]
        holder.tvJobName.text = item.dcJobName
        holder.tvJobTime.text = item.dcJobTime.toString() "s"
        holder.tvJobReward.text = "$" item.dcJobReward.toString()

        val maxAllowable=25
        val mills = (1000/maxAllowable)*item.dcJobTime

        holder.btnStartJob.setOnClickListener{

            val jobHandler = Handler()
            val jobRunnable: Runnable = object : Runnable {
                override fun run() {
                    counter  
                    if (counter<=maxAllowable){
                        currentProgress-=100/maxAllowable
                        holder.pbJobTime.progress = currentProgress
                        jobHandler.postDelayed(this, mills.toLong())
                    } else {
                        counter = 0
                        isTriggered=false
                    }
                }
            }
            if (!isTriggered) {
                currentProgress=100
                holder.pbJobTime.progress = currentProgress
                isTriggered=true
                jobHandler.post(jobRunnable)
            }

        }
    }

    override fun getItemCount() = dataset.size }

CodePudding user response:

The point of a RecyclerView is that you have a handful of ViewHolders that get reused (recycled) to display all the items in the list. So at any given time, a particular VH could be representing any arbitrary item. But your runnable (which is tied to a specific item) is updating a specific ViewHolder - so even if it's not actually displaying that item, it will update with that item's progress.

So you're looking for a way to get around that, right? You could try and find out if a particular VH is displaying that particular item, and update it if so - but that's extremely messy, could get pretty complicated (you're not cancelling existing runnables right?) and you could get some awkward bugs happening.


Personally I'd recommend breaking this down into a few separate parts:

  • your data and basic recycler view
  • something that holds running items and their current progress states
  • something like your Runnable that "ticks" every so often, updates the progress states, and tells the RecyclerView to update and display the current progress

The "current progress states" could just be a map of Item -> Progress. When you start a job, add that item and its initial progress to the map (this will clear any current progress, you could work around that if it's not what you want). Start the Handler if it's not already running

The Handler's runnable would just iterate over the map, update the progress for each item, remove it from the map if it's expired, and call notifyItemChanged on the Adapter (so it gets redrawn with the current progress). Ideally the Handler would stop if the map is empty (i.e. no jobs to keep updating).

Your display code in onBindViewHolder could just check that map, see if the item it's displaying has a progress value, and display that if it does, otherwise show the "no running job" state (remember you need to update everything since the ViewHolder might have been displaying some other state before)


I hope that makes sense - having a single Runnable task ticking every so often and updating the current state is way easier to manage than multiple Runnables doing their own thing. Having a single source of data about your jobs and their state makes it way easier to update them (and do things like persist that state). And having the RecyclerView update its own display when necessary, by referring to a source of data, makes it way easier to manage that displayed data, and not worry about internal details like what ViewHolder objects are visible and which item they currently represent

  • Related