I'm trying to build a simple chat interface. When a user click enter their message is sent to the server, added to the DB and simultaneously inserted into the MessageAdapter that's on the client's device so it appears on their screen.
Messages from the user are right-aligned and vice-versa for non-users.
When a user sends a message, as opposed to all the messages shifting up one spot, the text shifts up but not the actual message objects.
I'm trying to have a user send a message and all previous messages effectively shift up so the new message can appear below.
Initially the non-user message is Hello but after I send the user's message, the message below the non-user, Hey takes its place.
This is my code for the MessageAdapter:
public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
List<Message> data= Collections.emptyList();
List<Integer> message_type= Collections.emptyList();
// create constructor to initialize context and data sent from MainActivity
public MessageAdapter(List<Message> data, List<Integer> message_type) {
this.data=data;
this.message_type=message_type;
}
public void addItem(Message item, int message_type){
this.data.add(item);
this.message_type.add(0, message_type);
}
// Inflate the layout when ViewHolder created
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
switch (i) {
case 0:
View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.message_non_user, viewGroup, false);
return new ViewHolder0(v);
case 2:
View q = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.message_user, viewGroup, false);
return new ViewHolder1(q);
}
View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.message_non_user, viewGroup, false);
return new ViewHolder1(v);
}
// Bind data
#Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, final int position) {
switch (viewHolder.getItemViewType()) {
case 0:
ViewHolder0 viewHolder0 = (ViewHolder0)viewHolder;
Message current0 = data.get(position);
viewHolder0.body.setText(current0.message);
break;
case 2:
ViewHolder1 viewHolder1 = (ViewHolder1)viewHolder;
Message current1 = data.get(position);
viewHolder1.body.setText(current1.message);
break;
}
}
// return total item from List
#Override
public int getItemCount() {
return data.size();
}
#Override
public int getItemViewType(int position) {
int temp = message_type.get(position);
if(temp == 0 ){
return 0;
}
return 2;
}
class ViewHolder0 extends RecyclerView.ViewHolder {
TextView body;
public ViewHolder0(final View itemView) {
super(itemView);
body = (TextView) itemView.findViewById(R.id.message_body);
}
}
class ViewHolder1 extends RecyclerView.ViewHolder {
TextView body;
public ViewHolder1 (final View itemView) {
super(itemView);
body = (TextView) itemView.findViewById(R.id.message_body);
}
}
}
And then this is the code for the send-message action:
Message user_input = new Message();
user_input.message = chat_input.getText().toString();
messageAdapter.addItem(user_input, 0);
messageAdapter.notifyItemInserted(messageAdapter.getItemCount()+1);
MessagesRecyclerview.scrollToPosition(messageAdapter.getItemCount() - 1);
messageAdapter.notifyDataSetChanged();
chat_input.getText().clear();
Thanks for your help :)
It may be because you are adding your data like this:
this.data.add(item);
this.message_type.add(0, message_type);
you are adding your types at the begining of the list but you are adding your data at the end of the list, so it looks like there is a shift in your data.
use this:
this.data.add(0,item);
this.message_type.add(0, message_type);
Or this:
this.data.add(item);
this.message_type.add(message_type);
Related
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.
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.
(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;
}
}
I'm trying to set up a Recyclerview with multiple lists(like Facebook ads and user feed) from 2 API endpoints but getting some errors when I try to paginate the list(load more).
Here is my code. I've tried to call the addAll method in my activity after loading the two lists from the different API endpoints but I get an error(I also call this method in my loadMore listener[paginate stuff]).
private static final int TYPE_VIDEO = 0;
private static final int TYPE_PROMOTED_VIDEO = 1;
private static final int TYPE_LOADING = 3;
private Context context;
List<Video> videosList;
List<Promoted> promotedVideosList;
private boolean isLoadingAdded = true;
public MyAdapter(Context context){
this.context = context;
//Initalization stuffs
}
#NonNull
#Override
public RecyclerView.ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
RecyclerView.ViewHolder viewHolder = null;
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
switch (viewType) {
case TYPE_VIDEO:
View viewVideo = inflater.inflate(R.layout.item_video, parent, false);
viewHolder = new VideoVH(viewVideo);
break;
case TYPE_PROMOTED_VIDEO:
View viewPromoted = inflater.inflate(R.layout.item_promoted_video, parent, false);
viewHolder = new PromotedVH(viewPromoted);
break;
case TYPE_LOADING:
View viewLoading = inflater.inflate(R.layout.item_loading, parent, false);
viewHolder = new LoadingVH(viewLoading);
break;
}
return viewHolder;
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
switch (getItemViewType(position)) {
case TYPE_VIDEO:
//DO something when it a normal video
break;
case TYPE_PROMOTED_VIDEO:
//DO something when it's a promoted video
break;
case TYPE_LOADING:
//Loading view
break;
}
}
public void add(Video video, Promoted promoted) {
Toast.makeText(context, "Not Null", Toast.LENGTH_SHORT).show();
videosList.add(video);
promotedVideosList.add(promoted);
notifyItemInserted(videosList.size() + promotedVideosList.size() - 1);
}
public void addAll(List<Video> videoList, List<Promoted> promotedList) {
add(addVideo(videoList), addPromoted(promotedList));
}
private Video addVideo(List<Video> videoList){
for (Video result : videoList) return result;
return null;
}
private Promoted addPromoted(List<Promoted> promotedList{
for (Promoted result : promotedList) return result;
return null;
}
#Override
public int getItemCount() {
return videosList == null && promotedVideosList == null ? 0 : this.videosList.size() + this.promotedVideosList.size();
}
#Override
public int getItemViewType(int position) {
int videoSize = this.videosList.size();
int promotedSize = this.promotedVideosList.size();
if(position < videoSize){
return TYPE_VIDEO;
}
if(position - videoSize < promotedSize){
return TYPE_PROMOTED_VIDEO;
}
if (position == promotedSize + videoSize - 1 && isLoadingAdded){
return TYPE_LOADING;
}
return -1;
}
public static class VideoVH extends RecyclerView.ViewHolder{
VideoVH(View itemView) {
super(itemView);
//findviewbyid stuff
}
}
public static class PromotedVH extends RecyclerView.ViewHolder{
public PromotedVH(View itemView) {
super(itemView);
//findviewbyid stuff
}
}
public static class LoadingVH extends RecyclerView.ViewHolder{
#BindView(R.id.loading_progress)
ProgressBar loading;
public LoadingVH(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
}
I expected this to work but somehow it doesn't and I know I'm not doing something right.
Here is a snippet from my logcat.
java.lang.NullPointerException: Attempt to invoke interface method'boolean java.util.List.add(java.lang.Object)' on a null object reference
at com.myproject.adapters.MyAdapter.add(MyAdapter.java:508)
at com.myproject.adapters.MyAdapter.addAll(MyAdapter.java:515)
Have you initialize the lists before add data into it.In your constructor you must initialize all lists.
For Example : videosList = new List();
likewise
You are not initializing videosList and promotedVideosList . Do this
public MyAdapter(Context context){
//Initalization stuffs
this.context = context;
videosList=new ArrayList();
promotedVideosList=new ArrayList();
}
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.