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
}
Related
I have a layout with three spinners. They differ in the option presented in the drop-down.
In my onCreateView I have a method to setup the spinners. Inside that method I have something like this:
mySpinner = (Spinner) view.findViewById(R.id.my_spinner);
ArrayAdapter<String> mySpinner =
new ArrayAdapter<String>(getActivity(), R.layout.background,
new ArrayList<String>(Arrays.asList(getResources().getStringArray(R.array.spinner_one_data))));
mySpinner.setDropDownViewResource(R.layout.spinner_text);
mySpinner.setAdapter(mySpinner);
mySpinner.setOnItemSelectedListener(this);
As I said, my other two spinners are almost the same but with different options.
I know that onItemSelected is called once for every spinner in a "first setup" so I have a flag to prevent this problem. With this flag solution, my spinners are working as expected.
The problem is when I select in each spinner an option and then rotate the screen. Now, onItemSelected is called 6 times instead the 3 times that I was expecting (I've set a flag to manage this situation of the 3 times calling).
Why Is it happening and hoe should I handle this?
In general, I've found that there are many different events that can trigger the onItemSelected method, and it is difficult to keep track of all of them. Instead, I found it simpler to use an OnTouchListener to only respond to user-initiated changes.
Create your listener for the spinner:
public class SpinnerInteractionListener implements AdapterView.OnItemSelectedListener, View.OnTouchListener {
boolean userSelect = false;
#Override
public boolean onTouch(View v, MotionEvent event) {
userSelect = true;
return false;
}
#Override
public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
if (userSelect) {
// Your selection handling code here
userSelect = false;
}
}
}
Add the listener to the spinner as both an OnItemSelectedListener and an OnTouchListener:
SpinnerInteractionListener listener = new SpinnerInteractionListener();
mSpinnerView.setOnTouchListener(listener);
mSpinnerView.setOnItemSelectedListener(listener);
I've found a solution that is working for me.
I have the 3 spinners so onItemSelected is called 3 times at the initial spinner setup. To avoid onItemSelected from firing a method in the initial setup I've created a counter so onItemSelected only fires the method accordingly the counter value.
I've realized that in my situation, if a rotated the screen, onItemSelected is fired again the 3 times, plus a time for each spinner that is not in the position 0.
An example:
I have the 3 spinners and the user changes 2 of them to one of the available option other then position 0 so he ends up with a situation like this:
First spinner - > Item 2 selected
Second spinner -> Item 0 selected (no changes)
Third spinner -> Item 1 selected
Now, wen I rotate the screen, onItemSelected will be fired 3 times for the initial spinner setup plus 2 times for the spinners that aren't at position 0.
#Override
public void onSaveInstanceState(Bundle outState) {
int changedSpinners = 0;
if (spinner1.getSelectedItemPosition() != 0) {
changedSpinners += 1;
}
if (spinner2.getSelectedItemPosition() != 0) {
changedSpinners += 1;
}
if (spinner3.getSelectedItemPosition() != 0) {
changedSpinners += 1;
}
outState.putInt("changedSpinners", changedSpinners);
}
I've saved the state in onSaveInstanceState and then, in onCreateView I checked if savedInstanceState != null and if so, extracted changedSpinners from the bundle and updated my counter to act accordingly.
To expand on Andres Q.'s answer... If you are using Java 8 you can do this with fewer lines of code by making use of lambda expressions. This method also forgoes the need to create a separate class in order to implement onTouchListener
Boolean spinnerTouched; //declare this as an instance or class variable
spinnerTouched = false;
yourSpinner.setOnTouchListener((v,me) -> {spinnerTouched = true; v.performClick(); return false;});
yourSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
#Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if(spinnerTouched){
//do your stuff here
}
}
#Override
public void onNothingSelected(AdapterView<?> parent) {
//nothing selected
}
});
What about just to check if fragment is in resumed state? Somethink like this:
private AdapterView.OnItemSelectedListener mFilterListener = new AdapterView.OnItemSelectedListener() {
#Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (isResumed()) {
//your code
}
}
#Override
public void onNothingSelected(AdapterView<?> parent) {
}
};
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
//set mFilterListener
}
It eliminates the rotation problem and also the first setup problem. No flags etc. I was having the same problem with TextWatchers and found this answer with comment, which inspired me for this solution.
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")))
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...
It appears that android's Spinner class (and possibly ListView in general, although I don't know for sure) calls your OnItemSelectedListener's onItemSelected() method after you call setAdapter(), even if the user hasn't explicitly selected anything yet.
I can see how this would be useful in many situations, but there are times when I only want onItemSelected() to be called when an item is actually specifically selected.
Is there a way to control this behaviour and have Spinner NOT call onItemSelected() after setting the adapter?
I haven't used this solution for very long yet so I'm not totally confident that it works as expected, but I've had luck so far with this workaround:
spinner.setOnItemSelectedListener( new OnItemSelectedListener() {
protected Adapter initializedAdapter = null;
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
// Always ignore the initial selection performed after setAdapter
if( initializedAdapter !=parent.getAdapter() ) {
initializedAdapter = parent.getAdapter();
return;
}
...
}
}
Is there a better way?
Add listener to spinner like below:
spinner.post(new Runnable(){
public void run()
{
spinner.setOnItemSelectedListener( new OnItemSelectedListener() {
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
...
}
}
}
});
I've used the setTag and getTag methods, and created a resource id called "spinnerstate".
Then whenever I programmatically set the adapter, I set the "spinnerstate" tag to "init", and in the fired event, set it to "ready" and ignore the event. (note my code is Mono for Android se it will look different):
Set Adapter:
profileSpn.SetTag (Resource.Id.spinnerstate, "init");
profileSpn.Adapter = new ArrayAdapter (this, Android.Resource.Layout.SimpleSpinnerItem, items.ToArray ());
Item Selected event:
string state = (string)((Spinner)sender).GetTag (Resource.Id.spinnerstate);
if (state == "init") {
((Spinner)sender).SetTag (Resource.Id.spinnerstate, "ready");
return;
}
I agree that this is not desired behaviour in almost 100% of cases, and I don't think it's good design on the part of Google, but there you go.
I did similar things before, I used count value. Using parent adapter object is incomplete because it can be a problem when view is refreshed or getView() called again.
Therefore, I recommend that using array of counter.
At first, define array of count in adapter globally.
private int isInitializedView[];
And then initialize it on getView().
isInitializedView[position] = 0;
In the selection listener, do something that you want if it already initialized.
holder.mySpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
#Override
public void onItemSelected(AdapterView<?> parentView, View selectedItemView, int position, long id) {
isInitializedView[position]++;
if(isInitializedView[position] > 1) {
// do someting that you want
}
}
#Override
public void onNothingSelected(AdapterView<?> parentView) {}
});
(Note that isInitializedView[position]++; can be come after if() routine, and only trigger event when this value is >0 . It's your choice.)
I had three spinner in my activity and all spinner adapter data has been filled at runtime(from web-service data after call from onCreate method). So it automatically call onItemSelected(AdapterView<?> parent, View view, int position, long id) method of spinner.
I solved this issue by using onUserInteraction() method of activity
check this method that user is interacting with spinner or not. if yes then perform the action else not
Declare isUserIntract boolean variable globally
in onItemSelected method use following procedure
If(isUserIntract)
{
//perform Action
}
else{
//not perform action
}
use below code in activity
#Override
public void onUserInteraction() {
super.onUserInteraction();
isUserIntract = true;
}
The OnItemSelectedListener event handler gets called both when a spinner
selection is changed programmatically, and when a user physically clicks the spinner control.
Is is possible to determine if an event was triggered by a user
selection somehow?
Or is there another way to handle spinner user selections?
To workaround you need to remember the last selected position. Then inside of your spinner listener compare the last selected position with the new one. If they are different, then process the event and also update the last selected position with new position value, else just skip the event processing.
If somewhere within the code you are going to programatically change spinner selected position and you don't want the listener to process the event, then just reset the last selected position to the one you're going to set.
Yes, Spinner in Android is painful. I'd even say pain starts from its name - "Spinner". Isn't it a bit misleading? :) As far as we're talking about it you should also be aware there's a bug - Spinner may not restore (not always) its state (on device rotation), so make sure you handle Spinner's state manually.
Hard to believe that a year and a half later, the problem still exists and continues to boggle people...
Thought I'd share the workaround I came up with after reading Arhimed's most useful post (thanks, and I agree about spinners being painful!). What I've been doing to avoid these false positives is to use a simple wrapper class:
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
public class OnItemSelectedListenerWrapper implements OnItemSelectedListener {
private int lastPosition;
private OnItemSelectedListener listener;
public OnItemSelectedListenerWrapper(OnItemSelectedListener aListener) {
lastPosition = 0;
listener = aListener;
}
#Override
public void onItemSelected(AdapterView<?> aParentView, View aView, int aPosition, long anId) {
if (lastPosition == aPosition) {
Log.d(getClass().getName(), "Ignoring onItemSelected for same position: " + aPosition);
} else {
Log.d(getClass().getName(), "Passing on onItemSelected for different position: " + aPosition);
listener.onItemSelected(aParentView, aView, aPosition, anId);
}
lastPosition = aPosition;
}
#Override
public void onNothingSelected(AdapterView<?> aParentView) {
listener.onNothingSelected(aParentView);
}
}
All it does is trap item selected events for the same position that was already selected (e.g. the initial automatically triggered selection for position 0), and pass on other events to the wrapped listener. To use it, all you have to do is modify the line in your code that calls the listener to include the wrapper (and add the closing bracket of course), so instead of, say:
mySpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
...
});
you'd have this:
mySpinner.setOnItemSelectedListener(new OnItemSelectedListenerWrapper(new OnItemSelectedListener() {
...
}));
Obviously once you've tested it, you could get rid of the Log calls, and you could add the ability to reset the last position if required (you'd have to keep a reference to the instance, of course, rather than declaring on-the-fly) as Arhimed said.
Hope this can help someone from being driven crazy by this strange behaviour ;-)
In the past I've done things like this to distinguish
internal++; // 'internal' is an integer field initialized to 0
textBox.setValue("...."); // listener should not act on this internal setting
internal--;
Then in textBox's listener
if (internal == 0) {
// ... Act on user change action
}
I use ++ and -- rather than setting a boolean value to 'true' so that there is no worry when methods nest other methods that might also set the internal change indicator.
I had this situation lately when using spinners and the internet didn't came up with a suitable solution.
My application scenario:
X spinners (dynamically, 2 for each cpu, min & max) for setting & viewing the CPU-Frequency. They are filled when the application starts and they also get the current max/min freq of the cpu set. A thread runs in the background and checks for changes every second and updates the spinners accordingly. If a new frequency inside the spinner is set by the user the new frequency is set.
The issue was that the thread accessed setSelection to update the current frequency which in turn called my listener and I had no way of knowing if it was the user or the thread that changed the value. If it was the thread I didn't want the listener to be called since there would have been no need to change the frequency.
I came up with a solution that suits my needs perfectly and works around the listener on your call :) (and I think this solution gives you maximal control)
I extended Spinner:
import android.content.Context;
import android.widget.Spinner;
public class MySpinner extends Spinner {
private boolean call_listener = true;
public MySpinner(Context context) {
super(context);
}
public boolean getCallListener() {
return call_listener;
}
public void setCallListener(boolean b) {
call_listener = b;
}
#Override
public void setSelection(int position, boolean lswitch) {
super.setSelection(position);
call_listener = lswitch;
}
}
and created my own OnItemSelectedListener:
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
public class SpinnerOnItemSelectedListener implements OnItemSelectedListener {
public void onItemSelected(AdapterView<?> parent, View view, int pos,long id) {
MySpinner spin = (MySpinner) parent.findViewById(parent.getId());
if (!spin.getCallListener()) {
Log.w("yourapptaghere", "Machine call!");
spin.setCallListener(true);
} else {
Log.w("yourapptaghere", "UserCall!");
}
}
#Override
public void onNothingSelected(AdapterView<?> arg0) {
// TODO Auto-generated method stub
}
}
If you now create a MySpinner you can use this to set the selection:
setSelection(position, callListener);
Where callListener is either true or false. True will call the listener and is default, which is why user interactions are getting identified, false will also call the listener but uses code you want for this special case, exempli gratia in my case: Nothing.
I hope that someone else finds this useful and is spared a long journey to look if something like this already exists :)
I also looked for a good solution on the internet but didn't find any that satisfied my needs.
So I've written this extension on the Spinner class so you can set a simple OnItemClickListener, which has the same behaviour as a ListView.
Only when an item gets 'selected', the onItemClickListener is called.
Have fun with it!
public class MySpinner extends Spinner
{
private OnItemClickListener onItemClickListener;
public MySpinner(Context context)
{
super(context);
}
public MySpinner(Context context, AttributeSet attrs)
{
super(context, attrs);
}
public MySpinner(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
}
#Override
public void setOnItemClickListener(android.widget.AdapterView.OnItemClickListener inOnItemClickListener)
{
this.onItemClickListener = inOnItemClickListener;
}
#Override
public void onClick(DialogInterface dialog, int which)
{
super.onClick(dialog, which);
if (this.onItemClickListener != null)
{
this.onItemClickListener.onItemClick(this, this.getSelectedView(), which, this.getSelectedItemId());
}
}
}
Just to expand on aaamos's post above, since I don't have the 50 rep points to comment, I am creating a new answer here.
Basically, his code works for the case when the initial Spinner selection is 0. But to generalize it, I amended his code the following way:
#Override
public void setOnItemSelectedListener(final OnItemSelectedListener listener)
{
if (listener != null)
super.setOnItemSelectedListener(new OnItemSelectedListener()
{
private static final int NO_POSITION = -1;
private int lastPosition = NO_POSITION;
#Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id)
{
if ((lastPosition != NO_POSITION) && (lastPosition != position))
listener.onItemSelected(parent, view, position, id);
lastPosition = position;
}
#Override
public void onNothingSelected(AdapterView<?> parent)
{
listener.onNothingSelected(parent);
}
});
else
super.setOnItemSelectedListener(null);
}
Basically, this code will ignore the very first firing of onItemSelected(), and then all subsequent "same position" calls.
Of course, the requirement here is that the selection is set programatically, but that should be the case anyhow if the default position is not 0.
I did some logging and discovered that it only ever gets called on initialize, which is annoying. Can't see the need for all this code, I just created an instance variable that was initialised to a guard value, and then set it after the method had been called the first time.
I logged when the onItemSelected method was being called it was otherwise only being called once.
I had a problem where it was creating two of something and realised it was because I was calling add() on my custom adapter, which already had a reference to the list I was referencing and had added to outside the adapter. After I realised this and removed the add method the problem went away.
Are you guys sure you need all this code?
I know this is pretty late, but I came up with a very simple solution to this. It is based on Arhimed's answer, it's exactly the same. It's very easy to implement too. Refer the accepted answer:
Undesired onItemSelected calls