Олег Місько
Олег Місько

Reputation: 370

RecyclerView item's image bug

Problem: Glitch bug with RecyclerView's child's item ImageView

I have a RecyclerView.Each item has ImageView with id "like" which is basically an empty star.

When we click on some item's "Like", for example, the first, the star is changing from empty filled in yellow, which means that item is liked.

When we click on the first item everything is OK, everything is as it should be, but at the same time, we have a bug, changing the sixth item's "like" to a filled star, which shouldn't be done, as this item wasn't liked yet.

If we click on second item's like - the seventh item would have the same bug.

To fill the item's in ViewHolder I have a model - Recipe.

open class Recipe constructor(open var label: String,

                          open var image: String,

                          open var url: String,

                          open var ingredients: RealmList<Ingredient>?,

                          open var calories: Float,

                          open var totalWeight: Float,

                          open var id: Int,

                          open var isFavorite: Boolean) : RealmObject() 

So when clicking on some item I check whether it isFavorite or not, and change the "like" ImageView accordingly.

Have anyone got any ideas how I should fix this?

I tried to turn off recycling of items in the adapter, but unfortunately, in this case, the "liked" state isn't saved too.

OnClick listener for "like" button

itemView.like.onClick {
                if (recipe.isFavorite) {
                    itemView.like.image = ContextCompat.getDrawable(itemView.context, R.drawable.ic_star_before_like)
                    recipe.isFavorite = false
                    DatabaseService.removeRecipeFromFavorites(recipe)
                    itemView.context.toast("Recipe removed from favorites.")
                } else {
                    itemView.like.image = ContextCompat.getDrawable(itemView.context, R.drawable.ic_star_after_like)
                    recipe.isFavorite = true
                    DatabaseService.addNewFavoriteRecipe(recipe)
                    itemView.context.toast("Recipe added to favorites.")
                }

            }

XML-File

<LinearLayout
  android:layout_width="match_parent"
  android:layout_height="0dip"
  android:layout_weight="0.8"
  android:gravity="center|right"
  android:orientation="horizontal">

  <ImageView
    android:id="@+id/like"
    android:layout_width="@dimen/icon_size_card"
    android:layout_height="@dimen/icon_size_card"
    android:padding="5dp"
    android:layout_marginRight="@dimen/icon_margin_card"
    android:src="@drawable/ic_star_before_like" />

  <ImageView
    android:id="@+id/share"
    android:layout_width="@dimen/icon_size_card"
    android:layout_height="@dimen/icon_size_card"
    android:padding="5dp"
    android:layout_marginRight="@dimen/icon_margin_card"
    android:src="@drawable/ic_share" />

  <ImageView
    android:id="@+id/save"
    android:layout_width="@dimen/icon_size_card"
    android:layout_height="@dimen/icon_size_card"
    android:padding="5dp"
    android:layout_marginRight="@dimen/icon_margin_card"
    android:src="@drawable/ic_save_recipe" />

  <ImageView
    android:id="@+id/expand"
    android:layout_width="@dimen/icon_size_card"
    android:layout_height="@dimen/icon_size_card"
    android:padding="5dp"
    android:layout_marginRight="@dimen/icon_margin_card"
    android:src="@drawable/ic_down_arrow" />

</LinearLayout>

Upvotes: 0

Views: 889

Answers (2)

zsmb13
zsmb13

Reputation: 89548

RecyclerView reuses the viewholders it creates with onCreateViewHolder. To put it simply, think of it as the views going off screen on the top coming back around on the bottom, and vica versa. Therefore, the nice way of solving what you want is the following:

  1. Set the icon to the right drawable in the onBindViewHolder method. This way, whether a viewholder is being bound for the first time, or had a drawable reflecting a recipe's favourited state already, it will be refreshed properly.

    Without knowing what your adapter looks like, this should give you an idea of how to do this (the example code assumes you have your recipes in some list called recipes):

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val recipe = recipes[position]
        val itemView = holder.itemView
        itemView.like.image = ContextCompat.getDrawable(itemView.context,
                if(recipe.isFavorite) R.drawable.ic_star_before_like else R.drawable.ic_star_after_like)
    }
    
  2. In the listener, modify the Recipe the same way you're doing it now, but instead of then setting the image directly, notify the adapter that the recipe at the given position has changed (notifyItemChanged(position)), which will make it re-bind that item, refreshing the icon to reflect the new state. Again, I don't know where you're setting up the listener, but I assume you know what position the recipe is at in the list there.

Upvotes: 2

Joel Fernandes
Joel Fernandes

Reputation: 4856

RecyclerView is built to cache views and reuse them. When you scroll, your views are recycled and loaded from the memory.

After you have made changes to your underlying data model attached to the adapter, it is important to notify the adapter about the same. In your onClick method, after you've made the changes, make sure you call adapter.notifyDataSetChanged() or just notifyDataSetChanged() if you're calling it directly from your adapter class.

Upvotes: 2

Related Questions