Im using Recycler View with data Binding to display a list of Routes. For Testing there are always (and only) 10 Items (Route1 -10) Every Route has a delete Button, to delete the Row the User picked(From the List and from the RV). When deleting the first or second Item without scrolling, it works just fine and I can delete all Items. But After scrolling, (I think) the Adapter sets the Position to a wrong value and deletes the wrong items. Eventually the program crashes with a IndexOutOfBoundsException.
I tried to use other positions instead of the int position i get from the Adapter:
holder.absoluteAdapterPosition holder.adapterPosition holder.bindingAdapterPosition holder.layoutPosition
Unfortunately it didnt change the outcome.
Then I tried: Kotlin RecyclerView delete item. Is this a bug?
Works better, but after deleting the last item of the List the adapter position is corrupt again.
Finnaly I wrote the onClicklistener for the delete Button inside the View Holder of my Adapter.
Now it works, but I dont understand why. Can someone please explain me this behavior?
package com.example.testnestedxml
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.testnestedxml.databinding.ListItemBinding
class RouteAdapter(
private var routeList: ArrayList<Route>
) :
RecyclerView.Adapter<RouteViewHolder>() {
//Fügt alle Items (Views) in eine View
private lateinit var binding: ListItemBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RouteViewHolder {
//View wird erstellt
binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return RouteViewHolder(binding)
}
override fun onBindViewHolder(holder: RouteViewHolder, position: Int) {
val route = routeList[position]
holder.routeViewHolderBinding(route, routeList,this)
}
override fun getItemCount(): Int = routeList.size
}
package com.example.testnestedxml
import android.content.Intent
import android.os.Handler
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
import com.example.testnestedxml.databinding.ListItemBinding
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type
class RouteViewHolder(
private val binding: ListItemBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun routeViewHolderBinding(
route: Route,
routeList: ArrayList<Route>,
routeAdapter: RouteAdapter
) {
binding.name.text = route.name
binding.dauer.text = route.duration.toString()
binding.entfernung.text = route.length.toString()
val itemPosition = layoutPosition
binding.delete.setOnClickListener {
routeList.removeAt(itemPosition)
routeAdapter.notifyDataSetChanged()
saveDataRouteAdapter(routeList)
}
}
private fun saveDataRouteAdapter(routeList: ArrayList<Route>) {
val sharedPreferences = binding.root.context.getSharedPreferences(
"shared preferences",
AppCompatActivity.MODE_PRIVATE
)
val editor = sharedPreferences.edit()
val gson = Gson()
val json = gson.toJson(routeList)
editor.putString("Route list", json)
editor.apply()
}
}
Related
So I have found the source of the problem. Inside my Adapter for my Recyclerview I am trying to check if the imageName is null or empty, if it isn't then we can get the image from the local storage and put it into the holder's ImageView. When doing this check and only having an image in the first item in the list of items in the adapter, the image will be placed in multiple positions... And if I keep spinning the recyclerview it continuously adds more. The reason I want to do this is because I want a default image of a plus icon to trigger more logic, otherwise the image that's stored if it's available.
Below is the first part of the code that creates the issue, and some images for you to see what's happening.
package com.example.dukebox.adapters
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.dukebox.FORWARD_SLASH
import com.example.dukebox.R
import com.example.dukebox.RECORD_IMAGES
import com.example.dukebox.models.Record
import com.example.dukebox.viewmodel.HomeViewModel
class NumberSelectionAdapter(
private val recordInfoTitle: TextView,
private val viewModel: HomeViewModel
): RecyclerView.Adapter<NumberSelectionAdapter.ViewHolder>() {
var selectedPosition = 0
private var records: List<Record> = emptyList()
private val imagesPath = "${recordInfoTitle.context.getExternalFilesDir(null)?.absolutePath}$RECORD_IMAGES$FORWARD_SLASH"
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.number_item, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val record = records[position]
if (!record.imageName.isNullOrEmpty()){
Glide.with(recordInfoTitle.context).load("$imagesPath$FORWARD_SLASH${record.imageName}").into(holder.recordCover)
}
}
override fun getItemCount(): Int {
return records.size
}
fun updateList(newRecords: List<Record>) {
records = newRecords
notifyDataSetChanged()
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val recordCover: ImageView = itemView.findViewById(R.id.recordCover)
}
}
And then the code that only puts the image where it's supposed to be the entire time. As you can see, the only difference is the null or empty check in the onBindViewHolder.
package com.example.dukebox.adapters
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.dukebox.FORWARD_SLASH
import com.example.dukebox.R
import com.example.dukebox.RECORD_IMAGES
import com.example.dukebox.models.Record
import com.example.dukebox.viewmodel.HomeViewModel
class NumberSelectionAdapter(
private val recordInfoTitle: TextView,
private val viewModel: HomeViewModel
): RecyclerView.Adapter<NumberSelectionAdapter.ViewHolder>() {
var selectedPosition = 0
private var records: List<Record> = emptyList()
private val imagesPath = "${recordInfoTitle.context.getExternalFilesDir(null)?.absolutePath}$RECORD_IMAGES$FORWARD_SLASH"
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.number_item, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val record = records[position]
Glide.with(recordInfoTitle.context).load("$imagesPath$FORWARD_SLASH${record.imageName}").into(holder.recordCover)
}
override fun getItemCount(): Int {
return records.size
}
fun updateList(newRecords: List<Record>) {
records = newRecords
notifyDataSetChanged()
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val recordCover: ImageView = itemView.findViewById(R.id.recordCover)
}
}
And some pictures of what it looks like, because it tries to load an image that doesn't exist, the plus signs aren't there. But the image is only where it's suppose to be. I span it multiple times, and there is only ever one.
RecyclerViews reuse (recycle) their ViewHolders, so in onBindViewHolder you're usually getting one that's already displaying stuff for another item. You need to update it so it looks right for your current item.
Here's what you're doing
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val record = records[position]
// this ONLY updates IF there's an imageName
if (!record.imageName.isNullOrEmpty()){
Glide.with(recordInfoTitle.context).load("$imagesPath$FORWARD_SLASH${record.imageName}").into(holder.recordCover)
}
}
The problem is you set a new pic if there's an image name - but you don't clear it if there isn't. So if you're given a ViewHolder that was previously displaying an item with a image, that image is still there!
If you don't explicitly remove it, it's still going to show that old data. That goes for anything in onBindViewHolder - you need to update everything to the correct display state for the current item. That's why you're seeing the image "repeat" when it should only be on the first item - it's just the ViewHolder that was used for the first item showing up again, and unless you change the contents of recordCover, it's gonna be there every time
Since you're using Glide, you should use its clear() function to wipe the image, as it explains in the docs:
By calling clear() or into(View) on the View, you’re cancelling the load and guaranteeing that Glide will not change the contents of the view after the call completes. If you forget to call clear() and don’t start a new load, the load you started into the same View for a previous position may complete after you set your special Drawable and change the contents of the View to an old image.
Basically, because you're telling Glide to load an image into a particular ViewHolder's ImageView, and that can happen asynchronously, it's possible (if you're scrolling fast) that you'll get to another item that uses that ViewHolder, set the contents to blank, but then Glide will show up after a delay with that earlier requested image and set it on that ImageView.
By using the clear() call through Glide, it can internally keep track of what jobs are queued up, and it can cancel a request for that ImageView if a newer one comes in, including a request to clear it. Let Glide take care of it all, basically!
So:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val record = records[position]
// always update the ImageView to the correct state
if (!record.imageName.isNullOrEmpty()){
Glide.with(recordInfoTitle.context).load("$imagesPath$FORWARD_SLASH${record.imageName}").into(holder.recordCover)
} else {
Glide.with(recordInfoTitle.context).clear(holder.recordCover)
}
}
I can't manage to uset setImageResource on my holder (which is a part of a CardView).
I've already tried to add a image asset but it didn't seem to work.
I would like to know how can I change an image properly in this case.
This is the code of the entire page:
package com.example.revenuer.adapter
import android.R
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.revenuer.entity.Operation
import com.example.revenuer.listener.OperationListener
class HistoryAdapter(val list: List<Operation>): RecyclerView.Adapter<HistoryAdapter.OperationViewHolder>() {
private var listener: OperationListener? = null
fun setOnOperationListener(listener: OperationListener){
this.listener = listener;
}
class OperationViewHolder(view: View, private val listener: OperationListener?): RecyclerView.ViewHolder(view){
val nameView: TextView
val valueView: TextView
val dateView: TextView
val imageView: ImageView
init {
view.setOnClickListener{
listener?.onListItemClick(view, adapterPosition)
}
nameView = view.findViewById(com.example.revenuer.R.id.item_cardview_name)
valueView = view.findViewById(com.example.revenuer.R.id.item_cardview_value)
dateView = view.findViewById(com.example.revenuer.R.id.item_cardview_date)
imageView = view.findViewById(com.example.revenuer.R.id.operation_image)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OperationViewHolder{
val view = LayoutInflater.from(parent.context).inflate(com.example.revenuer.R.layout.operation_item, parent,false)
return OperationViewHolder(view, listener)
}
override fun onBindViewHolder(holder: OperationViewHolder, position: Int) {
val operation = list[position]
holder.nameView.text = operation.name
holder.valueView.text = operation.value
holder.dateView.text = operation.date
if (operation.type) {
holder.imageView.setImageResource(R.drawable.custom_arrow_up)
}
}
override fun getItemCount(): Int {
return list.size
}
}
You have import android.R, so in the rest of your code you need to write com.example.revenuer.R to refer to your own package's R, just like you did elsewhere.
Solution:
Replace import android.R with import com.example.revenuer.R and you can replace all com.example.revenuer.R with R. Explicitly write android.R when you need to.
Here is my code :-
Favourite Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat.startActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import java.security.AccessController.getContext
//this is my calling activity
class FavouriteActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.favourite_activity)
val mToolbar: Toolbar = findViewById(R.id.toolbar_favourite)
setSupportActionBar(mToolbar)
getSupportActionBar()?.setDisplayHomeAsUpEnabled(true);
getSupportActionBar()?.setDisplayShowHomeEnabled(true);
setTitle("Favourite Activity");
//getting recyclerview from xml
val recyclerView = findViewById(R.id.recyclerView) as RecyclerView
//adding a layoutmanager
recyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
//it can be staggered and grid
//creating our adapter
val adapter = CustomAdapter(star) //here I am calling the adapter activity
//now adding the adapter to recyclerview
recyclerView.adapter = adapter
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}
}
CustomAdapter class
class CustomAdapter(val userList: ArrayList<User>) : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
//this method is returning the view for each item in the list
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomAdapter.ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.list_layout_favourite, parent, false)
return ViewHolder(v)
}
//this method is binding the data on the list
override fun onBindViewHolder(holder: CustomAdapter.ViewHolder, position: Int) {
holder.bindItems(userList[position])
holder.imgCopy.setOnClickListener(View.OnClickListener {
holder.shareString(userList[position])
Toast.makeText(holder.itemView.getContext(),"Copy Button Clicked", Toast.LENGTH_SHORT).show()
})
}
//this method is giving the size of the list
override fun getItemCount(): Int {
return userList.size
}
//the class is holding the list view
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val imgCopy: ImageView = itemView.findViewById(R.id.img_copy) as ImageView
val textViewName = itemView.findViewById(R.id.tvTitle) as TextView
fun bindItems(user: User) {
textViewName.text = user.name
}
fun shareString(user: User)
{
val message : String = user.name
val intent = Intent()
intent.action = Intent.ACTION_SEND
intent.putExtra(Intent.EXTRA_TEXT,message)
intent.type = "text/plain"
startActivity(Intent.createChooser(intent,"Share to :")) ///Issue occur right here
}}}
Getting error : Required context , found Intent.
it is working fine in other FragmentActivity.
I have tried various methods to called the context. but anything is not working.
I have also passed the context from Fragment activity, but that also not worked.
Please let me know is there any way to start Intent.
As I am always getting error and stuck due to this.
The startActivity available in the ViewHolder class is different from the one available in activites. So in this method (available in viewholder), the first parameter should be a context. So pass the context as follows:
startActivity(itemView.context, Intent.createChooser(intent,"Share to :"))
I'am trying to handle my recyclerview put there is a problem
when i scroll down (onBindView function) works fine
but when scroll back to the first items in recyclerview everything becomes wrong like the following images.
package com.leaderspro.mrlawyer.adapters
import android.graphics.Paint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.leaderspro.mrlawyer.R
import com.leaderspro.mrlawyer.models.TODOModel
import kotlinx.android.synthetic.main.todo_list.view.*
class TODOAdapter(private val mArray: ArrayList<TODOModel>) :
RecyclerView.Adapter<TODOAdapter.ViewHolder>() {
var mView: View? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
mView = LayoutInflater.from(parent.context).inflate(R.layout.todo_list, parent, false)
return ViewHolder(mView!!)
}
override fun getItemCount(): Int {
return mArray.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val mTODO = mArray[position]
if (mTODO.isDone == 0) {//not complete
holder.ivIsDone.setImageResource(R.drawable.ic_checkbox_unchecked)
} else if (mTODO.isDone == 1) {
holder.ivIsDone.setImageResource(R.drawable.ic_checkbox_checked)
holder.tvTodoTask.paintFlags =
holder.tvTodoTask.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG //put line on done Tasks
}
holder.tvTodoTask.text = mTODO.task
holder.tvTODODate.text = mTODO.date
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val tvTodoTask = itemView.tvTodoTask!!
val tvTODODate = itemView.tvTODODate!!
val ivIsDone = itemView.ivIsDone!!
val todoListMainLinear = itemView.todoListMainLinear!!
}
}
works fine
https://i.stack.imgur.com/6mBUJ.png
works fine
https://i.stack.imgur.com/qcpLK.png
Wrongly called onBindView when Scroll Back
https://i.stack.imgur.com/2sRsu.png
use holder.adapterposition instead of position
val mTODO = mArray[holder.adapterposition]
for more info check: https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView.Adapter.html?hl=en#onBindViewHolder(VH,%20int)
edit:
as #Pawel has pointed out, the flags should be cleared when mTodo.isDone == 0.
that should get the job done
Im learning Kotlin and Mvvm for Android. I am using a recycler view, and when i try to set the adapter i cant import the Adapter class, I dont know if the problem is in the code because Android Studio let me import ViewHolder class inside the Adapter class but not Adapter class
FrontPageActivity.kt
package com.jmyp.resport.view
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.jmyp.resport.model.New
import com.jmyp.resport.R
import com.jmyp.resport.viewmodel.NewViewModel
class FrontPageActivity : AppCompatActivity() {
lateinit var adapter : NewsAdapter // I can not import this but i can
// NewsAdapter.NewViewHolder
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_front_page)
var viewModelNews = ViewModelProviders.of(this).get(NewViewModel::class.java)
viewModelNews.getNews().observe(this, Observer<ArrayList<New>> { news ->
adapter = NewsAdapter(this, news)
})
}
}
NewsAdapter.kt
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.jmyp.resport.model.New
import com.jmyp.resport.R
import com.jmyp.resport.view.FrontPageActivity
import kotlinx.android.synthetic.main.row_front_page.view.*
class NewsAdapter(private val context: FrontPageActivity, private val news : ArrayList<New>) : RecyclerView.Adapter<NewsAdapter.NewViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewViewHolder {
return NewViewHolder(LayoutInflater.from(context).inflate(R.layout.row_front_page,parent,false))
}
override fun getItemCount(): Int {
return news.size
}
override fun onBindViewHolder(holder: NewViewHolder, position: Int) {
holder.title.text = news.get(position).titulo
}
class NewViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val cover = itemView.iv_cover
val title = itemView.tv_title
}
}
You seem to be missing package in your Adapter class. Add it and try again :)