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 descargar el proyecto final en Android Studio desde el siguiente botó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:
Antes de iniciar, abre Android Studio y crea un proyecto llamado «Lawyers App» con la siguiente configuración:
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()
yonUpgrade()
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
: Asignamosnull
, 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 es1
. Si en algún momento la versión es mayor se llama al métodoonUpgrade()
para actualizar la base de datos a la nueva versión. Si es menor, se llama adownUpgrade()
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:
- Crea un objeto del tipo
ContentValues
. Este permite almacenar las columnas del registro en pares clave-valor - Añade los pares con el método
put()
- 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 valoresNULL
y de la cual no se proveen pares clave-valor envalues
.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 consultarString[] columns
: Lista de nombres de las columnas que se van a consultar. Si deseas obtener todas las columnas usasnull
.String selection
: Es el cuerpo de la sentenciaWHERE
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 elWHERE
.String groupBy
: Aquí puedes establecer cómo se vería la cláusulaGROUP BY
, si es que la necesitas.String having
: Establece la sentenciaHAVING
para condicionar agroupBy
.String orderBy
: Reordena las filas de la consulta a través deORDER 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 );
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.
Recorremos cada elemento con un bucle while
hasta que ya no existan 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í:
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í:
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:
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 tablalawyer
.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 llamantext1
ytext2
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:
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í:
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:
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:
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:
Asegurate de ejecutar Android Studio como administrador para evitar inconvenientes.
Ahora dirígete a la pestaña «File Explorer»
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
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.
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:
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.
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í:
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:
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í:
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:
- Usuario modifica campos de texto con datos de abogado
- Usuario pulsa botón de guardado
- Se inicia tarea asíncrona
- Actualización: Ejecutar método
update()
- Inserción: Ejecutar método
insert()
- Actualización: Ejecutar método
- 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.