Using multiple ViewHolders with RecyclerView - java

(Tho i am working with Xamarin(C#), the codes or the structure should seem identical to Java.)
So, in an Activity, i have a ViewPager and a TabLayout. Inside the ViewPager, there's a Fragment, which contains a RecyclerView inside a NestedScrollView.
I have two different layouts and i want to display them in the RecyclerView. After doing some research, this is what i ended up with :
Created two ViewHolder(s)
public class PostRegularViewHolder : RecyclerView.ViewHolder
{
public ImageView userImg { get; set; }
public TextView userFullName { get; set; }
public PostRegularViewHolder(View itemView) : base(itemView)
{
userImg = itemView.FindViewById<ImageView>(Resource.Id.feed_user_image);
userFullName = itemView.FindViewById<TextView>(Resource.Id.username);
}
}
public class CreatePostViewHolder : RecyclerView.ViewHolder
{
public ImageView userImg { get; set; }
public TextView userFirstName { get; set; }
public CreatePostViewHolder(View itemView) : base(itemView)
{
userImg = itemView.FindViewById<ImageView>(Resource.Id.topBar_user_image);
userFirstName = itemView.FindViewById<TextView>(Resource.Id.user_headerbar_title);
}
}
Inside the Adapter, i have:
public override int ItemCount
{
get
{
if (postCollection != null)
{
return postCollection.Count;
}
return 1; //Couldn't figure out what to return, so returned 1.
}
}
public PostRegularAdapter(List<PostRegular> postList = null, string userFirstName = null)
{
if (postList != null)
{
postCollection = postList;
}
UserFirstName = userFirstName;
}
public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType)
{
if (postCollection == null)
{
RecyclerView.ViewHolder vh = new CreatePostViewHolder(LayoutInflater.From(parent.Context).Inflate(Resource.Layout.customview_user_writepostbar, parent, false));
return vh;
}
else
{
RecyclerView.ViewHolder vh2 = new PostRegularViewHolder(LayoutInflater.From(parent.Context).Inflate(Resource.Layout.customview_postregular, parent, false));
return vh2;
}
}
Inside my Fragment, i have:
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
mainView = ///Inflated the mainview
var layoutManager = new LinearLayoutManager(Context, LinearLayoutManager.Vertical, false);;
recyclerView.SetLayoutManager(layoutManager);
recyclerView.SetAdapter(postRegularAdapter);
postRegularAdapter = new PostRegularAdapter(userFirstName: "Aousaf");
recyclerView.SetItemAnimator(new DefaultItemAnimator());
return mainView;
}
Here, in the OnCreateView method of the Fragment, i add the first view to the RecyclerView. To add the other view, i have this method inside the fragment :
public void PopulateWithPosts()
{
List<PostRegular> posts = new List<PostRegular>();
posts.Add(new PostRegular() { //passing the properties here });
postRegularAdapter.postCollection = posts;
postRegularAdapter.NotifyItemInserted(posts.Count - 1);
postRegularAdapter.NotifyDataSetChanged();
}
}
I call the PopulateWithPosts method from my activity, inside the OnTabSelected event of the TabLayout.
The problem here is that, the OnCreateViewHolder is not being called when i call the PopulateWithPosts method, or to narrow down, when i call the NotifyDataSetChanged method. What i can see using the debugger is that, OnCreateViewHolder is not called, rather, OnBindViewHolder is called.
The OnBindViewHolder method
public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
if (postCollection == null)
{
CreatePostViewHolder vh2 = holder as CreatePostViewHolder;
vh2.userFirstName.Text = UserFirstName + ", share something inspiring!";
}
else
{
PostRegularViewHolder vh = holder as PostRegularViewHolder;
//Working with the viewHolder...
}
According to the logic, the else block gets executed, however, as OnCreateViewHolder was not called this time, the holder remains CreatePostViewHolder aka the previous ViewHolder, which results in the cast failing, so i end up with vh being null.
I am a little puzzled here. Is there a way to call the OnCreateViewHolder when needed? Am i going in the right direction with adding multiple VIewHolders to a RecyclerView ? What am i missing here ?

When RecyclerView has multiple ViewHolders, we usually override GetItemViewType method.
getItemViewType(int position)
This method's default implementation will always return 0, indicating that there is only 1 type of view. In your case, it is not so, and so you will need find a way to assert which row corresponds to which view type.
Besides,when we should notice the viewType parameter of following method:
public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType)
According to the view type, we'll need to inflate the correct layout resource and create our view holder accordingly. The RecyclerView will handle recycling different view types in a way which avoids clashing of different view types.
For example:
(assume you can match your viewholders with your object's field Type)
private const int LAYOUT_ONE = 0;
private const int LAYOUT_TWO = 1;
method GetItemViewType
public override int GetItemViewType(int position)
{
if (items[position].Type == 0)
return LAYOUT_ONE;
else
return LAYOUT_TWO;
}
method OnCreateViewHolder
public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType)
{
View view = null;
switch (viewType)
{
case LAYOUT_ONE:
view = LayoutInflater.From(parent.Context).Inflate(Resource.Layout.customview_user_writepostbar, parent, false);
return new CreatePostViewHolder(view);
case LAYOUT_TWO:
view = LayoutInflater.From(parent.Context).Inflate(Resource.Layout.customview_postregular, parent, false);
return new PostRegularViewHolder(view);
}
}
method OnBindViewHolder
public override void
OnBindViewHolder (RecyclerView.ViewHolder holder, int position)
{
int type = GetItemViewType(position);
switch (type)
{
case LAYOUT_ONE:
CreatePostViewHolder vh2 = holder as CreatePostViewHolder;
vh2.userFirstName.Text = UserFirstName + ", share something inspiring!";
break;
case LAYOUT_TWO:
PostRegularViewHolder vh = holder as PostRegularViewHolder;
// other code
break;
default:
break;
}
}

Related

Recycleview with nested expandable lists in header

I am designing a relatively complex UI, i have searched stackoverflow and haven't found similar design. There could be many approaches to this, but i would like to ask expert opionions on how to achieve this and i would like to share my approach and make sure i am doing it the right way. My approach is that i have created a recycleview with header and inside header recycleview i am using an expandable recycleview library developed by h6ah4i (taken from github). Please let me know if there's a better approach to this.
The following image preview is a live mockup of final result i would like to get. It's not the actual screen. My question is what is the best way to achieve this, should i use expandable recycleview or expandable listview in recycleview header. I appreciate any answer as approaches or libraries it doesn't have to be similar to my code. Any suggestions are welcomed. I hope this post will also help other people like me in search of similar solution.
RecycleView Adapter
public class RecycleAdapterPlantSearch extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int TYPE_HEADER = 0;
private static final int TYPE_ITEM = 1;
private List<Plants> plantsList;
private Context context;
private OnItemClickListener onItemClickListener;
public RecycleAdapterPlantSearch(Context context, List<Plants> plantsList, OnItemClickListener onClickListener) {
this.context = context;
this.plantsList = plantsList;
onItemClickListener = onClickListener;
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_ITEM) {
// Here Inflating your recyclerview item layout
View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_view_plant_search_plant_item, parent, false);
return new ItemViewHolder(itemView, onItemClickListener);
} else if (viewType == TYPE_HEADER) {
// Here Inflating your header view
View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_view_plant_search_header, parent, false);
return new HeaderViewHolder(itemView, onItemClickListener);
} else return null;
}
#Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
/*
position 0 is for header
*/
if (holder instanceof HeaderViewHolder) {
// setheadersdata_flag = true;
HeaderViewHolder headerViewHolder = (HeaderViewHolder) holder;
// You have to set your header items values with the help of model class and you can modify as per your needs
// Setup expandable feature and RecyclerView
RecyclerViewExpandableItemManager expMgr = new RecyclerViewExpandableItemManager(null);
SimpleDemoExpandableItemAdapter.OnListItemClickMessageListener clickListener = message -> {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
};
List<BadgesVM> badgesVMList = null;
badgesVMList = new ArrayList() {{
add(new BadgesVM("447", "Bienenfreundlich", "bienenfreundlich", false));
add(new BadgesVM("320,322", "Vogelfreundlich", "vogelfreundlich", false));
add(new BadgesVM("321", "Insektenfreundlich", "insektenfreundlich", false));
add(new BadgesVM("445", "Ökologisch wertvoll", "oekologisch", false));
add(new BadgesVM("531", "Schmetterlings freundlich", "schmetterlings", false));
add(new BadgesVM("530", "Heimische Pflanze'", "heimische Pflanze'", false));
}};
// Create wrapped adapter: MyItemAdapter -> expMgr.createWrappedAdapter -> MyHeaderFooterAdapter
RecyclerView.Adapter adapter;
adapter = new SimpleDemoExpandableItemAdapter(context, expMgr,badgesVMList, clickListener);
adapter = expMgr.createWrappedAdapter(adapter);
//adapter = new DemoHeaderFooterAdapter(adapter, null);
headerViewHolder.recyclerViewExpandable.setAdapter(adapter);
headerViewHolder.recyclerViewExpandable.setLayoutManager(new LinearLayoutManager(context));
// NOTE: need to disable change animations to ripple effect work properly
((SimpleItemAnimator) headerViewHolder.recyclerViewExpandable.getItemAnimator()).setSupportsChangeAnimations(false);
expMgr.attachRecyclerView(headerViewHolder.recyclerViewExpandable);
} else if (holder instanceof ItemViewHolder) {
final ItemViewHolder itemViewHolder = (ItemViewHolder) holder;
itemViewHolder.plantDescText.setText(plantsList.get(position - 1).getDescription());
RequestOptions options = new RequestOptions()
.centerCrop()
.placeholder(R.drawable.background_small);
String imageUrl = APP_URL.BASE_ROUTE_INTERN + plantsList.get(position - 1).getImages().get(0).getSrcAttr();
Glide.with(context).load(imageUrl).apply(options).into(itemViewHolder.plantImg);
}
}
#Override
public int getItemViewType(int position) {
if (position == 0) {
return TYPE_HEADER;
}
return TYPE_ITEM;
}
#Override
public long getItemId(int position) {
return position;
}
// getItemCount increasing the position to 1. This will be the row of header
#Override
public int getItemCount() {
return plantsList.size() + 1;
}
public interface OnItemClickListener {
void OnItemClickListener(View view, int position);
void RecycleViewExtraDetails(ChipGroup chipGroup);
void nestedRecycleViewsSpecialOdd(RecyclerView nestedRecycleView);
}
private class HeaderViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
private TextView searchNameTxt, searchFamilyTxt, plantGroupTxt, plantFamilySearchTxt, ecologyFilterTxt,
frostSearchTxt;
private ChipGroup chipGroup;
private Button filterSearchBtn;
private CardView ecologyCv;
private CardView detailSearchCv;
private RecyclerView recyclerViewExpandable;
public HeaderViewHolder(View headerView, OnItemClickListener onItemClickListener) {
super(headerView);
searchNameTxt = headerView.findViewById(R.id.textView_plant_search_header_plant_search);
searchFamilyTxt = headerView.findViewById(R.id.textView_plant_search_header_plant_search);
ecologyCv = headerView.findViewById(R.id.cardView_plant_search_header_ecology);
detailSearchCv = headerView.findViewById(R.id.cardView_plant_search_header_detail_search);
plantGroupTxt = headerView.findViewById(R.id.textView_plant_search_header_plant_group);
plantFamilySearchTxt = headerView.findViewById(R.id.textView_plant_search_header_plant_family);
ecologyFilterTxt = headerView.findViewById(R.id.textView_plant_search_header_ecology_filter);
frostSearchTxt = headerView.findViewById(R.id.textView_plant_search_header_frost_filter);
chipGroup = headerView.findViewById(R.id.chip_group_plant_search_header);
filterSearchBtn = headerView.findViewById(R.id.button_plant_search_header_filter_search);
recyclerViewExpandable = headerView.findViewById(R.id.expandable_list_view_plant_search);
searchNameTxt.setOnClickListener(this);
searchFamilyTxt.setOnClickListener(this);
ecologyCv.setOnClickListener(this);
detailSearchCv.setOnClickListener(this);
plantGroupTxt.setOnClickListener(this);
plantFamilySearchTxt.setOnClickListener(this);
ecologyFilterTxt.setOnClickListener(this);
filterSearchBtn.setOnClickListener(this);
frostSearchTxt.setOnClickListener(this);
}
#Override
public void onClick(View view) {
onItemClickListener.OnItemClickListener(view, getAdapterPosition());
onItemClickListener.RecycleViewExtraDetails(chipGroup);
}
}
public class ItemViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
private Button readMoreBtn;
private TextView plantDescText;
private ImageView plantImg;
public ItemViewHolder(View itemView, OnItemClickListener onItemClickListener) {
super(itemView);
plantDescText = itemView.findViewById(R.id.textView_plant_search_plants_item_description_text);
readMoreBtn = itemView.findViewById(R.id.button_plant_search_plant_item_read_more);
plantImg = itemView.findViewById(R.id.imageView_plant_search_plants_item_plant_image);
readMoreBtn.setOnClickListener(this);
}
#Override
public void onClick(View view) {
onItemClickListener.OnItemClickListener(view, getAdapterPosition() - 1);
}
}
}
nested header recycleview
public class SimpleDemoExpandableItemAdapter extends AbstractExpandableItemAdapter<SimpleDemoExpandableItemAdapter.MyGroupViewHolder,
SimpleDemoExpandableItemAdapter.MyChildViewHolder> implements View.OnClickListener {
RecyclerViewExpandableItemManager mExpandableItemManager;
List<MyBaseItem> mItems;
OnListItemClickMessageListener mOnItemClickListener;
List<BadgesVM> badgesVMList;
Context context;
static class MyBaseItem {
public final int id;
public final String text;
public MyBaseItem(int id, String text) {
this.id = id;
this.text = text;
}
}
static abstract class MyBaseViewHolder extends AbstractExpandableItemViewHolder {
TextView textView;
Slider frostSlider;
RecyclerView detailRecycleView;
public MyBaseViewHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(android.R.id.text1);
frostSlider = itemView.findViewById(R.id.slider_plant_search_expandable);
detailRecycleView = itemView.findViewById(R.id.recycle_view_plant_search_detail_search);
}
}
static class MyGroupViewHolder extends MyBaseViewHolder {
public MyGroupViewHolder(View itemView) {
super(itemView);
}
}
static class MyChildViewHolder extends MyBaseViewHolder {
public MyChildViewHolder(View itemView) {
super(itemView);
}
}
public SimpleDemoExpandableItemAdapter(Context context, RecyclerViewExpandableItemManager expMgr, List<BadgesVM> badgesVMList, OnListItemClickMessageListener clickListener) {
mExpandableItemManager = expMgr;
mOnItemClickListener = clickListener;
this.badgesVMList = badgesVMList;
this.context = context;
setHasStableIds(true); // this is required for expandable feature.
mItems = new ArrayList<>();
mItems.add(new MyBaseItem(0, "Filter nach ökologischen Kriterien"));
mItems.add(new MyBaseItem(1, "Frosthärte"));
mItems.add(new MyBaseItem(2, "Detailsuche"));
}
#Override
public int getGroupCount() {
return mItems.size();
}
#Override
public int getChildCount(int groupPosition) {
int childCount = 0;
int groupId = mItems.get(groupPosition).id;
if (groupId == 0) {
childCount = badgesVMList.size();
} else if (groupId == 1) {
childCount = 1; //contains only one item
} else if (groupId == 2) {
childCount = 1; //contains only one item
}
return childCount;
}
#Override
public long getGroupId(int groupPosition) {
// This method need to return unique value within all group items.
return mItems.get(groupPosition).id;
}
#Override
public long getChildId(int groupPosition, int childPosition) {
// This method need to return unique value within the group.
int groupId = mItems.get(groupPosition).id;
int childId = 0;
if (groupId == 0) {
badgesVMList.get(childPosition).getId();
} else if (groupId == 1) {
childId = 0;
} else if (groupId == 2) {
childId = 0;
}
return childId;
}
#Override
#NonNull
public MyGroupViewHolder onCreateGroupViewHolder(#NonNull ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_group_item_for_expandable_minimal, parent, false);
MyGroupViewHolder vh = new MyGroupViewHolder(v);
vh.itemView.setOnClickListener(this);
return vh;
}
#Override
#NonNull
public MyChildViewHolder onCreateChildViewHolder(#NonNull ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_child_item_for_expandable_minimal, parent, false);
MyChildViewHolder vh = new MyChildViewHolder(v);
vh.itemView.setOnClickListener(this);
return vh;
}
#Override
public void onBindGroupViewHolder(#NonNull MyGroupViewHolder holder, int groupPosition, int viewType) {
MyBaseItem group = mItems.get(groupPosition);
holder.textView.setText(group.text);
}
#Override
public void onBindChildViewHolder(#NonNull MyChildViewHolder holder, int groupPosition, int childPosition, int viewType) {
int groupId = mItems.get(groupPosition).id;
if (groupId == 0) {
BadgesVM badgesVM = badgesVMList.get(childPosition);
holder.textView.setVisibility(View.VISIBLE);
holder.frostSlider.setVisibility(View.GONE);
holder.detailRecycleView.setVisibility(View.GONE);
holder.textView.setText(badgesVM.getName());
} else if (groupId == 1) {
holder.textView.setVisibility(View.GONE);
holder.frostSlider.setVisibility(View.VISIBLE);
holder.detailRecycleView.setVisibility(View.GONE);
} else if (groupId == 2) {
holder.textView.setVisibility(View.GONE);
holder.frostSlider.setVisibility(View.GONE);
holder.detailRecycleView.setVisibility(View.VISIBLE);
// Setup expandable feature and RecyclerView
RecyclerViewExpandableItemManager expMgr = new RecyclerViewExpandableItemManager(null);
DetailSearchExpandableItemAdapter.OnListItemClickMessageListener clickListener = message -> {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
};
List<BadgesVM> badgesVMList = null;
badgesVMList = new ArrayList() {{
add(new BadgesVM("447", "Bienenfreundlich", "bienenfreundlich", false));
add(new BadgesVM("320,322", "Vogelfreundlich", "vogelfreundlich", false));
add(new BadgesVM("321", "Insektenfreundlich", "insektenfreundlich", false));
add(new BadgesVM("445", "Ökologisch wertvoll", "oekologisch", false));
add(new BadgesVM("531", "Schmetterlings freundlich", "schmetterlings", false));
add(new BadgesVM("530", "Heimische Pflanze'", "heimische Pflanze'", false));
}};
// Create wrapped adapter: MyItemAdapter -> expMgr.createWrappedAdapter -> MyHeaderFooterAdapter
RecyclerView.Adapter adapter2;
adapter2 = new DetailSearchExpandableItemAdapter(context, expMgr, badgesVMList, clickListener);
adapter2 = expMgr.createWrappedAdapter(adapter2);
//adapter = new DemoHeaderFooterAdapter(adapter, null);
holder.detailRecycleView.setAdapter(adapter2);
holder.detailRecycleView.setLayoutManager(new LinearLayoutManager(context));
// NOTE: need to disable change animations to ripple effect work properly
((SimpleItemAnimator) holder.detailRecycleView.getItemAnimator()).setSupportsChangeAnimations(false);
expMgr.attachRecyclerView(holder.detailRecycleView);
}
}
#Override
public boolean onCheckCanExpandOrCollapseGroup(#NonNull MyGroupViewHolder holder, int groupPosition, int x, int y, boolean expand) {
// handles click event manually (to show Snackbar message)
return false;
}
#Override
public void onClick(View v) {
RecyclerView rv = RecyclerViewAdapterUtils.getParentRecyclerView(v);
RecyclerView.ViewHolder vh = rv.findContainingViewHolder(v);
int rootPosition = vh.getAdapterPosition();
if (rootPosition == RecyclerView.NO_POSITION) {
return;
}
// need to determine adapter local flat position like this:
RecyclerView.Adapter rootAdapter = rv.getAdapter();
int localFlatPosition = WrapperAdapterUtils.unwrapPosition(rootAdapter, this, rootPosition);
long expandablePosition = mExpandableItemManager.getExpandablePosition(localFlatPosition);
int groupPosition = RecyclerViewExpandableItemManager.getPackedPositionGroup(expandablePosition);
int childPosition = RecyclerViewExpandableItemManager.getPackedPositionChild(expandablePosition);
String message;
if (childPosition == RecyclerView.NO_POSITION) {
// Clicked item is a group!
// toggle expand/collapse
if (mExpandableItemManager.isGroupExpanded(groupPosition)) {
mExpandableItemManager.collapseGroup(groupPosition);
message = "COLLAPSE: Group " + groupPosition;
} else {
mExpandableItemManager.expandGroup(groupPosition);
message = "EXPAND: Group " + groupPosition;
}
} else {
// Clicked item is a child!
message = "CLICKED: Child " + groupPosition + "-" + childPosition;
}
mOnItemClickListener.onItemClicked(message);
}
public interface OnListItemClickMessageListener {
void onItemClicked(String message);
}
}
You were right to search a library that does most of the work for you, but I don't like the library you picked. It does not seem very flexible and concise. I suggest to take a look at Groupie, its API is pretty clean. Also check Reddit for some discussion on libraries.
If you want to write it yourself I think you can solve it without nested Adapter's. Just create an 'expandable group' item type. Then in getItemCount() you count all items and their nested items (when expanded). Take a look at the Groupie source code.
Some additional feedback on your code:
I would explicitly add the header to the list of items you give to your adapter. So instead of a List<Plants>, you rather provide a List<Item> and have a HeaderItem and PlantsItem. This way you have a clear separation between your domain models (Plants) and view models (the items) in your adapter.
Your onBindViewHolder() method does way too much. Let your ViewHolder subclasses take care of that. Create an abstract ViewHolder with an abstract bindTo(Item item) method. Then in your HeaderViewHolder subclass it and do the actual work (after an instanceof check).
Have a look at view binding, it can make your code more concise. (So does Kotlin.)
You can use ConcatAdapter to have multiple adapters with ViewHolders that hold different type of layouts even with the ones that contain RecyclerViews, i used in my last project and it works fine, you can check it out here, dashboard module uses multiple adapters to have different type of layouts.
You can also use the approach they used in Google iosched app to have one adapter with multiple layouts in better way where you move logic from adapter to ViewHolders and their wrapper class ViewBinders. ViewBinder is responsible of
calling onViewHolder, onCreateViewHolder and bind data type to a ViewBinder and ViewBinder to a layout. There is an article about how to use it in medium, i will post the link if i can find it. You can also check out this sample i created for animations but used ViewBinders in a simple form to create different type of layouts.
Below is the type of data and layout i wish to show in GridLayout and in which order
val data = mutableListOf<Any>().apply {
// Add Vector Drawables
add(HeaderModel("Animated Vector Drawable"))
add(AVDModel(R.drawable.avd_likes))
add(AVDModel(R.drawable.avd_settings))
add(HeaderModel("Seekable Vector Drawable"))
add(SeekableVDModel(R.drawable.avd_compass_rotation))
add(SeekableVDModel(R.drawable.avd_views))
add(SeekableVDModel(R.drawable.avd_hourglass))
add(HeaderModel("Clocks"))
add(AVDModel(R.drawable.avd_clock_alarm))
add(AVDModel(R.drawable.avd_clock_clock))
add(AVDModel(R.drawable.avd_clock_stopwatch))
}
These are correspond type of data i want to use in my RecyclerView, it's the types and binding to ViewHolder and layout in these classes.
private fun createViewBinders(): HashMap<ItemClazz, MappableItemBinder> {
val avdViewBinder = AVDViewBinder()
val seekableVDViewBinder = SeekableVDViewBinder()
val headViewBinder = HeaderViewBinder()
return HashMap<ItemClazz, MappableItemBinder>()
.apply {
put(
avdViewBinder.modelClazz,
avdViewBinder as MappableItemBinder
)
put(
seekableVDViewBinder.modelClazz,
seekableVDViewBinder as MappableItemBinder
)
put(
headViewBinder.modelClazz,
headViewBinder as MappableItemBinder
)
}
}
And set the data List to adapter and let adapter call corresponding layout that is bound to data
val dataList:List<Any> = getVectorDrawableItemList()
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
val adapter = MultipleViewBinderListAdapter(
createViewBinders(),
RecyclerView.Adapter.StateRestorationPolicy.ALLOW
).apply {
submitList(dataList)
}
For the expandable list, iosched app good way of doing it, there is video about how to animate expandable items in RecyclerVİew here. You can set state in ViewHolder and even use MotionLayout for animating from collapsed to expandable state. All can be done without any third party library and very clean way.

How can I stop my recyclerView view type from changing after scrolling?

Inside my RecyclerView Adapter class I have 2 view types to display the results of my query:
#Query("SELECT l.log_id, l.junction_id ,l.date, l.workout_id, l.total_weight_lifted,
l.reps, l.set_number FROM log_entries_table
AS l LEFT JOIN exercise_workout_junction_table AS ej
ON ej.exercise_workout_id = l.junction_id WHERE ej.exercise_id = :exerciseID
ORDER BY substr(l.date, -4) DESC, substr(l.date, -7) DESC, (l.date) DESC")
LiveData<List<Log_Entries>> getAllExerciseHistoryLogs(int exerciseID);
The first view type is used to display all logEntries in which the date is unique:
The second view type is to display the rest of the logEntries which share the same date as the above:
My current code works fine, however every time I scroll down and the recyclerView updates, all the log-Entries with 'unique' dates (which should use the first viewType) get changed to display the second view type.
How can I stop my recyclerView view type from changing?
Before scroll -> After Scroll
RecyclerView Adapter
public class ExerciseHistoryAdapter2 extends RecyclerView.Adapter {
private OnItemClickListener listener;
private List<Log_Entries> allLogEntries = new ArrayList<>();
private List<String> uniqueDates = new ArrayList<>();
String logEntryDate;
public void setExercises(List<Log_Entries> allLogEntries) {
this.allLogEntries = allLogEntries;
notifyDataSetChanged();
}
#NonNull
#Override
public RecyclerView.ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
View view;
if (viewType == 0) {
view = layoutInflater.inflate(R.layout.exercise_history_item, parent, false);
return new ViewHolderOne(view);
}
view = layoutInflater.inflate(R.layout.exercise_history_item_two, parent, false);
return new ViewHolderTwo(view);
}
#Override
public long getItemId(int position) {
return allLogEntries.get(position).getLog_id();
}
#Override
public void onBindViewHolder(#NonNull RecyclerView.ViewHolder holder, int position) {
logEntryDate = allLogEntries.get(position).getDate();
if (uniqueDates.contains(logEntryDate)) {
// bindViewHolder2
ViewHolderTwo viewHolderTwo = (ViewHolderTwo) holder;
viewHolderTwo.textViewWeight.setText(String.valueOf(allLogEntries.get(position).getTotal_weight_lifted()));
viewHolderTwo.textViewReps.setText(String.valueOf(allLogEntries.get(position).getReps()));
} else {
uniqueDates.add(logEntryDate);
//bind viewholder1
ViewHolderOne viewHolderOne = (ViewHolderOne) holder;
viewHolderOne.textViewDate.setText(allLogEntries.get(position).getDate());
viewHolderOne.textViewWeight.setText(String.valueOf(allLogEntries.get(position).getTotal_weight_lifted()));
viewHolderOne.textViewReps.setText(String.valueOf(allLogEntries.get(position).getReps()));
}
}
#Override
public int getItemCount() {
return allLogEntries.size();
}
#Override
public int getItemViewType(int position) {
logEntryDate = allLogEntries.get(position).getDate();
if (uniqueDates.contains(logEntryDate)) {
return 1;
}
return 0;
}
class ViewHolderOne extends RecyclerView.ViewHolder {
private TextView textViewDate;
private TextView textViewWeight;
private TextView textViewReps;
public ViewHolderOne(#NonNull View itemView) {
super(itemView);
textViewDate = itemView.findViewById(R.id.textView_dateH);
textViewWeight = itemView.findViewById(R.id.textView_weightH);
textViewReps = itemView.findViewById(R.id.textView_repss);
itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
int position = getAdapterPosition();
if (listener != null && position != RecyclerView.NO_POSITION) {
listener.onItemClick(allLogEntries.get(position));
}
}
});
}
}
class ViewHolderTwo extends RecyclerView.ViewHolder {
private TextView textViewWeight;
private TextView textViewReps;
public ViewHolderTwo(#NonNull View itemView) {
super(itemView);
textViewWeight = itemView.findViewById(R.id.textView_weightH2);
textViewReps = itemView.findViewById(R.id.textView_repss2);
itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
int position = getAdapterPosition();
if (listener != null && position != RecyclerView.NO_POSITION) {
listener.onItemClick(allLogEntries.get(position));
}
}
});
}
}
public interface OnItemClickListener {
void onItemClick(Log_Entries log_entries);
}
public void setOnItemClickListener(OnItemClickListener listener) {
this.listener = listener;
}
}
Your getItemViewType and onBindViewHolder has some issues.
#Override
public int getItemViewType(int position) {
// type 0 = with date header
// type 1 = without date header
// if list is sorted chronologically
if (position == 0) {
return 0
}
String currentDate = allLogEntries.get(position).getDate();
String previousDate = allLogEntries.get(position - 1).getDate();
if (currentDate.equals(previousDate)) {
return 1
} else {
return 0
}
}
The first item will always have the header since the list is sorted chronologically. For the rest of the items, you need to check whether the date for the current item is the same as the previous item. Based on that condition you return the type.
You do not need to manage a list of unique dates. You have shared mutable states between the functions that are being called multiple times and are not synced. Just delete these and the references of them from onBindViewHolder
private List<String> uniqueDates = new ArrayList<>();
String logEntryDate;
I think to set holder.setIsRecyclable(false); would solve the issue, because the recycler view will then no longer recycle the items... But this is not a good solution for long lists.
EDIT:
I reviewd your code in onBindViewHolder()...
I think the problem comes with uniqueDates.add(logEntryDate); and that the onBindViewHolder method is called multiple times.
This is how the recycler view proceeds:
the first item in list will be unique because uniqueDates is empty. Therefore it will be added to the list.
the other items will be added correctly, as you see in your first screenshot
when you scroll down, the onBindViewHolder method will be executed for every item again
because the uniqueDates list already contains the first date, as it was added in step one, this item will now recognized as not-unique one
the wrong list will be displayed after scrolling as you see in your second screenshot
SOLUTION:
You will have to add a logic which identifies unique dates in another way, which is independet of the onBindViewholder method
OR
you would have to add code, that removes dates on a specific point, so that the list identifies the first item every time as unique and not just the first time.

Get data in my RecyclerView

I started to work with Retrofit. In my application i have one RecyclerView with two ViewHolder's, where trying to get data from my models, in logs coming type:
#Override
public void onResponse(Call<DropDown> call, Response<DropDown> response) {
DropDown jsonResponse = response.body();
Log.d("type",jsonResponse.getForm().getmGroupss().get(1).getmControls().get(2).getTitle()+"");
adapter = new DataAdapter(response.body());
recyclerView.setAdapter(adapter);
}
How can I display this type in my RecyclerView, if there will be a few types?
Now i get next message:
09-13 09:20:40.958 2798-2798/com.random.secondproject E/RecyclerView: No adapter attached; skipping layout
DataAdapter:
public class DataAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
public static final String TYPE_TEXTVIEW = "0";
public static final String TYPE_EDITVIEW = "1";
private DropDown mList;
public DataAdapter(DropDown list) {
this.mList = list;
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view;
switch (viewType) {
case 0:
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.text_numeric, parent, false);
return new NumericViewHolder(view);
case 1:
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.drop_down_options, parent, false);
return new DropDownViewHolder(view);
}
return null;
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
DropDown object = mList.get(position);
if (object != null) {
switch (object.getTitle()){
case TYPE_TEXTVIEW:
((NumericViewHolder) holder).title.setText(String.valueOf(object.getTitle()));
break;
case TYPE_EDITVIEW:
((DropDownViewHolder) holder).title_options.setText(String.valueOf(object.getTitle()));
break;
}
}
}
#Override
public int getItemCount() {
if (mList == null)
return 0;
return mList.size();
}
#Override
public int getItemViewType(int position) {
DropDown object = mList.get(position);
if (object.getTitle().equals("drop_down_options"))
return 0;
else return 1;
}
public class NumericViewHolder extends RecyclerView.ViewHolder {
private TextView title;
public NumericViewHolder(View itemView) {
super(itemView);
title = (TextView) itemView.findViewById(R.id.title);
}
}
public class DropDownViewHolder extends RecyclerView.ViewHolder {
private EditText title_options;
public DropDownViewHolder(View itemView) {
super(itemView);
title_options = (EditText) itemView.findViewById(R.id.title_options);
}
As you can see, I'm going to take two types (TextView and EditText)
so I will write a few ways to model(array i think). For example, as I wrote in Log.d .
Everything seem ok to me in your adapter. Try to call recyclerView.invalidate(). The idea is that you draw your RecyclerView before calling the onResponse method. Thus, you have empty view. The invalidate() method will try to redraw the view.
There are some error so that onResponse function not call, or you do with element in recyclerview before onResponse called.
You need create an adapter with empty Dropdown
adapter = new DataAdapter(new Dropdown());
When onResponse called you only notifyDatasetChange to adapter, don't setAdapter again.
Or you put the code that pratice with element of recyclerview to onResponse.

Why is adding an OnClickListener inside onBindViewHolder of a RecyclerView.Adapter considered bad practice?

I have the following code for a RecyclerView.Adapter class and it works fine:
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.Viewholder> {
private List<Information> items;
private int itemLayout;
public MyAdapter(List<Information> items, int itemLayout){
this.items = items;
this.itemLayout = itemLayout;
}
#Override
public Viewholder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(itemLayout, parent, false);
return new Viewholder(v);
}
#Override
public void onBindViewHolder(Viewholder holder, final int position) {
Information item = items.get(position);
holder.textView1.setText(item.Title);
holder.textView2.setText(item.Date);
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Toast.makeText(view.getContext(), "Recycle Click" + position, Toast.LENGTH_SHORT).show();
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View v) {
Toast.makeText(v.getContext(), "Recycle Click" + position, Toast.LENGTH_SHORT).show();
return true;
}
});
}
#Override
public int getItemCount() {
return items.size();
}
public class Viewholder extends RecyclerView.ViewHolder {
public TextView textView1;
public TextView textView2;
public Viewholder(View itemView) {
super(itemView);
textView1=(TextView) itemView.findViewById(R.id.text1);
textView2 = (TextView) itemView.findViewById(R.id.date_row);
}
}
}
However, I believe it is bad practice to implement the OnClickListener in the onBindViewHolder method. Why is this bad practice, and what is a better alternative?
The reason it is better to handle your click logic inside the ViewHolder is because it allows for more explicit click listeners. As expressed in the Commonsware book:
Clickable widgets, like a RatingBar, in a ListView row had long been in conflict with click events on rows themselves. Getting rows that can be clicked, with row contents that can also be clicked, gets a bit tricky at times. With RecyclerView, you are in more explicit control over how this sort of thing gets handled… because you are the one setting up all of the on-click handling logic.
By using the ViewHolder model you can gain a lot of benefits for click handling in a RecyclerView than previously in the ListView. I wrote about this in a blog post comparing the differences - https://androidessence.com/recyclerview-vs-listview
As for why it is better in the ViewHolder instead of in onBindViewHolder(), that is because onBindViewHolder() is called for each and every item and setting the click listener is an unnecessary option to repeat when you can call it once in your ViewHolder constructor. Then, if your click responds depends on the position of the item clicked, you can simply call getAdapterPosition() from within the ViewHolder. Here is another answer I've given that demonstrates how you can use the OnClickListener from within your ViewHolder class.
The method onBindViewHolder is called every time when you bind your view with object which just has not been seen. And every time you will add a new listener.
Instead what you should do is, attaching click listener on onCreateViewHolder
example :
#Override
public Viewholder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(itemLayout, parent, false);
final ViewHolder holder = new ViewHolder(v);
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Log.d(TAG, "position = " + holder.getAdapterPosition());
}
});
return holder;
}
The onCreateViewHolder() method will be called the first several times a ViewHolder is needed of each viewType. The onBindViewHolder() method will be called every time a new item scrolls into view, or has its data change. You want to avoid any expensive operations in onBindViewHolder() because it can slow down your scrolling. This is less of a concern in onCreateViewHolder(). Thus it's generally better to create things like OnClickListeners in onCreateViewHolder() so that they only happen once per ViewHolder object. You can call getLayoutPosition() inside the listener in order to get the current position, rather than taking the position argument provided to onBindViewHolder().
Pavel provided great code example except one line in the end. You should return created holder. Not the new Viewholder(v).
#Override
public Viewholder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(itemLayout, parent, false);
final ViewHolder holder = new ViewHolder(v);
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Log.d(TAG, "position = " + holder.getAdapterPosition());
}
});
return holder;
}
Per https://developer.android.com/topic/performance/vitals/render, onBindViewHolder should do its work in "much less than one millisecond" to prevent slow rendering.
RecyclerView: Bind taking too long
Bind (that is, onBindViewHolder(VH, int)) should be very simple, and
take much less than one millisecond for all but the most complex
items. It simply should take POJO items from your adapter's internal
item data, and call setters on views in the ViewHolder. If RV
OnBindView is taking a long time, verify that you're doing minimal
work in your bind code.
This is how I implement the clicks of my buttons in my ViewHolder instead of my onBindViewHolder. This example shows how to bind more than one button with an interface, which will not generate more objects while populating rows.
The example is in Spanish and in Kotlin, but I'm sure the logic is understandable.
/**
* Created by Gastón Saillén on 26 December 2019
*/
class DondeComprarRecyclerAdapter(val context:Context,itemListener:RecyclerViewClickListener):RecyclerView.Adapter<BaseViewHolder<*>>() {
interface RecyclerViewClickListener {
fun comoLlegarOnClick(v: View?, position: Int)
fun whatsappOnClick(v:View?,position: Int)
}
companion object{
var itemClickListener: RecyclerViewClickListener? = null
}
init {
itemClickListener = itemListener
}
private var adapterDataList = mutableListOf<Institucion>()
fun setData(institucionesList:MutableList<Institucion>){
this.adapterDataList = institucionesList
}
fun getItemAt(position:Int):Institucion = adapterDataList[position]
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<*> {
val view = LayoutInflater.from(context)
.inflate(R.layout.dondecomprar_row, parent, false)
return PuntosDeVentaViewHolder(view)
}
override fun getItemCount(): Int {
return if(adapterDataList.size > 0) adapterDataList.size else 0
}
override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) {
val element = adapterDataList[position]
when(holder){
is PuntosDeVentaViewHolder -> holder.bind(element)
else -> throw IllegalArgumentException()
}
}
inner class PuntosDeVentaViewHolder(itemView: View):BaseViewHolder<Institucion>(itemView),View.OnClickListener{
override fun bind(item: Institucion) {
itemView.txtTitleDondeComprar.text = item.titulo
itemView.txtDireccionDondeComprar.text = item.direccion
itemView.txtHorarioAtencDondeComprar.text = item.horario
itemView.btnComoLlegar.setOnClickListener(this)
itemView.btnWhatsapp.setOnClickListener(this)
}
override fun onClick(v: View?) {
when(v!!.id){
R.id.btnComoLlegar -> {
itemClickListener?.comoLlegarOnClick(v, adapterPosition)
}
R.id.btnWhatsapp -> {
itemClickListener?.whatsappOnClick(v,adapterPosition)
}
}
}
}
}
And the BaseViewHolder to implement in each adapter
/**
* Created by Gastón Saillén on 27 December 2019
*/
abstract class BaseViewHolder<T>(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun bind(item: T)
}
i faced a small problem which i want to share in the answers if someone else also face it.
i had image and text to show in Recycleview as Cardview. Thus my code according to recommendations should be as follow.
#Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.books_item_row, parent, false);
final MyViewHolder holder = new MyViewHolder(itemView);
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Toast.makeText(getActivity(), "Recycle Click", Toast.LENGTH_LONG).show();
}
});
return holder;
}
however when i will click the card in recycle view it will not work as the itemview is below the image. Thus i slightly changed the code as follow.
#Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.books_item_row, parent, false);
final MyViewHolder holder = new MyViewHolder(itemView);
holder.thumbnail.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
//Log.d(TAG, "position = " + holder.getAdapterPosition());
Toast.makeText(getActivity(), "Recycle Click", Toast.LENGTH_LONG).show();
}
});
return holder;
}
i.e instead of itemview now person will have to click on thumbnail or image.
You can do in this way too..
MainActivity class
in this multiple type of interface triggers you can achieve this...
Adapter class

How to handle checkboxes in a listview and send information back from a custom base adapter

I have a list view and each item contains a check box and other various text views. In the Main Activity I have an ArrayList of objects called listOfStuff. From the main activity I'm defining and using a custom base adapter. In the getView method I defined a listener for the check box like so:
holder.cbCompletionStatus.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if(holder.cbCompletionStatus.isChecked()){
listOfStuff.get(position).setComplete(1);
} else {
listOfStuff.get(position).setComplete(0);
};
}
});
My problem is that I don't know how to access the listOfStuff and the objects within it to modify the information within. The code in the if/else statement hopefully gives an idea what I was trying to do. Just a quick warning, I'm not only new to Android and Java, but to programming field on the whole. Thanks.
UPDATE:
So I ended up figuring this out on my own. I just had to make the listOfStuff Arraylist a static in the Main Activity. Then I could call a static function in the Main Activity to manipulate whatever data in the Array list I needed like so:
MainActivity.checkBoxClicked(result, position);
Here is my class:
class ImageInfoAdapter extends BaseAdapter{
#Override
public int getCount() {
if(viewcount == 0){
return 0;
}
return viewcount;
}
#Override
public Object getItem(int position) {
return isSentAlList.get(position);
}
#Override
public long getItemId(int position) {
return position;
}
#Override
public View getView(int position, View view, ViewGroup parent) {
final ViewHolder viewHolder;
View rowView=view;
if(rowView==null){
LayoutInflater layoutinflate = LayoutInflater.from(ListPictures.this);
rowView=layoutinflate.inflate(R.layout.listviewayout, parent, false);
viewHolder = new ViewHolder();
viewHolder.textViewisSentFlag = (TextView)rowView.findViewById(R.id.textViewisSentFlag);
viewHolder.imageViewToSent = (ImageView)rowView.findViewById(R.id.imageViewToSent);
viewHolder.checkBoxToSend = (CheckBox)rowView.findViewById(R.id.checkBoxToSend);
rowView.setTag(viewHolder);
} else{
viewHolder = (ViewHolder) rowView.getTag();
}
viewHolder.ref = position;
Log.i("InfoLog","viewHolder.ref = position; "+viewHolder.ref);
viewHolder.textViewisSentFlag.setText(isSentAlList.get(position));
Bitmap blob = BitmapFactory.decodeByteArray(imageAlList.get(position), 0, imageAlList.get(position).length);
viewHolder.imageViewToSent.setImageBitmap(blob);
viewHolder.checkBoxToSend.setClickable(true);
if(checked.containsKey(""+viewHolder.ref)){ ///if this id is present as key in hashmap
Log.i("InfoLog","checked.containsKey "+viewHolder.ref);
if(checked.get(""+viewHolder.ref).equals("true")){ //also check whether it is true or false to check/uncheck checkbox
Log.i("InfoLog","checked.get(position) "+viewHolder.ref);
viewHolder.checkBoxToSend.setChecked(true);
} else
viewHolder.checkBoxToSend.setChecked(false);
} else
viewHolder.checkBoxToSend.setChecked(false);
viewHolder.checkBoxToSend.setOnCheckedChangeListener(new OncheckchangeListner(viewHolder));
return rowView;
}//End of method getView
}//End of class ImageInfo
class ViewHolder{
private TextView textViewisSentFlag = null;
private ImageView imageViewToSent = null;
private CheckBox checkBoxToSend = null;
int ref;
}//End of class ViewHolder
/////////////////////////
and here is my oncheckchangedlistener
////////////////////////
class OncheckchangeListner implements OnCheckedChangeListener{
ViewHolder viewHolder = null;
public OncheckchangeListner(ViewHolder viHolder) {
viewHolder = viHolder;
}
#Override
public void onCheckedChanged(CompoundButton buttonView,
boolean isChecked) {
if(viewHolder.checkBoxToSend.equals(buttonView)) {
if(!isChecked) {
Log.i("InfoLog","checked.get before "+checked.get(""+viewHolder.ref));
checked.put(""+viewHolder.ref,"false");
Log.i("InfoLog","checked.get after "+checked.get(""+viewHolder.ref));
} else
checked.put(""+viewHolder.ref,"true");
} else
Log.i("InfoLog","i m in checkchange ");
}
}

Categories