2 de abril de 2016

Android - Como evitar que onItemSelected se llame dos veces en Spinners

El otro día nos reportaron un bug diciendo que una de las pantallas de la aplicación hacía un pequeño parpadeo, en concreto esto pasaba en la pantalla de detalle de un item.
Nada más entrar en dicha pantalla, vi a lo que se referían, pero eso no pasaba antes ¿que había cambiado?
Lo que había cambiado es que ahora el usuario podía modificar la categoría del ítem desde esa pantalla, para hacer eso habíamos añadido a la vista un Spinner que permitía seleccionarla dentro de un listado y una vez seleccionada se repintaba la pantalla con nuevos datos relacionados con la categoría.
El problema no fue nada fácil de identificar, lo que estaba pasando es que estábamos haciendo un "mal uso" de Spinner.setSelection(), básicamente no estábamos llamando a Spinner.setSelection() al mismo tiempo que añadíamos las categorías al adapter.
Seguro que con algo de código queda más claro:
Este código llamará a OnItemSelectedListener.onItemSelecteddiciendo que el elemento 0 ha sido seleccionado.

    spinner = (Spinner) findViewById(R.id.spinner);
    spinner.setOnItemSelectedListener(this);
    adapter = new ArrayAdapter<String>(this, R.layout.simple_spinner_item, items);
    spinner.setAdapter(adapter);
    
¡Pero eso no es lo que esperamos! lo que esperamos es que onItemSelected séa llamado cuando el usuario selecciona un item del Spinner o en todo caso podríamos esperar que sea llamado cuando hacemos spinner.setSelection(position); diciendo que el elemento seleccionado fue el position.
En nuestro caso, el usuario podía haber elegido previamente una categoría, así que en algún momento tras cargar los datos de la base de datos hacíamos spinner.setSelection(position); y "et voilà"onItemSelected es llamado dos veces, la primera diciendo que se seleccionó el elemento 0 y la segunda que se seleccionó el elemento position.
Llegado a este punto decidí mirar en la red como solucionaba la gente este problema y encontré cosas muy interesantes (y locas):
  • Mantener contadores por cada elemento con OnItemSelectedListener que tengas en la pantalla para obviar la primera llamada a onItemSelected.
  • Crearse un custom adapter y añadir spinner.setOnTouchListener() para controlar cuando el usuario ha tocado la pantalla primero.
  • Crearte un custom view, sobre escribir el método onClick y llamar a CustomOnItemClickListener.onItemClicken él.
Ninguna de estas opciones me convencía (encontré más algunas más que tampoco), así que decidí investigar un poco más y descubrí que no se puede evitar que onItemSelected sea llamado cuando se añade el adapter al Spinner si el adapter ya tiene los items, pero si se podía hacer que se llamase con el elemento seleccionado correcto si spinnerView.setSelection(position) es llamado inmediatamente después de añadir los elementos a la vista/adapter. A mí se me ha ocurrido hacerlo de dos formas:

Primera solución

Creamos el adapter con los items y lo añadimos al Spinner cuando ya sabemos el elemento que debe ser seleccionado.

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

Segunda solución

Creamos el adapter sin los items y lo añadimos al Spinner.

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

Y cuando ya sabemos el elemento que debe ser seleccionado.

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

Conclusión

Sería perfecto poder poner el AdapterView.OnItemSelectedListener justo después de añadir los items al adapter y que no fuese disparado, pero no se puede, así que dentro de lo malo esto debería ser una solución aceptable.

Código completo solución 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);
  }

Código completo solución 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