Android

Tutorial de SQLite con Android: CRUD (create, read, update, delete)

Para guardar datos en una base de datos usando Android tenemos que recurrir al maravilloso gestor SQLite3. Pues bien, hoy veremos cómo trabajar con SQLite desde Android usando Java.

Lo que veremos será un CRUD o ABC de Android con SQLite en donde veremos un insert, update, delete y select de SQL con Android.

Al final tendremos una app móvil simple que permitirá interactuar con SQLite para realizar las operaciones básicas que se ve así:

CRUD de SQLite con Android – Mascotas

Todo esto usando SQLite y una clase que extiende de SQLiteOpenHelper.

Vistazo final

Al final tendremos múltiples actividades para interactuar con SQLite. La actividad principal tendrá una lista con RecyclerView y un botón FAB que abre la actividad para agregar.

Cuando se toque un elemento de la lista se mandará a una nueva activity para editar y finalmente en el toque largo se mostrará una alerta para eliminar.

Lo que vamos a gestionar es una base de datos de Mascotas, guardaremos el nombre y la edad. Será simple y a partir de ello podríamos extender los campos.

Proyecto, código fuente y descargas

Esta app es totalmente gratuita y open source, su repositorio lo encuentras aquí. Si quieres descargar la app visita la página releases.

Una demostración del proyecto puede ser vista en YouTube (ya que estamos en esto te invito a suscribirte):

Nota: a través del post iré mostrando fragmentos de código importantes, pero si quieres ver el código completo y actualizado míralo en GitHub.

Ahora sí comencemos.

La estructura de la base de datos

No vamos a hacer relaciones porque no son necesarias, tampoco agregaremos índices. Simplemente crearemos un campo autoincrementable y otros dos campos para el nombre y la edad.

Por lo tanto la creación de la tabla queda así:

CREATE TABLE IF NOT EXISTS mascotas(
  id integer primary key autoincrement, 
  nombre text,
  edad int
);

La base de datos de SQLite igualmente se llamará mascotas. En resumen, tanto la base de datos como la tabla tendrán el mismo nombre.

Ayudante de base de datos

Comenzamos definiendo una clase que extiende de un Helper, mejor dicho, SQLiteOpenHelper. Hay que sobrescribir algunas funciones de la interfaz, las cuales son:

  1. El constructor
  2. onCreate
  3. onUpgrade

Cada cosa tiene su forma. Comenzando con el constructor que es en donde se recibe el contexto y se llama al método padre con la versión de la base de datos.

El método onCreate es llamado cuando la base de datos ha sido creada, es llamado con un objeto de tipo SQLiteDatabase que es la base de datos “física” que ha sido creada.

Por otro lado, el método onUpgrade es cuando actualizamos la estructura o algo de la base de datos pero nuestra app ya está instalada, entonces en este momento comparamos versiones y ajustamos para aquellos usuarios que tengan otra versión. Esto de onUpgrade lo veremos en otra ocasión.

Código fuente del helper

Aquí tenemos el código del ayudante que nos va a permitir interactuar con la base de datos de SQLite:

package me.parzibyte.crudsqlite;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class AyudanteBaseDeDatos extends SQLiteOpenHelper {
    private static final String NOMBRE_BASE_DE_DATOS = "mascotas",
            NOMBRE_TABLA_MASCOTAS = "mascotas";
    private static final int VERSION_BASE_DE_DATOS = 1;

    public AyudanteBaseDeDatos(Context context) {
        super(context, NOMBRE_BASE_DE_DATOS, null, VERSION_BASE_DE_DATOS);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(String.format("CREATE TABLE IF NOT EXISTS %s(id integer primary key autoincrement, nombre text, edad int)", NOMBRE_TABLA_MASCOTAS));
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }


}

Además de sobrescribir los métodos estamos definiendo unas constantes que nos van a ayudar a escribir mejor código.

Una parte importante es dentro del método onCreate, pues ahí creamos la tabla. Si hubiera más tablas las crearíamos ahí con db.execSQL.

Nota sobre las inyecciones SQL

Algunos pensarán que como la base de datos permanece local, no hay problema de que haya inyecciones SQL porque el usuario se haría daño a sí mismo; sin embargo no vamos a confiarnos de ello y vamos a escapar las consultas para asegurar nuestra app.

No vamos a quebrarnos mucho la cabeza, pues los métodos que ofrece la API están enfocados a prevenir inyecciones SQL y a usar algo como sentencias preparadas. Es decir, este tutorial es totalmente seguro en cuanto a las inyecciones SQL.

El controlador de las mascotas

Esto no es MVC ni esas cosas que están de moda; pero he tratado de separar los componentes para que al final sea una programación modular y sencilla.

He puesto un controlador de mascotas, que no hace más que “pegar” la vista con el ayudante de la base de datos; así podemos tener múltiples métodos que gestionen nuestras mascotas, los cuales podrían ser reutilizados.

El controlador se encarga de gestionar todas las operaciones de la base de datos con la siguiente relación:

  • eliminarMascota: se encarga de eliminar una mascota a través de su id, es decir, hace la operación delete
  • nuevaMascota: crea o inserta una mascota dentro de la base de datos
  • guardarCambios: operación update de la base de datos. Recibe la mascota editada, de la cual tomará el id para hacer el where.
  • obtenerMascotas: la operación select. Regresa una lista de la clase Mascota.

Como te habrás fijado, todo esto depende de la clase Mascota, que es la abstracción de una mascota del mundo real. Veámoslo:

package me.parzibyte.crudsqlite.modelos;

public class Mascota {

    private String nombre;
    private int edad;

    private long id; // El ID de la BD

    public Mascota(String nombre, int edad) {
        this.nombre = nombre;
        this.edad = edad;
    }

    // Constructor para cuando instanciamos desde la BD
    public Mascota(String nombre, int edad, long id) {
        this.nombre = nombre;
        this.edad = edad;
        this.id = id;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getNombre() {
        return nombre;
    }

    public void setNombre(String nombre) {
        this.nombre = nombre;
    }

    public int getEdad() {
        return edad;
    }

    public void setEdad(int edad) {
        this.edad = edad;
    }

    @Override
    public String toString() {
        return "Mascota{" +
                "nombre='" + nombre + '\'' +
                ", edad=" + edad +
                '}';
    }
}

Veamos operación por operación.

Insertar en base de datos

El código queda así:

public long nuevaMascota(Mascota mascota) {
    // writable porque vamos a insertar
    SQLiteDatabase baseDeDatos = ayudanteBaseDeDatos.getWritableDatabase();
    ContentValues valoresParaInsertar = new ContentValues();
    valoresParaInsertar.put("nombre", mascota.getNombre());
    valoresParaInsertar.put("edad", mascota.getEdad());
    return baseDeDatos.insert(NOMBRE_TABLA, null, valoresParaInsertar);
}

Comenzamos obteniendo la base de datos a la que se puede escribir, es decir, la writable. Preparamos un objeto de ContentValues que llevará el nombre del campo de la base de datos y el valor que se va a insertar.

Es importante que pongamos el mismo nombre de la columna de la tabla dentro del ContentValues. Como vemos, tomamos los datos del objeto mascota y sus métodos get.

Llamamos al método put por cada valor que vamos a insertar, y después de ello llamamos al método insert de la base de datos con estos argumentos:

  1. El nombre de la base de datos en donde se va a insertar
  2. Un posible string para insertar un valor nulo, más detalles aquí. En este caso no lo usamos y por eso lo ponemos en null.
  3. Valores para insertar, que serán los datos que realmente se insertarán, de tipo ContentValues.

Todo esto regresa un dato de tipo long que regresamos igualmente, el cual representa el id del valor insertado o -1 en caso de que algo haya fallado.

Obtener de la base de datos

Para hacer un select llamamos al método query de la base de datos. Por cierto, ahora solamente necesitamos la base de datos en modo lectura, no escritura.

Al inicio declaramos una lista de mascotas en un ArrayList de tipo Mascota; estamos reutilizando la clase.

public ArrayList<Mascota> obtenerMascotas() {
    ArrayList<Mascota> mascotas = new ArrayList<>();
    // readable porque no vamos a modificar, solamente leer
    SQLiteDatabase baseDeDatos = ayudanteBaseDeDatos.getReadableDatabase();
    // SELECT nombre, edad, id
    String[] columnasAConsultar = {"nombre", "edad", "id"};
    Cursor cursor = baseDeDatos.query(
            NOMBRE_TABLA,//from mascotas
            columnasAConsultar,
            null,
            null,
            null,
            null,
            null
    );

    if (cursor == null) {
        /*
            Salimos aquí porque hubo un error, regresar
            lista vacía
            */        return mascotas;

    }
    // Si no hay datos, igualmente regresamos la lista vacía
    if (!cursor.moveToFirst()) return mascotas;

    // En caso de que sí haya, iteramos y vamos agregando los
    // datos a la lista de mascotas
    do {
        // El 0 es el número de la columna, como seleccionamos
        // nombre, edad,id entonces el nombre es 0, edad 1 e id es 2
        String nombreObtenidoDeBD = cursor.getString(0);
        int edadObtenidaDeBD = cursor.getInt(1);
        long idMascota = cursor.getLong(2);
        Mascota mascotaObtenidaDeBD = new Mascota(nombreObtenidoDeBD, edadObtenidaDeBD, idMascota);
        mascotas.add(mascotaObtenidaDeBD);
    } while (cursor.moveToNext());

    // Fin del ciclo. Cerramos cursor y regresamos la lista de mascotas :)
    cursor.close();
    return mascotas;
}

Este método de query es realmente complejo a primera vista, pero déjame explicarte el significado de los argumentos. Por cierto, si no queremos mandar un argumento lo dejamos en null.

  1. El nombre de la tabla en donde se hará la consulta (o la query como dicen los chicos de hoy)
  2. Una lista de las columnas que se van a consultar. Es decir, lo que hacemos con select columna1, otracolumna, otracolumna lo hacemos aquí pero con un arreglo de tipo String y sin el select.
  3. Un filtro, lo que iría en el where. Por ejemplo, si fuera un where edad = 4 aquí mandaríamos un String únicamente con el valor edad = ?; para hacer la comparación veamos el siguiente argumento.
  4. Argumentos de selección, la complementación del anterior. En este caso es un arreglo de tipo String que sustituye los signos de interrogación del filtro anterior. Si en el anterior ponemos muchos signos (p. ej. edad = ? and nombre = ? entonces aquí podríamos mandar un arreglo de dos valores, la edad y el nombre.
  5. Agrupación. Pasar algo como group by edad pero sin el group by.
  6. Having, si necesitamos algo con having lo ponemos en forma de cadena sin el “having” en sí
  7. Ordenamiento. Pasar algo como order by pero sin el order by en sí.

Como estamos recuperando todas las mascotas, solamente nos importan los 2 primeros argumentos.

Este método de query regresa un cursor que vamos a iterar. Para empezar, si regresa nulo regresamos la lista vacía.

Luego comprobamos si tiene datos con moveToFirst y en caso de que no tenga, también regresamos la lista vacía.

Finalmente, en caso de que sí tenga datos, lo recorremos con un ciclo while que seguirá mientras el cursor se mueva hacia adelante, es decir, que el método moveToNext regrese true.

Dentro del ciclo obtenemos el valor de la base de datos; podemos llamar a cursor.getTipoDeDato por ejemplo cursor.getString o cursor.getInt; le pasamos como argumento el índice de la columna. Aquí importa el orden de las columnas que mandamos en el segundo argumento de query.

Luego creamos un objeto de tipo Mascota y la agregamos con add a nuestra lista

Al final de la función regresamos la lista de mascotas.

Actualizar en base de datos (update)

Este método es un poco más complicado que insertar pero menos difícil que obtener. Utilizamos de nuevo los ContentValues para mandar los nuevos datos, pero para hacer el where necesitamos dos cosas:

  1. El campo o columna que vamos a tomar para el where, por ejemplo, "where id = ?"
  2. El valor que sustituirá a los signos de interrogación

En este caso es solamente un valor pero podrían ser muchos, es por eso que se mandan en un arreglo de tipo String. Queda así:

public int guardarCambios(Mascota mascotaEditada) {
    SQLiteDatabase baseDeDatos = ayudanteBaseDeDatos.getWritableDatabase();
    ContentValues valoresParaActualizar = new ContentValues();
    valoresParaActualizar.put("nombre", mascotaEditada.getNombre());
    valoresParaActualizar.put("edad", mascotaEditada.getEdad());
    // where id...
    String campoParaActualizar = "id = ?";
    // ... = idMascota
    String[] argumentosParaActualizar = {String.valueOf(mascotaEditada.getId())};
    return baseDeDatos.update(NOMBRE_TABLA, valoresParaActualizar, campoParaActualizar, argumentosParaActualizar);
}

Por cierto, regresamos lo mismo que regresa el método update, lo cual es un entero que indica el número de columnas que fueron afectadas al realizar la transacción. Teóricamente en este caso siempre sería 1, porque como actualizamos por id y el id es único, solamente se actualizaría una columna.

Sin embargo, si actualizáramos usando al nombre y hubiera nombres repetidos entonces se podrían actualizar más columnas. Casi lo olvido, si mandamos los 2 últimos argumentos en null, no se hace filtro y se modifica toda la tabla.

Eliminar de la base de datos con delete

Veamos el último método que igualmente recibe un objeto de tipo Mascota y elimina de la base de datos.

public int eliminarMascota(Mascota mascota) {
    SQLiteDatabase baseDeDatos = ayudanteBaseDeDatos.getWritableDatabase();
    String[] argumentos = {String.valueOf(mascota.getId())};
    return baseDeDatos.delete(NOMBRE_TABLA, "id = ?", argumentos);
}

Es muy parecido al de actualizar. Primero recibe el nombre de la tabla, luego el where que se hará para eliminar y finalmente los argumentos que se le pasan al where.

Código completo y final del controlador

Poniendo todo junto, en controlador queda de la siguiente manera:

package me.parzibyte.crudsqlite.controllers;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

import java.util.ArrayList;

import me.parzibyte.crudsqlite.AyudanteBaseDeDatos;
import me.parzibyte.crudsqlite.modelos.Mascota;


public class MascotasController {
    private AyudanteBaseDeDatos ayudanteBaseDeDatos;
    private String NOMBRE_TABLA = "mascotas";

    public MascotasController(Context contexto) {
        ayudanteBaseDeDatos = new AyudanteBaseDeDatos(contexto);
    }


    public int eliminarMascota(Mascota mascota) {

        SQLiteDatabase baseDeDatos = ayudanteBaseDeDatos.getWritableDatabase();
        String[] argumentos = {String.valueOf(mascota.getId())};
        return baseDeDatos.delete(NOMBRE_TABLA, "id = ?", argumentos);
    }

    public long nuevaMascota(Mascota mascota) {
        // writable porque vamos a insertar
        SQLiteDatabase baseDeDatos = ayudanteBaseDeDatos.getWritableDatabase();
        ContentValues valoresParaInsertar = new ContentValues();
        valoresParaInsertar.put("nombre", mascota.getNombre());
        valoresParaInsertar.put("edad", mascota.getEdad());
        return baseDeDatos.insert(NOMBRE_TABLA, null, valoresParaInsertar);
    }

    public int guardarCambios(Mascota mascotaEditada) {
        SQLiteDatabase baseDeDatos = ayudanteBaseDeDatos.getWritableDatabase();
        ContentValues valoresParaActualizar = new ContentValues();
        valoresParaActualizar.put("nombre", mascotaEditada.getNombre());
        valoresParaActualizar.put("edad", mascotaEditada.getEdad());
        // where id...
        String campoParaActualizar = "id = ?";
        // ... = idMascota
        String[] argumentosParaActualizar = {String.valueOf(mascotaEditada.getId())};
        return baseDeDatos.update(NOMBRE_TABLA, valoresParaActualizar, campoParaActualizar, argumentosParaActualizar);
    }

    public ArrayList<Mascota> obtenerMascotas() {
        ArrayList<Mascota> mascotas = new ArrayList<>();
        // readable porque no vamos a modificar, solamente leer
        SQLiteDatabase baseDeDatos = ayudanteBaseDeDatos.getReadableDatabase();
        // SELECT nombre, edad, id
        String[] columnasAConsultar = {"nombre", "edad", "id"};
        Cursor cursor = baseDeDatos.query(
                NOMBRE_TABLA,//from mascotas
                columnasAConsultar,
                null,
                null,
                null,
                null,
                null
        );

        if (cursor == null) {
            /*
                Salimos aquí porque hubo un error, regresar
                lista vacía
             */            return mascotas;

        }
        // Si no hay datos, igualmente regresamos la lista vacía
        if (!cursor.moveToFirst()) return mascotas;

        // En caso de que sí haya, iteramos y vamos agregando los
        // datos a la lista de mascotas
        do {
            // El 0 es el número de la columna, como seleccionamos
            // nombre, edad,id entonces el nombre es 0, edad 1 e id es 2
            String nombreObtenidoDeBD = cursor.getString(0);
            int edadObtenidaDeBD = cursor.getInt(1);
            long idMascota = cursor.getLong(2);
            Mascota mascotaObtenidaDeBD = new Mascota(nombreObtenidoDeBD, edadObtenidaDeBD, idMascota);
            mascotas.add(mascotaObtenidaDeBD);
        } while (cursor.moveToNext());

        // Fin del ciclo. Cerramos cursor y regresamos la lista de mascotas :)
        cursor.close();
        return mascotas;
    }
}

Para usarlo, se crea una instancia del mismo y se le pasa el contexto para que a su vez construya la base de datos con ese contexto.

Consumiendo controlador desde la vista

Ahora veamos cómo es que el controlador de las mascotas interactúa directamente con la vista, es decir, lo que ve el usuario. Comencemos con la actividad principal que tiene una lista (un RecyclerView) cuyos elementos son un layout:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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:background="?android:attr/selectableItemBackground"
    android:clickable="true"
    android:focusable="true"
    android:padding="5dp">

    <TextView
        android:id="@+id/tvNombre"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:text="Aquí el nombre de la mascota"
        android:textColor="@color/colorPrimaryDark"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tvEdad"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:text="Aquí su edad"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tvNombre" />
</android.support.constraint.ConstraintLayout>

Eso será repetido por el RecyclerView dentro de la actividad principal, cuyo diseño queda así:

<?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"
    tools:context=".MainActivity">

    <android.support.constraint.ConstraintLayout 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"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:app="http://schemas.android.com/apk/res-auto"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent">

            <android.support.v7.widget.RecyclerView
                android:id="@+id/recyclerViewMascotas"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
        </ScrollView>

    </android.support.constraint.ConstraintLayout>

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fabAgregarMascota"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:fabSize="normal"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        android:src="@drawable/ic_add_white_24dp" />

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

En esa actividad ponemos un FAB y la lista. Todo va así:

Actividad principal con FAB y RecyclerView

Ahora veamos el código de la actividad principal:

package me.parzibyte.crudsqlite;

import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;

import java.util.ArrayList;
import java.util.List;

import me.parzibyte.crudsqlite.controllers.MascotasController;
import me.parzibyte.crudsqlite.modelos.Mascota;

public class MainActivity extends AppCompatActivity {
    private List<Mascota> listaDeMascotas;
    private RecyclerView recyclerView;
    private AdaptadorMascotas adaptadorMascotas;
    private MascotasController mascotasController;
    private FloatingActionButton fabAgregarMascota;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Ojo: este código es generado automáticamente, pone la vista y ya, pero
        // no tiene nada que ver con el código que vamos a escribir
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        // Lo siguiente sí es nuestro ;)
        // Definir nuestro controlador
        mascotasController = new MascotasController(MainActivity.this);

        // Instanciar vistas
        recyclerView = findViewById(R.id.recyclerViewMascotas);
        fabAgregarMascota = findViewById(R.id.fabAgregarMascota);


        // Por defecto es una lista vacía,
        // se la ponemos al adaptador y configuramos el recyclerView
        listaDeMascotas = new ArrayList<>();
        adaptadorMascotas = new AdaptadorMascotas(listaDeMascotas);
        RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());
        recyclerView.setLayoutManager(mLayoutManager);
        recyclerView.setItemAnimator(new DefaultItemAnimator());
        recyclerView.setAdapter(adaptadorMascotas);

        // Una vez que ya configuramos el RecyclerView le ponemos los datos de la BD
        refrescarListaDeMascotas();

        // Listener de los clicks en la lista, o sea el RecyclerView
        recyclerView.addOnItemTouchListener(new RecyclerTouchListener(getApplicationContext(), recyclerView, new RecyclerTouchListener.ClickListener() {
            @Override // Un toque sencillo
            public void onClick(View view, int position) {
                // Pasar a la actividad EditarMascotaActivity.java
                Mascota mascotaSeleccionada = listaDeMascotas.get(position);
                Intent intent = new Intent(MainActivity.this, EditarMascotaActivity.class);
                intent.putExtra("idMascota", mascotaSeleccionada.getId());
                intent.putExtra("nombreMascota", mascotaSeleccionada.getNombre());
                intent.putExtra("edadMascota", mascotaSeleccionada.getEdad());
                startActivity(intent);
            }

            @Override // Un toque largo
            public void onLongClick(View view, int position) {
                final Mascota mascotaParaEliminar = listaDeMascotas.get(position);
                AlertDialog dialog = new AlertDialog
                        .Builder(MainActivity.this)
                        .setPositiveButton("Sí, eliminar", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                mascotasController.eliminarMascota(mascotaParaEliminar);
                                refrescarListaDeMascotas();
                            }
                        })
                        .setNegativeButton("Cancelar", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                dialog.dismiss();
                            }
                        })
                        .setTitle("Confirmar")
                        .setMessage("¿Eliminar a la mascota " + mascotaParaEliminar.getNombre() + "?")
                        .create();
                dialog.show();

            }
        }));

        // Listener del FAB
        fabAgregarMascota.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // Simplemente cambiamos de actividad
                Intent intent = new Intent(MainActivity.this, AgregarMascotaActivity.class);
                startActivity(intent);
            }
        });

        // Créditos
        fabAgregarMascota.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                new AlertDialog.Builder(MainActivity.this)
                        .setTitle("Acerca de")
                        .setMessage("CRUD de Android con SQLite creado por parzibyte [parzibyte.me]\n\nIcons made by Freepik from www.flaticon.com ")
                        .setNegativeButton("Cerrar", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialogo, int which) {
                                dialogo.dismiss();
                            }
                        })
                        .setPositiveButton("Sitio web", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                Intent intentNavegador = new Intent(Intent.ACTION_VIEW, Uri.parse("https://parzibyte.me"));
                                startActivity(intentNavegador);
                            }
                        })
                        .create()
                        .show();
                return false;
            }
        });

    }

    @Override
    protected void onResume() {
        super.onResume();
        refrescarListaDeMascotas();
    }

    public void refrescarListaDeMascotas() {
        /*
         * ==========
         * Justo aquí obtenemos la lista de la BD
         * y se la ponemos al RecyclerView
         * ============
         *
         * */        if (adaptadorMascotas == null) return;
        listaDeMascotas = mascotasController.obtenerMascotas();
        adaptadorMascotas.setListaDeMascotas(listaDeMascotas);
        adaptadorMascotas.notifyDataSetChanged();
    }
}

Lo importante aquí es el método refrescarListaDeMascotas, el cual llama a nuestro controlador de mascotas y las dibuja. Por cierto, el controlador está siendo instanciado más arriba.

Más abajo se definen escuchadores. Por ejemplo, se escucha cuando se toca una fila (eso está dentro del adaptador que es algo más avanzado, así como el listener de la lista) o cuando se mantiene presionado un elemento.

Cuando se hace click en una fila, se va a la actividad de editar. Cuando se mantiene pulsada, sale una alerta que pide una confirmación.

Finalmente al presionar el FAB (el botón de la esquina) se va a la actividad de crear.

Eliminar mascota

Cuando se mantiene presionado el elemento se ejecuta este código:

@Override // Un toque largo
public void onLongClick(View view, int position) {
    final Mascota mascotaParaEliminar = listaDeMascotas.get(position);
    AlertDialog dialog = new AlertDialog
            .Builder(MainActivity.this)
            .setPositiveButton("Sí, eliminar", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    mascotasController.eliminarMascota(mascotaParaEliminar);
                    refrescarListaDeMascotas();
                }
            })
            .setNegativeButton("Cancelar", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    dialog.dismiss();
                }
            })
            .setTitle("Confirmar")
            .setMessage("¿Eliminar a la mascota " + mascotaParaEliminar.getNombre() + "?")
            .create();
    dialog.show();

}

Para obtener la mascota que fue tocada se usa el método get de la lista de mascotas, se le pasa el índice que obtenemos del listener.

Alerta al eliminar mascota en el LongClick

Creamos un diálogo y cuando confirman la operación, la mascota se elimina desde el controlador.

 

Crear mascota

Cuando se toca el botón de la esquina, se dirige a una nueva actividad que tiene este diseño:

Actividad para insertar nueva mascota

El código XML es este:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
    tools:context=".AgregarMascotaActivity">
    <EditText
        android:id="@+id/etNombre"
        android:layout_width="278dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:ems="10"
        android:hint="Nombre de la mascota"
        android:inputType="textPersonName"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/etEdad"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="12dp"
        android:ems="10"
        android:hint="Edad"
        android:inputType="number"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/etNombre" />

    <Button
        android:id="@+id/btnAgregarMascota"
        style="@style/Widget.AppCompat.Button.Colored"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:text="Guardar"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/etEdad" />

    <Button
        android:id="@+id/btnCancelarNuevaMascota"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:text="Cancelar"
        style="@style/Widget.AppCompat.Button.Colored"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/btnAgregarMascota"
        app:layout_constraintTop_toBottomOf="@+id/etEdad" />

</android.support.constraint.ConstraintLayout>

Y el de Java este:

package me.parzibyte.crudsqlite;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import me.parzibyte.crudsqlite.controllers.MascotasController;
import me.parzibyte.crudsqlite.modelos.Mascota;

public class AgregarMascotaActivity extends AppCompatActivity {
    private Button btnAgregarMascota, btnCancelarNuevaMascota;
    private EditText etNombre, etEdad;
    private MascotasController mascotasController;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_agregar_mascota);

        // Instanciar vistas
        etNombre = findViewById(R.id.etNombre);
        etEdad = findViewById(R.id.etEdad);
        btnAgregarMascota = findViewById(R.id.btnAgregarMascota);
        btnCancelarNuevaMascota = findViewById(R.id.btnCancelarNuevaMascota);
        // Crear el controlador
        mascotasController = new MascotasController(AgregarMascotaActivity.this);

        // Agregar listener del botón de guardar
        btnAgregarMascota.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // Resetear errores a ambos
                etNombre.setError(null);
                etEdad.setError(null);
                String nombre = etNombre.getText().toString(),
                        edadComoCadena = etEdad.getText().toString();
                if ("".equals(nombre)) {
                    etNombre.setError("Escribe el nombre de la mascota");
                    etNombre.requestFocus();
                    return;
                }
                if ("".equals(edadComoCadena)) {
                    etEdad.setError("Escribe la edad de la mascota");
                    etEdad.requestFocus();
                    return;
                }

                // Ver si es un entero
                int edad;
                try {
                    edad = Integer.parseInt(etEdad.getText().toString());
                } catch (NumberFormatException e) {
                    etEdad.setError("Escribe un número");
                    etEdad.requestFocus();
                    return;
                }
                // Ya pasó la validación
                Mascota nuevaMascota = new Mascota(nombre, edad);
                long id = mascotasController.nuevaMascota(nuevaMascota);
                if (id == -1) {
                    // De alguna manera ocurrió un error
                    Toast.makeText(AgregarMascotaActivity.this, "Error al guardar. Intenta de nuevo", Toast.LENGTH_SHORT).show();
                } else {
                    // Terminar
                    finish();
                }
            }
        });

        // El de cancelar simplemente cierra la actividad
        btnCancelarNuevaMascota.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });
    }
}

Como vemos, se hace una validación simple y en caso de que se pase entonces se crea una nueva mascota para ser pasada como argumento al método del controlador que se encarga de hacer el insert.

En caso de que se inserte correctamente, se cierra la actividad. Y si no, se indica.

Editar mascota

Cuando se toca una mascota se va a una actividad que es idéntica al de insertar, pero que actualiza en lugar de insertar; es decir, llama a otro método del controlador.

Por cierto, como se va a editar debemos pasarle a la actividad un dato, el cual es la mascota editada. Por ello es que cuando se toca se hace esto:

@Override // Un toque sencillo
public void onClick(View view, int position) {
    // Pasar a la actividad EditarMascotaActivity.java
    Mascota mascotaSeleccionada = listaDeMascotas.get(position);
    Intent intent = new Intent(MainActivity.this, EditarMascotaActivity.class);
    intent.putExtra("idMascota", mascotaSeleccionada.getId());
    intent.putExtra("nombreMascota", mascotaSeleccionada.getNombre());
    intent.putExtra("edadMascota", mascotaSeleccionada.getEdad());
    startActivity(intent);
}

Del otro lado, en la actividad que edita, se tiene este código:

package me.parzibyte.crudsqlite;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import me.parzibyte.crudsqlite.controllers.MascotasController;
import me.parzibyte.crudsqlite.modelos.Mascota;

public class EditarMascotaActivity extends AppCompatActivity {
    private EditText etEditarNombre, etEditarEdad;
    private Button btnGuardarCambios, btnCancelarEdicion;
    private Mascota mascota;//La mascota que vamos a estar editando
    private MascotasController mascotasController;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_editar_mascota);

        // Recuperar datos que enviaron
        Bundle extras = getIntent().getExtras();
        // Si no hay datos (cosa rara) salimos
        if (extras == null) {
            finish();
            return;
        }
        // Instanciar el controlador de las mascotas
        mascotasController = new MascotasController(EditarMascotaActivity.this);

        // Rearmar la mascota
        // Nota: igualmente solamente podríamos mandar el id y recuperar la mascota de la BD
        long idMascota = extras.getLong("idMascota");
        String nombreMascota = extras.getString("nombreMascota");
        int edadMascota = extras.getInt("edadMascota");
        mascota = new Mascota(nombreMascota, edadMascota, idMascota);


        // Ahora declaramos las vistas
        etEditarEdad = findViewById(R.id.etEditarEdad);
        etEditarNombre = findViewById(R.id.etEditarNombre);
        btnCancelarEdicion = findViewById(R.id.btnCancelarEdicionMascota);
        btnGuardarCambios = findViewById(R.id.btnGuardarCambiosMascota);


        // Rellenar los EditText con los datos de la mascota
        etEditarEdad.setText(String.valueOf(mascota.getEdad()));
        etEditarNombre.setText(mascota.getNombre());

        // Listener del click del botón para salir, simplemente cierra la actividad
        btnCancelarEdicion.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });

        // Listener del click del botón que guarda cambios
        btnGuardarCambios.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // Remover previos errores si existen
                etEditarNombre.setError(null);
                etEditarEdad.setError(null);
                // Crear la mascota con los nuevos cambios pero ponerle
                // el id de la anterior
                String nuevoNombre = etEditarNombre.getText().toString();
                String posibleNuevaEdad = etEditarEdad.getText().toString();
                if (nuevoNombre.isEmpty()) {
                    etEditarNombre.setError("Escribe el nombre");
                    etEditarNombre.requestFocus();
                    return;
                }
                if (posibleNuevaEdad.isEmpty()) {
                    etEditarEdad.setError("Escribe la edad");
                    etEditarEdad.requestFocus();
                    return;
                }
                // Si no es entero, igualmente marcar error
                int nuevaEdad;
                try {
                    nuevaEdad = Integer.parseInt(posibleNuevaEdad);
                } catch (NumberFormatException e) {
                    etEditarEdad.setError("Escribe un número");
                    etEditarEdad.requestFocus();
                    return;
                }
                // Si llegamos hasta aquí es porque los datos ya están validados
                Mascota mascotaConNuevosCambios = new Mascota(nuevoNombre, nuevaEdad, mascota.getId());
                int filasModificadas = mascotasController.guardarCambios(mascotaConNuevosCambios);
                if (filasModificadas != 1) {
                    // De alguna forma ocurrió un error porque se debió modificar únicamente una fila
                    Toast.makeText(EditarMascotaActivity.this, "Error guardando cambios. Intente de nuevo.", Toast.LENGTH_SHORT).show();
                } else {
                    // Si las cosas van bien, volvemos a la principal
                    // cerrando esta actividad
                    finish();
                }
            }
        });
    }
}

Si se actualiza correctamente, la actividad se cierra. Si no, se indica.

Escuchar el método onResume

Cuando se cierra una actividad se vuelve a la principal, ahí escuchamos el método onResume y refrescamos la lista de mascotas.

De este modo, cuando se “regresa” de editar o crear una mascota, se vuelven a consultar. Como esto es después de haber realizado la operación, se obtiene una lista “fresca” de las mascotas.

Conclusión

Así es como terminamos este largo tutorial. ¿Crees que es difícil de leer? imagina qué tan complicado fue escribir el artículo y el código.

Recuerda, puedes comentar tus dudas y ver el código completo, así como descargar la app. No olvides seguirme en mis redes sociales.

Te invito a ver más sobre Android, Java o Bases de datos.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

Ver comentarios

  • Buenas noches estoy guiándome con tu tutorial pero en la mía guardo productos con su nombre, precio y stock, tienes alguna idea de como hacer que al registrar un producto en el edittext stock aparezca por defecto el número 0 (No me refiero a la propiedad Text del EditText), después se guarde ese registro en la BD y luego al dar click en el recyclerview lo pueda editar para cambiar el 0 por otro numero? Soy novata en esto.

  • Hola tengo una consulta, hice algunos cambios y al seleccionar un ítem de la lista muestro el activity solamente para ver los datos porque a los edit text les puse input type en null, luego con un botón para editar me mostraría otro activity pero con los edit text rellenados y listos para editar, pero al oprimir el botón que me manda a el 3er. Activity no sucede nada, talvez al ser el 3er. Activity no puede traer de manera correcta los datos seleccionados del primero, es lo único que se me ocurre, que puedo estar haciendo mal?

  • hola tengo un problema ala hora de que me muestre los datos en los respectivos tvNombre , tvEdad, yo cree 5 TextView más, me edita los datos, y elimina y ademas los lista , pero soo muestra el nombre y la edad , nose en que estoy fallando , pero debo de tenerlo para mañana ,algún dato de como mostrar esos tv que añadi ?

  • Que tal muy buen día colega, me ha servido bastante tu post sobre la app solo tengo una duda, si quisiera agregar un elemento SerchView en la app, para que este solo me muestre los resultados encontrados en la lista como se lo integrarías?

    Saludos!

Entradas recientes

Creador de credenciales web – Aplicación gratuita

Hoy te voy a presentar un creador de credenciales que acabo de programar y que…

1 semana hace

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

2 semanas hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

2 semanas hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

2 semanas hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

2 semanas hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

2 semanas hace

Esta web usa cookies.