Recyclerview notifyItemChanged doesn't work - android

I try to update just one specific item after a "basic action" (like a tap on one item) in my recycler view, but the method notifyItemChanged seems to doesn't work as expected.
Actually, the method onBindViewHolder is correctly called, and datas that I want to change in my item is correctly done. BUT, I see nothing changing in my view. I don't understand why...
My code :
- MyFragment
private void initRecyclerView() {
_recyclerView = (RecyclerView) _rootView.findViewById(R.id.recyclerView);
_recyclerView.setHasFixedSize(true);
_layoutManager = new LinearLayoutManager(getActivity());
_recyclerView.setLayoutManager(_layoutManager);
_adapter = new MyAdapter(datas, this, getContext());
_recyclerView.setAdapter(_adapter);
}
When an item is selected, I call "_adapter.update(position)" in my fragment. And so in my adapter I've this :
-Adapter
public void updateItem(int position) {
notifyItemChanged(position);
}
After this call I can see that the method OnBindViewHolder is correctly call, but nothing is changed on the view :(
EDIT : code of onBindViewHolder :
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
MyOBject object = datas.get(position);
holder.text1.setText(object.getValue1());
holder.text2.setText(object.getValue2());
if (object.getLastTimeItemClicked() != null && object.getLastTimeItemClicked().compareTo(object.getLastTimeNewContent()) > 0) {
Log.d("test", "item clicked, content changed")
holder._backgroundItem.setCardBackgroundColor(_context.getResources().getColor(bgItemRead)); // I see this log after a tap on one item, so this code is working!
}
}
EDIT 2 : I'm using tabs, maybe there is something to manage with that ?! (In each fragments linked to each tabs, there is a recycler view etc managed by a fragment)

Related

RecyclerView ItemTouchHelper doesn't remove item

I'm trying to implement swipe to archive note in RecyclerView.
It was working fine but after I added these codes to refresh the RecyclerView from onResume(), Swiping although does archive the Note, but the item doesn't get removed and stays at a state you can see in image below:
This is what I do in onResume() :
#Override
protected void onResume() {
super.onResume();
notes = noteDAO.getAllNotes();
noteAdapter = new NoteAdapter(notes,this);
recyclerView.setAdapter(noteAdapter);
}
ItemTouchHelper onSwiped():
#Override
public void onSwiped(#NonNull RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAdapterPosition();
noteAdapter.deleteItem(position,rv);
}
deleteItem method in Adapter:
public void deleteItem(int position, RecyclerView rv) {
noteDAO = DBInjector.provideNoteDao(context);
recentlyDeletedNote = notes.get(position);
recentlyDeletedNotePosition = position;
recentlyDeletedNote.setArchive(true);
notes.remove(position);
noteDAO.archiveNote(recentlyDeletedNote);
notifyItemRemoved(position);
}
I have tried a lot of solutions, the only reason why it is not updating the view is that on updating recycler views, views are getting updated especially when ItemTouchHelper is used. As I didn't have much choice I used
recreate()
function for refreshing the whole activity and the error went off.
PS: This is not the ideal solution, it is just workaround fix.

How to reset recyclerView position item views to original state after refreshing adapter

I have a RecyclerView with rows that have views that when clicked will be disabled for that row position.
The problem is after I update the adapter like this:
adapterData.clear();
adapterData.addAll(refreshedAdapterData);
notifyDataSetChanged();
After refreshing the data, the disabled views at the previous recycler position still remain disabled even though the data is refreshed.
How can I reset the views to the original state after refreshing adapter data.
Use below code.
adapterData.clear();
adapterData.addAll(refreshedAdapterData);
adapter.notifyDataSetChanged();
OR
recyclerView.invalidate();
When you call notifyDataSetChanged(), the onBindViewHolder() method of every view is called. So you could add something like this in the onBindViewHolder() of your Adapter method:
#Override
public void onBindViewHolder(final RecyclerView.ViewHolder viewHolder, final int position) {
if (refreshedAdapterData.get(position).isInDefaultState()) {
//set Default View Values
} else {
//the code you already have
}
}
I have resolved this by putting a conditional statement inside onBindViewHolder method instructing all positions to reset the disabled views if data meets the required conditions for a refreshed data.
#Christoph Mayr, thanks for your comments. It helped point me in the right direction.
I cleared the data then notify change but the selected checkbox doesn't reset but just moves up one position. Let say I selected item #1 , move out of the RecyclerView, came back and it will auto select item #0.
So, I created new adapter again at onResume(), i worked for me but i don't know if it's the right way to handle this situation.
#Override
public void onResume() {
super.onResume();
if(selectedItems != null && selectedItems.size() > 0){
selectedItems.clear(); // if no selected items before then no need to reset anything
if(adapter != null && recyclerView != null){
// to remove the checked box
adapter = null;
adapter = new MyAdapter(items, new MyAdapter.MyAdapterListener() {
#Override
public void onSelected(int pos) {
selectedItems.add(items.get(pos));
}
#Override
public void onUnSelected(int pos) {
selectedItems.remove(items.get(pos));
}
});
recyclerView.setAdapter(adapter);
}
}
}

RecyclerView scrolls to top on notifyDataSetChanged in chat screen

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());
}

RecyclerView.Adapter onBindViewHolder() gets wrong position

I'll show the code and after the steps to get the problem.
I have a recyclerview inside a tabbed fragment that takes the dataset from a custom object:
mRecyclerView = (RecyclerView) v.findViewById(R.id.recyclerview);
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerAdapter = new MyRecyclerAdapter(mMes.getListaItens(), this, getActivity());
mRecyclerView.setAdapter(mRecyclerAdapter);
I set the longclick behavior of the list items in onBindViewHolder() of the adapter:
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
ItemMes item = mListaItens.get((position));
holder.descricao.setText(item.getDescrição());
holder.valor.setText(MainActivity.decimalFormatWithCod.format(item.getValor()));
...
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View v) {
new MaterialDialog.Builder(mContext)
.title(holder.descricao.getText().toString())
.items(R.array.opcoes_longclick_item)
.itemsCallbackSingleChoice(-1, new MaterialDialog.ListCallbackSingleChoice() {
#Override
public boolean onSelection(MaterialDialog dialog, View view, int which, CharSequence text) {
switch (which) {
case 0:
mParentFragment.showUpdateItemDialog(position);
return true;
case 1:
mParentFragment.showDeleteItemDialog(position);
return true;
}
return false;
}
})
.show();
return true;
}
});
}
Then, the methods in the fragment that take care of delete the item itself:
public void showDeleteItemDialog(int position) {
final ItemMes item = mMes.getListaItens().get(position);
new MaterialDialog.Builder(getActivity())
.title("Confirmar Remoção")
.content("Tem certeza que deseja remover " + item.getDescrição() + "?")
.positiveText("Sim")
.negativeText("Cancelar")
.onPositive(new MaterialDialog.SingleButtonCallback() {
#Override
public void onClick(#NonNull MaterialDialog dialog, #NonNull DialogAction which) {
deleteItem(item);
}
})
.show();
}
public void deleteItem(ItemMes item) {
getMainActivity().deleteItemFromDatabase(item.getID());
int position = mMes.getListaItens().indexOf(item);
mMes.getListaItens().remove(position);
mRecyclerAdapter.notifyItemRemoved(position);
atualizaFragment();
}
And finally the method in activity that do the DB operation:
public int deleteItemFromDatabase(long id) {
SQLiteDatabase db = dataBaseHelper.getWritableDatabase();
String where = DBHelper.COLUNA_ID + " = ?";
String[] args = {String.valueOf(id)};
int rowsAffected = db.delete(DBHelper.TABELA_ITEM, where, args);
db.close();
return rowsAffected;
}
Now i'll reproduce the steps:
I'm showing 3 itens in the listview. Then I try to remove the first:
1 - The longclick is intercepted passing the correct index:
2 - The item is correctly deleted from the database:
3 - After all this, as expected, the adapter is storing and showing 2 items...
SO, if I try to delete the first item of this 2 item list I get the wrong position (should be 0, is 1):
And also if I try to delete the last item of this 2 item list I get the wrong position (should be 1, is 2):
The question is: If I have a dataset of size 2 (and the adapter knows it), how can it call onBindViewHolder(ViewHolder holder, int [last index +1])?
I have no idea what could be wrong. So I ask help cause I'm thinking about give up this project cause I do everything right but always something dont works, and Im tired.
Thanks in advance.
I've noticed that in method onBindViewHolder(VH holder, int position) while the position was comming wrong, the holder.getAdapterPosition() gives me always the correct position.
So I changed my code from:
ItemMes item = mListaItens.get((position));
...
mParentFragment.showUpdateItemDialog(position);
...
mParentFragment.showDeleteItemDialog(position);
....
To:
ItemMes item = mListaItens.get((holder.getAdapterPosition()));
...
mParentFragment.showUpdateItemDialog(holder.getAdapterPosition());
...
mParentFragment.showDeleteItemDialog(holder.getAdapterPosition());
....
And everything works well. This is very strange but...
Thanks everybody.
Took a look at the adapter code you provided in the comment and it's pretty straightforward. Try this: rather than call notifyItemRemoved(), call notifyDataSetChanged(). This is rather expensive as it will cause your adapter to re-bind the data set (and re-create ViewHolders), but since you're using an ArrayList where you are removing an element, it's really the simplest way to do it. Otherwise you'll have to track the position of the items and when an item is removed it cannot change the position of other items - or handle the case where items shift their position in the data set.
Try this code in onBindViewHolder()
int adapterPos=holder.getAdapterPosition();
if (adapterPos<0){
adapterPos*=-1;
}
ItemMes item = mListaItens.get((adapterPos));
mParentFragment.showUpdateItemDialog(adapterPos);
Use adapterPos instead of position variable.
According to RecyclerView's getAdapterPosition documentation:
RecyclerView does not handle any adapter updates until the next layout traversal. This
may create temporary inconsistencies between what user sees on the screen and what
adapter contents have. This inconsistency is not important since it will be less than
16ms but it might be a problem if you want to use ViewHolder position to access the
adapter. Sometimes, you may need to get the exact adapter position to do
some actions in response to user events. In that case, you should use this method which
will calculate the Adapter position of the ViewHolder.
So in case of implementing user events, using getAdapterPosition is a recommended way to go.

StickyGridHeaders custom adapter

I'm trying to implement my own StickyGridHeadersBaseAdapter, my current source code here - http://paste.org.ru/?11jrjh, and I use it like
ModeAdapter adapter = new ModeAdapter(this);
modeGridView.setAdapter(adapter);
Problems which I have is that
1) I have no idea how to call notifyDataSetChanged() for this adapter, so I can't change items
2) And implementation of AdapterView.OnItemClickListener (http://paste.org.ru/?mvgt7b) works strange
Mode mode = (Mode) adapter.getItem(position);
returns null for items with 1st and 2nd positions, item on 3rd position is actual 1st item in adapter.
Where is my fault here?
One more question is why I can't cast adapterView.getAdapter() in my OnItemClickListener to my ModeAdapter class. What if I want to call notifyDataSetChanged() here?
I didn't find any examples for custom implementation of StickyGridHeadersBaseAdapter here.
Thanks in advance.
I had the same issue as you 2):
after the first header i got the item of the previous row, after the second header got the item of two rows up, etc...
The reason is the following:
StickyHeadersGridView:
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
mOnItemClickListener.onItemClick(parent, view, mAdapter.translatePosition(position).mPosition, id);
}
The position is corrected. so in your onItemClick you get the corrected value of position.
If than you request the item with: Mode mode = (Mode) adapter.getItem(position);
you get StickyGridHeadersBaseAdapterWrapper.getItem(int pos)
#Override
public Object getItem(int position) throws ArrayIndexOutOfBoundsException {
Position adapterPosition = translatePosition(position);
if (adapterPosition.mPosition == POSITION_FILLER || adapterPosition.mPosition == POSITION_HEADER) {
// Fake entry in view.
return null;
}
return mDelegate.getItem(adapterPosition.mPosition);
}
In StickyGridHeadersBaseAdapterWrapper.getItem() position gets corrected for the second time which causes the wrong item to be returned...
I added a work-around:
In StickyHeadersBaseAdapterWrapper I added:
public Object getItemAtDelegatePosition(int pos) {
return mDelegate.getItem(pos);
}
And I use this in onItemClick:
Mode item = (Mode) ((StickyGridHeadersBaseAdapterWrapper)parent.getAdapter()).getItemAtDelegatePosition(position);
An easier way to get an item would be:
StickyGridHeadersBaseAdapterWrapper wrapper = (StickyGridHeadersBaseAdapterWrapper) parent.getAdapter();
Mode item = (Mode ) wrapper.getWrappedAdapter().getItem(position);

Categories

Resources