Use custom View in a RecyclerView Adapter?
Solution 1
Assuming a CustomView
class that looks something like this:
public class CustomView extends RelativeLayout {
private User user;
private ImageView profilePicture;
// override all constructors to ensure custom logic runs in all cases
public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public CustomView(
Context context,
AttributeSet attrs,
int defStyleAttr,
int defStyleRes
) {
super(context, attrs, defStyleAttr, defStyleRes);
// put all custom logic in this constructor, which always runs
inflate(getContext(), R.layout.custom_layout, this);
profilePicture = (ImageView) findViewById(R.id.profilePicture);
}
public void setUser(User newUser) {
user = newUser;
// ACCESS USER MODEL HERE
// e.g. user.getUsername()
}
}
Your RecyclerView.Adapter
and RecyclerView.ViewHolder
could look something like this:
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
// no Context reference needed—can get it from a ViewGroup parameter
private List<User> userData;
public MyAdapter(List<User> userData) {
// make own copy of the list so it can't be edited externally
this.userData = new ArrayList<User>(userData);
}
@Override
public int getItemCount() {
return userData.size();
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// no need for a LayoutInflater instance—
// the custom view inflates itself
CustomView itemView = new CustomView(parent.getContext());
// manually set the CustomView's size
itemView.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
));
return new ViewHolder(itemView);
}
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
holder.getCustomView().setUser(userData.get(position));
}
public class ViewHolder extends RecyclerView.ViewHolder {
private CustomView customView;
public ViewHolder(View v) {
super(v);
customView = (CustomView) v;
}
public CustomView getCustomView() {
return customView;
}
}
}
- The
CustomView
manages its own setup, which occurs in its own constructor and in this case uses inflation of an XML file. (Alternatively, it could set up its child views programmatically.) - Because of this, the
RecyclerView.Adapter
doesn't need to perform any inflation—it just creates a newCustomView
instance, and lets theCustomView
worry about its own setup. - The
CustomView
can't get aUser
instance until itssetUser
method is called, so user access cannot occur in the constructor. In any case, over oneCustomView
lifetime, aRecyclerView
could ask it to show information for many different users at different times. TheCustomView
needs to be able to do this. Therefore, asetUser
method is introduced. - Because the
CustomView
is instantiated by code instead of by XML, attributes for size can't be defined in XML. Therefore, sizing is done programmatically after instantation. -
onBindViewHolder
simply callssetUser
on theCustomView
to link theCustomView
with the correctUser
instance. - The
ViewHolder
class is now just a link between aRecyclerView
item and aCustomView
.
Using pre-built custom views from another class within RecyclerView
s (i.e. not inflating XML within the RecyclerView.Adapter
) never seems to be discussed. I think it's an excellent idea even when the custom view is exclusively used within a RecyclerView
, because it promotes separation of concerns and adherence to the Single Responsibility Principle.
Solution 2
CustomView extends RelativeLayout {
You have a View already (well, a ViewGroup
)
HOW TO INFLATE THE CUSTOM VIEW?
You don't need to... The point of a Custom View object is to not need XML, therefore no inflation.
You can create new CustomView()
, but you need to set all the layout parameters, which looks cleaner in XML, I think.
Most RecyclerView
tutorials show inflating via XML though.
View customView = inflater.inflate(...); ViewHolder viewHolder = new ViewHolder(customView);
That should work because in the class chain, you have CustomView > RelativeLayout > ViewGroup > View
LayoutInflater inflater = LayoutInflater.from(context);
Like I said before, you don't need this if there is no XML file you want to inflate.
You also don't need the context
variable.
parent.getContext()
is a fine solution.
// ANYTHING HERE?
Well, yeah, you should "bind" the ViewHolder
with the data that the ViewHolder
should hold.
Again, most, if not all, tutorials cover this.
Related videos on Youtube
user7638732
Updated on January 06, 2020Comments
-
user7638732 over 4 years
I have a basic custom View which looks like this:
public class CustomView extends RelativeLayout { private User user; private ImageView profilePicture; public CustomView(Context context) { super(context); init(); } public CustomView(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { inflate(getContext(), R.layout.custom_layout, this); profilePicture = (ImageView) findViewById(R.id.profilePicture); // ACCESS USER MODEL HERE // e.g. user.getUsername() } }
As you can see, I'd like to access user data in the View (i.e.:
user.getUsername()
).I also need to be able to use the custom View in a
RecyclerView
Adapter.Here is what my Adapter currently looks like:
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> { private Context context; private List<User> userData; public MyAdapter(Context context, List<User> userData) { this.context = context; this.userData = userData; } public class ViewHolder extends RecyclerView.ViewHolder { public ViewHolder(View v) { super(v); } } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(context); // HOW TO INFLATE THE CUSTOM VIEW? // ViewHolder viewHolder = new ViewHolder(customView); return viewHolder; } @Override public void onBindViewHolder(final ViewHolder holder, int position) { // ANYTHING HERE? } @Override public int getItemCount() { return userData.size(); } }
How can I inflate the custom View in the Adapter?
Also, should I put anything inonBindViewHolder()
?Note: I must use a custom View, as I use this View under different Adapters (i.e.: not just this
RecyclerView
Adapter). -
user7638732 about 7 years"Note: I must use a custom view, as I use this view under different adapters (i.e. not just this RecyclerView adapter)."
-
Alex Peters over 6 yearsYou mention that most/all tutorials cover using an external view class as the view for a
ViewHolder
. Could you please link one or two? I feel I've looked at a lot of tutorials on the topic ofRecyclerView
s andViewHolder
s and I don't think I've seen this particular thing mentioned ever. -
OneCricketeer over 6 years
-
Alex Peters over 6 yearsOkay. The question states that the custom view needs to be used both within and outside a
RecyclerView
though, so inflating XML directly isn't really an option. Nonetheless, your answer hits the nail on the head as far as instantiating the custom view, passing aContext
instance, and needing to manually set layout parameters go. -
Luke Needham over 5 yearsThis is a great answer - and with the last paragraph it becomes a fantastic answer. I've started using custom views in all my RecyclerView Adapters, and the result is so clean
-
Alex Peters over 5 yearsThanks, Luke—I appreciate your compliments. I'm still amazed that this approach isn't adopted more often in projects I see.
-
Michael almost 4 yearsremember to change the RecyclerView.Adapter adapter parameter to your custom view holder
-
drmrbrewer almost 3 years@AlexPeters you say "Because the
CustomView
is instantiated by code instead of by XML, attributes for size can't be defined in XML. Therefore, sizing is done programmatically after instantiation." What about this way: stackoverflow.com/a/38690715/4070848 ? -
Alex Peters almost 3 years@drmrbrewer, sorry, but I don't understand. Could you please rephrase the question?
-
drmrbrewer almost 3 years@AlexPeters it was more an observation than a question I suppose. The linked post shows that you can still define your attributes in XML, then read them as an AttributeSet and pass them as the second argument in your custom view constructor. Maybe handy if you already have lots of custom attributes defined in XML... can migrate to your suggested approach (thanks btw) and yet keep the attributes defined via XML.
-
Alex Peters almost 3 yearsOkay, I understand now. Sounds like a perfectly good approach if your custom component inflates from XML in the first place, and especially if that component relies on specific size constraints at its topmost level. I don't think I gave too much consideration to what size values are possible/acceptable once wrapped in a
ViewHolder
. I guess the sizing constraints I put in my answer above were based purely off what I figured might be the most appropriate. I don't know how well things might function if the child isn't as wide as the parent, for example.