miércoles, 10 de abril de 2013

Android: Persistencia de datos con SQLite

En el post de hoy veremos la persistencia de datos con SQLite y rozaremos un poco la clase ListActivity para montar una lista en pantalla, aunque entraré más en detalle sobre los listados en un futuro post.
El objetivo será crear una aplicación que nos permita insertar registros en una base de datos persistente y consultar el listado de elementos que contiene.

Dibujando el layout

Crearemos una pantalla simple, con un EditText donde introducir el texto a guardar y un Button para realizar la acción. Por otro lado, tendremos un TextView que nos informará de cuantos registros hay en la tabla, así como de un listado (ListView) para ver los elementos, y un Button para refrescar el listado.



En código hablamos de:

<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:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Introduzca Texto a guardar:" />

    <EditText
        android:id="@+id/edtTexto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentRight="true"
        android:layout_below="@+id/textView1"
        android:ems="10" >

        <requestFocus />
    </EditText>

    <Button
        android:id="@+id/btnGuardar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/edtTexto"
        android:layout_alignParentRight="true"
        android:layout_below="@+id/edtTexto"
        android:text="Guardar" />

    <TextView
        android:id="@+id/textView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/textView5"
        android:layout_below="@+id/textView5"
        android:text="Registros: " />

    <TextView
        android:id="@+id/txtRegistros"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/textView3"
        android:layout_alignBottom="@+id/textView3"
        android:layout_toRightOf="@+id/textView3"
        android:text="TextView" />

    <TextView
        android:id="@+id/textView5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/btnGuardar"
        android:layout_below="@+id/btnGuardar"
        android:layout_marginTop="37dp"
        android:text="Datos Almacenados"
        android:textAppearance="?android:attr/textAppearanceLarge" />

    <Button
        android:id="@+id/btnRefrescar"
        style="?android:attr/buttonStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@+id/txtRegistros"
        android:layout_alignRight="@+id/btnGuardar"
        android:layout_alignTop="@+id/textView5"
        android:text="Refrescar" />
   
       <ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/textView3" />
   
</RelativeLayout>
Ojito con el id del ListView: respetadlo tal cual.

Controlador de base de datos (SQLiteOpenHelper)

Para acceder a la base de datos usaremos una clase genérica como controlador que extenderá a la clase SQLiteOpenHelper. Dicha clase provee de todo lo necesario para manejar el acceso a datos, por lo que nos ayudará, tal y como su nombre indica, a manejar la base de datos SQLite.

A esta clase la llamaremos BDController  y lo primero que debemos hacer es definir una serie de campos estáticos:

  • Nombre de la base de datos:


private static final String DATABASE_NAME = "DEMODB.db";

  • Versión de la base de datos:


private static final int DATABASE_VERSION = 1;

  • Nombres de las tablas: (en nuestro caso solo una)


public static final String TABLE_TEXTOS = "TEXTOS";

  • Nombre de los campos: (no es necesario, pero nunca está de más)


public static final String COLUMN_ID = "ID_TEXTO";
public static final String COLUMN_TEXTO = "TEXTO";

  • Script de creación de tablas:


private static final String DATABASE_CREATE = "create table " + TABLE_TEXTOS
             + "(" + COLUMN_ID + " integer primary key autoincrement, "
             + COLUMN_TEXTO + " text not null);";

Esta clase define un contructor que recibe el contexto de la aplicación, e invoca al constructor de su clase padre (SQLiteOpenHelper) con el contexto, el nombre de la base de datos, una factoria (a null en nuestro caso ya que no lo vamos a usar) y la versión.

       public BDController(Context context) {
             super(context, DATABASE_NAME, null, DATABASE_VERSION);
       }

Cuando se ejecuta, si no existe la base de datos, se lanza el evento onCreate que es capturado por nuestra clase y genera las tablas:

       @Override
       public void onCreate(SQLiteDatabase db) {
             db.execSQL(DATABASE_CREATE);
       }

En el caso de una actualización de la base de datos (que no coincidan las versiones, se lanzará el evento onUpgrade.

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        Log.w(BDController.class.getName(),
                     "Upgrading database from version " + oldVersion + " to "
                         + newVersion + ", which will destroy all old data");
                 db.execSQL("DROP TABLE IF EXISTS " + TABLE_TEXTOS);
                 onCreate(db);
}

En nuestro caso tenemos una tabla simple (Textos) con dos columnas (idtextos y texto) por lo que podríamos definir un tipo para estos registros:

public class Texto {
       private long id;
       private String texto;
      
       public long getId() {
             return id;
       }
      
       public void setId(long id) {
             this.id = id;
       }
      
       public String getTexto() {
             return texto;
       }
      
       public void setTexto(String texto) {
             this.texto = texto;
       }
      
       public String toString() {
             return texto;
       }
}


Origen de Datos (DataSource)

Con la clase BDController no es suficiente para lo que nosotros necesitamos. Esta clase se limita a crear la base de datos. Necesitamos una que nos administre las operaciones sobre sus tablas, el administrador del origen de datos. A esta clase la llamaremos BDDataSource.
Por un lado, definiremos los siguientes de esta clase:

  • Objeto de base de datos:


private SQLiteDatabase database;

  • Controlador:


private BDController dbHelper;

  • Columnas de la base de datos:


private String[] allColumns = { BDController.COLUMN_ID, BDController.COLUMN_TEXTO };

En el constructor crearemos el objeto dbHelper (el controlador de nuestra base de datos), por lo que necesitamos recibir el contexto de la aplicación:

         public BDDataSource(Context context) {
               dbHelper = new BDController(context);
         }


Crearemos un método para abrir y cerrar la conexión:

         public void open() throws SQLException {
               database = dbHelper.getWritableDatabase();
         }
        
         public void close() {
               dbHelper.close();
         }

Un método para insertar registros:

  public Texto createTexto(String texto) {
         ContentValues values = new ContentValues();
         values.put(BDController.COLUMN_TEXTO, texto);
         long insertId = database.insert(BDController.TABLE_TEXTOS, null, values);
         Cursor cursor = database.query(BDController.TABLE_TEXTOS, allColumns, BDController.COLUMN_ID + " = " + insertId, null, null, null, null);
         cursor.moveToFirst();
         Texto newTexto = cursorToTexto(cursor);
         cursor.close();
         return newTexto;
  }

Un método para la conversión de base de datos a objetos:

  private Texto cursorToTexto(Cursor cursor)
  {
         Texto texto = new Texto();
         texto.setId(cursor.getLong(0));
         texto.setTexto(cursor.getString(1));
        
         return texto;
  }

Uno para eliminar registros (que no usaremos inicialmente):

  public void deleteTexto(Texto texto) {
         long id = texto.getId();
         System.out.println("Texto borrado con id: " + id);
         database.delete(BDController.TABLE_TEXTOS,
                      BDController.COLUMN_ID + " = " + id, null);
  }

Y finalmente uno para obtener el listado de objetos en la tabla:

  public List<Texto> getAllTextos() {
         List<Texto> lista = new ArrayList<Texto>();
      
         Cursor cursor = database.query(BDController.TABLE_TEXTOS,
           allColumns, null, null, null, null, null);
      
         cursor.moveToFirst();
         while (!cursor.isAfterLast()) {
               Texto texto = cursorToTexto(cursor);
               lista.add(texto);
               cursor.moveToNext();
         }
         // Make sure to close the cursor
         cursor.close();
         return lista;
  }

Esta clase ya se puede usar en cualquier aplicación que creemos, pues tenemos toda la lógica de las operaciones que necesitamos hacer en la base de datos y además nos simplifica bastante el trabajo con ella.

Codificando la pantalla

Antes de nada, es importante tener presente que nuestra pantalla, al tener un ListView, heredará de la clase ListActivity en lugar de Activity. Esta clase implementa métodos que nos facilitarán el uso de listados.

public class MainActivity extends ListActivity {
(...)
}

Nada más cargar la pantalla (onCreate) debemos lanzar el método cargarObjetosPantalla, que vincula los elementos del layout y sus eventos con el código de nuestra clase java:

private void cargaObjetosPantalla()
{
       edtTexto = (EditText)findViewById(R.id.edtTexto);
       btnGuardar = (Button)findViewById(R.id.btnGuardar);
       txtRegistros = (TextView)findViewById(R.id.txtRegistros);
       btnRefrescar = (Button)findViewById(R.id.btnRefrescar);
      
       //base de datos
       cargaDatos();
      
       //eventos
       btnGuardar.setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(View v) {
                    guardar();
             }
       });
      
       btnRefrescar.setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(View v) {
                    refrescar();
             }
       });
      
}

Dentro de esta función podemos ver referencias a 3 funciones relacionadas con la base de datos que veremos en detalle:

Cargar datos

La función cargaDatos creará el datasource e invocará  a la función refrescar para mostrar por pantalla el listado de objetos en la tabla.

       private void cargaDatos()
       {
             //creamos el datasource
             datasource = new BDDataSource(this);
             //lo abrimos
           datasource.open();
           //cargamos los datos en la lista
           refrescar();
       }

Guardar

Para la inserción de registros en la tabla, habíamos definido un EditText (edtTexto) y un Button (btnGuardar). Al evento onClick del botón btnGuardar le hemos asociado esta función que invoca a createTexto del datasource, insertando un registro en la tabla con el texto del elemento edtTexto.

private void guardar()
{
       Texto texto = datasource.createTexto(edtTexto.getText().toString());
       refrescar();
}

Refrescar (mostrar/actualizar listado)

Finalmente, el método refrescar, que carga los registros en el ListView: obtiene los elementos del data adapter, crea un adaptador para el ListView y se lo asigna con el método setListAdapter de la clase ListActivity.

private void refrescar()
{
    //obtenemos todos los registros
       List<Texto> values = datasource.getAllTextos();
    //creamos el adaptador para la lista
    ArrayAdapter<Texto> adapter = new ArrayAdapter<Texto>(this,
            android.R.layout.simple_list_item_1, values);
    //y finalmente lo asignamos
    setListAdapter(adapter);
    //actualizamos el contador
    txtRegistros.setText(Integer.toString(values.size()));
}

Y ya tenemos la aplicación lista para ejecutar:


El codigo de ejemplo lo tienes aquí.