2 de abril de 2016

Android - How to avoid onItemSelected to be called twice in Spinners

The other day the QA team reported us a bug about a blinking effect in one of our screens, specifically in the item detail screen.
It was just open that screen and I saw perfectly what they were talking about, but that wasn't happening before, what had changed?
The thing that had changed was that in that screen, now the user is able to modify the item's category, to be able to do that we added a Spinner to select the category from a given list, once the user select a category we redraw the screen with the new data related to that category.
The problem wasn't easy to be identified, it was happening that we were doing a "bad use" of Spinner.setSelection(), basically we weren't calling to Spinner.setSelection() in the same method where the categories were added to the adapter.
I'm sure that with a piece of code it will be clearer:
This code will call to OnItemSelectedListener.onItemSelected saying that the item 0 was selected.

    spinner = (Spinner) findViewById(R.id.spinner);
    spinner.setOnItemSelectedListener(this);
    adapter = new ArrayAdapter<String>(this, R.layout.simple_spinner_item, items);
    spinner.setAdapter(adapter);
    
But we didn't expect that thing to happen! what we expect is onItemSelected to be called when the user select a item from the Spinner, or maybe we could expect it to be called when spinner.setSelection(position);saying that the item position is selected.
In our app, the user could have selected a category previously, then at some point after the data is loaded from the database we do spinner.setSelection(position);"et voilà"onItemSelected is called twice, the first call is done saying that the item 0 was selected and the second one with the position item was selected.
At this point I decided to search in internet to see how other people had fixed that problem and I found some interesting approaches (and crazy ones):
  • To keep counters for each Spinner with OnItemSelectedListener that are in the screen to skip the first call to onItemSelected.
  • To create a custom adapter and to add spinner.setOnTouchListener() to manage when the user has touched the screen previously.
  • To create a custom view and override the method onClick and call CustomOnItemClickListener.onItemClickwithin it.
None of those options convinced me (I found some more that didn't either), so I decided to investigate a little more and I found that you cannot skip onItemSelected to be called when the adapter is added to the Spinner if the adapter has already items, but we can make it called with the correct element selected if spinnerView.setSelection(position) is called immediately after the items are added to the view/adapter. I've thought in two ways to do that:

First approach

We create the adapter with the items in it and we add it to the Spinner once that we know the item that has to be selected.

    adapter = new ArrayAdapter<>(this, R.layout.simple_spinner_item, items);
    spinner.setAdapter(adapter);
    spinner.setSelection(position);

Second approach

We create the adapter without the items in it and we add it to the Spinner.

    adapter = new ArrayAdapter<>(this, R.layout.simple_spinner_item);
    spinner.setAdapter(adapter);

And once that we know the item that has to be selected.

    spinner.setSelection(10);
    adapter.addAll(items);

In conclusion

It would be nice to set AdapterView.OnItemSelectedListener just after add the items to the adapter and to not see onItemSelected be called, but it isn't, so at least we can do this that is a smart approach.

Complete code for approach 1


  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_base_layout);

    final Spinner spinner = (Spinner) findViewById(R.id.spinner);
    spinner.setOnItemSelectedListener(this);

    final ArrayAdapter<String> adapter =
        new ArrayAdapter<>(this, R.layout.simple_spinner_item, items);
    spinner.setAdapter(adapter);
    spinner.setSelection(10);
  }

Complete code for approach 2



  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_base_layout);

    final Spinner spinner = (Spinner) findViewById(R.id.spinner);
    spinner.setOnItemSelectedListener(this);

    final ArrayAdapter<String> adapter =
        new ArrayAdapter<>(this, R.layout.simple_spinner_item);
    spinner.setAdapter(adapter);

    findViewById(R.id.select_item).setOnClickListener(new View.OnClickListener() {
      @Override public void onClick(View v) {
        spinner.setSelection(10);
        adapter.addAll(items);
      }
    });
  }

No hay comentarios:

Publicar un comentario