javaandroidandroid-recyclerviewandroid-listadapter

Items in Recyclerview + Listadapter won't redraw on update


I'm setting up a RecyclerView that uses a ListAdapter to calculate animations on changes. The RecyclerView is receiving data through a ViewModel that fetches a list via Firebase. The items shown are of the Mensa kind. A Mensa item can change its visibility, its occupancy, or the distance displayed.

I want to implement two buttons that favorite/hide items, therefore changing their position in the list. Two buttons in every item allow the user to favorite or hide an item. This will move an item to the top / to the bottom of a list, in accordance with the sorting strategy, which places favorites first, defaults second, and hiddens last.

However, when I click on a button, the list will rearrange, but the item clicked won't rebind. Buttons retain their old state (and OnClickListeners), and only scrolling the list will call the onBind method. Is my problem with the DiffUtil.Callback? I really don't know what is wrong with my code.

I am already providing a new list in the submitList method of the adapter (this suggestion from another stackoverflow question enabled animations in my case), but the clicked item still won't redraw.

in MensaListActivity.java

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mensa_list);

        viewModel = ViewModelProviders.of(this).get(MensaListModel.class);

        final RecyclerView recyclerView =findViewById(R.id.mensa_list_recyclerview);
        final MensaListAdapter adapter = new MensaListAdapter(this, new MensaListAdapter.ItemButtonsListener() {
            @Override
            public void visibilityButtonClicked(Mensa mensa, VisibilityPreference newVisibility) {
                viewModel.visibilityChanged(mensa, newVisibility);
            }
        });
        recyclerView.setAdapter(adapter);
        recyclerView.setHasFixedSize(false);
        recyclerView.setLayoutManager(new GridLayoutManager(this, 1, RecyclerView.VERTICAL, false));

        viewModel.getMensaData().observe(this, new Observer<LinkedList<Mensa>>() {
            @Override
            public void onChanged(LinkedList<Mensa> mensas) {
                adapter.submitList(new LinkedList<>(mensas));
            }
        });

in MensaListModel.java

    public LiveData<LinkedList<Mensa>> getMensaData() {
        return mensaData;
    }

    // ...

    public void visibilityChanged(Mensa changedItem, VisibilityPreference newVisibility) {
        LinkedList<Mensa> newData = getMensaData().getValue();
        int index = newData.indexOf(changedItem);

        newData.remove(index);
        newData.add(changedItem);
        sortMensaData(newData);
        // sortMensaData calls postValue method

MensaListAdapter.java

public class MensaListAdapter extends ListAdapter<Mensa, MensaListAdapter.MensaViewHolder> {

    private final ItemButtonsListener listener;
    private final Context context;

    class MensaViewHolder extends RecyclerView.ViewHolder {

        TextView nameLabel;
        TextView addressLabel;
        TextView restaurantTypeLabel;
        TextView occupancyLabel;
        TextView distanceLabel;
        ImageButton favoriteButton;
        ImageButton hideButton;

        public MensaViewHolder(@NonNull View itemView) {
            super(itemView);

            // a bunch of assignments
        }

        public void bindData(final Mensa newMensa) {

            nameLabel.setText(newMensa.getName());
            addressLabel.setText(newMensa.getAddress());
            restaurantTypeLabel.setText(newMensa.getType().toString());
            String occText = "Occupancy: " + newMensa.getOccupancy().toInt();
            occupancyLabel.setText(occText);
            if (newMensa.getDistance() != -1) {
                distanceLabel.setVisibility(View.VISIBLE);
                distanceLabel.setText(Double.toString(newMensa.getDistance()));
            } else {
                distanceLabel.setVisibility(View.INVISIBLE);
            }

            switch(newMensa.getVisibility()){
                case FAVORITE:
                    favoriteButton.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.favorite_active, null));
                    favoriteButton.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            listener.visibilityButtonClicked(newMensa, VisibilityPreference.DEFAULT);
                        }
                    }); break;
                case DEFAULT:
                    favoriteButton.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.favorite_inactive, null));
                    favoriteButton.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            listener.visibilityButtonClicked(newMensa, VisibilityPreference.FAVORITE);
                        }
                    }); break;
                case HIDDEN:
                    favoriteButton.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.favorite_inactive, null));
                    favoriteButton.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            listener.visibilityButtonClicked(newMensa, VisibilityPreference.FAVORITE);
                        }
                    }); break;

// removed hidebutton assignments, as they're identical to the favoritebutton assignment
            }

        }
    }

    public MensaListAdapter(Context context, ItemButtonsListener listener) {
        super(DIFF_CALLBACK);
        this.context = context;
        this.listener = listener;
    }

    private static final DiffUtil.ItemCallback<Mensa> DIFF_CALLBACK =
            new DiffUtil.ItemCallback<Mensa>() {
                @Override
                public boolean areItemsTheSame(@NonNull Mensa oldItem, @NonNull Mensa newItem) {
                    return oldItem.equals(newItem);
                }

                @Override
                public boolean areContentsTheSame(@NonNull Mensa oldItem, @NonNull Mensa newItem) {
                    return oldItem.getDistance() == newItem.getDistance()
                            && oldItem.getOccupancy().equals(newItem.getOccupancy())
                            && oldItem.getVisibility().equals(newItem.getVisibility());
                }
            };

    @Override
    public int getItemViewType(int position) {
        return R.layout.mensa_list_item;
    }

    @NonNull
    @Override
    public MensaViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
        return new MensaViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull MensaViewHolder holder, int position) {
        holder.bindData(getItem(position));
    }

    public interface ItemButtonsListener{
        void visibilityButtonClicked(Mensa mensa, VisibilityPreference newVisibility);
    }

}

Mensa.java

public class Mensa {

    private String uID;
    private String name;
    private String address;
    private Occupancy occupancy;
    private RestaurantType type;
    private VisibilityPreference visibility;
    private double latitude;
    private double longitude;
    // distance is calculated lazily as soon as location coordinates are available, -1 means not calculated.
    private double distance = -1;

    public Mensa() {

    }

    // generated by android studio
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Mensa mensa = (Mensa) o;
        return uID.equals(mensa.uID);
    }

    @Override
    public int hashCode() {
        return Objects.hash(uID);
    }

    // a bunch of getters and setters

}

1

The list before clicking the favorite button (heart). Relevant are the heart and eye buttons.

2

The list after favoriting the "Akademiestraße" item. It has changed positions, but the heart icon has not changed and the OnClickListeners are still the same.

3

The list after scrolling and returning to the top of the list. The heart is now filled, and OnClickListeners are changed.


Solution

  • It seems to me that your data is updating but the RecyclerView is only updating the order and not the item's view. Try calling your adapter's notifyDataSetChanged() after you update an item in your view.