Spinner's onItemSelected callback called twice after a rotation if non-zero position is selected
Asked Answered
C

10

32

When I create my activity, I setup a Spinner, assigning it a listener and an initial value. I know that the onItemSelected callback is called automatically during application initialization. What I find strange is that this happens twice when the device is rotated, causing me some problems that I will have to circumvent someway. This does not happen if the spinner initial selection is zero. I was able to isolate the issue, here's the simplest activity triggering it:

public class MainActivity extends Activity implements OnItemSelectedListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.i("Test","Activity onCreate");
    setContentView(R.layout.activity_main);
    ((Spinner)findViewById(R.id.spinner1)).setSelection(2);
    ((Spinner)findViewById(R.id.spinner1)).setOnItemSelectedListener(this);
}
@Override
public void onItemSelected(AdapterView<?> spin, View selview, int pos, long selId)
{
    Log.i("Test","spin:"+spin+" sel:"+selview+" pos:"+pos+" selId:"+selId);
}
@Override
public void onNothingSelected(AdapterView<?> arg0) {}
}

And here's the logcat shown when the application is started and then the device rotated:

    I/Test( 9881): spin:android.widget.Spinner@4052f508 sel:android.widget.TextView@40530b08 pos:2 selId:2
    I/Test( 9881): Activity onCreate
    I/Test( 9881): spin:android.widget.Spinner@40535d80 sel:android.widget.TextView@40538758 pos:2 selId:2
    I/Test( 9881): spin:android.widget.Spinner@40535d80 sel:android.widget.TextView@40538758 pos:2 selId:2

Is this the expected behaviour? Am I missing something?

Circumstantial answered 28/1, 2013 at 11:10 Comment(1)
Do you find a solution? I'm still stuck to this...Mra
M
51

Managed to find a solution in another stackoverflow question:

spinner.post(new Runnable() {
    public void run() {
        spinner.setOnItemSelectedListener(listener);
    }
});
Mra answered 4/4, 2013 at 18:27 Comment(8)
Could you put a link to that SOF question?Garrard
Sure @AlvaroSantisteban, if I'm not mistaken it comes from this question: #2562748 but this has more than one year so I don't remember the details anymore :(.Mra
3 years later, thank you very much <3 Solved my problem of Spinner firing twice when I'm going back from fragment B to fragment A, where Spinner lies on fragment A.Housewifely
Don't do this! Is a ugly workaround. The right and simplest way is to use setSelection(#, false) before setting the listener. See https://mcmap.net/q/260791/-spinner-39-s-onitemselected-callback-called-twice-after-a-rotation-if-non-zero-position-is-selectedViscountess
This solves the initial double call, when setting the listener. Unfortunately doesn't solve the double call each time you change the selectionSpringe
@OneCodeMan Doing that solves the initial call to onItemSelected when initializing, but then you still get a call to onItemSelected (instead of 2) after rotating. Setting the listener inside the runnable stops both issues: no more false calls to onItemSelected.Playbill
@Flyview, this is another situation. In fact, this is not a issue specific to Spinners. You are missing the [android:saveEnabled="false"] parameter in your Spinner XML. Because if you are handling the view states by yourself, you must disable this function (not only for Spinners). Otherwise, the Android system will try to restore the view state (firing the onItemSelected again, in this case) in the onRestoreInstanceState() event, that is processed after the onCreate().Viscountess
@OneCodeMan hmmm on the newest libraries and Android 10, the setSelection(#, false) doesn't even work anymore to prevent it from firing during initialization or rotation. Also, posting the runnable does not work for initialization anymore, although I'm pretty sure it did when I made my first comment. I now use a combination of a boolean flag for the initial initialization, and the handler post for the rotation!Playbill
G
26

In general, there seem to be many events that trigger the onItemSelected call, and it is difficult to keep track of all of them. This solution allows you to only respond to user-initiated changes using an OnTouchListener.

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);
Galactic answered 11/2, 2015 at 23:38 Comment(3)
IMO this is a pretty elegant solution. Self-contained and easy to understand. I've been using the View.post() hack for a long time but glad I came looking around just in case someone came up with something new. +1Paleface
I think this is cleaner than only setting the listener on the next looper event, considering the selection listener should by default only send events if the user themselves has selected it. Just make sure you also set the touch listener, I forgot for a sec.Flapdoodle
This solution doesn't seem accessible though... users who are using their devices using TalkBack or some other screen reader application likely won't trigger selections this way.Adest
F
10

The first time the onItemSelected runs, the view is not yet inflated. The second time it is already inflated. The solution is to wrap methods inside onItemSelected with if (view != null).

@Override
public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
    if (view != null) { 
        //do things here

    }
}
Fronia answered 11/6, 2017 at 15:12 Comment(2)
Thanks a lot!! This solved my use case of onItemSelected() getting fired twice when I was returning to my fragment (onBackPressed) from another fragment. This behaviour of spinner was driving me nuts. You saved me hours :)Strongwilled
This is an unnecessary workaround, check the solution https://mcmap.net/q/260791/-spinner-39-s-onitemselected-callback-called-twice-after-a-rotation-if-non-zero-position-is-selectedViscountess
V
9

Just use setSelection(#, false) before setting the listener:

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    spinner.setSelection(2, false);
    spinner.setOnItemSelectedListener(this);
}

The key is the second parameter. It says to not animate the transition, executing the action immediately and preventing onItemSelected from being fired twice, because a call is already made by the system.

Viscountess answered 7/7, 2017 at 20:31 Comment(5)
This solves the initial duplicated call, but does not solve the 2nd call every time you change the selection in the spinnerSpringe
Check your logic @Jose_GD, maybe you are triggering the selection again, directly or indirectly, by some action inside your OnItemSelected listener. Start checking your listener.Viscountess
I did, thanks. Nothing that could trigger that callback again. Solved it with flags finally.Springe
Ok, but attention to what you are doing, because the Spinner do not fire the listener twice by itself and if it's happening, something is weird in your code and may generate other unexpected behaviors.Viscountess
I prefer this solution compared to the other alternatives proposed as answers, as it is straightforward and does not use any extra variable.Daysidayspring
S
2

This is what i did:

Do a local variable

Boolean changeSpinner = true;

On the saveInstanceMethod save the selected item position of the spinner

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt("ItemSelect",mySpinner.getSelectedItemPosition());
}

Then on the activity created get that int from savedInstanceState and if the int is != 0 then set the boolean variable on false;

@Override
    public void onActivityCreated(Bundle savedInstanceState) {

    if (savedInstanceState!=null) {
        if (savedInstanceState.getInt("ItemSelect")!=0) {
           changeSpinner = false;
        }
    }

}

And for last on the OnItemSelected from the spinner do this

mySpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
    public void onItemSelected(AdapterView<?> parent,android.view.View v, int position, long id) {
        if (changeSpinner) {
           [...]
        } else {
           changeSpinner= true;
        }
    });

So, the first time when is called is not going to do anything, just make the boolean variable true, and the second time is going to execute the code. Maybe not the best solution but it work.

Stanwin answered 14/2, 2015 at 23:56 Comment(1)
Seems like an okay solution.Niemann
K
1

I am updating @Andres Q.'s answer in Kotlin.

Create an inner class in which you're using Spinner

inner class SpinnerInteractionListener : AdapterView.OnItemSelectedListener, View.OnTouchListener {
        override fun onNothingSelected(parent: AdapterView<*>?) {

        }

        override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
            if (userSelect) {
                //Your selection handling code here
                userSelect = false
            }
        }

        @SuppressLint("ClickableViewAccessibility")
        override fun onTouch(v: View?, event: MotionEvent?): Boolean {
            userSelect = true
            return false
        }

        internal var userSelect = false
    }

Then declare instance variable outside onCreate() as globally like

lateinit var spinnerInteractionListener: SpinnerInteractionListener

then initialise it inside onCreate() by

spinnerInteractionListener = SpinnerInteractionListener()

and use it like

spinnerCategory.onItemSelectedListener = spinnerInteractionListener
spinnerCategory.setOnTouchListener(spinnerInteractionListener)

here spinnerCategory is Spinner

Khalkha answered 3/10, 2018 at 10:40 Comment(0)
W
0

Try this:

boolean mConfigChange = false;

@Override
protected void onCreate(Bundle savedInstanceState) {
    // TODO Auto-generated method stub
    mConfigChange = false;
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_mainf);

    Log.i("SpinnerTest", "Activity onCreate");
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.colors,
            android.R.layout.simple_spinner_item);
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    ((Spinner) findViewById(R.id.spin)).setAdapter(adapter);

     ((Spinner) findViewById(R.id.spin)).setSelection(2);
    ((Spinner) findViewById(R.id.spin)).setOnItemSelectedListener(this);

}

@Override
protected void onResume() {
    mConfigChange = true;
    super.onResume();
}

@Override
public void onItemSelected(AdapterView<?> spin, View selview, int pos, long selId) {
    if (!mConfigChange)
        Log.i("Test", "spin:" + spin + " sel:" + selview + " pos:" + pos + " selId:" + selId);
    else
        mConfigChange = false;
}
Weatherbound answered 28/1, 2013 at 13:32 Comment(1)
Thank you, but this doesn't seem to solve my issue, it just makes the first callback call "uneffective" (but the second still gets triggered after rotation). I was already doing something like that, the problem is the apparent inconsistency between the number of calls on first activity creation vs device rotation.Circumstantial
T
0

You can just call to setSelection once you know have the list of items and the position to be selected, in that way you avoid onItemSelected to be called twice.

I've created an article about what I think is a better approach How to avoid onItemSelected to be called twice in Spinners

Trimer answered 2/4, 2016 at 15:41 Comment(0)
E
0

I wrote an extension function that skips all selection events except those initiated by the user. Dont forget to override defPosition if you use not first default spinner position

fun Spinner.setFakeSelectSkipWatcher(execute: (position: Int) -> Unit, defPosition: Int = 0) {
val listener = object : AdapterView.OnItemSelectedListener {
    var previousIsNull = -1
    var notSkip = false
    override fun onItemSelected(p0: AdapterView<*>?, view: View?, position: Int, p3: Long) {
        if (notSkip) execute(position)
        else {
            if ((view != null && position == defPosition) ||
                (view == null && position == defPosition) ||
                (view != null && previousIsNull == 1 && position != defPosition)
            ) notSkip = true
        }
        previousIsNull = if (view == null) 1 else 0
    }
    override fun onNothingSelected(p0: AdapterView<*>?) {}
}
onItemSelectedListener = listener

}

Epithet answered 18/4, 2021 at 7:42 Comment(0)
I
0

In kotlin stateflow make it easy. In fragment also it can hold data on rotation. In my code I solved as

In ViewModel:

private val _selectedPosition = MutableStateFlow(0)
val selectedPosition = _selectedPosition.asStateFlow()

fun setPosition(position: Int) {
    _selectedPosition.value = position
}

in Fragment

val selectedPosition= viewModel.selectedPosition.value
spinner.setSelection(selectedPosition)
Intosh answered 22/2, 2022 at 23:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.