Reputation: 2203
In my application, I want to use multiple CountDownTimer
to show the time remaining on offers in a RecyclerView
. I have written the code below in Kotlin, but while scrolling the timers keep restarting. For example, the timer starts at 4:19 and while scrolling instead of showing 4:09 after 10 seconds still shows 4:19.
Activity code:
class MainActivity : AppCompatActivity() {
private lateinit var apisList: ApisList
private lateinit var retrofit: Retrofit
private lateinit var todayAdapter: AuctionsTodayAdapter
private val todayModel: MutableList<Today> = mutableListOf()
private lateinit var layoutManager: RecyclerView.LayoutManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//Initialize
retrofit = ApiClient.instance
apisList = retrofit.create(ApisList::class.java)
todayAdapter = AuctionsTodayAdapter(themedContext, todayModel)
layoutManager = LinearLayoutManager(themedContext)
//RecyclerView
main_list.setHasFixedSize(true)
main_list.layoutManager = layoutManager
main_list.adapter = todayAdapter
if (isNetworkAvailable()) getData(1, 10)
}
private fun getData(page: Int, limit: Int) {
main_loader.visibility = View.VISIBLE
val call = apisList.getAuctionsToday(page, limit)
call.let {
it.enqueue(object : Callback<AuctionsTodayResponse> {
override fun onFailure(call: Call<AuctionsTodayResponse>, t: Throwable) {
main_loader.visibility = View.GONE
Log.e("auctionsTodayList", t.message)
}
override fun onResponse(call: Call<AuctionsTodayResponse>, response: Response<AuctionsTodayResponse>) {
if (response.isSuccessful) {
response.body()?.let { itBody ->
main_loader.visibility = View.GONE
if (itBody.toString().isNotEmpty()) {
todayModel.clear()
todayModel.addAll(itBody.res.today)
todayAdapter.notifyDataSetChanged()
}
}
}
}
})
}
}
}
Adapter code:
class AuctionsTodayAdapter(val context: Context, val model: MutableList<Today>) :
RecyclerView.Adapter<AuctionsTodayAdapter.MyHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyHolder {
val view = LayoutInflater.from(context).inflate(R.layout.row_main_list, parent, false)
val holder = MyHolder(view)
//holder.setIsRecyclable(false)
return holder
}
override fun getItemCount(): Int {
return model.size
}
override fun onBindViewHolder(holder: MyHolder, position: Int) {
val modelUse = model[position]
holder.setData(modelUse)
if (holder.newCountDownTimer != null) {
holder.newCountDownTimer!!.cancel()
}
var timer = modelUse.calculateEnd
timer = timer * 1000
holder.newCountDownTimer = object : CountDownTimer(timer, 1000) {
override fun onTick(millisUntilFinished: Long) {
var seconds = (millisUntilFinished / 1000).toInt()
val hours = seconds / (60 * 60)
val tempMint = seconds - hours * 60 * 60
val minutes = tempMint / 60
seconds = tempMint - minutes * 60
holder.rowMain_timer.rowMain_timer.text =
String.format("%02d", hours) + ":" + String.format(
"%02d",
minutes
) + ":" + String.format("%02d", seconds)
}
override fun onFinish() {
holder.rowMain_timer.text = "00:00:00"
}
}.start()
}
inner class MyHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var newCountDownTimer: CountDownTimer? = null
lateinit var rowMain_timer: TextView
init {
rowMain_timer = itemView.findViewById(R.id.rowMain_timer)
}
fun setData(model: Today) {
model.image.let {
Glide.with(context)
.load(Constants.MAIN_BASE_URL + it)
.apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.RESOURCE))
.into(itemView.rowMain_img)
}
model.title.let { itemView.rowMain_title.text = it }
}
}
How can I fix this?
Upvotes: 1
Views: 1698
Reputation: 2305
When you want use Timer
into your adapter, Shouldn't use this in onBindViewHolder
.
Because onBindViewHolder
call every when you scrolling on items
You should use Timer into constructor of your Adapter, and every second notifyDataSetChanged
your adapter.
Don't worry, this is way not bad structure and not memory leak. you can check this in profile tab!
class AuctionsTodayAdapter(val context: Context, val model: MutableList<Today>) :
RecyclerView.Adapter<AuctionsTodayAdapter.MyHolder>() {
private var newData: Long = 0
init {
for (items in model) {
items.end.let {
newData = items.end.toLong()
}
}
//set the timer which will refresh the data every 1 second.
object : CountDownTimer(newData, 1000) {
override fun onFinish() {
notifyDataSetChanged()
}
override fun onTick(p0: Long) {
var i = 0
val dataLength = model.size
while (i < dataLength) {
val item = model[i]
item.end -= 1000
i++
}
notifyDataSetChanged()
}
}.start()
}
override fun onBindViewHolder(holder: MyHolder, position: Int) {
var modelUse = model[position]
//Img
modelUse.image.let {
Glide.with(context)
.load(Constants.MAIN_BASE_URL + it)
.apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.RESOURCE))
.into(holder.rowMAIN_img)
}
//Timer
modelUse.end.let {
if (modelUse.calculateEnd > 0) {
holder.rowMAIN_timer.text = getDurationBreakdown(modelUse.end.toLong())
} else {
holder.rowMain_timer.text = "Finished"
}
}
}
private fun millToMins(milliSec: Long): String {
var seconds = (milliSec / 1000).toInt()
val hours = seconds / (60 * 60)
val tempMint = seconds - hours * 60 * 60
val minutes = tempMint / 60
seconds = tempMint - minutes * 60
return String.format("%02d", hours) + ":" + String.format(
"%02d",
minutes
) + ":" + String.format("%02d", seconds)
}
}
I hope i can help you.
Upvotes: 1
Reputation: 559
You have CountDownTimer
for each ViewHolder
. Every time, you scroll, recyclerView creates new CountDownTimer
. So, if I correctly got your question, you should have single CountDownTimer
for the whole adapter. Make it a local field of adapter class.
Upvotes: 1