I'm using RecyclerView to display name of the items. My row contains single TextView. Item names are stored in List<String> mItemList.
To change contents of RecyclerView, I replace Strings in mItemList and call notifyDataSetChanged() on RecyclerViewAdapter.
But If I try to change contents of the mItemList while RecyclerView is scrolling, sometimes it gives me
java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 157(offset:157).state:588
This happens if size of mItemList is less than before. So what is the correct way to change contents of the RecyclerView ? Is this a bug in RecyclerView ?
Here's full stack trace of Exception:
java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 157(offset:157).state:588
at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:3300)
at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:3258)
at android.support.v7.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:1803)
at android.support.v7.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1302)
at android.support.v7.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1265)
at android.support.v7.widget.LinearLayoutManager.scrollBy(LinearLayoutManager.java:1093)
at android.support.v7.widget.LinearLayoutManager.scrollVerticallyBy(LinearLayoutManager.java:956)
at android.support.v7.widget.RecyclerView$ViewFlinger.run(RecyclerView.java:2715)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:725)
at android.view.Choreographer.doCallbacks(Choreographer.java:555)
at android.view.Choreographer.doFrame(Choreographer.java:524)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:711)
at android.os.Handler.handleCallback(Handler.java:615)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4921)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1027)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:794)
at dalvik.system.NativeStart.main(Native Method)
AdapterView code:
private static class FileListAdapter extends RecyclerView.Adapter<FileHolder> {
private final Context mContext;
private final SparseBooleanArray mSelectedArray;
private final List<String> mList;
FileListAdapter(Context context, List<String> list, SparseBooleanArray selectedArray) {
mList = list;
mContext = context;
mSelectedArray = selectedArray;
}
#Override
public FileHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
View view = LayoutInflater.from(viewGroup.getContext()).inflate(
R.layout.file_list_item, viewGroup, false);
TextView tv = (TextView) view
.findViewById(R.id.file_name_text);
Typeface font = Typeface.createFromAsset(viewGroup.getContext().getAssets(),
viewGroup.getContext().getString(R.string.roboto_regular));
tv.setTypeface(font);
return new FileHolder(view, tv);
}
#Override
public void onBindViewHolder(FileHolder fileHolder, final int i) {
String name = mList.get(i);
// highlight view if selected
setSelected(fileHolder.itemView, mSelectedArray.get(i));
// Set text
fileHolder.mTextView.setText(name);
}
#Override
public int getItemCount() {
return mList.size();
}
}
private static class FileHolder extends RecyclerView.ViewHolder {
public final TextView mTextView;
public FileHolder(View itemView, TextView tv) {
super(itemView);
mTextView = tv;
}
}
Edit: The bug is fixed now, if you're still getting the same Exception, please make sure you're updating your Adapter data source only from the main thread and calling appropriate adapter notify method after it.
Old answer: It seems to be a bug in RecyclerView, it's reported here and here. Hopefully it will be fixed in the next release.
No problem for me. Use NotifyDataSetChanged();
public class MyFragment extends Fragment{
private MyAdapter adapter;
// Your code
public void addArticle(){
ArrayList<Article> list = new ArrayList<Article>();
//Add one article in this list
adapter.addArticleFirst(list); // or adapter.addArticleLast(list);
}
}
public class ArticleAdapterRecycler extends RecyclerView.Adapter<ArticleAdapterRecycler.ViewHolder> {
private ArrayList<Article> Articles = new ArrayList<Article>();
private Context context;
// Some functions from RecyclerView.Adapter<ArticleAdapterRecycler.ViewHolder>
// Add at the top of the list.
public void addArticleFirst(ArrayList<Article> list) {
Articles.addAll(0, list);
notifyDataSetChanged();
}
// Add at the end of the list.
public void addArticleLast(ArrayList<Article> list) {
Articles.addAll(Articles.size(), list);
notifyDataSetChanged();
}
}
Just prohibit RecyclerView's scroll when data is changing.
Like as my code:
mRecyclerView.setOnTouchListener(
new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (mIsRefreshing) {
return true;
} else {
return false;
}
}
}
);
More about: http://drakeet.me/recyclerview-bug-indexoutofboundsexception-inconsistency-detected-invalid-item-position-solution
Although a couple of useful hyperlinks about this issue are given in the accepted answer, it is not true that this behavior of RecyclerView while scrolling is a bug.
If you see this exception, most probably you forget to notify the adapter after the content of RecyclerView is "changed". People call notifyDataSetChanged() only after an item is added to the data set. However, the inconsistency occurs not only after you refill the adapter, but also when you remove an item or you clear the data set, you should refresh the view by notifying the adapter about this change:
public void refillAdapter(Item item) {
adapter.add(item);
notifyDataSetChanged();
}
public void cleanUpAdapter() {
adapter.clear();
notifyDataSetChanged(); /* Important */
}
In my case, I tried to clean up the adapter in onStop(), and refill it in onStart(). I forgot to call notifyDataSetChanged() after the adapter is cleaned by using clear(). Then, whenever I changed the state from onStop() to onStart() and swiftly scrolled the RecyclerView while the data set is reloading, I saw this exception. If I waited the end of reloading without scrolling, there would be no exception since the adapter can be reinstated smoothly this time.
In short, the RecyclerView is not consistent when it is on view change. If you try to scroll the view while the changes in the data set are processed, you see java.lang.IndexOutOfBoundsException: Inconsistency detected. To eliminate this problem, you should notify the adapter immediately after the data set is changed.
The problem is definitely not because of recyclerview scrolling, but it is related to notifyDataSetChanged(). I had a recycler view in which i was constantly changing data i.e. adding and removing data. I was calling notifyDataSetChanged() everytime I was adding items to my list, But was not refreshing the adapter whenever the item is being removed or the list was cleared.
So to fix the :
java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 2(offset:2).state:12 at android.support.v7.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:5456)
I called adapter.notifyDataSetChanged() after list.clear(), wherever it was required.
if (!myList.isEmpty()) {
myList.clear();
myListAdapter.notifyDataSetChanged();
}
Since then, I never encountered the exception.
Hope it works out the same for others as well. :)
This problem is coming to recyclerview if you use
adapter.setHasStableIds(true);
if you set so, remove this, and to update your dataset inside adaptor;
if you still face the issue, invalidate all views once you get new data and then update your dataset.
I was facing same issue, java.lang.IndexOutOfBoundsException: Inconsistency detected.
Create Custom LinearLayoutManager.
HPLinearLayoutManager.java
public class HPLinearLayoutManager extends LinearLayoutManager {
public HPLinearLayoutManager(Context context) {
super(context);
}
public HPLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public HPLinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
/**
* Magic here
*/
#Override
public boolean supportsPredictiveItemAnimations() {
return false;
}
}
create instance of HPLinearLayoutManager.
HPLinearLayoutManager hpLinearLayoutManager = new HPLinearLayoutManager(mContext);
recyclerView.setLayoutManager(hpLinearLayoutManager);
Hope this would help you.
I am altering data for the RecyclerView in the background Thread. I got the same Exception as the OP. I added this after changing data:
myRecyclerView.post(new Runnable() {
#Override
public void run() {
myRecyclerAdapter.notifyDataSetChanged();
}
});
Hope it helps
I had a similar issue but with deleting the contents. I also wanted to keep the animations too. I ended up using the notifyRemove, then passing the range. This seems to fix any issues...
public void deleteItem(int index) {
try{
mDataset.remove(index);
notifyItemRemoved(index);
} catch (IndexOutOfBoundsException e){
notifyDataSetChanged();
e.printStackTrace();
}
}
Seems to be working and getting rid of the IOB Exception...
I got this to work using Cocorico suggestion in a previous answer (https://stackoverflow.com/a/26927186/3660638) but there's a catch: since I'm using a SortedList, using notifyDataSetChanged() everytime there's a change in data (add, remove, etc) makes you lose the item animations that you get with notifyItemXXXXX(position), so what I ended up doing was using it only when I change the data in batch, like:
public void addAll(SortedList<Entity> items) {
movieList.beginBatchedUpdates();
for (int i = 0; i < items.size(); i++) {
movieList.add(items.get(i));
}
movieList.endBatchedUpdates();
notifyDataSetChanged();
}
You must use in your getitem count
public int getItemCount() {
if (mList!= null)
return mList.size();
else
return 0;
}
Also refreshing the recycler view please use this
if (recyclerView.getAdapter() == null) {
recyclerView.setHasFixedSize(true);
mFileListAdapter= new FileListAdapter(this);
recyclerView.setAdapter(mFileListAdapter);
recyclerView.setItemAnimator(new DefaultItemAnimator());
} else {
mFileListAdapter.notifyDataSetChanged();
}
By using this solution you are not able to solve the issue
you simply use a condition inside the onBindViewHolder to resolve the java.lang.IndexOutOfBoundsException
public void onBindViewHolder(FileHolder fileHolder, final int i) {
if(i < mList.size)
{
String name = mList.get(i);
setSelected(fileHolder.itemView, mSelectedArray.get(i));
fileHolder.mTextView.setText(name);
}
}
create CustomLinearLayoutManager:
public class CustomLinearLayoutManager extends LinearLayoutManager {
public CustomLinearLayoutManager(Context context) {
super(context);
}
public CustomLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public CustomLinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
public boolean supportsPredictiveItemAnimations() {
return false;
}
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
try {
super.onLayoutChildren(recycler, state);
} catch (IndexOutOfBoundsException e) {
e.printStackTrace();
}
}
#Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
try {
return super.scrollVerticallyBy(dy, recycler, state);
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
}
I figure out that, for me this exception comes when two things happens at the same time i.e.
1) Scrolling of recyclerview
2) data set getting changed
So, I solved this problem by disabling the scroll till the notifydatasetchanged is called.
leaderAdapter.notifyDataSetChanged();
pDialog.hide();
To disable the scroll, I have used a progress dialog, whose setCancelable is false.
pDialog = new ProgressDialog(getActivity());
pDialog.setMessage("Please wait...");
pDialog.setCancelable(false);
The trick here is to enable the scrolling only when the data set has been updated.
I have replicated this issue.
This happened when we remove items in the background thread from mList but dont call notifyDataSetChanged(). Now If we scroll This exception is comming.
java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 86(offset:86).state:100
Initially I had 100 items and removed few items from background thread.
Seems like Recyclerview calls getItemCount() itself to validate the state.
Avoid notifyDatasetHasChanged() and do the following:
public void setItems(ArrayList<Article> newArticles) {
//get the current items
int currentSize = articles.size();
//remove the current items
articles.clear();
//add all the new items
articles.addAll(newArticles);
//tell the recycler view that all the old items are gone
notifyItemRangeRemoved(0, currentSize);
//tell the recycler view how many new items we added
notifyItemRangeInserted(0, articles.size());
}
I had same issue when I was setting new adapter instance on based on some selection criteria.
I have fixed my issue by using RecyclerView.swapAdapter(adapter, true)
When we set new adapter.
I have same issue with this problem, I'm very tired to search and resolve it. But I have found answer to resolve and exceptions have not been thrown out again.
public class MyLinearLayoutManager extends LinearLayoutManager
{
public MyLinearLayoutManager(Context context) {
super(context);
}
public MyLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public MyLinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
public boolean supportsPredictiveItemAnimations() {
return false;
}
#Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//override this method and implement code as below
try {
super.onLayoutChildren(recycler, state);
} catch (Exception e) {
e.printStackTrace();
}
}
}
I hope this answer will be resolve your problem.
I dont see anthing wrong with the code you posted. The only thing weird to me is this line
setSelected(fileHolder.itemView, mSelectedArray.get(i));
in your onBindViewHolder method in the adapter.. are you updating this array too when you change the size of your list of items in the array?
I had similar problem while i try to add first item into recyclerView with notifyItemInserted method, so i modified addItem function on my adapter as below and it resolved.
Weird problem, hope that it'll be fixed soon thoug!
public void addItem(int position, TableItem item) {
boolean firstEntry = false;
if (items.size() == 0) {
firstEntry = true;
}
items.add(position, item);
if (firstEntry) {
notifyDataSetChanged();
} else {
notifyItemInserted(position);
}
}
there is one sentence in sound code :
/**
* Used when LayoutState is constructed in a scrolling state. It should
* be set the amount of scrolling we can make without creating a new
* view. Settings this is required for efficient view recycling.
*/
int mScrollingOffset;
In my case it solved by changing
mRecyclerView.smoothScrollToPosition(0) to
mRecyclerView.scrollToPosition(0)
I also had the same issue and I have fixed it with not using notifyItemRangeChanged() method. It is nicely explained at
https://code.google.com/p/android/issues/detail?id=77846#c10
try to use a boolean flag, initialize it as false and inside OnRefresh method make it true, clear your dataList if flag is true just before adding the new data to it and after that make it false.
your code might be like this
private boolean pullToRefreshFlag = false ;
private ArrayList<your object> dataList ;
private Adapter adapter ;
public class myClass extend Fragment implements SwipeRefreshLayout.OnRefreshListener{
private void requestUpdateList() {
if (pullToRefresh) {
dataList.clear
pullToRefreshFlag = false;
}
dataList.addAll(your data);
adapter.notifyDataSetChanged;
#Override
OnRefresh() {
PullToRefreshFlag = true
reqUpdateList() ;
}
}
In my case the problem was me.
My setup is a Recyclerview, Adapter & Cursor/Loader mechanism.
At one point in my App the loader is destroyed.
supportLoaderManager.destroyLoader(LOADER_ID_EVENTS)
I was expecting the Recyclerview would display an empty list since i just deleted their datasource. What makes the error finding more complicated was, that the list was visible and the well known Exception occured only on a fling/scroll/animation.
That cost me a few hrs. :)
do this when you want to add view(like notifyData or addView or something like that)
if(isAdded()){
//
// add view like this.
//
// celebrityActionAdapter.notifyItemRangeInserted(pageSize, 10);
//
//
}
I recently ran into this issue and discovered my problem was I was modifying the adapter's data source on one loop/call stack and then calling notifyDataSetChanged on a subsequent loop/call stack. Between changing the data source and notifyDataSetChanged occurring, the RecyclerView was trying to fill in views due to the scrolling and noticed the adapter was in a weird state and justifiably threw this exception.
Yigit Boyar explains over and over again two reasons why this crash would occur in your app:
You must be on the same call stack when you change your adapter source and notifyDataSetChanged()
You must be on the Main Thread when changing your adapter's data source
If you're unsure how to debug this do, add the following Kotlin code where you change your adapter source and where you call notifyDataSetChanged
Log.d("TEST", "isMainThread: ${Looper.myLooper() == Looper.getMainLooper()}")
Log.d("TEST", Log.getStackTraceString(Exception("Debugging RV and Adapter")))
Related
I have a problem with my recyclerViev, specifically with the scrolling.
I have some list, which is updated in real time, some item is added, some removed, and everything is sorted by some parameter.
So the item which was initially first on the list, can have its parameter changed, which will be in different position after the sorting.
So my recyclerView is for example focusing on the initial item, and after change, when some item has "better" parameter is changing position with that initial item.
Problem is, i want to focus on the new item, with "better" parameter when I'm not scrolling, but i don't want to focusing on it when i scroll by touch(so my touch will not be interrupted by scrolling to current first item on the list).
So i don't want to force this code after every change in my recyclerView data:
recyclerView.scrollToPosition(0);
because as i said, i will be interrupted by this scroll when i am touching my recyclerView list and go down to see other items and in the same time there will be a change in my list.
Is there a way to accomplish this?
To be specific, i am using DiffCallback from the DiffUtil, to support animations when there is a change in my current recyclerView list - it compares the old list with another new list and apply all the wanted animations and notifications(item added, removed, changed position). So i never call
notifyDataSetChanged
or anything like that
Here is my DiffUtil callback:
public static class DevicesDiffCallback extends DiffUtil.Callback{
List<DeviceInfo> oldDevices;
List<DeviceInfo> newDevices;
public DevicesDiffCallback(List<NexoDeviceInfo> newDevices, List<NexoDeviceInfo> oldDevices) {
this.newDevices = newDevices;
this.oldDevices = oldDevices;
}
#Override
public int getOldListSize() {
return oldDevices != null ? oldDevices.size() : 0;
}
#Override
public int getNewListSize() {
return newDevices != null ? newDevices.size() : 0;
}
#Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldDevices.get(oldItemPosition).getNexoIdentifier().getSerialNumber().equals(newDevices.get(newItemPosition).getNexoIdentifier().getSerialNumber());
}
#Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return oldDevices.get(oldItemPosition).equals(newDevices.get(newItemPosition));
}
#Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
return super.getChangePayload(oldItemPosition, newItemPosition);
}
}
And i set it like this in my adapter, when i get the list of new data to be populated and replace the old data:
public void setData(List<DeviceInfo> data) {
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DevicesDiffCallback(this.mData, data), false);
diffResult.dispatchUpdatesTo(this);
mData = data;
}
I'm not sure about this answer but, I think your code to call DiffUtil is not proper. Try using this :
public void addItems(List<Recipe> recipeList) {
List<Recipe> newRecipeList = new ArrayList<>();
newRecipeList.addAll(this.recipeList);
newRecipeList.addAll(recipeList);
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new RecipeDiffUtilCallback(this.recipeList, newRecipeList));
this.recipeList.addAll(recipeList);
diffResult.dispatchUpdatesTo(this);
}
I have a simple adapter
public class ConversationListAdapter extends
RecyclerView.Adapter<Conversation.ViewHolder> {
private List<Conversation> items;
private Activity activity;
public ConversationListAdapter(Activity activity, List<Conversation> items) {
super();
this. items = items;
this.activity = activity;
}
#Override
public void onBindViewHolder(ViewHolder viewHolder, int i) {
Conversation conversation = mItems.get(i);
viewHolder.name.setText(conversation.getName());
if ( conversation.getUrl() != null) {
Picasso.with(activity.getApplicationContext()).load(conversation.getUrl())
.into(viewHolder.imageView);
}
}
and a basic
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {..}
}
Now in the fragment as always:
mRecyclerView.setAdapter(new ConversationAdapter(getActivity(), mItems));
Now Im calling my rest api to get the data and the first time it works great everything is where it should be (let's say in c there is only 2 items and the order is conv1 and conv2)
private void handleResult(List<Conversation> c) {
items.clear()
items.addAll(c)
adapter.notifyDataSetChanged()
}
But... now if I refresh for example and the data in the List comes in a different order (conv2 and then conv1) after the adapter.notifyDataSetChanged() both of my imageView in the list have the same pictures.. ! But the textView however has the right text
This only happens with view filled with Picasso and cannot understand why
Could someone help me on this ?
you use if condition in any adapter you also have to set else part of it. i also don't know exactly why this is happen that if condition is given it takes same condition for child which are not match this, may be a bug in android. please try else part of it also. may be this work for you.
You have to replace your items in your adapter or create a new adapter with the new items
1st solution:
private void handleResult(List<Conversation> c) {
mRecyclerView.setAdapter(new ConversationAdapter(getActivity(), c));
}
2nd solution:
private void handleResult(List<Conversation> c) {
adapter.setList(c);
adapter.notifyDataSetChanged();
}
And don't to forget to create setList(List<Conversation> c) method in your Adapter
I know there are lots of threads already on this topic, but none of the given solutions worked for me so far. I'm trying to add or update an item of a RecyclerView. Here's my code so far:
MainActivity
private MyListItemAdapter mAdapter;
private RecyclerView recyclerView;
// called on activity create
private void init() {
// initialize activity, load items, etc ...
mAdapter = new MyListItemAdapter(this, items);
recyclerView.setAdapter(mAdapter);
}
// called when I want to replace an item
private void updateItem(final Item newItem, final int pos) {
mAdapter.replaceItem(newItem, pos);
}
MyListItemAdapter
public class MyListItemAdapter extends RecyclerView.Adapter<MyListItemAdapter.MyListItemViewHolder> {
private List<Item> mItems;
public void replaceItem(final Item newItem, final int pos) {
mItems.remove(position);
mItems.add(position, newItem);
notifyItemChanged(position);
notifyDataSetChanged();
}
}
I tried to make this changes from the MainActivity aswell, but in every case I tried my list doesn't get updated. The only way it worked was when I reset the adapter to the recyclerView:
mAdapter.notifyDataSetChanged();
recyclerView.setAdapter(mAdapter);
which obviously is a bad idea. (aside from the bad side effects wouldn't even work when I'm using lazy loading on my lists).
So my question is, how can I make notifyDataSetChanged() work properly?
edit
I found a solution for replacing items. After mAdapter.replaceItem(newItem, pos); I had to call recyclerView.removeViewAt(position);
This works for replacing an item, but doesn't solve my problem when I want to add items (e.g. lazy loading) to my list
edit2
I found a working solution for adding items
Adapter:
public void addItem(final Item newItem) {
mItems.add(newItem);
notifyDataSetChanged();
}
Activity:
private void addItem(final Item newItem) {
mAdapter.addItem(newItem);
recyclerView.removeViewAt(0); // without this line nothing happens
}
For some reason this works (also: it doesn't remove the view at position 0), but I'm sure this isn't the correct way to add items to a recyclerView
This should work:
private ArrayList<Item> mItems;
public void replaceItem(final Item newItem, final int position) {
mItems.set(position, newItem);
notifyItemChanged(position);
}
ArrayList.set() is the way to go to replace items.
For adding items, just append them to mItems and then go notifyDatasetChanged(). Another way to go is to use notifyItemRangeInserted(). Depending on where/how are you adding new items and how many of them, it might be worth it.
Use
mItems.set(position, newItem);
instead of
mItems.add(position, newItem);
because .set method will replace your data to particular position.
I have 36 spinners that I have initialized with some values. I have used onItemSelectedListener with them. As usual, the user can interact with these spinners, firing the onItemSeected function.
One problem is that the call is made during init, but I found solutions to it here and avoided that using a global variable "count" and checking if count > 36 before executing code inside onItemSelected.
My problem is this:
The user has the option to click on a button called "Previous", upon which I have to reset SOME of the spinner values.
I tried changing the value of count to 0 before resetting the spinners, and then changing it back to 37 after resetting, but I have come to understand that the onItemSelected is called only after every other function is done executing, so it is called AFTER count is changed back to 37 even though the spinner values are set as soon as they are selected by user.
I need to repeatedly refresh some spinners WITHOUT firing off the onItemSelected function. Can anyone please help me find a solution? Thanks.
I found a simple and, I think, elegant solution.
Using tags.
I first created a new XML file called 'tags' and put in the following code:
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<item name="pos" type="id" />
</resources>
Whenever I myself use spin.setSelection(pos), I also do spin.setTag(R.id.pos, pos), so I am setting the current position as a tag.
Then, in onItemSelected, I am executing code only if(spin.getTag(R.id.pos) != position), where position is the position variable supplied by the function.
In this way, my code is executed only when the user is making a selection.
Since the user has made a selection, the tag has not been updated, so after the processing is done, I update the tag as spin.setTag(R.id.pos, position).
NOTE: It is important to use the same adapter throughout, or the "position" variable might point to different elements.
EDIT: As kaciula pointed out, if you're not using multiple tags, you can use the simpler version, that is spin.setTag(pos) and spin.getTag() WITHOUT the need for an XML file.
When Spinner.setSelection(position) is used, it always activates setOnItemSelectedListener()
To avoid firing the code twice I use this solution:
private Boolean mIsSpinnerFirstCall = true;
...
Spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
//If a new value is selected (avoid activating on setSelection())
if(!mIsSpinnerFirstCall) {
// Your code goes gere
}
mIsSpinnerFirstCall = false;
}
public void onNothingSelected(AdapterView<?> arg0) {
}
});
I don't know if this solution is as foolproof as the chosen one here, but it works well for me and seems even simpler:
boolean executeOnItemSelected = false;
spinner.setSelection(pos)
And then in the OnItemSelectedListener
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if(executeOnItemSelected){
//Perform desired action
} else {
executeOnItemSelected = true;
}
}
The way I solved this is by saving the OnItemSelectedListener first. Then set the OnItemSelectedListener of the Spinner to the null value. After setting the item in the Spinner by code, restore the OnItemSelectedListener again. This worked for me.
See code below:
// disable the onItemClickListener before changing the selection by code. Set it back again afterwards
AdapterView.OnItemSelectedListener onItemSelectedListener = historyPeriodSpinner.getOnItemSelectedListener();
historyPeriodSpinner.setOnItemSelectedListener(null);
historyPeriodSpinner.setSelection(0);
historyPeriodSpinner.setOnItemSelectedListener(onItemSelectedListener);
Here's my solution to this problem. I extend AppCompatSpinner and add a method pgmSetSelection(int pos) that allows programmatic selection setting without triggering a selection callback. I've coded this with RxJava so that the selection events are delivered via an Observable.
package com.controlj.view;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.AdapterView;
import io.reactivex.Observable;
/**
* Created by clyde on 22/11/17.
*/
public class FilteredSpinner extends android.support.v7.widget.AppCompatSpinner {
private int lastSelection = INVALID_POSITION;
public void pgmSetSelection(int i) {
lastSelection = i;
setSelection(i);
}
/**
* Observe item selections within this spinner. Events will not be delivered if they were triggered
* by a call to setSelection(). Selection of nothing will return an event equal to INVALID_POSITION
*
* #return an Observable delivering selection events
*/
public Observable<Integer> observeSelections() {
return Observable.create(emitter -> {
setOnItemSelectedListener(new OnItemSelectedListener() {
#Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
if(i != lastSelection) {
lastSelection = i;
emitter.onNext(i);
}
}
#Override
public void onNothingSelected(AdapterView<?> adapterView) {
onItemSelected(adapterView, null, INVALID_POSITION, 0);
}
});
});
}
public FilteredSpinner(Context context) {
super(context);
}
public FilteredSpinner(Context context, int mode) {
super(context, mode);
}
public FilteredSpinner(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FilteredSpinner(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public FilteredSpinner(Context context, AttributeSet attrs, int defStyleAttr, int mode) {
super(context, attrs, defStyleAttr, mode);
}
}
An example of its usage, called in onCreateView() in a Fragment for example:
mySpinner = view.findViewById(R.id.history);
mySpinner.observeSelections()
.subscribe(this::setSelection);
where setSelection() is a method in the enclosing view that looks like this, and which is called both from user selection events via the Observable and also elsewhere programmatically, so the logic for handling selections is common to both selection methods.
private void setSelection(int position) {
if(adapter.isEmpty())
position = INVALID_POSITION;
else if(position >= adapter.getCount())
position = adapter.getCount() - 1;
MyData result = null;
mySpinner.pgmSetSelection(position);
if(position != INVALID_POSITION) {
result = adapter.getItem(position);
}
display(result); // show the selected item somewhere
}
I'm displaying a list of contacts (name + picture) using the ListView. In order to make the initial load fast, I only load the names first, and defer picture loading. Now, whenever my background thread finishes loading a picture, it schedules my adapter's notifyDataSetChanged() to be called on the UI thread. Unfortunately, when this happens the ListView does not re-render (i.e. call getView() for) the items that are already on-screen. Because of this, the user doesn't see the newly-loaded picture, unless they scroll away and back to the same set of items, so that the views get recycled. Some relevant bits of code:
private final Map<Long, Bitmap> avatars = new HashMap<Long, Bitmap>();
// this is called *on the UI thread* by the background thread
#Override
public void onAvatarLoaded(long contactId, Bitmap avatar) {
avatars.put(requestCode, avatar);
notifyDataSetChanged();
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
// snip...
final Bitmap avatar = avatars.get(contact.id);
if (avatar != null) {
tag.avatar.setImageBitmap(avatar);
tag.avatar.setVisibility(View.VISIBLE);
tag.defaultAvatar.setVisibility(View.GONE);
} else {
tag.avatar.setVisibility(View.GONE);
tag.defaultAvatar.setVisibility(View.VISIBLE);
if (!avatars.containsKey(contact.id)) {
avatars.put(contact.id, null);
// schedule the picture to be loaded
avatarLoader.addContact(contact.id, contact.id);
}
}
}
AFAICT, if you assume that notifyDataSetChanged() causes the on-screen items to be re-created, my code is correct. However, it seems that is not true, or maybe I'm missing something. How can I make this work smoothly?
Here I go answering my own question with a hackaround that I've settled on. Apparently, notifyDataSetChanged() is only to be used if you are adding / removing items. If you are updating information about items that are already displayed, you might end up with visible items not updating their visual appearance (getView() not being called on your adapter).
Furthermore, calling invalidateViews() on the ListView doesn't seem to work as advertised. I still get the same glitchy behavior with getView() not being called to update on-screen items.
At first I thought the issue was caused by the frequency at which I called notifyDataSetChanged() / invalidateViews() (very fast, due to updates coming from different sources). So I've tried throttling calls to these methods, but still to no avail.
I'm still not 100% sure this is the platform's fault, but the fact that my hackaround works seems to suggest so. So, without further ado, my hackaround consists in extending the ListView to refresh visible items. Note that this only works if you're properly using the convertView in your adapter and never returning a new View when a convertView was passed. For obvious reasons:
public class ProperListView extends ListView {
private static final String TAG = ProperListView.class.getName();
#SuppressWarnings("unused")
public ProperListView(Context context) {
super(context);
}
#SuppressWarnings("unused")
public ProperListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
#SuppressWarnings("unused")
public ProperListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
class AdapterDataSetObserver extends DataSetObserver {
#Override
public void onChanged() {
super.onChanged();
refreshVisibleViews();
}
#Override
public void onInvalidated() {
super.onInvalidated();
refreshVisibleViews();
}
}
private DataSetObserver mDataSetObserver = new AdapterDataSetObserver();
private Adapter mAdapter;
#Override
public void setAdapter(ListAdapter adapter) {
super.setAdapter(adapter);
if (mAdapter != null) {
mAdapter.unregisterDataSetObserver(mDataSetObserver);
}
mAdapter = adapter;
mAdapter.registerDataSetObserver(mDataSetObserver);
}
void refreshVisibleViews() {
if (mAdapter != null) {
for (int i = getFirstVisiblePosition(); i <= getLastVisiblePosition(); i ++) {
final int dataPosition = i - getHeaderViewsCount();
final int childPosition = i - getFirstVisiblePosition();
if (dataPosition >= 0 && dataPosition < mAdapter.getCount()
&& getChildAt(childPosition) != null) {
Log.v(TAG, "Refreshing view (data=" + dataPosition + ",child=" + childPosition + ")");
mAdapter.getView(dataPosition, getChildAt(childPosition), this);
}
}
}
}
}
Add the following line to onResume()
listview.setAdapter(listview.getAdapter());
According to the documentation:
void notifyDataSetChanged ()
Notify any registered observers that the data set has changed.
... LayoutManagers will be forced to fully rebind and relayout all visible views...
In my case, the items were not visible (then whole RecycleView was outside the screen), and later on when it animated in, the item views didn't refresh either (thus showing the old data).
Workaround in the Adapter class:
public void notifyDataSetChanged_fix() {
// unfortunately notifyDataSetChange is declared final, so cannot be overridden.
super.notifyDataSetChanged();
for (int i = getItemCount()-1; i>=0; i--) notifyItemChanged(i);
}
Replaced all calls of notifyDataSetChanged() to notifyDataSetChanged_fix() and my RecyclerView happily refreshing ever since...