Android spinner onItemSelected called multiple times after screen rotation
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);
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.
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
}
});
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.