I wana create mobile application that uses RecyclerView with pagination that loads each time from dataBase 10 items, then when the list reaches the bottom I load 10 other items, so I used this metho to be notified if I reached the end of the list :
public boolean reachedEndOfList(int position) {
// can check if close or exactly at the end
return position == getItemCount() - 1;
}
and I used this function to load items :
#Override
public void onBindViewHolder(Info holder, int position, List<Object> payloads) {
if (reachedEndOfList(position)) {
Log.d("reachedEnd", "true");
this.autoCompletesTmp = getTmp();
notifyDataSetChanged();
}
}
getTmp() update the list of items with another 10 items, but i get this exception when I reached the bottom of the list:
java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling
I had the same problem a while ago:
This helped:
Handler handler = new Handler();
final Runnable r = new Runnable() {
public void run() {
//Do something like notifyDataSetChanged()
}
};
handler.post(r);
Related
I used notifyDataSetChanged in in my recycleriew. like this:
private void setList(List<Article> articles) {
mainList.addAll(articles);
notifyDataSetChanged();
}
But I want to use diffUtill in my recycleriew. I created my own diffUtill like this:
public class ArticleListDiffTool extends DiffUtil.Callback {
List<Article> oldList;
List<Article> newList;
private static final String TAG = "ArticleListDiffTool";
public ArticleListDiffTool(List<Article> oldList, List<Article> newList) {
this.oldList = oldList;
this.newList = newList;
Log.d(TAG, "ArticleListDiffTool: " + this.oldList.size() + "\n" + this.newList.size());
}
#Override
public int getOldListSize() {
return oldList.size();
}
#Override
public int getNewListSize() {
return newList.size();
}
#Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldList.get(oldItemPosition).getId() .equals( newList.get(newItemPosition).getId());
}
#Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return oldList.get(oldItemPosition).equals(newList.get(newItemPosition));
}
#Nullable
#Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
//you can return particular field for changed item.
return super.getChangePayload(oldItemPosition, newItemPosition);
}
}
And I use it in my adapter :
private void setList(List articles) {
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new ArticleListDiffTool(this.mainList, articles),true);
mainList.addAll(articles);
diffResult.dispatchUpdatesTo(this);
}
I want to add new data to my old list. But when new data received, the recyclerView will be scrolled to the top of the list.
But I want to recyclerView be in its user state and new data add to the rest of the old list.
RecylcerView updates knows nothing about your views. When you call notifyDataSetChanged it tries to determine which views moved, or were replaced. I don't see you using setHasStableIds so when calling notifyDataSetChanged it will assume all of the content was replaced. It will jump to position 0 and be done with it. When you use setHasStableIds it will check the ids of the visible items and update the content in them. It will stop jumping around.
Now you also show that you are using DiffUtil. This is great! When you're not working with setHasStableIds this is the way to properly tell the recyclerView about what changed.
The problem you are facing is that you're using both. Either move to long ids and let the recyclerview do the diffing itself, or use DiffUtil and remove the call to notifyDataSetChanged. Either variant should work.
If you add new items at the end of the recyclerview, before you add the data, store the 1st visible item in recyclerview:
int position = ((LinearLayoutManager) recyclerView.GetLayoutManager()).findFirstVisibleItemPosition();
and after you make all the changes and call notifyDatasetChanged() scroll to position:
recyclerView.scrollToPosition(position);
I Found the problem after a few hours.
I every time that wants to update my recyclerView send received data to my adapter
And DiffUtil goes to compare my old list with received new parts and because of that (completely new items) DiffUtil decides to refresh whole recyclerView list.
Solution:
Now I get the current list from the recyclerView adapter and use addAll to insert new items that received from the server, then I pass this complete list to the adapter.
Now DiffUtil can compare diffrent between my old and new lists and recyclerView will be stay at it's current position.
I'm using a recycler view in which there are some items containing complex custom component to load. It takes times to load and the problem is the recycler view call whenever necessary the method "onBindViewHolder" (during scroll etc) to recreate the views and so it needs time again to regenerate all the item (I'm not talking about the item's XML layout). And so... not very cool for performance.
How to avoid recreating a item ?
I tried to call :
setIsRecyclable
But it doesn't work.
Example :
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
// CODE 1 : treatments applied to the item view
// the problem is here, I don't want to repeat this code when it's already done
}
Why not have a hashmap where you store a boolean value to indicate if you have performed operation at the position?
HashMap<Integer,boolean> operations = new HashMap<>();
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if(operations.contains(position){
//Do nothing..
}
else
{
//Do operations...
operations.put(position,true);
}
}
final Handler timerHandler;
timerHandler = new Handler();
Runnable timerRunnable = new Runnable() {
#Override
public void run() { // Here you can update your adapter data
mRecyclerView.invalidate();
My_function();
// mRecyclerView.scrollToPosition(mRecyclerView.getAdapter().getItemCount() - 1);
//mAdapter.notifyDataSetChanged();
timerHandler.postDelayed(this, 3000);
//Toast.makeText(Chat_Activity.this, "Refreace", Toast.LENGTH_SHORT).show();
}
};
timerHandler.postDelayed(timerRunnable, 3000);
in your onBindViewHolder
generate Asynctask and in doInBackground do your content setting process.
I am trying to create messaging kind of screen using recyclerView which will start from bottom and will loadMore data when user reached top end of chat. But I am facing this weird issue.
My recyclerView scrolls to top on calling notifyDataSetChanged. Due to this onLoadMore gets called multiple times.
Here is my code:
LinearLayoutManager llm = new LinearLayoutManager(context);
llm.setOrientation(LinearLayoutManager.VERTICAL);
llm.setStackFromEnd(true);
recyclerView.setLayoutManager(llm);
** In Adapter
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (messages.size() > 8 && position == 0 && null != mLoadMoreCallbacks) {
mLoadMoreCallbacks.onLoadMore();
}
** In Activity
#Override
public void onLoadMore() {
// Get data from database and add into arrayList
chatMessagesAdapter.notifyDataSetChanged();
}
It's just that recyclerView scrolls to top. If scrolling to top stops, this issue will be resolved. Please help me to figure out the cause of this issue. Thanks in advance.
I think you shouldn't use onBindViewHolder that way, remove that code, the adapter should only bind model data, not listen scrolling.
I usually do the "onLoadMore" this way:
In the Activity:
private boolean isLoading, totallyLoaded; //
RecyclerView mMessages;
LinearLayoutManager manager;
ArrayList<Message> messagesArray;
MessagesAdapter adapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//...
mMessages.setHasFixedSize(true);
manager = new LinearLayoutManager(this);
manager.setStackFromEnd(true);
mMessages.setLayoutManager(manager);
mMessages.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (manager.findFirstVisibleItemPosition() == 0 && !isLoading && !totallyLoaded) {
onLoadMore();
isLoading = true;
}
}
});
messagesArray = new ArrayList<>();
adapter = new MessagesAdapter(messagesArray, this);
mMessages.setAdapter(adapter);
}
#Override
public void onLoadMore() {
//get more messages...
messagesArray.addAll(0, moreMessagesArray);
adapter.notifyItemRangeInserted(0, (int) moreMessagesArray.size();
isLoading = false;
}
This works perfeclty for me, and the "totallyLoaded" is used if the server doesn't return more messages, to stop making server calls. Hope it helps you.
You see, it's natural for List to scroll to the most top item when you insert new Items. Well you going in the right direction but I think you forgot adding setReverseLayout(true).
Here the setStackFromEnd(true) just tells List to stack items starting from bottom of the view but when used in combination with the setReverseLayout(true) it will reverse order of items and views so the newest item is always shown at the bottom of the view.
Your final layoutManager would seems something like this:
mLayoutManager = new LinearLayoutManager(getActivity());
mLayoutManager.setReverseLayout(true);
mLayoutManager.setStackFromEnd(true);
mRecyclerView.setLayoutManager(mLayoutManager);
DON'T call notifyDataSetChanged() on the RecyclerView. Use the new methods like notifyItemChanged(), notifyItemRangeChanged(), notifyItemInserted(), etc...
And if u use notifyItemRangeInserted()--
don't call setAdapter() method after that..!
This is my way to avoid scrollview move to top
Instead of using notifyDataSetChanged(), I use notifyItemRangeChanged();
List<Object> tempList = new ArrayList<>();
tempList.addAll(mList);
mList.clear();
mList.addAll(tempList);
notifyItemRangeChanged(0, mList.size());
Update:
For another reason, Your another view in the top is focusing so it will jump to top when you call any notifies, so remove all focuses by adding android:focusableInTouchMode="true" in the GroupView.
I do not rely on onBindViewHolder for these kind of things. It can be called multiple times for a position. For the lists which has load more option maybe you should use something like this after your recyclerview inflated.
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if ((((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition() == 0)) {
if (args.listModel.hasMore && null != mLoadMoreCallback && !loadMoreStarted) {
mLoadMoreCallbacks.onLoadMore();
}
}
}
});
Hope it helps.
I suggest you to use notifyItemRangeInserted method of RecyclerView.Adapter for LoadMore operations. You add a set of new items to your list so you do not need to notify whole dataset.
notifyItemRangeInserted(int positionStart, int itemCount)
Notify any registered observers that the currently reflected itemCount
items starting at positionStart have been newly inserted.
For more information:
https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html
You need to nofity the item in specific range like below:
#Override
public void onLoadMore() {
// Get data from database and add into arrayList
List<Messages> messegaes=getFromDB();
chatMessagesAdapter.setMessageItemList(messages);
// Notify adapter with appropriate notify methods
int curSize = chatMessagesAdapter.getItemCount();
chatMessagesAdapter.notifyItemRangeInserted(curSize,messages.size());
}
Checkout Firebase Friendlychat source-code on Github.
It behaves like you want, specially at:
mFirebaseAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
int friendlyMessageCount = mFirebaseAdapter.getItemCount();
int lastVisiblePosition = mLinearLayoutManager.findLastCompletelyVisibleItemPosition();
// If the recycler view is initially being loaded or the user is at the bottom of the list, scroll
// to the bottom of the list to show the newly added message.
if (lastVisiblePosition == -1 ||
(positionStart >= (friendlyMessageCount - 1) && lastVisiblePosition == (positionStart - 1))) {
mMessageRecyclerView.scrollToPosition(positionStart);
}
}
});
You have this issue because every time your condition be true you call loadMore method even loadMore was in running state, for solving this issue you must put one boolean value in your code and check that too.
check my following code to get more clear.
1- declare one boolean value in your adapter class
2- set it to true in your condition
3- set it to false after you've got data from database and notified your adapter.
so your code must be like as following code:
public class YourAdapter extend RecylerView.Adapter<.....> {
private boolean loadingDataInProgress = false;
public void setLoadingDataInProgress(boolean loadingDataInProgress) {
this.loadingDataInProgress = loadingDataInProgress
}
....
// other code
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (messages.size() > 8 && position == 0 && null != mLoadMoreCallbacks && !loadingDataInProgress){
loadingDataInProgress = true;
mLoadMoreCallbacks.onLoadMore();
}
......
//// other adapter code
}
in Activity :
#Override
public void onLoadMore() {
// Get data from database and add into arrayList
chatMessagesAdapter.notifyDataSetChanged();
chatMessagesAdapter. setLoadingDataInProgress(false);
}
This must fix your problem but I prefer to handle loadMore inside Activity or Presenter class with set addOnScrollListener on RecyclerView and check if findFirstVisibleItemPosition in LayoutManager is 0 then load data.
I've wrote one library for pagination, feel free to use or custom it.
PS: As other user mentioned don't use notifyDataSetChanged because this will refresh all view include visible views that you don't want to refresh those, instead use notifyItemRangeInsert, in your case you must notify from 0 to size of loaded data from database.
In your case as you load from top, notifyDataSetChanged will change scroll position to top of new loaded data, so you MUST use notifyItemRangeInsert to get good feel in your app
You need to nofity the item in specific range
#Override
public void onLoadMore() {
// Get data from database and add into arrayList
List<Messages> messegaes=getFromDB();
chatMessagesAdapter.setMessageItemList(messages);
// Notify adapter with appropriate notify methods
int curSize = chatMessagesAdapter.getItemCount();
chatMessagesAdapter.notifyItemRangeInserted(curSize,messages.size());
}
I have a simple RecyclerView for displaying items. Currently, from the RecyclerView.Adapter, I can delete items successfully using the following.
private void removeItem(int pos) {
filteredDataSet.remove(pos);
notifyItemRemoved(pos);
notifyItemRangeChanged(pos, getItemCount());
}
I call it from the onClick() function in the ViewHolder.The animations work, the view is updated, everything works. Pretty standard RecyclerView.
However, what I'd like to do is have the user verify the item deletion via a Dialog. Here's the basic setup for the Dialog (leaving out unnecessary code):
...
AlertDialog.Builder builder = new AlertDialog.Builder(v.getContext());
builder.setTitle("Delete this item?");
builder.setView(layout);
final int itemPos = pos;
builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
removeItem(itemPos);
}
});
...
So, I'm just moving the method call into the onClickListener of the Dialog.
The problem I'm having is that when the RecyclerView animates away the removed item, it animates it back in the exact same position, and the list stays the same. Like it's still there.
But, if I scroll down I get a out of bounds error:
java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position
Which means it's not actually there, and when go back and come into the View again, it's gone. So, it seems like it's cached and not updating the adapter or dataset. I read that it needs called on the main thread, so I modified my method to this:
private void removeItem(int pos) {
final int itemPos = pos;
Handler handler = new Handler(Looper.getMainLooper());
Runnable runnable = new Runnable() {
#Override
public void run() {
filteredDataSet.remove(itemPos);
notifyItemRemoved(itemPos);
notifyItemRangeChanged(itemPos, getItemCount());
}
};
handler.post(runnable);
}
It's still not working, and I'm at a loss. I suspect it's a thread issue, but not sure where to turn from here.
This problem was solved by using a callback to trigger the removeItem() function when the item is deleted from the database. Apparently, adapter was being notified before the item was actually deleted from the database.
I'm using DBFlow to perform queries, so the solution is only applicable to DBFlow based solutions. This is called from the view that holds the RecyclerView:
public void deleteItem(int pos, long id) {
DatabaseDefinition database = FlowManager.getDatabase(AppDatabase.class);
//hold id to remove later
final long tempId = id;
final int tempPos = pos;
final ItemModel currentItem = getItem(id);
Transaction transaction = database.beginTransactionAsync(new ITransaction() {
#Override
public void execute(DatabaseWrapper databaseWrapper) {
currentItem.delete();
}
}).success(new Transaction.Success() {
#Override
public void onSuccess(Transaction transaction) {
mAdapter.removeItem(tempPos); //called here
}
}).error(new Transaction.Error() {
#Override
public void onError(Transaction transaction, Throwable error) {
Log.d("delete error", "item not deleted");
}
}).build();
transaction.execute();
}
if your removing from onClick from ViewHolder class then use getAdapterPosition() to get clicked location
if this code not working can you tell what is onItemClick() from your ViewHolder
Sometimes, when I call goToLast() it throws me a null exception in vista=lista.getChildAt(), it happens when the list is full, I dont know why I have this code:
private void goToLast() {
lista.post(new Runnable() {
#Override
public void run() {
lista.setSelection(mensajes.getCount() - 1);
View vista = lista.getChildAt(mensajes.getCount() - 1);
TextView txtMensaje = (TextView)vista.findViewById(R.id.txtMensajeLista);
txtMensaje.setBackgroundColor(Color.RED);
}
});
}
You should log lista.getChildCount(), see how many children the ListView has. ListView recycles views, which means it only hold limit number of views.
So if you want to get the last view, you should do something like this
private void goToLast() {
lista.post(new Runnable() {
#Override
public void run() {
lista.setSelection(mensajes.getCount() - 1);
// Figure out the last position of the view on the list.
int lastViewIndex = lista.getChildCount() - 1;
View vista = lista.getChildAt(lastViewIndex);
TextView txtMensaje = (TextView)vista.findViewById(R.id.txtMensajeLista);
txtMensaje.setBackgroundColor(Color.RED);
}
});
}
ListView.getChildAt() position is different to the position in your adapter. ListView recycles its views, so if you have more items in your adapter, than can fit on the screen it will not create views for all of those, but only the ones that are visible. If you want to update an item in the ListView you need to update it in the adapter and call notifyDataSetChanged()