I'm trying to make a simple chat application for my own learning - no firebase involved (the messages won't be stored between sessions). I've implemented a RecyclerView to show all the messages. The problem is that every time I add a new message, the RecyclerView Adapter will iterate through all previous messages before populating the latest one. Whilst this isn't causing any major bugs, it does seem very inefficient. The relevant functions in my adapter class are shown below:
#Override
public void onBindViewHolder(#NonNull ViewHolder holder, int position) {
MessageItem newMsgItem = messages.get(position);
holder.txtMsgContent.setText(newMsgItem.getMsgContent());
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) holder.msgParentView.getLayoutParams();
if (newMsgItem.isSent()) {
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
holder.msgParentView.setLayoutParams(layoutParams);
holder.msgParentView.setCardBackgroundColor(0xFF03DAC5);
} else {
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
holder.msgParentView.setLayoutParams(layoutParams);
holder.msgParentView.setCardBackgroundColor(0xFF67706F);
}
}
// boolean sent: false = received, true = sent
public void addMessage (boolean sent, String msgContent) {
messages.add(new MessageItem(sent, msgContent));
notifyDataSetChanged();
}
I could implement a condition-check like below, but that isn't a satisfying solution as it only masks the problem - i.e. the program is still iterating unnecessarily through all previous messages:
#Override
public void onBindViewHolder(#NonNull ViewHolder holder, int position) {
if (position == messages.size() - 1) {
//... do function
}
}
Is there a way to make the program only call onBindViewHolder for the newest item that's been added? I also saw this forum, but as I'm a beginner I couldn't tell if they were having the same issue as me.
RecyclerView populating each item every time i add a new item
notifyDataSetChanged() always reloads the whole view. Use notifyItemInserted() instead.
public void addMessage (boolean sent, String msgContent) {
messages.add(new MessageItem(sent, msgContent));
notifyItemInserted(messages.size()-1);
}
Don't use notifyDataSetChanged() method, you can use notifyItemInserted() method, this will not refresh every time.
public void addMessage (boolean sent, String msgContent) {
messages.add(new MessageItem(sent, msgContent));
notifyItemInserted(messages.size()-1);}
Good day. I have a small chat app where i want to simply delete the chat item locally.
By debugging around i can reckon that there is something wrong with notifyItemRemoved() as follows :
• I have debugged the returned index from my List and the index was correct for list.
• I have debugged the returned Model Class from the ViewHolder and the returned model class was correct.
• I have debugged my custom equals() method inside my Model and it was comparing correctly returning correct item.
The main complain about my debugging is i dont know why,but the equals() method is being called 2 times...pretty weird though if you would have time please consider this case as well.
The main problem is the notifyItemRemoved() is always removing not the correct item,but always 1 item above the item which must be deleted (in means of deleted i mean deleted from the view as deleting from the list is happening correctly)
So i have no clue what is going on here.
Here is my custom equals() methods
#Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj instanceof ChatModel) {
ChatModel chatModel = (ChatModel) obj;
if (chatModel.getMessageId().equals(messageId)) {
return true;
}
}
return false;
}
#Override
public int hashCode() {
return messageId.hashCode();
}
Here is how i insert items into adapter list.
For the list of models->
public void insertChatModelWithItemNotify(List<ChatModel> chatModel, boolean firstPaging) {
if (firstPaging) {
this.chatModelList.addAll(chatModel);
notifyDataSetChanged();
} else {
this.chatModelList.addAll(0, chatModel);
notifyItemRangeInserted(0, chatModel.size());
}
}
For a single model - >
public void insertChatModel(ChatModel chatModel) {
this.chatModelList.add(chatModel);
notifyDataSetChanged();
}
Here is how i remove the item from the list
public void removeItem(ChatModel chatModel) {
int position = this.chatModelList.indexOf(chatModel);
this.chatModelList.remove(chatModel);
notifyItemRemoved(position);
}
Any ideas?
You are probably adding some kind of header to the RecyclerView which makes the object index in your object list different from the View index in the RecyclerView.
I'm building an application with Firebase on Android. The scenario for my application is as follows. Look at the following screen.
As you can see, in the above screen, I'm displaying a list of transactions based on the user role: Renter and Owner. Also, at the toolbar, user can easily filter any transaction statuses, shown in the following screen.
To achieve this scenario, I've modeled my database with the following structure:
- transactionsAll:
- transactionID1:
- orderDate: xxx
- role: Renter / Owner
- transactionID2:
....
- transactionsWaitingApproval:
- transactionID1:
- orderDate: xxx
- role: Renter / Owner
The thing is, in each of the Fragment, I've used an orderByChild query just to display the list of transactions based on the user role in each of the fragment, whether it's the Renter or the Owner, like so
public void refreshRecyclerView(final String transactionStatus) {
Query transactionsQuery = getQuery(transactionStatus);
//Clean up old data
if (mFirebaseRecyclerAdapter != null) {
mFirebaseRecyclerAdapter.cleanup();
}
mFirebaseRecyclerAdapter = new FirebaseRecyclerAdapter<Transaction, TransactionViewHolder>(Transaction.class, R.layout.item_transaction,
TransactionViewHolder.class, transactionsQuery) {
#Override
public int getItemCount() {
int itemCount = super.getItemCount();
if (itemCount == 0) {
mRecyclerView.setVisibility(View.GONE);
mEmptyView.setVisibility(View.VISIBLE);
} else {
mRecyclerView.setVisibility(View.VISIBLE);
mEmptyView.setVisibility(View.GONE);
}
return itemCount;
}
#Override
protected void populateViewHolder(final TransactionViewHolder viewHolder, final Transaction transaction, final int position) {
final CardView cardView = (CardView) viewHolder.itemView.findViewById(R.id.transactionCardView);
cardView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
}
});
viewHolder.bindToPost(getActivity(), transaction, new View.OnClickListener() {
#Override
public void onClick(View view) {
}
});
}
};
mRecyclerView.setAdapter(mFirebaseRecyclerAdapter);
mFirebaseRecyclerAdapter.notifyDataSetChanged();
}
Where the getQuery method is as follows:
private Query getQuery(String transactionStatus) {
Query transactionsQuery = null;
int sectionNumber = getArguments().getInt(SECTION_NUMBER);
if (sectionNumber == 0) { // Renter fragment
if (transactionStatus == null || transactionStatus.equals(MyConstants.TransactionStatusConstants.allTransactionsValue))
transactionsQuery = FirebaseDatabaseHelper.getTransactionsAllReference().orderByChild("productRenter").equalTo(UserHelper.getCurrentUser().getUid());
else if (transactionStatus.equals(MyConstants.TransactionStatusConstants.waitingApprovalValue))
transactionsQuery = FirebaseDatabaseHelper.getTransactionsWaitingApprovalReference().orderByChild("productRenter").equalTo(UserHelper.getCurrentUser().getUid());
...
}
if (sectionNumber == 1) { // Owner fragment
if (transactionStatus == null || transactionStatus.equals(MyConstants.TransactionStatusConstants.allTransactionsValue))
transactionsQuery = FirebaseDatabaseHelper.getTransactionsAllReference().orderByChild("productOwner").equalTo(UserHelper.getCurrentUser().getUid());
else if (transactionStatus.equals(MyConstants.TransactionStatusConstants.waitingApprovalValue))
transactionsQuery = FirebaseDatabaseHelper.getTransactionsWaitingApprovalReference().orderByChild("productOwner").equalTo(UserHelper.getCurrentUser().getUid());
...
}
return transactionsQuery;
}
With the above query, I've ran out of options to perform another orderByKey/Child/Value on the query. As written in the docs, I can't perform a double orderBy query.
You can only use one order-by method at a time. Calling an order-by
method multiple times in the same query throws an error.
The problem: With every new Transaction object pushed to the database, it is shown on the bottom of the recycler view. How can I sort the data based on the orderDate property, in descending order? So that every new transaction will be shown as the first item the recycler view?
In the same documentation page, it is said that:
Use the push() method to append data to a list in multiuser
applications. The push() method generates a unique key every time a
new child is added to the specified Firebase reference. By using these
auto-generated keys for each new element in the list, several clients
can add children to the same location at the same time without write
conflicts. The unique key generated by push() is based on a timestamp,
so list items are automatically ordered chronologically.
I want the items to be ordered chronologically-reversed.
Hopefully someone from the Firebase team can provide me with a suggestion on how to achieve this scenario gracefully.
Originally, I was thinking there would be some additional Comparators in the works, but it turns out Frank's answer led me to the right direction.
Per Frank's comment above, these two tricks worked for me:
Override the getItem method inside the FirebaseRecyclerAdapter as follows:
#Override
public User getItem(int position) {
return super.getItem(getItemCount() - 1 - position);
}
But I finally went with setting the LinearLayoutManager as follows:
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false);
linearLayoutManager.setStackFromEnd(true);
linearLayoutManager.setReverseLayout(true);
mRecyclerView.setLayoutManager(linearLayoutManager);
Although this solution does solve my problem, hopefully there will be more enhancements coming to the Firebase library for data manipulation.
In RecyclerView I have a ViewHolder with VideoView and Button "Like" with the state (selected or not).
My Presenter have a method which will update "Like" status in the model - VideoEntity.
In callback I need to update View form Presenter, so I call getView().updateItem(VideoEntity entity).
After that, I should find recyclerView entity position, and invalidate item.
So I want to avoid that.
In classic architecture, i can call some method from ViewHolder, get some callback there and update changed data.
How to migrate this pseudo-code to MVP pattern?
#Override
public void onBindViewHolder(ContainerViewHolder holder, int position) {
...
rx.subscribe(result -> holder.update(result));
}
#Override
public void onViewRecycled(ContainerViewHolder holder) {
if (haveBackgroundRequests) { rx.unsubscribe(holder); }
}
One presenter for one viewHolder.
I found solution here: https://github.com/remind101/android-arch-sample
I am getting data from server and then parsing it and storing it in a List. I am using this list for the RecyclerView's adapter. I am using Fragments.
I am using a Nexus 5 with KitKat. I am using support library for this. Will this make a difference?
Here is my code: (Using dummy data for the question)
Member Variables:
List<Business> mBusinesses = new ArrayList<Business>();
RecyclerView recyclerView;
RecyclerView.LayoutManager mLayoutManager;
BusinessAdapter mBusinessAdapter;
My onCreateView():
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Getting data from server
getBusinessesDataFromServer();
View view = inflater.inflate(R.layout.fragment_business_list,
container, false);
recyclerView = (RecyclerView) view
.findViewById(R.id.business_recycler_view);
recyclerView.setHasFixedSize(true);
mLayoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(mLayoutManager);
mBusinessAdapter = new BusinessAdapter(mBusinesses);
recyclerView.setAdapter(mBusinessAdapter);
return view;
}
After getting data from server, parseResponse() is called.
protected void parseResponse(JSONArray response, String url) {
// insert dummy data for demo
mBusinesses.clear();
Business business;
business = new Business();
business.setName("Google");
business.setDescription("Google HeadQuaters");
mBusinesses.add(business);
business = new Business();
business.setName("Yahoo");
business.setDescription("Yahoo HeadQuaters");
mBusinesses.add(business);
business = new Business();
business.setName("Microsoft");
business.setDescription("Microsoft HeadQuaters");
mBusinesses.add(business);
Log.d(Const.DEBUG, "Dummy Data Inserted\nBusinesses Length: "
+ mBusinesses.size());
mBusinessAdapter = new BusinessAdapter(mBusinesses);
mBusinessAdapter.notifyDataSetChanged();
}
My BusinessAdapter:
public class BusinessAdapter extends
RecyclerView.Adapter<BusinessAdapter.ViewHolder> {
private List<Business> mBusinesses = new ArrayList<Business>();
// Provide a reference to the type of views that you are using
// (custom viewholder)
public static class ViewHolder extends RecyclerView.ViewHolder {
public TextView mTextViewName;
public TextView mTextViewDescription;
public ImageView mImageViewLogo;
public ViewHolder(View v) {
super(v);
mTextViewName = (TextView) v
.findViewById(R.id.textView_company_name);
mTextViewDescription = (TextView) v
.findViewById(R.id.textView_company_description);
mImageViewLogo = (ImageView) v
.findViewById(R.id.imageView_company_logo);
}
}
// Provide a suitable constructor (depends on the kind of dataset)
public BusinessAdapter(List<Business> myBusinesses) {
Log.d(Const.DEBUG, "BusinessAdapter -> constructor");
mBusinesses = myBusinesses;
}
// Create new views (invoked by the layout manager)
#Override
public BusinessAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
Log.d(Const.DEBUG, "BusinessAdapter -> onCreateViewHolder()");
// create a new view
View v = LayoutInflater.from(parent.getContext()).inflate(
R.layout.item_business_list, parent, false);
ViewHolder vh = new ViewHolder(v);
return vh;
}
// Replace the contents of a view (invoked by the layout manager)
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
// - get element from your dataset at this position
// - replace the contents of the view with that element
Log.d(Const.DEBUG, "BusinessAdapter -> onBindViewHolder()");
Business item = mBusinesses.get(position);
holder.mTextViewName.setText(item.getName());
holder.mTextViewDescription.setText(item.getDescription());
holder.mImageViewLogo.setImageResource(R.drawable.ic_launcher);
}
// Return the size of your dataset (invoked by the layout manager)
#Override
public int getItemCount() {
Log.d(Const.DEBUG, "BusinessAdapter -> getItemCount()");
if (mBusinesses != null) {
Log.d(Const.DEBUG, "mBusinesses Count: " + mBusinesses.size());
return mBusinesses.size();
}
return 0;
}
}
But I don't get the data displayed in the view. What am I doing wrong?
Here is my log,
07-14 21:15:35.669: D/xxx(2259): Dummy Data Inserted
07-14 21:15:35.669: D/xxx(2259): Businesses Length: 3
07-14 21:26:26.969: D/xxx(2732): BusinessAdapter -> constructor
I don't get any logs after this. Shouldn't getItemCount() in adapter should be called again?
In your parseResponse() you are creating a new instance of the BusinessAdapter class, but you aren't actually using it anywhere, so your RecyclerView doesn't know the new instance exists.
You either need to:
Call recyclerView.setAdapter(mBusinessAdapter) again to update the RecyclerView's adapter reference to point to your new one
Or just remove mBusinessAdapter = new BusinessAdapter(mBusinesses); to continue using the existing adapter. Since you haven't changed the mBusinesses reference, the adapter will still use that array list and should update correctly when you call notifyDataSetChanged().
Try this method:
List<Business> mBusinesses2 = mBusinesses;
mBusinesses.clear();
mBusinesses.addAll(mBusinesses2);
//and do the notification
a little time consuming, but it should work.
Just to complement the other answers as I don't think anyone mentioned this here: notifyDataSetChanged() should be executed on the main thread (other notify<Something> methods of RecyclerView.Adapter as well, of course)
From what I gather, since you have the parsing procedures and the call to notifyDataSetChanged() in the same block, either you're calling it from a worker thread, or you're doing JSON parsing on main thread (which is also a no-no as I'm sure you know). So the proper way would be:
protected void parseResponse(JSONArray response, String url) {
// insert dummy data for demo
// <yadda yadda yadda>
mBusinessAdapter = new BusinessAdapter(mBusinesses);
// or just use recyclerView.post() or [Fragment]getView().post()
// instead, but make sure views haven't been destroyed while you were
// parsing
new Handler(Looper.getMainLooper()).post(new Runnable() {
public void run() {
mBusinessAdapter.notifyDataSetChanged();
}
});
}
PS Weird thing is, I don't think you get any indications about the main thread thing from either IDE or run-time logs. This is just from my personal observations: if I do call notifyDataSetChanged() from a worker thread, I don't get the obligatory Only the original thread that created a view hierarchy can touch its views message or anything like that - it just fails silently (and in my case one off-main-thread call can even prevent succeeding main-thread calls from functioning properly, probably because of some kind of race condition)
Moreover, neither the RecyclerView.Adapter api reference nor the relevant official dev guide explicitly mention the main thread requirement at the moment (the moment is 2017) and none of the Android Studio lint inspection rules seem to concern this issue either.
But, here is an explanation of this by the author himself
I had same problem. I just solved it with declaring adapter public before onCreate of class.
PostAdapter postAdapter;
after that
postAdapter = new PostAdapter(getActivity(), posts);
recList.setAdapter(postAdapter);
at last I have called:
#Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
// Display the size of your ArrayList
Log.i("TAG", "Size : " + posts.size());
progressBar.setVisibility(View.GONE);
postAdapter.notifyDataSetChanged();
}
May this will helps you.
Although it is a bit strange, but the notifyDataSetChanged does not really work without setting new values to adapter. So, you should do:
array = getNewItems();
((MyAdapter) mAdapter).setValues(array); // pass the new list to adapter !!!
mAdapter.notifyDataSetChanged();
This has worked for me.
Clear your old viewmodel and set the new data to the adapter and call notifyDataSetChanged()
In my case, force run #notifyDataSetChanged in main ui thread will fix
public void refresh() {
clearSelection();
// notifyDataSetChanged must run in main ui thread, if run in not ui thread, it will not update until manually scroll recyclerview
((Activity) ctx).runOnUiThread(new Runnable() {
#Override
public void run() {
adapter.notifyDataSetChanged();
}
});
}
I always have this problem that I forget that the RecyclerView expects a new instance of a List each time you feed the adapter.
List<X> deReferenced = new ArrayList(myList);
adapter.submitList(deReferenced);
Having the "same" List (reference) means not declaring "new" even if the List size changes, because the changes performed to the List also propagates to other Lists (when they are simply declared as this.localOtherList = myList) emphasis on the keyword being "=", usually components that compare collections make a copy of the result after the fact and store it as "old", but not Android DiffUtil.
So, if a component of yours is giving the same List each and every time you submit it, the RecyclerView won't trigger a new layout pass.
The reason is that... AFAIR, before the DiffUtil even attempts to apply the Mayers algorithm, there is a line doing a:
if (newList == mList)) {return;}
I am not sure how much "good practice" does de-referencing within the same system is actually defined as "good" ...
Specially since a diff algorithm is expected to have a new(revised) vs old(original) component which SHOULD in theory dereference the collection by itself after the process has ended but... who knows...?
But wait, there is more...
doing new ArrayList() dereferences the List, BUT for some reason Oracle decided that they should make a second "ArrayList" with the same name but a different functionality.
This ArrayList is within the Arrays class.
/**
* Returns a fixed-size list backed by the specified array. (Changes to
* the returned list "write through" to the array.) This method acts
* as bridge between array-based and collection-based APIs, in
* combination with {#link Collection#toArray}. The returned list is
* serializable and implements {#link RandomAccess}.
*
* <p>This method also provides a convenient way to create a fixed-size
* list initialized to contain several elements:
* <pre>
* List<String> stooges = Arrays.asList("Larry", "Moe", "Curly");
* </pre>
*
* #param <T> the class of the objects in the array
* #param a the array by which the list will be backed
* #return a list view of the specified array
*/
#SafeVarargs
#SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a); //Here
}
This write-through is funny because if you:
Integer[] localInts = new Integer[]{1, 2, 8};
Consumer<List<Integer>> intObserver;
public void getInts(Consumer<List<Integer>> intObserver) {
this.intObserver = intObserver;
dispatch();
}
private void dispatch() {
List<Integer> myIntegers = Arrays.asList(localInts);
intObserver.accept(myIntegers);
}
... later:
getInts(
myInts -> {
adapter.submitList(myInts); //myInts = [1, 2, 8]
}
);
Not only does the List dispatched obeys the dereferencing on each submission, but when the localInts variable is altered,
public void set(int index, Integer value) {
localInts[index] = value;
dispatch(); // dispatch again
}
...
myModel.set(1, 4) // localInts = [1, 4, 8]
this alteration is also passed to the List WITHIN the RecyclerView, this means that on the next submission, the (newList == mList) will return "false" allowing the DiffUtils to trigger the Mayers algorithm, BUT the areContentsTheSame(#NonNull T oldItem, #NonNull T newItem) callback from the ItemCallback<T> interface will throw a "true" when reaching index 1. basically, saying "the index 1 inside RecyclerView (that was supposed to be 2 in th previous version) was always 4", and a layout pass will still not perform.
So, the way to go in this case is:
List<Integer> trulyDereferenced = new ArrayList<>(Arrays.asList(localInts));
adapter.submitList(trulyDereferenced);