Revista Informática

Poblar Spinners en Android desde una Base de Datos SQLite

Publicado el 13 noviembre 2014 por Jamesrevelo @herprogramacion
Tutorial sobre Spinners en Android

Los Spinners son menús desplegables en Android que permiten al usuario decidirse por una camino entre varias opciones. De seguro alguna vez habrás visto un formulario en una aplicación donde te pidan elegir tu ciudad de una lista o cuando te permiten cambiar la temporalidad de los resultados proyectados desde un menú en la Action Bar. Ambos casos son ejemplos de Spinners y espero que este tutorial te sirva de guía para poder usarlos.

Si sigues leyendo podrás aprender los siguientes tópicos:

  • ¿Qué es un Spinner?
  • Poblar un Spinner desde una Base de Datos SQLite
  • Añadir un Spinner en la Action Bar
    • Paso 1: Crear el Adaptador del Spinner
    • Paso 2: Implementar la Interfaz OnNavigationListener
    • Paso 3: Habilitar el Modo de Navegación de Lista

¿Qué es un Spinner?

Un Spinner es un View que despliega una lista de elementos que están expectantes a la selección del usuario. Dependiendo del elemento seleccionado nuestra aplicación tomará las decisiones pertinentes para seguir el flujo de ejecución.

Ejemplo de Spinner en Contactos de Android

El concepto es sencillo y he preparado un pequeño ejemplo para que puedas interiorizarlo. Se trata de una pequeña aplicación llamada Gamerty, que permite seleccionar la guía del videojuego que elija el usuario del Spinner. Veamos como se ve:

Aplicación Android con un Spinner

Puedes descargar el proyecto del siguiente enlace:

Descargar Código

Ahora procedamos a recrear su elaboración:

Ubicar un Spinner en Android Studio

El primero paso es la creación de un nuevo proyecto en Android Studio con una actividad principal, a la cual le llamarás Main. Inmediatamente vas a su archivo de diseño (activity_main.xml) y añades desde la ventana Pallete el Spinner. Le asignarás el identificador "GameSpinner", lo centras horizontalmente (centerHorizontal) y luego lo haces limitar con el borde superior del padre (alignParentTop).

Añadir Spinner en Android Studio

Si todo te ha salido excelente, tendrías el siguiente archivo de diseño:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context=".Main">
<Spinner
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/GameSpinner"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true" />
</RelativeLayout>

Crear un Origen de Datos

Al igual que cuando vimos listas en Android, un Spinner también necesita algún origen de datos para definir sus elementos. Esto implica el uso de un adaptador que nos permita crear la comunicación.

Así que vas a crear una lista de strings que representen las opciones que desplegará nuestro Spinner. Como bien sabes, la aplicación Gamerty mostrará al usuario un menu con nombres de videojuegos a consultar. Supón que los videojuegos disponibles son: League of Legends, Diablo III, God of War, Halo 4 y StarCraft II.

Podríamos crear una lista de Strings(ArrayList<String>) para almacenar en nuestra actividad estas cadenas, pero te quedaría mas cómodo y flexible aislarlas en tu archivo de recursos strings.xml. Esta preferencia te permitirá mayor flexibilidad en tu código.

Recuerda que la clase R.java almacena referencias constantes de nuestros recursos, así que no debes preocuparte por la alteración indebida de tus elementos <string> si vas a almacenar las opciones del Spinner.

Si te ha quedado claro, entonces comprenderás la siguiente estructura del strings.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Strings generales -->
<string name="app_name">Gamerty</string>
<!-- Conjunto de Strings para el View GameSpinner -->
<string-array name="Games">
<item>League of Legends</item>
<item>Diablo III</item>
<item>God of War</item>
<item>Halo 4</item>
<item>StarCraft II</item>
</string-array>
</resources>

Si te fijas en la definición, verás un elemento <string-array> con nombre "Games". Si aún no lo sabes, este elemento representa un array de strings (como su nombre lo indica). Dentro de él debes incluir cada dato como un nodo <item>, cuyo contenido es la cadena.

Poblar un Spinner con un Adaptador

Una vez hayas definido tu origen de datos vas a crear un adaptador que se ajuste a tus necesidades. En el caso de Gamerty se usa un ArrayAdapter con elementos simples, ya que solo necesitas proyectar el nombre del juego.

Para ello se obtiene la instancia del Spinner, luego declaras el adaptador, lo inicializas en el método onCreate() de la actividad Main y finalmente los relacionas:


//ArrayAdapter para conectar el Spinner a nuestros recursos strings.xml
protected ArrayAdapter<CharSequence> adapter;
...
//Obtener instancia del GameSpinner
Spinner spinner = (Spinner) findViewById(R.id.GameSpinner);
//Asignas el origen de datos desde los recursos
adapter = ArrayAdapter.createFromResource(this, R.array.Games,
android.R.layout.simple_spinner_item);
//Asignas el layout a inflar para cada elemento
//al momento de desplegar la lista
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
//Seteas el adaptador
spinner.setAdapter(adapter);

El código anterior muestra el uso del método de clase createFromResource(), el cual nos permite crear un nuevo ArrayAdapter a partir de los recursos string que están en el proyecto. El primer parámetro que recibe es el contexto, en este caso es la actividad actual, luego la referencia del <array-string> que declaraste y por último el layout para inflar cada item, donde se usa el recurso del sistema simple_spinner_item.xml, que representa un TextView sencillo.

Luego usamos el método del adaptador setDropDownViewResource(). Su objetivo es setear un layout para cada item del Spinner, para cuando se despliegue la lista. Por lo que usamos el recurso de la plataforma simple_spinner_dropdown_item.xml que representa un TextView seleccionable o CheckedTextView. Este tipo de text view hereda los comportamientos de uno normal, solo que tiene algunas características adicionales en su forma de iluminarse.

Si deseas establecer la posición inicial del Spinner usa el método setSelection() con un indice entero como parámetro. Si pones atención, en Gamerty creamos una constante llamada DEFAULT_POSITION a la cual se le asigno el entero 3 que corresponde a "Halo 4".

Poblar un Spinner con el Atributo entries

Debido a que los elementos de nuestro Spinner son estáticos, podemos utilizar un atributo muy útil del elemento <Spinner> llamado android:entries. En él puedes asignar el arreglo de strings que se ha predefinido en los recursos y automáticamente se inflarán implícitamente los elementos el Spinner sin necesidad de un adaptador.

Solo basta con que te dirijas a la pestaña de diseño y modifiques el atributo con el siguiente valor "@array/Games":

Atributo entries de un Spinner

O ir a la definición XML y realizar la respectiva asignación:

<Spinner
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/GameSpinner"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:entries="@array/Games" />

Manejo de eventos de un Spinner

En esta sección verás como definir el flujo de tu aplicación de acuerdo a la opción del Spinner que sea seleccionada por el usuario. Para cumplir ese objetivo se empleará la escucha OnSelectedItemListener.

El primer paso que debes realizar es implementar sobre tu actividad Main la escucha. Luego en el método onCreate() le asignas la referencia de la escucha a tu Spinner:

public class Main extends Activity implements OnItemSelectedListener {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
...
//Asignado la escucha
spinner.setOnItemSelectedListener(this);
...
}
...
}

A continuación se deben implementar los métodos onItemSelected() y onNothingSelected() relacionados con OnItemSelectedListener. El primero es llamado cuando has seleccionado un elemento del Spinner y el segundo cuando no tiene items seleccionados.

¿Cual es la idea?, vas a sobrescribir onItemSelected() y añadirás las instrucciones necesarias para que visualice un Toast. Este debe mostrar el nombre del elemento que se ha seleccionado.

Veamos:

@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
//Salvar la posición y valor del item actual
this.position = position;
selection = parent.getItemAtPosition(position).toString();
//Mostramos la selección actual del Spinner
Toast.makeText(this,"Selección actual: "+selection,Toast.LENGTH_SHORT).show();
}

La primera tarea que se ha realizado es guardar la posición y el valor del texto en dos variables privadas llamadas position y selection (Si revisas el proyecto, han sido declaradas en el inicio de Main). El parámetro position del método onItemSelected() se refiere al indice del elemento seleccionado, por lo que te queda fácil obtener una instancia con getItemAtPosition(). Luego puedes consultar el valor de su atributo text con toString().

Finalmente creas un Toast prefabricado y muestras el contenido de la selección.

Poblar un Spinner desde una Base de Datos SQLite

En esta sección aprenderás a crear los items de un Spinner desde un cursor que contenga el resultado de una consulta en SQLite (Aprender mas sobre Android y SQLite).

El proceso es el mismo que con las listas. Como ya sabes, solo debes usar la clase SimpleCursorAdapter y todo estará resuelto.

Suena fácil pero es mejor aclararlo con un ejemplo. La siguiente aplicación que estudiarás a continuación se llama Lyrik. Esta aplicación muestra las letras de las canciones de un artista disponible, clasificados por género musical.

Aplicación Android con dos Spinners

Puedes descargar el proyecto desde aquí:

Descargar Código

Su diseño se basa en una actividad principal (Main) con dos Spinners(GenreSpinner y ArtistSpinner). El primero para desplegar los géneros disponibles y el segundo para proyectar los artistas que pertenecen a ese género. Por lo que el Spinner de los artistas depende del género, representando una cardinalidad uno a muchos (1:N) en la base de datos.

Modelo relacional para Géneros y Artistas

Teniendo en cuenta lo anterior, la base de datos tendría dos tablas: Géneros(Genres) y Artistas(Artists). Donde Artistas recibiría una llave foránea relacionada con la llave principal de Generos.

Una vez definido todas las características de la base de datos se crea un script que automatice su creación en cualquier momento. El nombre de clase es DataBaseScript (Obsérvala en el proyecto).

Usar la clase SimpleCursorAdapter con un Spinner

Si detallas la clase Main del proyecto, verás que el primer paso fue inicializar un adaptador del tipo SimpleCursorAdapter de la siguiente forma:

//Creando Adaptador para GenreSpinner
genreSpinnerAdapter = new SimpleCursorAdapter(this,
android.R.layout.simple_spinner_item,
dataSource.getAllGenres(),
new String[]{DataBaseScript.GenreColumns.NAME_GENRE},
new int[]{android.R.id.text1},
SimpleCursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);

El constructor ha recibido el layout prefabricado simple_spinner_item.xml y como fuente de datos se ha establecido el cursor que retorna el método getAllGenres() de la clase LyrikDataSource. Como su nombre lo indica, este método obtiene todas las filas de la tabla Genres de la base de datos. Su definición no es mas que una sentencia SELECT que selecciona todas las columnas y registros:

public Cursor getAllGenres(){
//Seleccionamos todas las filas de la tabla Genres
return database.rawQuery(
"select * from " + DataBaseScript.GENRES_TABLE_NAME, null);
}

Hasta aquí todo muy bien. Ahora veamos la estrategia para poblar el Spinner de artistas.

Crear una dependencia entre dos Spinners

¿Como hacer para que el segundo Spinner sea poblado al elegirse un item del primer Spinner?

La respuesta está en los eventos de selección. Bien sabemos que la interfaz OnItemSelectedListener es quién implementa el método onItemSelected() para la ejecución de actividades de selección, así que es allí donde debemos aplicar nuestra lógica.

Ahora pregúntate: ¿Como obtengo los nombres de los artistas que están relacionados con un género?

En esencia, esta es una duda que debemos resolver primero desde el modelo de datos, es decir, en la base de datos. Y la solución es sencilla, simplemente debes consultar aquellos artistas cuyo valor de su campo idGenre sea igual al asignado en tiempo real por el flujo del programa. Por ejemplo, la siguiente consulta obtiene el id y nombre de los artistas del genero con código 1001:

SELECT _id,name
FROM Artists
WHERE idGenre = 1001;

Así que generalizaremos esta sentencia con el siguiente método llamado getArtistsByGenre() - "Obtener artistas por genero":

public Cursor getArtistsByGenre(String genreSelection) {
//Argumentos del WHERE
String selectionArgs[] = new String[]{genreSelection};
String query =
"select "+DataBaseScript.ArtistColumns.ID_ARTIST+","+DataBaseScript.ArtistColumns.NAME_ARTIST+
" from "+DataBaseScript.ARTISTS_TABLE_NAME +
" where "+DataBaseScript.ArtistColumns.ID_GENRE+
"= ?";
return database.rawQuery(query,selectionArgs);
}

Si detallas bien, usamos el placeholder '?' para reemplazarlo por genreSelection, que es el parámetro de entrada que representa el id del género requerido. Esto facilita tu trabajo para crear el adaptador del Spinner de artistas.

Finalmente implementamos el método onItemSelected(), donde obtienes el id del género seleccionado y luego inicias el adaptador:

@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
//Obteniendo el id del Spinner que recibió el evento
int idSpinner = parent.getId();
switch(idSpinner) {
case R.id.GenreSpinner:
//Obteniendo el id del género seleccionado
Cursor c1 = (Cursor) parent.getItemAtPosition(position);
String genreSelection = c1.getString(
c1.getColumnIndex(DataBaseScript.GenreColumns.ID_GENRE));
//Poblar el ArtistSpinner
activeArtistSpinner(genreSelection);
break;
...

Recuerda que si el adaptador maneja cursores el resultados obtenido por getItemAtPosition() debe ser casteado a Cursor. Seguidamente puedes obtener el valor de la fila deseado.

Para activar el Spinner de artistas de forma personalizada se creó el método activeArtistSpinner(). Su trabajo es crear el adaptador con el id de entrada a través del método getArtistsByGenre(). Luego lo asignas al Spinner y finalmente relacionas la escucha:

private void activeArtistSpinner(String genreSelection) {
//Creando Adaptador para ArtistSpinner con el id del género
artistSpinnerAdapter = new SimpleCursorAdapter(this,
android.R.layout.simple_spinner_item,
dataSource.getArtistsByGenre(genreSelection),
new String[]{DataBaseScript.ArtistColumns.NAME_ARTIST},
new int[]{android.R.id.text1},
SimpleCursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
//Seteando el adaptador creado
artistSpinner.setAdapter(artistSpinnerAdapter);
//Relacionado la escucha
artistSpinner.setOnItemSelectedListener(this);
}

Ahora solo queda agregar al método onItemSelected() las acciones que se realizaran cuando sea seleccionado un elemento del ArtistSpinner:

case R.id.ArtistSpinner:
//Obteniendo el nombre del artista seleccionado
Cursor c2 = (Cursor) parent.getItemAtPosition(position);
String artistSelection = c2.getString(
c2.getColumnIndex(DataBaseScript.ArtistColumns.NAME_ARTIST));
//Cambiando el texto de LyricList según el Artista seleccionado
lyricList.setText(getResources().getString(R.string.LyricList)+" "+artistSelection);
break;

La idea fue asignarle al TextView un mensaje del tipo "Letras de <Artista>", donde <Artista> será reemplazado por la selección actual del usuario.

Añadir un Spinner a la Action Bar

Si logras recordar un poco sobre la anatomía de la Action Bar tendrás presente que existe una sección llamada View Control. Se había dicho que en este espacio era posible inflar Views que interactuan con el contenido actual de la actividad o fragmento. Muy bien, en este apartado veremos como inflar un Spinner justo en ese lugar para implementar un filtro de datos.

Dicho filtro será incluido en una aplicación llamada Fazhion. Esta aplicación fue pedida por un cliente imaginario que desea proyectar en una actividad todos los estilos de zapatos que vende su negocio. Quiere que se muestre el nombre del zapato, su precio, la descripción y una miniatura de su aspecto. Ademáss desea que el cliente pueda filtrar los productos por precio y nombre.

Así que lo que harás es añadir un Spinner a la Action Bar para solucionar este problema. Donde el resultado final será algo como esto:

Spinner en la Action Bar

Puedes descargar el código completo presionando el siguiente botón:

Descargar Código

La aplicación Fazhion se compone de una ListActivity principal llamada Main. Los elementos de la lista son inflados a través de un CursorAdapter personalizado que accede a la base de datos Fazhion previamente creada.

Ahora veamos como se hizo para añadir el Spinner a la Action Bar:

Paso 1: Crear el Adaptador del Spinner

Puedes usar un ArrayAdapter personalizado o un SimpleCursorAdapter como vimos con anterioridad. También puedes personalizar los items al igual que en la captura de la aplicación Fazhion, cuyo Spinner tiene un ImageView en cada item.

Si deseas añadir un ImageView al Spinner debes crear un layout personalizado. Fazhion utiliza el layout spinner_item.xml, cuyo diseño se proyecta en el siguiente bosquejo:

Layout de los Items de un Spinner

Y su descripción xml es la siguiente:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="5dp">
<ImageView
android:layout_width="25dp"
android:layout_height="25dp"
android:id="@+id/ItemIcon"
android:src="@drawable/ic_launcher"
android:layout_alignParentLeft="true"
android:layout_centerInParent="true"
android:layout_marginRight="3dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="Texto"
android:id="@+id/TextItem"
android:layout_centerVertical="true"
android:layout_toRightOf="@+id/ItemIcon" />
</RelativeLayout>

Luego debes crear un adaptador personalizado que soporte el anterior layout. Esto implica crear un origen de datos basado en una clase que represente cada elemento del Spinner.

Si abstraes las características de cada elemento del Spinner, deducirás que poseen un icono que proyecta su significado y un nombre. Así que crea una nueva clase con estas características y llámala SpinnerItem:

public class SpinnerItem {
private String Name;
private int iconId;
public SpinnerItem(String name, int iconId) {
Name = name;
this.iconId = iconId;
}
public String getName() {
return Name;
}
public void setName(String name) {
Name = name;
}
public int getIconId() {
return iconId;
}
public void setIconId(int iconId) {
this.iconId = iconId;
}
}

Por ultimo debes crear un adaptador que infle cada view a través de una lista de tipo SpinnerItem. A este le denominarás FilterAdapter:

public class FilterAdapter extends ArrayAdapter {
public FilterAdapter(Context context, List<SpinnerItem> objects) {
super(context, 0, objects);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
/*
Obtener el objeto procesado actualmente
*/
SpinnerItem currentItem = getItem(position);
/*
Obtener LayoutInflater de la actividad
*/
LayoutInflater inflater = (LayoutInflater) parent.getContext().
getSystemService(Context.LAYOUT_INFLATER_SERVICE);
/*
Evitar inflar de nuevo un elemento previamente inflado
*/
if(convertView==null){
convertView = inflater.inflate(R.layout.spinner_item, parent, false);
}
/*
Instancias del Texto y el Icono
*/
TextView name = (TextView)convertView.findViewById(R.id.TextItem);
ImageView icon = (ImageView)convertView.findViewById(R.id.ItemIcon);
/*
Asignar valores
*/
name.setText(currentItem.getName());
icon.setImageResource(currentItem.getIconId());
return convertView;
}
@Override
public View getDropDownView(int position, View convertView, ViewGroup parent) {
/*
Debido a que deseamos usar spinner_item.xml para inflar los
items del Spinner en ambos casos, entonces llamamos a getView()
*/
return getView(position,convertView,parent);
}
}

Si eres buen observador verás algo inusual que nunca antes habíamos hecho al definir un adaptador. Hemos sobrescrito un método llamado getDropDownnView(). Este método tiene las mismas funciones de getView(), solo que se ejecuta cuando el Spinner es desplegado. Como ya habías visto, los items del Spinner pueden utilizar otro diseño en ese momento.

Debido a que no deseamos que tomen otra forma, simplemente retornamos en el mismo view que genera el método getView().

Ahora solo crea en tu actividad principal una lista de items para el Spinner y luego asignalos en el constructor del nuevo FilterAdapter:

/*
Inicializar los items estáticamente
*/
List items = new ArrayList();
items.add(new SpinnerItem("Mayor Precio",R.drawable.ic_higher_prices));
items.add(new SpinnerItem("Menor Precio",R.drawable.ic_lower_prices));
items.add(new SpinnerItem("Alfabéticamente",R.drawable.ic_alphabetical_order));
//Creando el nuevo adaptador para el Spinner
spinnerAdapter = new FilterAdapter(this,items);

Paso 2: Implementar la interfaz OnNavigationListener

Para los eventos usaremos la clases OnNavigationListener. Esta interfaz te permite decidir que sucederá cuando el usuario selecciona uno de los elementos del Spinner en la Action Bar. En el caso de Fazhion se ejecutaron las consultas necesarias en la base de datos para obtener los registros de forma ordenada:

OnNavigationListener navigationListener =new OnNavigationListener() {
/*
0 = Mayor Precio
1 = Menor Precio
2 = Alfabeticamente
*/
@Override
public boolean onNavigationItemSelected(int position, long itemId) {
switch(position){
case 0:
/*
Obtener precios en orden descendente
*/
fazhionAdapter.changeCursor(dataSource.getShoesOrderPrice(false));
break;
case 1:
/*
Obtener precios en orden ascendente
*/
fazhionAdapter.changeCursor(dataSource.getShoesOrderPrice(true));
break;
case 2:
/*
Obtener nombres en orden alfabético
*/
fazhionAdapter.changeCursor(dataSource.getShoesOrderAZ(true));
break;
}
return true;
}
};

La escucha OnNavigationLisner te entrega a disposición el método onNavigationItemSelected() para definir las acciones de cada elemento del Spinner. Donde el parámetro entero position será nuestro referente para identificar cada elemento.

Al seleccionar el item en la posición 0, el adaptador de la lista actualiza el cursor a través de un método llamado getShoesOrderPrice(). Este ordena por precio la lista de los zapatos. Será en orden descendente si recibe false y en orden ascendente si recibe true. En este caso consulta los registros de forma descendente y para la posición 1 se hace en forma ascendente.

El elemento en la posición 2 llama a getShoesOrderAZ(). Este método ordena alfabéticamente los nombres de los zapatos. Si recibe true lo hace de A-Z, si es false ordena de Z-A.

Revisa la clase FazhionDataSource del proyecto. Allí encontrarás la definición de ambos métodos.

La navegación de lista le indica a la action bar que inflaremos un menú con una lista de items, para gestionar la navegación entre fragmentos o actividades. Pero en el caso de Fazhion no habrá navegación, si no que filtraremos los elementos de la ListActivity. Si deseas activar este modo y relacionar la escucha haz lo siguiente:

//Obteniendo una instancia de la Action bar
ActionBar actionBar = getActionBar();
if( actionBar != null) {
//Habilitiando el modo de navegación con lista
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
//Seteando la escucha y el spinner relacionado
actionBar.setListNavigationCallbacks(spinnerAdapter, navigationListener);
}

El código anterior muestra como se activa el modo de navegación por lista con el método setNavigationMode(). Este recibe como parámetro una constante llamada NAVIGATION_MODE_LIST para que el Spinner sea funcional sobre la Action Bar.

Luego debes asignar la escucha y el adaptador del Spinner con el método setListNavigationCallbacks(). Con ello tendrás habilitado el modo de navegación y la funcionalidad DropDown del Spinner.

James Revelo Urrea - Desarrollador independiente http://hermosaprogramacion.blogspot.com

Volver a la Portada de Logo Paperblog