Tutorial De Bases De Datos SQLite En Aplicaciones Android

En este artículo aprenderás a crear una app con una base de datos SQLite para añadir persistencia de datos a tus desarrollos Android.

A medida que vayamos avanzando veremos la utilidad de clases como SQLiteOpenHelper, SQLiteDatabase, Cursor, CursorAdapter y de Herramientas como sqlite3 y SQLite Browser.

Con el fin de facilitar tu aprendizaje usaremos un ejemplo de guía, que te permitirá ir la práctica en paralelo a las teorías y conceptos estudiados.

Puedes desbloquear el enlace descarga para el proyecto final en Android Studio convirtiéndote en suscriptor de Hermosa Programación:

¿Qué Es SQLite?

Es un ligero motor de bases de datos de código abierto, que se caracteriza por mantener el almacenamiento de información persistente de forma sencilla.

A diferencia de otros Sistemas gestores de bases de datos como MySQL, SQL Server y Oracle DB, SQLite tiene las siguientes ventajas:

  • No requiere el soporte de un servidor: SQLite no ejecuta un proceso para administrar la información, si no que implementa un conjunto de librerías encargadas de la gestión.
  • No necesita configuración: Libera al programador de todo tipo de configuraciones de puertos, tamaños, ubicaciones, etc.
  • Usa un archivo para el esquema: Crea un archivo para el esquema completo de una base de datos, lo que permite ahorrarse preocupaciones de seguridad, ya que los datos de las aplicaciones Android no pueden ser accedidos por contextos externos.
  • Es de Código Abierto: Esta disponible al dominio público de los desarrolladores al igual que sus archivos de compilación e instrucciones de escalabilidad.

Es por eso que SQLite es una tecnología cómoda para los dispositivos móviles. Su simplicidad, rapidez y usabilidad permiten un desarrollo muy amigable.

Refuerza tus conocimientos en diseño de base de datos con el ebook Metodología para diseño conceptual de bases de datos de Hermosa Programación.

Ejemplo Base De Datos SQLite: Lawyers App

Lawyers App es un pequeño ejemplo que demuestra cómo crear una aplicación Android con bases de datos relacionales.

Su función es servir como plataforma de persistencia para todos los abogados de una compañía que presta servicios de asesoría a los usuarios.

Se compone de 3 screens:

  • Lawyers: Contiene una lista con todos los abogados del gabinete.
  • Lawyer Detail: Muestra el detalle de un abogado al presionar un ítem de lista.
  • Add/Edit Lawyer: Formulario con campos de texto para crear o modificar un abogado.

El siguiente es un wireframe que muestra los puntos de interacción:

Wireframe de app Android para abogados

 

Antes de iniciar, abre Android Studio y crea un proyecto llamado “Lawyers App” con la siguiente configuración:

New project lawyers Android Studio

Con esto listo, procedamos a los pasos para implementar nuestra SQLite App.

Definir Contrato De La Base De Datos

La forma en que una base de datos está estructurada (cantidad de tablas, registros, índices, etc.) y el conjunto de convenciones para nombrar sus objetos se les llama Esquema. Por lo general el esquema inicial se guarda en un Script que nos permita recuperar las condiciones previas en cualquier momento.

Con SQLite no es diferente, por lo que debes crear un esquema predefinido para implementarlo a la hora de crear tu base de datos.

La documentación de Android nos recomienda crear una clase llamada Contract Class, la cual guarda como constantes todas las características de la base de datos.

Crear clase de la entidad abogado

Nuestro ejemplo está basado es un diseño compuesto de una entidad llamada Lawyer, cuyos atributos son:

  • id
  • nombre
  • especialidad
  • número de teléfono
  • biografía
  • avatar

Para representarla crea un nuevo paquete Java con el nombre data. Dentro de este, añade una clase llamada Lawyer.

Lawyer.java

/**
 * Entidad "abogado"
 */
public class Lawyer {
    private String id;
    private String name;
    private String specialty;
    private String phoneNumber;
    private String bio;
    private String avatarUri;

    public Lawyer(String name,
                  String specialty, String phoneNumber,
                  String bio, String avatarUri) {
        this.id = UUID.randomUUID().toString();
        this.name = name;
        this.specialty = specialty;
        this.phoneNumber = phoneNumber;
        this.bio = bio;
        this.avatarUri = avatarUri;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getSpecialty() {
        return specialty;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public String getBio() {
        return bio;
    }

    public String getAvatarUri() {
        return avatarUri;
    }
}

Crear esquema de Lawyer App

El esquema se establecerá en una clase donde definirás los nombres de tablas, columnas y uris para un uso global.

Añade dentro del paquete data una nueva clase llamada LawyersContract y define una clase interna con los datos de la tabla "lawyer" que se creará en la base de datos:

LawyersContract.java

/**
 * Esquema de la base de datos para abogados
 */
public class LawyersContract {

    public static abstract class LawyerEntry implements BaseColumns{
        public static final String TABLE_NAME ="lawyer";

        public static final String ID = "id";
        public static final String NAME = "name";
        public static final String SPECIALTY = "specialty";
        public static final String PHONE_NUMBER = "phoneNumber";
        public static final String AVATAR_URI = "avatarUri";
        public static final String BIO = "bio";
    }
}

En el anterior código podemos notar los siguientes detalles:

  • Creamos la clase interna LawyerEntry para guardar el nombre de las columnas de la tabla.
  • Se implementó la interfaz BaseColumns con el fin de agregar una columna extra que se recomienda tenga toda tabla.

Estas declaraciones facilitan el mantenimiento del esquema, por si en algún momento cambian los nombres de las tablas o columnas.

Crear Base De Datos En SQLite

El Android SDK nos provee una serie de clases para administrar nuestro archivo de base de datos en SQLite.

Normalmente cuando conectamos otro gestor de bases de datos tenemos que validar los datos del equipo, el usuario y el esquema, pero con SQLite no se requiere nada de eso, ya que podemos trabajar directamente sobre la base de datos.

La clase que nos permitirá comunicar nuestra aplicación con la base de datos se llama SQLiteOpenHelper. Se trata de una clase abstracta que nos provee los mecanismos básicos para la relación entre la aplicación Android y la información.

Para implementar este controlador debes:

  • Crear una clase que extienda de SQLiteOpenHelper
  • Configurar un constructor apropiado
  • Sobrescribir los métodos onCreate() y onUpgrade()

Creando helper de abogados

1. Crea nueva clase que extienda de SQLiteOpenHelper y llamala LawyersDbHelper.

public class LawyersDbHelper extends SQLiteOpenHelper {

2. Escribe tú constructor y usa super para mantener la herencia del helper.

public static final int DATABASE_VERSION = 1;
public static final String DATABASE_NAME = "Lawyers.db";

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

Los parámetros del constructor tienen la siguiente finalidad:

  • Context context: Contexto de acción para el helper.
  • String name: Nombre del archivo con extensión .db, donde se almacenará la base de datos, que a su vez corresponde al nombre de la base de datos.
  • CursorFactory factory: Asignamos null, por ahora no es necesario comprender el funcionamiento de este parámetro.
  • int version: Entero que representa la versión de la base de datos. Su valor inicial por defecto es 1. Si en algún momento la versión es mayor se llama al método onUpgrade() para actualizar la base de datos a la nueva versión. Si es menor, se llama a downUpgrade() para volver a una versión previa.

3. Sobrescribe el método onCreate().

Este método es llamado automáticamente cuando creamos una instancia de la clase SQLiteOpenHelper. En su interior establecemos la creación de las tablas y registros.

Recibe como parámetro una referencia de la clase SQLiteDataBase, la cual actua como manejadora de la base de datos.

@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
    // Comandos SQL
}

Por defecto el archivo de la base de datos será almacenado en:

 /data/data/<paquete>/databases/<nombre-de-la-bd>.db

4. Sobrescribe el método onUpgrade().

Este es ejecutado si se identificó que el usuario tiene una versión antigua de la base de datos.

En su interior establecerás instrucciones para modificar el esquema de la base de datos, como por ejemplo eliminar todo el esquema y recrearlo, agregar una nueva tabla, añadir una nueva columna, etc.

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    // No hay operaciones
}

Recibe tres parámetros:

  • SQLiteDatabase db: Manejador de la base de datos.
  • int oldVersion: Se trata de un entero que indica la versión antigua de la base de datos.
  • int newVersion: Entero que se refiere a la versión nueva de la base de datos.

Código SQL para crear una base de datos

Una vez terminado el esquema, procede a crear la tabla de abogados en onCreate() con el metodo execSQL() y el comando CREATE TABLE:

@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
    sqLiteDatabase.execSQL("CREATE TABLE " + LawyerEntry.TABLE_NAME + " ("
            + LawyerEntry._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
            + LawyerEntry.ID + " TEXT NOT NULL,"
            + LawyerEntry.NAME + " TEXT NOT NULL,"
            + LawyerEntry.SPECIALTY + " TEXT NOT NULL,"
            + LawyerEntry.PHONE_NUMBER + " TEXT NOT NULL,"
            + LawyerEntry.BIO + " TEXT NOT NULL,"
            + LawyerEntry.AVATAR_URI + " TEXT,"
            + "UNIQUE (" + LawyerEntry.ID + "))");

}

Este método ejecuta una sola sentencia SQL que no retorne en filas. Por lo que el comando SELECT no es posible usarlo dentro de él.

Es recomendable que la llave primaria sea BaseColumns._ID, ya que el framework de Android usa esta referencia internamente en varios procesos.

Sin embargo puedes usar tu propio ID y añadirle un índice UNIQUE para mantener la unicidad de tus filas según tus reglas de negocio.

Evita ejecutar múltiples sentencias en una sola invocación del método execSQL(). Puede que se ejecute la primera, pero las otras no surtirán efecto.

Insertar Información En La Base De Datos

El método cuya funcionalidad es añadir filas a nuestras tablas se llama SQLiteDatabase.insert().

La receta a seguir para usarlo es:

  1. Crea un objeto del tipo ContentValues. Este permite almacenar las columnas del registro en pares clave-valor
  2. Añade los pares con el método put()
  3. Invoca a insert() a través de la instancia de la base de datos

Sus parámetros funcionan así:

  • String table: Nombre de la tabla donde se insertará la info.
  • String nullColumnHack: Nombre de una columna que acepta valores NULL y de la cual no se proveen pares clave-valor en values.
  • ContentValues values: Conjunto de pares clave-valor para las columnas.

Ejemplo…

@Override
public void onCreate(SQLiteDatabase db) {
    // Create table...

    // Contenedor de valores
    ContentValues values = new ContentValues();

    // Pares clave-valor
    values.put(LawyerEntry.ID, "L-001");
    values.put(LawyerEntry.NAME, "Carlos solarte");
    values.put(LawyerEntry.SPECIALTY, "Abogado penalista");
    values.put(LawyerEntry.PHONE_NUMBER, "300 200 1111");
    values.put(LawyerEntry.BIO, "Carlos es una profesional con 5 años de trayectoria...");
    values.put(LawyerEntry.AVATAR_URI, "carlos_solarte.jpg");

    // Insertar...
    db.insert(LawyerEntry.TABLE_NAME, null, values);

}

Con esto en mente, simplifica el guardado de abogados creando un método llamado saveLawyer(). Este recibirá una instancia Lawyer, se convertirá a ContentValues y luego se inserta:

public long saveLawyer(Lawyer lawyer) {
    SQLiteDatabase sqLiteDatabase = getWritableDatabase();

    return sqLiteDatabase.insert(
            LawyerEntry.TABLE_NAME,
            null,
            lawyer.toContentValues());

}

Usa getWritableDatabase() para obtener el manejador de la base de datos para operaciones de escritura. En cuestiones de lectura usa getReadableDatabase().

El método toContentValues() es solo una traducción de pares:

public ContentValues toContentValues() {
    ContentValues values = new ContentValues();
    values.put(LawyerEntry.ID, id);
    values.put(LawyerEntry.NAME, name);
    values.put(LawyerEntry.SPECIALTY, specialty);
    values.put(LawyerEntry.PHONE_NUMBER, phoneNumber);
    values.put(LawyerEntry.BIO, bio);
    values.put(LawyerEntry.AVATAR_URI, avatarUri);
    return values;
}

Podrías usar el comando execSQL() para ejecutar una sentencia INSERT, pero como estás recibiendo datos externos, es mejor usar insert() para evitar inyecciones SQL.

Crear abogados de prueba

Inserta 8 registros de prueba en onCreate() para tener un mock funcional cuando creemos la lista de abogados.

@Override
public void onCreate(SQLiteDatabase db) {
    // create table

    // Insertar datos ficticios para prueba inicial
    mockData(db);

}

private void mockData(SQLiteDatabase sqLiteDatabase) {
    mockLawyer(sqLiteDatabase, new Lawyer("Carlos Perez", "Abogado penalista",
            "300 200 1111", "Gran profesional con experiencia de 5 años en casos penales.",
            "carlos_perez.jpg"));
    mockLawyer(sqLiteDatabase, new Lawyer("Daniel Samper", "Abogado accidentes de tráfico",
            "300 200 2222", "Gran profesional con experiencia de 5 años en accidentes de tráfico.",
            "daniel_samper.jpg"));
    mockLawyer(sqLiteDatabase, new Lawyer("Lucia Aristizabal", "Abogado de derechos laborales",
            "300 200 3333", "Gran profesional con más de 3 años de experiencia en defensa de los trabajadores.",
            "lucia_aristizabal.jpg"));
    mockLawyer(sqLiteDatabase, new Lawyer("Marina Acosta", "Abogado de familia",
            "300 200 4444", "Gran profesional con experiencia de 5 años en casos de familia.",
            "marina_acosta.jpg"));
    mockLawyer(sqLiteDatabase, new Lawyer("Olga Ortiz", "Abogado de administración pública",
            "300 200 5555", "Gran profesional con experiencia de 5 años en casos en expedientes de urbanismo.",
            "olga_ortiz.jpg"));
    mockLawyer(sqLiteDatabase, new Lawyer("Pamela Briger", "Abogado fiscalista",
            "300 200 6666", "Gran profesional con experiencia de 5 años en casos de derecho financiero",
            "pamela_briger.jpg"));
    mockLawyer(sqLiteDatabase, new Lawyer("Rodrigo Benavidez", "Abogado Mercantilista",
            "300 200 1111", "Gran profesional con experiencia de 5 años en redacción de contratos mercantiles",
            "rodrigo_benavidez.jpg"));
    mockLawyer(sqLiteDatabase, new Lawyer("Tom Bonz", "Abogado penalista",
            "300 200 1111", "Gran profesional con experiencia de 5 años en casos penales.",
            "tom_bonz.jpg"));
}

public long mockLawyer(SQLiteDatabase db, Lawyer lawyer) {
    return db.insert(
            LawyerEntry.TABLE_NAME,
            null,
            lawyer.toContentValues());
}

Leer Información De La Base De Datos

Para obtener los registros de nuestra tabla usaremos el método query().

query (String table,
       String[] columns,
       String selection,
       String[] selectionArgs,
       String groupBy,
       String having,
       String orderBy)

Por ejemplo, si quisiéramos consultar todos los datos de la tabla lawyer usaríamos el siguiente código:

Cursor c = db.query(
    LawyerEntry.TABLE_NAME,  // Nombre de la tabla
    null,  // Lista de Columnas a consultar                              
    null,  // Columnas para la cláusula WHERE                               
    null,  // Valores a comparar con las columnas del WHERE                           
    null,  // Agrupar con GROUP BY                                    
    null,  // Condición HAVING para GROUP BY                                     
    null  // Cláusula ORDER BY                                
    );

Este método te ayuda a añadir todas las partes posibles de las cuales se podría componer una consulta, además que te protege de inyecciones SQL, separando las cláusulas de los argumentos.

Los parámetros tienen los siguientes propósitos:

  • String table: Nombre de la tabla a consultar
  • String[] columns: Lista de nombres de las columnas que se van a consultar. Si deseas obtener todas las columnas usas null.
  • String selection: Es el cuerpo de la sentencia WHERE con las columnas a condicionar. Es posible usar el placeholder '?' para generalizar la condición.
  • String[] selectionArgs: Es una lista de los valores que se usaran para reemplazar las incógnitas de selection en el WHERE.
  • String groupBy: Aquí puedes establecer cómo se vería la cláusula GROUP BY, si es que la necesitas.
  • String having: Establece la sentencia HAVING para condicionar a groupBy.
  • String orderBy: Reordena las filas de la consulta a través de ORDER BY.

Debido a la simplicidad de nuestra consulta anterior, la mayoría de parámetros fueron null, ya que se consultan todas las columnas de la tabla y todos los registros.

Pero si quisieras consultar el nombre del abogado con el id "L-001", tendrías que usar la siguiente cláusula WHERE:

String columns[] = new String[]{LawyerEntry.NAME};
String selection = LawyerEntry.ID + " LIKE ?"; // WHERE id LIKE ?
String selectionArgs[] = new String[]{"L-001"};

Cursor c = db.query(
        LawyerEntry.TABLE_NAME,
        columns,
        selection,
        selectionArgs,
        null,
        null,
        null
);

Aprende más sobre la cláusula WHERE

Ahora, existe otro método alternativo para realizar consultas llamado rawQuery(). Con él pasas como parámetro un String del código SQL de la consulta.

Veamos:

db.rawQuery("select * from " + LawyerEntry.TABLE_NAME, null);

Si deseas crear una consulta generalizada usa el placeholder '?' en la cláusula WHERE. Luego asigna los valores a cada incógnita en el segundo parámetro:

String query = "select * from " + LawyerEntry.TABLE_NAME + " WHERE _id=?";
database.rawQuery(query, new String[]{"3"});

Cursores en SQLite

Tanto query() como rawQuery() retornan un objeto de tipo Cursor.

Este objeto es un apuntador al conjunto de valores obtenidos de la consulta. Al inicio el cursor apunta a una dirección previa a la primera fila. Por lo que debes leer cada tupla moviendo el cursor a la fila siguiente.

Emplea el método booleano moveToNext() para avanzar al siguiente registro. Este retorna true si fue posible o false si ya no existen más elementos.

Este concepto es similar a cuando vimos cursores en SQL Server o en MySQL. Donde recorríamos cada elemento con un bucle while hasta que ya no existieran más elementos que referenciar.

Por ejemplo…

while(c.moveToNext()){
 String name = c.getString(c.getColumnIndex(LawyerEntry.NAME));
 // Acciones...
}

Usa métodos get*() para obtener el valor de cada columna a través del índice según su tipo de dato. Es decir, obtienes enteros con getInt(), flotantes con getFloat(), etc.

El índice de la columna se obtiene con getColumnsIndex().

Leer abogados de la base de datos

Puedes aprovechar este nuevo concepto e implementar un método de lectura para todos los abogados (getAllLawyers()) y otro por ID (getLawyerById()).

La diferencia estaría en la lectura por id requiere ese elemento como parámetro…

public Cursor getAllLawyers() {
    return getReadableDatabase()
            .query(
                    LawyerEntry.TABLE_NAME,
                    null,
                    null,
                    null,
                    null,
                    null,
                    null);
}

public Cursor getLawyerById(String lawyerId) {
    Cursor c = getReadableDatabase().query(
            LawyerEntry.TABLE_NAME,
            null,
            LawyerEntry.ID + " LIKE ?",
            new String[]{lawyerId},
            null,
            null,
            null);
    return c;
}

Crear lista de abogados

1. Crea un paquete nuevo llamado lawyers y haz click derecho sobre este. Selecciona New > Activity > Basic Activity para llamar al asistente de creación y configura los datos de la activity así:

New Lawyers Activity

2. Abre el layout activity_lawyers.xml y simplifica su contenido a una AppBar junto al contenido principal:

activity_lawyers.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context=".lawyers.LawyersActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </android.support.design.widget.AppBarLayout>

    <include layout="@layout/content_lawyers" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        android:tint="@android:color/white"
        app:fabSize="normal"
        app:srcCompat="@drawable/ic_account_plus" />
</android.support.design.widget.CoordinatorLayout>

3. Abre el archivo que se hace referencia en la etiqueta <include> del layout de la actividad de abogados y agrega el identificador lawyers_container al nodo principal. Esto con el fin de tener la referencia del contenedor donde se agregará un fragmento posterior.

content_lawyers.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/lawyers_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context=".lawyers.LawyersActivity"
    tools:showIn="@layout/activity_lawyers"/>

Agregar fragmento de abogados

1. En mismo paquete anterior presiona click derecho y selecciona New > Fragment > Fragment (Blank).

Nombralo LawyersFragment y configura las siguientes características así:

New Lawyers Fragment

Abre el código prefabricado que te aparezca y límpialo para que te quede así:

LawyersFragment.java

/**
 * Vista para la lista de abogados del gabinete
 */
public class LawyersFragment extends Fragment {   

    public LawyersFragment() {
        // Required empty public constructor
    }

    public static LawyersFragment newInstance() {
        return new LawyersFragment();
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.fragment_lawyers, container, false);

        return root;
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        
    }
   
}

La aparición del método onActivityResult() se debe a que la lista que tendrá el fragmento debe refrescarse en caso de que las screens de inserción o detalle hayan producido una modificación de la tabla lawyer.

Como vimos en el artículo de comunicaciones entre actividades, una actividad la cual se espera decida un resultado debe ser llamada con startActivityForResult().

2. Agrega un ListView al layout fragment_lawyers.xml para crear una lista en la interfaz.

<FrameLayout 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"
    tools:context=".lawyers.LawyersFragment">

    <ListView
        android:id="@+id/lawyers_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:divider="@null"/>

</FrameLayout>

3. El siguiente paso es crear el diseño de los ítems de la lista.

Si retomas el wireframe visto al inicio del tutorial, verás que la foto de perfil del abogado está al lado izquierdo de la distribución y justo a su derecha va el nombre completo.

Llevándolo a un mock de alta definición tendrás:

Item de lista con avatar en Material Design

Con ello en mente, crea un nuevo layout llamado list_item_lawyer.xml y agrega la siguiente definición XML:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:minHeight="?listPreferredItemHeight"
    android:orientation="vertical"
    android:paddingBottom="8dp"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="8dp">

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_marginLeft="72dp"
        android:text="New Text"
        android:textAppearance="?textAppearanceListItem"
        tools:text="Carlos Giron" />

    <ImageView
        android:id="@+id/iv_avatar"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_centerVertical="true"
        android:scaleType="fitXY"
        app:srcCompat="@drawable/ic_account_circle" />
</RelativeLayout>

4. Ve a la actividad LawyersActivity y realiza una transacción del tipo add() para insertar el fragmento en el contenedor principal.

public class LawyersActivity extends AppCompatActivity {

    public static final String EXTRA_LAWYER_ID = "extra_lawyer_id";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_lawyers);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        LawyersFragment fragment = (LawyersFragment)
                getSupportFragmentManager().findFragmentById(R.id.lawyers_container);

        if (fragment == null) {
            fragment = LawyersFragment.newInstance();
            getSupportFragmentManager()
                    .beginTransaction()
                    .add(R.id.lawyers_container, fragment)
                    .commit();
        }
    }
}

Crear adaptador con cursor

Existe un adaptador especial para el manejo de bases de datos llamado CursorAdapter. Esta clase permite poblar una lista a través de un cursor.

CursorAdapter es una clase abstracta de la cual se ha de crear tu adaptador personalizado. Con ArrayAdapter teníamos que sobrescribir el método getView() para inflar nuestras filas con los datos de la lista.

Pero no es el caso con CursorAdapter. Esta vez debemos sobrescribir dos métodos aislados llamados bindView() y newView().

bindView() es el encargado de poblar la lista con los datos del cursor y newView() es quien infla cada view de la lista. Al implementar ambos métodos no debemos preocuparnos por iterar el curso, esto es manejado internamente.

Adaptador de abogados

1. Escribe nuestra nueva clase llamada LawyersCursorAdapter y extiendela de CursorAdapter.

/**
 * Adaptador de abogados
 */
public class LawyersCursorAdapter extends CursorAdapter {

2. Agrega un constructor que transmita los parámetros a través de super para mantener la herencia.

public LawyersCursorAdapter(Context context, Cursor c) {
    super(context, c, 0);
}

3. Implementa newView() para acceder a la instancia del LayoutInflater a través de context y luego invoca inflate() para inflar el layout del ítem.

@Override
public View newView(Context context, Cursor cursor, ViewGroup viewGroup) {
    LayoutInflater inflater = LayoutInflater.from(context);
    return inflater.inflate(R.layout.list_item_lawyer, viewGroup, false);
}

4. Implementa bindView() para obtener el valor de las columnas name y avatarUri. Luego setealos en los views del layout.

@Override
public void bindView(View view, final Context context, Cursor cursor) {

    // Referencias UI.
    TextView nameText = (TextView) view.findViewById(R.id.tv_name);
    final ImageView avatarImage = (ImageView) view.findViewById(R.id.iv_avatar);

    // Get valores.
    String name = cursor.getString(cursor.getColumnIndex(LawyerEntry.NAME));
    String avatarUri = cursor.getString(cursor.getColumnIndex(LawyerEntry.AVATAR_URI));

    // Setup.
    nameText.setText(name);
    Glide
            .with(context)
            .load(Uri.parse("file:///android_asset/" + avatarUri))
            .asBitmap()
            .error(R.drawable.ic_account_circle)
            .centerCrop()
            .into(new BitmapImageViewTarget(avatarImage) {
                @Override
                protected void setResource(Bitmap resource) {
                    RoundedBitmapDrawable drawable
                            = RoundedBitmapDrawableFactory.create(context.getResources(), resource);
                    drawable.setCircular(true);
                    avatarImage.setImageDrawable(drawable);
                }
            });

}

El singleton Glide hace parte de una librería con el mismo nombre, cuyo objetivo es cargar imágenes de forma eficiente.

Básicamente ese código carga la imagen desde la carpeta assets en forma de Bitmap sobre el view avatarImage.

Si quieres usar un RecyclerView para la lista, ve al siguiente artículo: RecyclerView Con Cursor En Android.

Otra alternativa para adaptadores con cursor

Si el diseño de las filas de tu lista es sencillo (uno o dos text views), entonces la clase SimpleCursorAdapter podría ahorrarte la escritura de un adaptador personalizado.

Esta es una subclase de CursorAdapter, que posee una implementación completa para los android developers.

Con ella no debes sobrescribir métodos ni crear un archivo de diseño, ya que permite usar layouts del sistema.

Por ejemplo…

Crear un SimpleCursorAdapter para mostrar el nombre y especialidad de los abogados.

Solución

Usa el layout del sistema two_line_list_item en el constructor del adaptador:

//Iniciando el nuevo Adaptador
mLawyersAdapter = new SimpleCursorAdapter(
        getActivity(), // Context context
        android.R.layout.two_line_list_item, // int layout
        mLawyersDbHelper.getAllLawyers(), // Cursor c
        new String[]{LawyerEntry.NAME, LawyerEntry.SPECIALTY}, // String[] from
        new int[]{android.R.id.text1, android.R.id.text2}, // int[] to
        SimpleCursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER // int flags
);

mLawyersList.setAdapter(mLawyersAdapter);

El constructor se configura así:

  • Context context: Es el contexto donde se encuentra la lista.
  • int layout: Es el layout que usaremos para inflar los elementos de cada fila. En nuestro caso usamos el prefabricado por Android para los elementos con dos text views.
  • Cursor c: Es el cursor que representa el origen de datos para el adaptador. Asignamos el cursor hacia todos los registros de la tabla lawyer.
  • String[]from: Es un arreglo de strings que contiene el nombre de las columnas a consultar.
  • int[] to: Es un arreglo de enteros con las referencias directas de los text views en el layout. Deben tener el mismo orden que las columnas. Los textos dentro de two_line_list_item.xml se llaman text1 y text2 respectivamente.
  • int flags: Es una bandera para establecer el comportamiento del adaptador. FLAG_REGISTER_CONTENT_OBSERVER registra un observador adherido al cursor para saber cuándo cambio su información y así refrescar la lista.

Cargar datos del cursor a la lista

Dentro del fragmento LawyersFragment:

1. En onCreateView() obtén la referencia de la lista de abogados y setea un nuevo adaptador. Crea una instancia del helper y escribe un método loadLawyers() que será llamado para cargar los datos.

private LawyersDbHelper mLawyersDbHelper;

private ListView mLawyersList;
private LawyersCursorAdapter mLawyersAdapter;
private FloatingActionButton mAddButton;


public LawyersFragment() {
    // Required empty public constructor
}

public static LawyersFragment newInstance() {
    return new LawyersFragment();
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
    View root = inflater.inflate(R.layout.fragment_lawyers, container, false);

    // Referencias UI
    mLawyersList = (ListView) root.findViewById(R.id.lawyers_list);
    mLawyersAdapter = new LawyersCursorAdapter(getActivity(), null);
    mAddButton = (FloatingActionButton) getActivity().findViewById(R.id.fab);

    // Setup
    mLawyersList.setAdapter(mLawyersAdapter);

    // Instancia de helper
    mLawyersDbHelper = new LawyersDbHelper(getActivity());

    // Carga de datos
    loadLawyers();

    return root;
}
private void loadLawyers() {
    // Cargar datos...
}

2. Crea una tarea asíncrona dentro del fragmento, la cuál reciba como resultado un Cursor. Esto con el fin de no entorpecer el hilo principal con el acceso a la base de datos.

Sobrescribe doInBackground() para usar el método getAllLawyers() y luego cambia el cursor del adaptador en onPostExecute() con swapCursor().

private class LawyersLoadTask extends AsyncTask<Void, Void, Cursor> {

    @Override
    protected Cursor doInBackground(Void... voids) {
        return mLawyersDbHelper.getAllLawyers();
    }

    @Override
    protected void onPostExecute(Cursor cursor) {
        if (cursor != null && cursor.getCount() > 0) {
            mLawyersAdapter.swapCursor(cursor);
        } else {
            // Mostrar empty state
        }
    }
}

3. Ejecuta la tarea dentro de loadLawyers():

private void loadLawyers() {
    new LawyersLoadTask().execute();
}

Incrustar fragmento de abogados

Abre LawyersActivity y escribe una transacción de fragmentos del tipo add() con la referencia de LawyersFragment:

LawyersActivity.java

public class LawyersActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_lawyers);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        LawyersFragment fragment = (LawyersFragment)
                getSupportFragmentManager().findFragmentById(R.id.lawyers_container);

        if(fragment==null){
            fragment = LawyersFragment.newInstance();
            getSupportFragmentManager()
                    .beginTransaction()
                    .add(R.id.lawyers_container, fragment)
                    .commit();
        }
    }
}

Si corres la app tendrás el siguiente estado:

Lawyers App

Mostrar detalle de abogados

La screen del detalle se muestra cuando el usuario pulse uno de los ítems de la lista.

En ella se proyectarán el resto de datos del abogado para mostrar la entidad completa.

Crear actividad de detalle

1. Crea un nuevo paquete llamado lawyerdetail y presiona click derecho sobre este. Ve a New > Activity > Scrolling Activity y nombra al archivo LawyerDetailActivity.

La configuración debería quedarte así:

New Scrolling Activity

2. Abre el layout de la actividad y simplifícalo a una App Bar y el contenido principal.

Debido a que se usará scroll con la foto de perfil del abogado, incluye un elemento <ImageView> por encima de la toolbar.

activity_lawyer_detail.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context=".lawyerdetail.LawyerDetailActivity">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="@dimen/app_bar_height"
        android:fitsSystemWindows="true"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <!-- Imagen del detalle -->
            <ImageView
                android:id="@+id/iv_avatar"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:fitsSystemWindows="true"
                app:layout_collapseMode="parallax" />

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin"
                app:popupTheme="@style/AppTheme.PopupOverlay" />

        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

    <include layout="@layout/content_lawyer_detail" />

</android.support.design.widget.CoordinatorLayout>

3. En el layout referenciado por el componente <include>, deja tan solo un nodo RelativeLayout y márcalo con el identificador lawyer_detail_container.

content_lawyer_detail.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/lawyer_detail_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context=".lawyerdetail.LawyerDetailActivity"
    tools:showIn="@layout/activity_lawyer_detail">

</android.support.v4.widget.NestedScrollView>

Crear el fragmento de detalle de abogados

1. Crea en el paquete lawyerdetail un nuevo fragmento llamado LawyerDetailFragment y genera la siguiente configuración:

New Lawyer Detail Fragment

Para saber que detalle vamos a consultar, es necesario tener el ID del abogado que será consultado en la base de datos. Así que en el método de fabricación newInstance() incluye tan solo un parámetro String para este cometido.

LawyerDetailFragment.java

/**
 * Vista para el detalle del abogado
 */
public class LawyerDetailFragment extends Fragment {

    private String mLawyerId;

    private CollapsingToolbarLayout mCollapsingView;
    private ImageView mAvatar;
    private TextView mPhoneNumber;
    private TextView mSpecialty;
    private TextView mBio;

    private LawyersDbHelper mLawyersDbHelper;


    public LawyerDetailFragment() {
        // Required empty public constructor
    }

    public static LawyerDetailFragment newInstance(String lawyerId) {
        LawyerDetailFragment fragment = new LawyerDetailFragment();
        Bundle args = new Bundle();
        args.putString(ARG_LAWYER_ID, lawyerId);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (getArguments() != null) {
            mLawyerId = getArguments().getString(ARG_LAWYER_ID);
        }

    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.fragment_lawyer_detail, container, false);
        mCollapsingView = (CollapsingToolbarLayout) getActivity().findViewById(R.id.toolbar_layout);
        mAvatar = (ImageView) getActivity().findViewById(R.id.iv_avatar);
        mPhoneNumber = (TextView) root.findViewById(R.id.tv_phone_number);
        mSpecialty = (TextView) root.findViewById(R.id.tv_specialty);
        mBio = (TextView) root.findViewById(R.id.tv_bio);
        
        mLawyersDbHelper = new LawyersDbHelper(getActivity());

        return root;
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        // Acciones
    }    

}

Se requiere onActivityResult() ya que cuando se intente eliminar o editar el abogado se estará pendiente de cambios en la base de datos, así que podremos reportar a LawyersFragment la necesidad de actualizar la lista.

2. La interfaz de usuario del detalle se basa en el patrón Flexible space with image de las técnicas de scrolling de Material Design, donde se contraen la App Bar extendida para desplegar el contenido inferior de la pantalla.

El wireframe inicial mostraba la foto de perfil del abogado en la app bar y por debajo una serie de pares etiqueta-contenido con los demás datos.

Basado en esa idea, intenta recrear el siguiente mock con la organización que desees:

Datos de abogado

En mi caso el resultado final me quedó así:

fragment_lawyer_detail.xml

<LinearLayout 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:layout_marginLeft="72dp"
    android:layout_marginRight="@dimen/activity_horizontal_margin"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginTop="@dimen/activity_vertical_margin"
        android:text="Teléfono"
        android:textColor="?colorPrimary" />

    <TextView
        android:id="@+id/tv_phone_number"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="@style/TextAppearance.AppCompat.Body1"
        tools:text="300 20 1111" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginTop="@dimen/activity_vertical_margin"
        android:text="Especialidad"
        android:textColor="?colorPrimary" />

    <TextView
        android:id="@+id/tv_specialty"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="@style/TextAppearance.AppCompat.Body1"
        tools:text="Abogado penalista" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginTop="@dimen/activity_vertical_margin"
        android:text="Biografía"
        android:textColor="?colorPrimary" />

    <TextView
        android:id="@+id/tv_bio"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="@style/TextAppearance.AppCompat.Body1"
        tools:text="@string/large_text" />

</LinearLayout>

3. Abre la actividad contenedora y realiza una transacción de agregación en onCreate().

Ten en cuenta que la instancia del fragmento recibe el id del abogado.

Este debe venir en un Intent explicito desde LawyersActivity y ser de tipo String. Así que crea una constante en la actividad de lista para la clave del extra. Luego obtenla con getIntent() en LawyerDetailActivity.

public class LawyerDetailActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_lawyer_detail);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);

        String id = getIntent().getStringExtra(LawyersActivity.EXTRA_LAWYER_ID);

        LawyerDetailFragment fragment = (LawyerDetailFragment)
                getSupportFragmentManager().findFragmentById(R.id.lawyer_detail_container);
        if (fragment == null) {
            fragment = LawyerDetailFragment.newInstance(id);
            getSupportFragmentManager()
                    .beginTransaction()
                    .add(R.id.lawyer_detail_container, fragment)
                    .commit();
        }


    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_lawyer_detail, menu);
        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onSupportNavigateUp() {
        onBackPressed();
        return true;
    }

}

Obtener abogado por id

Carga el detalle del abogado con el método getLawyerById() con una tarea asíncrona. Llámala en onCreateView() a través de un método loadLawyer().

En onPostExecute() extrae cada uno de los valores de la columna y asígnalos en los views de texto para poblar el detalle.

public class LawyerDetailFragment extends Fragment {
    
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // más...

        mLawyersDbHelper = new LawyersDbHelper(getActivity());

        loadLawyer();

        return root;
    }

    private void loadLawyer() {
        new GetLawyerByIdTask().execute();
    }
   
    private void showLawyer(Lawyer lawyer) {
        mCollapsingView.setTitle(lawyer.getName());
        Glide.with(this)
                .load(Uri.parse("file:///android_asset/" + lawyer.getAvatarUri()))
                .centerCrop()
                .into(mAvatar);
        mPhoneNumber.setText(lawyer.getPhoneNumber());
        mSpecialty.setText(lawyer.getSpecialty());
        mBio.setText(lawyer.getBio());
    }

    private void showLoadError() {
        Toast.makeText(getActivity(),
                "Error al cargar información", Toast.LENGTH_SHORT).show();
    }

    private class GetLawyerByIdTask extends AsyncTask<Void, Void, Cursor> {

        @Override
        protected Cursor doInBackground(Void... voids) {
            return mLawyersDbHelper.getLawyerById(mLawyerId);
        }

        @Override
        protected void onPostExecute(Cursor cursor) {
            if (cursor != null && cursor.moveToLast()) {
                showLawyer(new Lawyer(cursor));
            } else {
                showLoadError();
            }
        }

    }

}

Fíjate que usé un constructor nuevo de la clase Lawyer, donde se recibe un cursor. Su función es fabricar un nuevo abogado:

public Lawyer(Cursor cursor) {
    id = cursor.getString(cursor.getColumnIndex(LawyerEntry.ID));
    name = cursor.getString(cursor.getColumnIndex(LawyerEntry.NAME));
    specialty = cursor.getString(cursor.getColumnIndex(LawyerEntry.SPECIALTY));
    phoneNumber = cursor.getString(cursor.getColumnIndex(LawyerEntry.PHONE_NUMBER));
    bio = cursor.getString(cursor.getColumnIndex(LawyerEntry.BIO));
    avatarUri = cursor.getString(cursor.getColumnIndex(LawyerEntry.AVATAR_URI));
}

Iniciar actividad de detalle al pulsar ítem de lista

1. Abre LawyersFragment y agrega una escucha OnItemClickListener a la lista.

// Eventos
mLawyersList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
        // ...
    }
});

2. Procesa el controlador onItemClick() para extraer el id del elemento seleccionado. Usa getItem() del adaptador para conseguir el ítem pulsado.

Cursor currentItem = (Cursor) mLawyersAdapter.getItem(i);
String currentLawyerId = currentItem.getString(
        currentItem.getColumnIndex(LawyerEntry.ID));

showDetailScreen(currentLawyerId);

3. Con el ID obtenido inicia la actividad de detalle.

private void showDetailScreen(String lawyerId) {
    Intent intent = new Intent(getActivity(), LawyerDetailActivity.class);
    intent.putExtra(LawyersActivity.EXTRA_LAWYER_ID, lawyerId);
    startActivityForResult(intent, REQUEST_UPDATE_DELETE_LAWYER);
}

REQUEST_UDAPTE_DELETE_LAWYER es una constante entera que representa la vía de comunicación entre la screen de abogados y la de detalles.

4. Actualiza la lista en onActivityResult() si el resultado fue positivo:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (Activity.RESULT_OK == resultCode) {
        switch (requestCode) {
            case REQUEST_UPDATE_DELETE_LAWYER:
                loadLawyers();
                break;
        }
    }
}

La Herramienta sqlite3

sqlite3 es una herramienta de administración para nuestras bases de datos SQLite a través de la línea de comandos. Normalmente puedes descargarla de la página oficial de SQLite, pero tanto como la distribución de Android y Android Studio ya la traen consigo.

Antes de ejecutarla en el dispositivo, primero usaremos la herramienta Android Device Monitor del SDK, la cual permite visualizar las características del dispositivo que se está ejecutando. En ella podemos visualizar estadísticas de rendimiento, monitorear recursos y navegar por el sistema de archivos.

Si deseas ejecutarla solo presiona el siguiente icono en Android Studio:

Herramienta Dalvik Debug Monitor Server En Android

Asegurate de ejecutar Android Studio como administrador para evitar inconvenientes.

Ahora dirígete a la pestaña “File Explorer

File Explorer En El DDMS Android

Como ves, se visualizan todos los directorios que se encuentran en el dispositivo. Así que para ver si existe nuestro archivo de base de datos, iremos a la ruta de la cual hablamos al inicio /data/data/<paquete>/databases/Lawyers.db

Base de datos en Android Device Monitor

Si todo salió bien, veremos nuestro archivo Lawyers.db.

Ahora guárdalo con el botón de la parte superior derecha denominado “Pull a file from the device“. En la siguiente sección veremos algo interesante con él.

Pasar Un Archivo De Android Studio Al Escritorio

Ya que hemos comprobado que existe nuestra base de datos, iniciaremos sqlite3 dentro del dispositivo.

Sigue los siguientes pasos:

1. Inicia el terminal de Windows (cmd) o usa la pestaña Terminal de Android Studio:

Terminal en Android Studio

2. Navega hasta el directorio platform-tools del SDK de Android.

La sintaxis de la dirección por defecto es:

<sdk>/platform-tools/

Recuerda que para navegar a través de carpetas en DOS se utiliza el comando cd.

3. Una vez hayas encontrado el directorio, digita la siguiente linea de comandos:

adb shell

Este comando conecta remotamente la consola de comandos del dispositivo Android con tu consola local. Cuando ya estés conectado a la consola del AVD, verás en el terminal algo como esto:

root@android:/ #

4. Inicia sqlite3 en el dispositivo con el siguiente comando:

sqlite3 data/data/<package>/databases/<nombre-base-de-datos>

La anterior instrucción accede a sqlite3 y al mismo tiempo le pide que abra la base de datos expuesta en el directorio especificado. Si accedió a la base de datos verás los siguientes mensajes:

SQLite version 3.8.10.2 2015-05-20 18:17:19
Enter ".help" for usage hints.
sqlite>

5. Usa el comando .schema para ver el resumen del esquema de la base de datos:

 CREATE TABLE android_metadata
             (
                          locale TEXT
             );

CREATE TABLE lawyer
             (
                          _id INTEGER PRIMARY KEY autoincrement,
                          id text NOT NULL,
                          name text NOT NULL,
                          specialty text NOT NULL,
                          phonenumber text NOT NULL,
                          bio text NOT NULL,
                          avataruri text,
                          UNIQUE (id)
             );

El log muestra que la tabla lawyer ha sido creada correctamente.

La tabla llamada android_metadata es parte de la configuración local de la base de datos, por lo que siempre la encontrarás.

Usar sqlite3 en estación de trabajo

Otra forma de comprobar el esquema de nuestra base de datos es usar sqlite3.exe en nuestro equipo local.

1. Ve a la ruta para encontrar la carpeta platform-tools del SDK de Android y ejecuta la aplicación.

2. Luego usa .open para abrir el archivo en una ruta especificada o copia y pega el archivo Lawyers.db en la carpeta:

sqlite>.open Lawyers.db

3. Finalmente usa .schema y tendrás el mismo resultado anterior.

SQLite App Browser

Si deseas conocer un SQL Manager más visual, entonces SQLite Browser es una opción que te gustaría considerar.

Se trata de un editor para archivos de bases de datos SQLite de código abierto y súper sencillo de usar.

Solo basta con iniciarlo en tu pc y arrastrar el archivo Lawyers.db a su editor.

Inmediatamente nos mostrará el esquema en forma de tablas con interfaz gráfica de usuario. Además de permitir editar la estructura y ejecutar sentencias SQL dentro de ella.

SQLite Browser App

Borrar Información De La Base De Datos

Eliminar registros es muy sencillo, solo tenemos que usar el método delete().

Recibe como parámetros el nombre de la tabla, el estilo de la selección de la cláusula WHERE y los valores de comparación para determinar que filas borrar.

Por ejemplo…

Eliminar el abogado que donde _id = 3:

String selection = LawyerEntry._ID + " = ?";
String[] selectionArgs = {"3"};

db.delete(
        LawyerEntry.TABLE_NAME, 
        selection, 
        selectionArgs);

Crea un método de eliminación de abogados en LawyersDbHelper llamado deleteLawyer():

public int deleteLawyer(String lawyerId) {
    return getWritableDatabase().delete(
            LawyerEntry.TABLE_NAME,
            LawyerEntry.ID + " LIKE ?",
            new String[]{lawyerId});
}

Eliminar un abogado

La eliminación y edición van como action buttons en la Toolbar de la actividad de detalle.

1. Para agregarlos ve a res/menu y abre menu_lawyer_detail.xml.

Agrega dos nodos <item>. El primero con el título de edición y el segundo refiriendose a la eliminación.

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".lawyerdetail.LawyerDetailActivity">
    <item
        android:id="@+id/action_edit"
        android:orderInCategory="1"
        android:title="@string/action_edit"
        android:icon="@drawable/ic_pencil"
        app:showAsAction="ifRoom" />

    <item
        android:id="@+id/action_delete"
        android:orderInCategory="2"
        android:icon="@drawable/ic_delete"
        android:title="@string/action_delete"
        app:showAsAction="ifRoom" />
</menu>

2. Habilita la contribución de LawyerDetailFragment a la Toolbar con el método setHasOptionsMenu() con el valor de true en onCreate().

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // ...

    setHasOptionsMenu(true);
}

3. Implementa el método onOptionsItemSelected() en el fragmento. En el abre una estructura switch y procesa los casos de edición y eliminación.

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case R.id.action_edit:
            showEditScreen();
            break;
        case R.id.action_delete:
            new DeleteLawyerTask().execute();
            break;
    }
    return super.onOptionsItemSelected(item);
}

4. Para manejar el evento de borrado, crea una nueva tarea asíncrona que llame a DeleteLawyerTask.

En doInBackground() llama a LawyersDbHelper.deleteLawyer().

private class DeleteLawyerTask extends AsyncTask<Void, Void, Integer> {

    @Override
    protected Integer doInBackground(Void... voids) {
        return mLawyersDbHelper.deleteLawyer(mLawyerId);
    }

    @Override
    protected void onPostExecute(Integer integer) {
        showLawyersScreen(integer > 0);
    }

}

En postExecute() cierra la actividad de detalle con un resultado favorable hacia la actividad de abogados en caso de que la eliminación fuese exitosa.

private void showLawyersScreen(boolean requery) {
    if (!requery) {
        showDeleteError();
    }
    getActivity().setResult(requery ? Activity.RESULT_OK : Activity.RESULT_CANCELED);
    getActivity().finish();
}

De lo contrario muestra un error:

private void showDeleteError() {
    Toast.makeText(getActivity(),
            "Error al eliminar abogado", Toast.LENGTH_SHORT).show();
}

5. El evento de edición debe iniciar la actividad que crearemos ahora, pero puedes dejarla expresada a través de un nuevo método llamado showAddEditScreen(). En iniciarás la actividad de edición con un extra cuyo valor sea el ID del abogado.

private void showEditScreen() {
    Intent intent = new Intent(getActivity(), AddEditLawyerActivity.class);
    intent.putExtra(LawyersActivity.EXTRA_LAWYER_ID, mLawyerId);
    startActivityForResult(intent, LawyersFragment.REQUEST_UPDATE_DELETE_LAWYER);
}

Actualizar Información De La Base De Datos

En este caso usaremos el método update(). Es exactamente el mismo estilo de uso que los anteriores métodos.

Especificaremos:

  • Nombre de la tabla
  • Valores nuevos
  • Instrucción WHERE
  • Argumentos del WHERE

Por ejemplo…

// Valores
ContentValues values = new ContentValues();

// Valores nuevos del nombre y teléfono
values.put(LawyerEntry.NAME, "Fracisco Palomino");
values.put(LawyerEntry.PHONE_NUMBER, "222 222 2222");

// WHERE
String selection = LawyerEntry.ID + " LIKE ?";
String[] selectionArgs = {"L-009"};

// Actualizar
db.update(
        LawyerEntry.TABLE_NAME,
        values,
        selection,
        selectionArgs);

Actualizar abogados

Dentro de LawyersDbHelper crea un nuevo método llamado updateLawyer(), cuyos parámetros sean un objeto Lawyer y un string con el ID a modificar.

Su propósito es convertir el POJO en ContentValues y luego llamar a update():

public int updateLawyer(Lawyer lawyer, String lawyerId) {
    return getWritableDatabase().update(
            LawyerEntry.TABLE_NAME,
            lawyer.toContentValues(),
            LawyerEntry.ID + " LIKE ?",
            new String[]{lawyerId}
    );
}

Screen de creación de abogados

Aún no hemos creado la screen para la creación de abogados. Recuerda que esta también servirá para la edición así que veamos cómo será la implementación:

Crear actividad para añadir/editar abogados

1. Para aislar esta caracteristica crea un nuevo paquete con el nombre addeditlawyer. Dentro de él agrega una nueva actividad del tipo Basic Activity denominada AddEditLawyerActivity y configurala así:

New Add Edit Lawyer Activity

2. En su layout activity_add_edit_lawyer.xml modifica el fab button que viene por defecto para que traiga un tamaño normal y su icono sea una marca de check.

Este será el encargado de guarda nuevos registros o los cambios a uno existente.

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context=".addeditlawyer.AddEditLawyerActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </android.support.design.widget.AppBarLayout>

    <include layout="@layout/content_add_edit" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        android:tint="@android:color/white"
        app:fabSize="normal"
        app:srcCompat="@drawable/ic_check" />

</android.support.design.widget.CoordinatorLayout>

3. En el layout de contenido, modifica al identificador del nodo principal con el valor add_edit_lawyer_container.

content_add_edit.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/add_edit_lawyer_container"
    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"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context=".addeditlawyer.AddEditLawyerActivity"
    tools:showIn="@layout/activity_add_edit">

</RelativeLayout>

Crear fragmento para añadir/editar abogados

1. Crea un nuevo fragmento con el nombre AddEditLawyerFragment. Su configuración sería:

New Add Edit Lawyer Fragment

Este fragmento cuando actúa como editor requiere el identificador del abogado, para una carga previa de la información que se cargará en el formulario.

Así que en el método newInstance() deja un argumento String como se hizo en LawyerDetailFragment:

AddEditLawyerFragment.java

/**
 * Vista para creación/edición de un abogado
 */
public class AddEditLawyerFragment extends Fragment {
    private static final String ARG_LAWYER_ID = "arg_lawyer_id";

    private String mLawyerId;

    private LawyersDbHelper mLawyersDbHelper;

    private FloatingActionButton mSaveButton;
    private TextInputEditText mNameField;
    private TextInputEditText mPhoneNumberField;
    private TextInputEditText mSpecialtyField;
    private TextInputEditText mBioField;
    private TextInputLayout mNameLabel;
    private TextInputLayout mPhoneNumberLabel;
    private TextInputLayout mSpecialtyLabel;
    private TextInputLayout mBioLabel;


    public AddEditLawyerFragment() {
        // Required empty public constructor
    }

    public static AddEditLawyerFragment newInstance(String lawyerId) {
        AddEditLawyerFragment fragment = new AddEditLawyerFragment();
        Bundle args = new Bundle();
        args.putString(ARG_LAWYER_ID, lawyerId);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            mLawyerId = getArguments().getString(ARG_LAWYER_ID);
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.fragment_add_edit_lawyer, container, false);

        // Referencias UI
        mSaveButton = (FloatingActionButton) getActivity().findViewById(R.id.fab);
        mNameField = (TextInputEditText) root.findViewById(R.id.et_name);
        mPhoneNumberField = (TextInputEditText) root.findViewById(R.id.et_phone_number);
        mSpecialtyField = (TextInputEditText) root.findViewById(R.id.et_specialty);
        mBioField = (TextInputEditText) root.findViewById(R.id.et_bio);
        mNameLabel = (TextInputLayout) root.findViewById(R.id.til_name);
        mPhoneNumberLabel = (TextInputLayout) root.findViewById(R.id.til_phone_number);
        mSpecialtyLabel = (TextInputLayout) root.findViewById(R.id.til_specialty);
        mBioLabel = (TextInputLayout) root.findViewById(R.id.til_bio);

        // Eventos
        mSaveButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                addEditLawyer();
            }
        });

        mLawyersDbHelper = new LawyersDbHelper(getActivity());

        // Carga de datos
        if (mLawyerId != null) {
            loadLawyer();
        }

        return root;
    }

    private void loadLawyer() {
        // AsyncTask
    }

}

Fijate que onCreateView() se verifica el contenido del ID del abogado para determinar si se cargan los datos de un elemento existente.

2. El diseño de la interfaz para el formulario de añadir/editar consta de cuatro campos de texto para la obtención de los datos: Nombre, Especialidad, Número de telefono y Biografía.

El avatar no lo capturaremos ya que requiere un proceso extra que se escapa del alcance de este artículo.

El mock de alto nivel para la screen se vería así:

Formulario de abogado

Es muy sencillo, ya que está basada en EditTexts organizados de forma vertical.

Si quieres usar etiquetas flotantes usa el wrapper TextInputLayout junto a la variación derivada TextInputEditText:

fragment_add_edit_lawyer.xml

<LinearLayout 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:descendantFocusability="beforeDescendants"
    android:focusableInTouchMode="true"
    android:orientation="vertical"
    tools:context=".addeditlawyer.AddEditLawyerFragment">

    <android.support.design.widget.TextInputLayout
        android:id="@+id/til_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/activity_vertical_margin">

        <android.support.design.widget.TextInputEditText
            android:id="@+id/et_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Nombre"
            android:inputType="textPersonName"
            android:textAppearance="@style/TextAppearance.AppCompat.Display1"
            tools:text="Alejandro Riascos" />
    </android.support.design.widget.TextInputLayout>

    <android.support.design.widget.TextInputLayout
        android:id="@+id/til_phone_number"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/activity_vertical_margin">

        <android.support.design.widget.TextInputEditText
            android:id="@+id/et_phone_number"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Número de teléfono"
            android:inputType="phone"
            tools:text="300 200 4564" />
    </android.support.design.widget.TextInputLayout>

    <android.support.design.widget.TextInputLayout
        android:id="@+id/til_specialty"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/activity_vertical_margin">

        <android.support.design.widget.TextInputEditText
            android:id="@+id/et_specialty"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Especialidad"
            android:inputType="text"
            tools:text="Abogado penalista" />
    </android.support.design.widget.TextInputLayout>

    <android.support.design.widget.TextInputLayout
        android:id="@+id/til_bio"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/activity_vertical_margin">

        <android.support.design.widget.TextInputEditText
            android:id="@+id/et_bio"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="top|start"
            android:hint="Biografía"
            android:imeOptions="actionDone"
            android:inputType="text|textMultiLine"
            tools:text="@string/lorem" />
    </android.support.design.widget.TextInputLayout>

</LinearLayout>

3. Realiza una transacción de inserción de fragmento en AddEditLawyerActivity recibiendo el identificador del abogado a través del intent entrante.

AddEditLawyerActivity.java

public class AddEditLawyerActivity extends AppCompatActivity {

    public static final int REQUEST_ADD_LAWYER = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_add_edit);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);

        String lawyerId = getIntent().getStringExtra(LawyersActivity.EXTRA_LAWYER_ID);

        setTitle(lawyerId == null ? "Añadir abogado" : "Editar abogado");

        AddEditLawyerFragment addEditLawyerFragment = (AddEditLawyerFragment)
                getSupportFragmentManager().findFragmentById(R.id.add_edit_lawyer_container);
        if (addEditLawyerFragment == null) {
            addEditLawyerFragment = AddEditLawyerFragment.newInstance(lawyerId);
            getSupportFragmentManager()
                    .beginTransaction()
                    .add(R.id.add_edit_lawyer_container, addEditLawyerFragment)
                    .commit();
        }
    }

    @Override
    public boolean onSupportNavigateUp() {
        onBackPressed();
        return true;
    }
}

Guardar/Modificar abogado

El punto de interacción en el fab button obedece al siguiente flujo:

  1. Usuario modifica campos de texto con datos de abogado
  2. Usuario pulsa botón de guardado
  3. Se inicia tarea asíncrona
    1. Actualización: Ejecutar método update()
    2. Inserción: Ejecutar método insert()
  4. Se muestra la lista de abogados con la modificación

Con esto en mente veamos cómo proceder…

1. Obtén la instancia del fab en onCreateView() y asignale una escucha OnClickListener.

// Eventos
mSaveButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        addEditLawyer();
    }
});

Crea una tarea asíncrona que compruebe en doInBackground() el contenido de mLawyerId. Si este no es null, entonces realizar una actualización, de lo contrario una inserción.

private class AddEditLawyerTask extends AsyncTask<Lawyer, Void, Boolean> {

    @Override
    protected Boolean doInBackground(Lawyer... lawyers) {
        if (mLawyerId != null) {
            return mLawyersDbHelper.updateLawyer(lawyers[0], mLawyerId) > 0;

        } else {
            return mLawyersDbHelper.saveLawyer(lawyers[0]) > 0;
        }

    }

    @Override
    protected void onPostExecute(Boolean result) {
        showLawyersScreen(result);
    }

}

Muestra la actividad de abogados en onPostExecute() con un nuevo método llamado showLawyersScreen(). Setea el resultado de la actividad dependiendo del comportamiento de la tarea asíncrona:

private void showLawyersScreen(Boolean requery) {
    if (!requery) {
        showAddEditError();
        getActivity().setResult(Activity.RESULT_CANCELED);
    } else {
        getActivity().setResult(Activity.RESULT_OK);
    }

    getActivity().finish();
}

private void showAddEditError() {
    Toast.makeText(getActivity(),
            "Error al agregar nueva información", Toast.LENGTH_SHORT).show();
}

Crea un nuevo método llamado addEditLawyer(). En él debes extraer primero los datos de los campos de texto, comprobar que no estén vacío y luego crea una nueva instancia Lawyer con ellos. Finalmente inicia la tarea con este objeto.

private void addEditLawyer() {
    boolean error = false;

    String name = mNameField.getText().toString();
    String phoneNumber = mPhoneNumberField.getText().toString();
    String specialty = mSpecialtyField.getText().toString();
    String bio = mBioField.getText().toString();

    if (TextUtils.isEmpty(name)) {
        mNameLabel.setError(getString(R.string.field_error));
        error = true;
    }

    if (TextUtils.isEmpty(phoneNumber)) {
        mPhoneNumberLabel.setError(getString(R.string.field_error));
        error = true;
    }

    if (TextUtils.isEmpty(specialty)) {
        mSpecialtyLabel.setError(getString(R.string.field_error));
        error = true;
    }


    if (TextUtils.isEmpty(bio)) {
        mBioLabel.setError(getString(R.string.field_error));
        error = true;
    }

    if (error) {
        return;
    }

    Lawyer lawyer = new Lawyer(name, specialty, phoneNumber, bio, "");

    new AddEditLawyerTask().execute(lawyer);

}

Iniciar actividad de creación

Ve a LawyersFragment, obtén la referencia del fab para agregar y setea una escucha OnClickListenener.

mAddButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        showAddScreen();
    }
});

Crea un nuevo método llamado showAddScreen(). Dentro de este inicia a AddEditLawyerActivity. Luego llamalo dentro del controlador onClick().

private void showAddScreen() {
    Intent intent = new Intent(getActivity(), AddEditLawyerActivity.class);
    startActivityForResult(intent, AddEditLawyerActivity.REQUEST_ADD_LAWYER);
}

Luego procesa el resultado del código de petición en onActivityResult() para recargar la lista y mostrar un mensaje de guardado exitoso:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (Activity.RESULT_OK == resultCode) {
        switch (requestCode) {
            case AddEditLawyerActivity.REQUEST_ADD_LAWYER:
                showSuccessfullSavedMessage();
                loadLawyers();
                break;
            case REQUEST_UPDATE_DELETE_LAWYER:
                loadLawyers();
                break;
        }
    }
}

El mensaje sería:

private void showSuccessfullSavedMessage() {
    Toast.makeText(getActivity(),
            "Abogado guardado correctamente", Toast.LENGTH_SHORT).show();
}

Conclusión

Hasta aquí ya tienes claro cómo usar una base de datos SQLite sencilla en Android.

Viste como usar una clase tipo contrato para estandarizar la estructura de las tablas y columnas.

Ya sabes que existen herramientas como sqlite3 y SQLite Browser para editar y visualizar tu modelo de datos.

Incluso viste adaptadores especializados para alimentar listas desde cursores.

Ahora solo queda seguir aumentando el nivel de implementación. Es necesario que aprendas a cubrir una base de datos con múltiples tablas y a proteger los datos con un ContentProvider.

También es recomendado emplear las clases AsyncTask o IntentService para aislar las operaciones sobre la base de datos a un segundo plano.

Y no te olvides de conseguir el código completo del ejemplo del artículo.