I use a binding for a ListAdapter with the definition of a DiffUtil.ItemCallback. When deleting items (at least 2) I have an IndexOutOfBoundsException.
The update of the list works (the number of elements is indeed N-1 after deletion) but not the position of the item, which is kept is the call. The exception's therefore thrown when calling getItem(position) (in the onBindViewHolder). NB: A log of getItemCount() just before the getItem(position) shows that the list contains N-1 elements.
I created a small repo: https://github.com/jeremy-giles/DiffListAdapterTest (with a same configuration to my project) which reproduces the problem.
ItemAdapter class
class ItemAdapter(
var listener: ListAdapterListener) : DataBindingAdapter<Item>(DiffCallback()) {
class DiffCallback : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem == newItem
}
}
override fun getItemViewType(position: Int) = R.layout.recycler_item
override fun onBindViewHolder(holder: DataBindingViewHolder<Item>, position: Int) {
super.onBindViewHolder(holder, position)
holder.itemView.tv_position.text = "Pos: $position"
holder.itemView.setOnLongClickListener {
Timber.d("List item count: ${itemCount}, position: $position")
listener.onLongViewClick(getItem(position), position)
}
}
interface ListAdapterListener {
fun onLongViewClick(item: Item, position: Int) : Boolean
}
}
BindingUtils classes
abstract class DataBindingAdapter<T>(diffCallback: DiffUtil.ItemCallback<T>) :
ListAdapter<T, DataBindingViewHolder<T>>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataBindingViewHolder<T> {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = DataBindingUtil.inflate<ViewDataBinding>(layoutInflater, viewType, parent, false)
return DataBindingViewHolder(binding)
}
override fun onBindViewHolder(holder: DataBindingViewHolder<T>, position: Int) {
holder.bind(getItem(position))
}
}
class DataBindingViewHolder<T>(private val binding: ViewDataBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: T) {
binding.setVariable(BR.item, item)
binding.executePendingBindings()
}
}
And in my MainActivity class I use a LiveData to update the recyclerView
itemViewModel.getListObserver().observe(this, Observer {
Timber.d("List Observer, items count ${it.size}")
itemAdapter.submitList(it.toList())
})
In your onBindViewHolder update usage of 'position' to 'holder.getAdapterPosition()':
override fun onBindViewHolder(holder: DataBindingViewHolder<Item>, position: Int) {
super.onBindViewHolder(holder, position)
holder.itemView.tv_position.text = "Pos: $position"
holder.itemView.setOnLongClickListener {
Timber.d("List item count: ${itemCount}, position: $position")
listener.onLongViewClick(getItem(holder.getAdapterPosition()), holder.getAdapterPosition())
}
}
Currently holder.getAdapterPosition() is deprecated. As per documentation, you might want to use either one of the following two methods.
If you are calling this in the context of an Adapter, you probably
want to call getBindingAdapterPosition() or if you want the position
as RecyclerView sees it, you should call getAbsoluteAdapterPosition().
Related
Hey I am working in android kotlin. I am working in reyclerview. I want to do single selection in my items. I tried some code which is working fine.
OptionsViewAdapter.kt
class OptionsViewAdapter : ListAdapter<ProductVariant, OptionsViewHolder>(PRODUCT_COMPARATOR) {
private var selectedItemPosition: Int = 0
companion object {
private val PRODUCT_COMPARATOR = object : DiffUtil.ItemCallback<ProductVariant>() {
override fun areItemsTheSame(
oldItem: ProductVariant,
newItem: ProductVariant
): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(
oldItem: ProductVariant,
newItem: ProductVariant
): Boolean {
return oldItem == newItem
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OptionsViewHolder {
return OptionsViewHolder.bindView(parent)
}
override fun onBindViewHolder(holder: OptionsViewHolder, position: Int) {
holder.binding.root.setOnClickListener {
selectedItemPosition = holder.bindingAdapterPosition
notifyAdapter()
}
val drawableColor = if (selectedItemPosition == position)
R.drawable.options_item_selected_background
else
R.drawable.options_item_default_background
holder.binding.root.background =
ContextCompat.getDrawable(holder.binding.root.context, drawableColor)
holder.bindItem(getItem(position), position)
}
fun notifyAdapter() {
notifyDataSetChanged()
}
}
OptionsViewHolder.kt
class OptionsViewHolder(
val binding: OptionsItemLayoutBinding,
) : RecyclerView.ViewHolder(binding.root) {
companion object {
fun bindView(parent: ViewGroup): OptionsViewHolder {
return OptionsViewHolder(
OptionsItemLayoutBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
}
fun bindItem(item: ProductVariant?, position: Int) {
}
}
Video for single click is working fine.
When I move onBindViewHolder code inside bindItem it not working. Can someone guide me why this is happening.
vs
OptionsViewAdapter.kt
class OptionsViewAdapter : ListAdapter<ProductVariant, OptionsViewHolder>(PRODUCT_COMPARATOR) {
companion object {
private val PRODUCT_COMPARATOR = object : DiffUtil.ItemCallback<ProductVariant>() {
override fun areItemsTheSame(
oldItem: ProductVariant,
newItem: ProductVariant
): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(
oldItem: ProductVariant,
newItem: ProductVariant
): Boolean {
return oldItem == newItem
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OptionsViewHolder {
return OptionsViewHolder.bindView(parent)
}
override fun onBindViewHolder(holder: OptionsViewHolder, position: Int) {
holder.bindItem(getItem(position), position ,::notifyAdapter)
}
fun notifyAdapter() {
notifyDataSetChanged()
}
}
OptionsViewHolder.kt
class OptionsViewHolder(
val binding: OptionsItemLayoutBinding,
) : RecyclerView.ViewHolder(binding.root) {
private var selectedItemPosition: Int = 0
companion object {
fun bindView(parent: ViewGroup): OptionsViewHolder {
return OptionsViewHolder(
OptionsItemLayoutBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
}
fun bindItem(
item: ProductVariant?,
position: Int,
onAdapterChange: () -> Unit
) {
binding.root.setOnClickListener {
selectedItemPosition = position
onAdapterChange()
}
val drawableColor = if (selectedItemPosition == position)
R.drawable.options_item_selected_background
else
R.drawable.options_item_default_background
binding.root.background =
ContextCompat.getDrawable(binding.root.context, drawableColor)
}
}
The video for single click is not working.
You moved the selectedItemPosition into the ViewHolder class, so you have a separate copy of this property for every instance of the ViewHolder class so you are no longer affecting other items in the list when any one list item is clicked.
This would be much easier to implement by making the view holder class an inner class of the Adapter so it can directly modify the adapter’s selectedItemPosition property. And I would give the property a custom setter so you can automatically notify the adapter of the change instead of having to work with a separate function call. You can also make it notify the adapter specifically of which two row items changed instead of the whole data set (more efficient and by doing it in the property setter you have access to the old and new row positions in one place easily—field and value).
private var selectedItemPosition: Int = 0
set(value) {
val oldPos = field
field = value
notifyItemChanged(oldPos)
notifyItemChanged(value)
}
Even though you are passing the method reference notifyAdapter() in bindItem() function you are not calling it in the OnClickListener that is why it is not working.
I have found an issue when dragging an item that doesn't trigger a move on a GridLayoutManager with 2 columns.
When dragging one item from the first column to the top of the list (which scrolls the list upwards) and then back to its original position, the recyclerView creates an empty space next to the original position, pushing the item that was next to the dragged item to the next row.
I have coded the simplest app to show you what I mean. On the first screenshot I start dragging one item towards the top of the list. On the second screenshot I reach the top of the screen. Then I move it back to its position and you can already see how the items have not been laid down properly.
Has anyone encountered this as well? I have not found any solution to this issue.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val myAdapter = MyAdapter()
with(findViewById<RecyclerView>(R.id.recyclerView)) {
adapter = myAdapter
layoutManager = GridLayoutManager(this#MainActivity, 2)
ItemTouchHelper(object : ItemTouchHelper.Callback() {
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
return makeFlag(ItemTouchHelper.ACTION_STATE_DRAG, DOWN or UP or START or END)
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
return false
}
override fun canDropOver(recyclerView: RecyclerView, current: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
TODO("Not yet implemented")
}
}).attachToRecyclerView(this)
}
myAdapter.submitList(data)
}
data class ListItem(val id: String, val name: String)
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
}
class MyAdapter : ListAdapter<ListItem, MyViewHolder>(MyAdapterDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false)
return MyViewHolder(view)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
}
class MyAdapterDiffCallback : DiffUtil.ItemCallback<ListItem>() {
override fun areItemsTheSame(oldItem: ListItem, newItem: ListItem) = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: ListItem, newItem: ListItem) = oldItem == newItem
}
}
I'm having a problem with my adapter/recyclerview. I want the user to see an overview of appointments like this:
But i can only do this be setting the adapter for the recylerview in the observe method.
This the code:
val adapter = CalendarAdapter()
binding.appointmentList.adapter = adapter
calendarDataViewModel?.appointments?.observe(viewLifecycleOwner, Observer {
it?.let {
adapter.data = it
binding.appointmentList.adapter = adapter
}
})
Like you see, if I set the adapter again inside the observe method, the items will be shown, but if I don't do that, nothing will be shown and it will just be an empty screen.
I also tried the following code, but this also doesnt work:
val adapter = CalendarAdapter()
binding.appointmentList.adapter = adapter
calendarDataViewModel?.appointments?.observe(viewLifecycleOwner, Observer {
it?.let {
adapter.submitList(it)
}
})
I can't seem to figure out why this isnt working.
Appointments is of the type LiveData<List>
Does anyone know how I can fix this?
This is my CalendarAdapter:
class CalendarAdapter : ListAdapter<CalendarData, RecyclerView.ViewHolder>(AppointmentDiffCallback()) {
var data = listOf<CalendarData>()
override fun getItemCount(): Int {
return data.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return AppointmentViewHolder(CalendarItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val appointment = data[position]
(holder as AppointmentViewHolder).bind(appointment)
}
}
private class AppointmentDiffCallback : DiffUtil.ItemCallback<CalendarData>() {
override fun areItemsTheSame(oldItem: CalendarData, newItem: CalendarData): Boolean {
return oldItem.appointmentId == newItem.appointmentId
}
#SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: CalendarData, newItem: CalendarData): Boolean {
return oldItem == newItem
}
}
You do not need to override the getItemCount method. In your case, data.size returns 0 due to which the items are never shown.
Also, you do not need to keep track of your items, ListAdapter abstract class already does that. You can get your Item using getItem(Int) method. Your adapter should be something like so:
class CalendarAdapter : ListAdapter<CalendarData, RecyclerView.ViewHolder>(AppointmentDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return AppointmentViewHolder(CalendarItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val appointment = getItem(position)
(holder as AppointmentViewHolder).bind(appointment)
}
}
Then in your observer you can the submitList method like in your second code block in the question.
I know The ListAdpater extends a recyclerview adapter. And for initializing items, I use submitList method. the parameter in submitList method is defined like this, typed list.
public void submitList(#Nullable List<T> list) {
mDiffer.submitList(list);
}
I can't remove or add items. So I tried using arraylist variable for adding and removing, and casted it to List. But I'm not sure this is the good way...
Because arraylist is not good to add or remove items. Is there other way?
Delete item from ListAdapter. (Kotlin)
fun removeItem(position: Int){
val currentList = adapter.currentList.toMutableList()
currentList.removeAt(position)
adapter.submitList(currentList)
}
Tested and working fine.
Just call submitList with a new list, the diffutil will take care of the old list and the new list.
If you add or remove something, add to your original list or remove an item from your list and then call submitList.
you can call use adapter.currentList to get your current list, or just use a viewModel to hold your list items.
MyAdapter
class FavoritesAdapter(
private val listeners: OnFavoriteDetailPage,
private val itemClick: (title: String, price: String) -> Unit) :ListAdapter<Favorites, FavoritesAdapter.RecyclerViewHolder>(ListComparator()) {
//bind the recycler list items
inner class RecyclerViewHolder(val binding: FavoriteListBinding) :
RecyclerView.ViewHolder(binding.root) {
#SuppressLint("SetTextI18n")
fun bind(list: Favorites) {
binding.deal.text = "Deal " + list.id.toString()
binding.price.text = "Ghc " + list.price
binding.pizzaSize.text = list.size.toString()
binding.posterBanner.load(list.imgUrl)
}
}
inflate the List
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewHolder {
return RecyclerViewHolder(
FavoriteListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
//bind the model list the recycler list
#SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: RecyclerViewHolder, position: Int) {
val getItemPosition = getItem(position)
holder.bind(getItemPosition)
Glide.with(holder.itemView.context).load(getItemPosition.imgUrl)
.into(holder.binding.posterBanner)
holder.binding.addCart.setOnClickListener {
itemClick("Deal ${getItemPosition.id}", getItemPosition.price)
}
holder.itemView.setOnClickListener {
listeners.viewDetail(getItemPosition)
}
holder.binding.favoriteHeart.setOnClickListener {
listeners.onItemRemoveClick(getItemPosition.id)
}
}
class ListComparator : DiffUtil.ItemCallback<Favorites>() {
override fun areItemsTheSame(oldItem: Favorites, newItem: Favorites): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Favorites, newItem: Favorites): Boolean {
return oldItem.id == newItem.id
}
}
interface OnFavoriteDetailPage {
fun viewDetail(favorites: Favorites)
fun onItemRemoveClick(position: Int)
}
}
Inside my fragment
override fun onItemRemoveClick(position: Int) {
val currentList = recyclerAdapter.currentList.toMutableList()
currentList.removeAt(position)
recyclerAdapter.submitList(currentList)
}
Everythings works fine but mine delete from the bottom of the list instead of the current postion. What could be the problem?
I am using paging library with recyclerView and my task is to shows empty viewHolder when I receive empty arrayList from server.
I tried to check that list is empty or not, but when I get response it calls the Observer method 2 times (1st time empty list, 2nd time real list), after when I use swipeRefresh it DataSource class sends empty list as it has same list already in cache. Also I have pagination and checking for dataset size is difficult as there are many cases.
/**
* Simple Adapter used to show list of Appeals with pagination
*/
class MyAppealsAdapter : PagedListAdapter<Appeal, RecyclerView.ViewHolder>(diffCallBack) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return AppealsViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_my_appeal, parent, false))
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is AppealsViewHolder -> holder.bind(position)
}
}
inner class AppealsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
#SuppressLint("SimpleDateFormat")
fun bind(position: Int) {
getItem(position)?.let { item ->
item.fields?.get(AppealKeyObj.K_SUB_ISSUE)?.let {
itemView.mTypeOfAppeal.text = it
}
}
}
}
companion object {
open var diffCallBack: DiffUtil.ItemCallback<Appeal> = object : DiffUtil.ItemCallback<Appeal>() {
override fun areItemsTheSame(oldItem: Appeal, newItem: Appeal): Boolean {
return oldItem.id === oldItem.id
}
override fun areContentsTheSame(oldItem: Appeal, newItem: Appeal): Boolean {
return oldItem == newItem
}
}
}
}
Please help to show empty viewHolder when I receive empty arrayList
I did it like this
override fun getItemViewType(position: Int): Int {
return if (super.getItemCount() == 0) EMPTY else FILLED
}
override fun getItemCount(): Int {
return if (super.getItemCount() == 0) 1 else super.getItemCount()
}
And also changed linearlayoutmanager to custom one with overriden method
override fun onLayoutChildren(recycler: Recycler?, state: RecyclerView.State?) {
try {
super.onLayoutChildren(recycler, state)
} catch (e: IndexOutOfBoundsException) {
Timber.e(e)
}
}