Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to access the data source of a RecyclerView adapter's ViewHolder?

Tags:

java

android

The constructor of my RecyclerView's adapter looks like:

Context context;
List<ConnectionItem> connections;

public ConnectionsListAdapter(Context context, List connections) {
    this.context = context;
    this.connections = connections;
}

After the adapter's declarations, I declare a static ViewHolder class for the RecyclerView, which also handles the onClicks of any buttons:

public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

    public Context context;

    public ImageView connectionImage;
    public TextView connectionName;
    public ImageButton startMsg;

    public ViewHolder(View itemView, List<ConnectionItem> connections) {
        super(itemView);
        ...
        startMsg.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        Intent intent = new Intent(v.getContext(), ChatActivity.class);
        intent.putExtra("name", connections.get(getAdapterPosition()).getName()); // Error because accessing from static context
        intent.putExtra("id", connections.get(getAdapterPosition()).getUid()); // Error because accessing from static context
        context.startActivity(intent);
    }
}

The problem is that connections is not accessible from a static context in the ViewHolder static class. What's the best way for my ViewHolder to get information from the RecyclerView adapter? As a workaround, I am passing the data source to the constructor of the ViewHolder and having a new instance variable in the ViewHolder for the data source:

public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
    public Context context;
    public List<ConnectionItem> connections;

    public ImageView connectionImage;
    public TextView connectionName;
    public ImageButton startMsg;

public ViewHolder(View itemView, List<ConnectionItem> connections) {
        super(itemView);
        this.connections = connections;
        ...
        startMsg.setOnClickListener(this);
}

    @Override
    public void onClick(View v) {
        Intent intent = new Intent(v.getContext(), ChatActivity.class);
        intent.putExtra("name", connections.get(getAdapterPosition()).getName()); // Okay now
        intent.putExtra("id", connections.get(getAdapterPosition()).getUid()); // Okay now
        context.startActivity(intent);
    }

Does my way at all break the ViewHolder pattern or create any other problems in the future?

like image 721
QuayShawn Avatar asked Jul 25 '16 18:07

QuayShawn


People also ask

What is ViewHolder in RecyclerView?

A ViewHolder describes an item view and metadata about its place within the RecyclerView. Adapter implementations should subclass ViewHolder and add fields for caching potentially expensive findViewById results. While LayoutParams belong to the LayoutManager , ViewHolders belong to the adapter.

How can I access any component outside RecyclerView from RecyclerView in Android?

Go to the res -> layout -> activity_main.


1 Answers

Your responsibilities/concerns are a little scrambled.

In MVC terms, a ViewHolder is really a View. It's just an object that has references to the subviews so that findViewById() doesn't have to be called over and over. But it's still really a view.

However, you have a constructor with Model data as an argument, and that has a bad smell to it.

  • The only properties/variables your ViewHolder should have are Views.

  • The only thing your ViewHolder constructor should ever be calling is findViewById()

You haven't really talked about your adapter much, but you'll notice that you override two methods: onCreateViewHolder and onBindViewHolder.

Within onCreateViewHolder you construct the ViewHolder that's applicable to your view type. That's it. You may have to inflate different layouts depending on the view type, but you are just dealing with views here. Don't pass any Model data to the ViewHolder in the constructor.

Within onBindViewHolder, here is where you hook up the View to the Model data.

Something that I do frequently is define a bind() method for my ViewHolder so that it's very clear what Model data is being handed off. The bind() method takes the data and calls setText and similar methods to make the views reflect the Model data for that adapter position.

But now we come to the ugly part of RecyclerView's Adapter design. There is no OnItemClickListener for the adapter.

Google argues that this is a better design; that events should be handled by the list item view anyway. I get that. But the problem is that the event has to meet up with its model data, and it's the adapter that has the model data, not the list item view.

And Google has emphasized that you can't use final on values like the position; you need to call getAdapterPosition() in order to index the model data.

So now I have gone to a pattern that accommodates all these constraints.

  • I define an interface for an event listener with a method that takes the position argument.

  • I create an instance of the listener in the adapter

  • Every time I bind to a ViewHolder I pass this listener instance (so the ViewHolder does have a reference to the listener, but not the adapter)

  • On the event handler in the ViewHolder, I call getAdapterPosition() to get the position of the list item, then I call the listener method with that position

  • The adapter gets the listener callback, accesses the correct model data and performs the desired action

So here's an example: I have a list of items that have been selected, i.e. products in an online shopping cart that have a Remove button with a big X. Now I have to handle that remove button.

Define the interface:

    interface OnItemRemovedListener {

        void itemRemoved(int position);
    }

Create an instance of the listener in the adapter

    private OnItemRemovedListener mCallback;

Set it up in the adapter constructor:

    mCallback = new OnItemRemovedListener() {
        @Override
        public void itemRemoved(int position) {
            mItemList.remove(position);
            notifyDataSetChanged();
        }
    };

The ViewHolder subclass:

public static class ProductItemViewHolder extends RecyclerView.ViewHolder {

    private TextView mProductName;

    private Button mRemoveButton;

    private OnItemRemovedListener mListener;

    public ProductItemViewHolder(View itemView) {
        super(itemView);
        mProductName = (TextView) itemView.findViewById(R.id.product_name);
        mRemoveButton = (Button) itemView.findViewById(R.id.remove_button);
    }

    public void bind(Model data, OnItemRemovedListener listener) {

        mProductName.setText(data.getProductName());
        mListener = listener;
        mRemoveButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View view) {
                int position = getAdapterPosition();
                if (position >= 0) {
                    mListener.itemRemoved(position);
                }
            }
        });
    }
}

Then onBindViewHolder override looks like this:

    @Override
    public void onBindViewHolder(ProductItemViewHolder holder, int position) {

        holder.bind(mItemList.get(position), mCallback);
    }
like image 181
kris larson Avatar answered Oct 31 '22 08:10

kris larson