Índice

En el anterior artículo dimos un repaso a fondo del típico Hola Mundo, hoy vamos a ver cómo trabajar con persistencia de datos en Android.



La mayoría de apps necesitan guardar datos, ya sea para no perder la información cuando se ejecuta el método onPause(), información de preferencias o cantidades mayores de información en bases de datos. Los distintos métodos de almacenamiento disponibles son:

Pantalla principal

Empezaremos describiendo la pantalla principal (DataPersistencyMainActivity), su función es la siguiente:

Si se tiene una colección de datos relativamente pequeña en forma de pares clave-valor, se puede usar la API de SharedPreferences. Un objeto de éste tipo apunta a un archivo conteniendo pares de datos. Pueden ser privados o públicos.

Ésta API no debe confundirse con la API de Preference, que ayuda a construir una interfaz gráfica para las preferencias de nuestra aplicación (Por debajo ésta usa SharedPreferences

Para mayores cantidades de datos se usarán bases de datos en SQLite. (El código se va explicando en los comentarios).

En el código siguiente están tanto la parte que trata con el SharedPreferences como el que accede a la base de datos. Para la base de datos es necesario crear un AsyncTask (El el código de abajo se nombra a esta clase insertPerson) que se encarga de realizar las consultas en un hilo separado al principal, para no bloquear la interfaz gráfica. Todo está comentado en el código.

public class DataPersistencyMainActivity extends Activity {

    public static final String EDIT_TEXT_SHARED_PREFERENCES = "ejShared";
    private SharedPreferences mSharedPrefs;
    private PersonDbBHelper mDbHelper;
    private TextView mDbRecordsText;

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

        mSharedPrefs = getPreferences(Context.MODE_PRIVATE);

        // Inicializar elementos gráficos...
        // ...

        // Comprobamos si hay datos guardados.
        String value = mSharedPrefs.getString(EDIT_TEXT_SHARED_PREFERENCES, "");
        if (value != "") {
            editTextSharedPrefs.setText(value);
        }

        mDbHelper = new PersonDbBHelper(this);
        // Mostrar los datos en la base de datos si existen.
        new getPersons().execute();

        editTextSharedPrefs.setOnEditorActionListener(new OnEditorActionListener() {

            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (actionId == R.id.save_shared || actionId == EditorInfo.IME_NULL) {

                    /*
                     * Con getSharedPreferences() es posible crear varios archivos de preferencias
                     * identificados por nombre.
                     * Con getPreferences(), usado desde un Activity únicamente se crea un archivo
                     * con el nombre de la actividad.
                     */

//                    Context context = getActivity();
//                    SharedPreferences sharedPref = context.getSharedPreferences(
//                            getString(R.string.preference_file_key), Context.MODE_PRIVATE);

                    mSharedPrefs
                            .edit()
                            .putString(EDIT_TEXT_SHARED_PREFERENCES, v.getText().toString())
                            .apply();

                    Toast.makeText(getApplicationContext(), "Guardando " + v.getText(),
                            Toast.LENGTH_LONG).show();

                    hideKB();
                    return true;
                }
                return false;
            }
        });

        saveToDbButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                new insertPerson().execute(firstNameED.getText().toString(),
                        secondNameED.getText().toString());
            }
        });

        showDbRecordsButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                mDbRecordsText.setText("ID \t FName");
                new getPersons().execute();
            }
        });

        deleteRowButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                new deleteRow().execute(Integer.parseInt(deleteRowED.getText().toString()));
            }
        });
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {

        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();
        if (id == R.id.action_settings) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    private void hideKB() {
        InputMethodManager inputManager = (InputMethodManager)
                getSystemService(Context.INPUT_METHOD_SERVICE);
        inputManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(),
                InputMethodManager.HIDE_NOT_ALWAYS);
    }

    /**
     * Siempre que se realizen operaciones que conlleven carga a la CPU o puedan
     * tardar bastante tiempo, hay que lanzarlas en hilos separdos del principal,
     * encargado de la interfaz gráfica.
     */
    private class insertPerson extends AsyncTask<String, Void, Long> {

        @Override
        protected Long doInBackground(String... what) {

            SQLiteDatabase db = mDbHelper.getWritableDatabase();
            // Mapa de valores, cuyas claves serán los nombres de las columnas
            ContentValues values = new ContentValues();
            values.put(PersonEntry.COLUMN_NAME_FIRST_NAME, what[0]);
            values.put(PersonEntry.COLUMN_NAME_SECOND_NAME, what[1]);

            long newRowId = db.insert(PersonEntry.TABLE_NAME, null, values);
            return newRowId;

        }

        @Override
        protected void onPostExecute(Long result) {
            Toast.makeText(getApplicationContext(), "Insertada fila con ID: " + result,
                    Toast.LENGTH_SHORT).show();
        }
    }

    private class getPersons extends AsyncTask<Void, Void, String[]> {

        @Override
        protected String[] doInBackground(Void... params) {
            SQLiteDatabase db = mDbHelper.getReadableDatabase();

            // Definir la proyección de los datos que queremos, en este caso solo ID y primer nombre
            String[] projection = {
                    PersonEntry._ID,
                    PersonEntry.COLUMN_NAME_FIRST_NAME
            };

            // Definir el orden en que devolver los datos
            String sortOrder = PersonEntry.COLUMN_NAME_FIRST_NAME + " DESC";

            Cursor c = db.query(
                    PersonEntry.TABLE_NAME, // Nombre de la tabla
                    projection,             // Columnas a devolver
                    null,                   // Columnas para la cláusula WHERE
                    null,                   // Valores para la cláusula WHERE
                    null,                   // GROUP BY
                    null,                   // HAVING
                    sortOrder);             // ORDER BY

            // Recorrer la información devuelta
            String[] data = new String[c.getCount()];
            int i = 0;
            //c.moveToFirst();
            while (c.moveToNext()) {
                data[i++] =
                        c.getString(c.getColumnIndex(PersonEntry._ID)) + " \t " +
                                c.getString(c.getColumnIndex(PersonEntry.COLUMN_NAME_FIRST_NAME));
            }

            c.close();

            return data;
        }

        @Override
        protected void onPostExecute(String[] result) {
            for (String row : result) {
                mDbRecordsText.append(" \n " + row);
            }
        }
    }

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

        @Override
        protected Integer doInBackground(Integer... params) {
            SQLiteDatabase db = mDbHelper.getWritableDatabase();

            String selection;
            String[] selectionArgs = null;

            // Definición de la parte WHERE
            if (params[0] == -1) {
                selection = "1"; // Borra todas las filas
            } else {
                selection = PersonEntry._ID + " LIKE ?";
                selectionArgs = new String[]{String.valueOf(params[0])};
            }

            // Ejecutar la consulta SQL
            int rows_deleted = db.delete(PersonEntry.TABLE_NAME, selection, selectionArgs);

            return rows_deleted;
        }

        @Override
        protected void onPostExecute(Integer result) {
            Toast.makeText(getApplicationContext(), "Elminadas " + result + " filas",
                    Toast.LENGTH_SHORT).show();
        }
    }

}

Abstracción de la base de datos

Para facilitar la reutilización de código y que el código sea escalable, se crea una abstacción de las tablas en la base de datos, en este caso para la tabla persona.

Clase PersonContract

La finalidad de esta clase es servir abstracción a la base de datos. Aquí se definirá el esquema de representación de la base de datos, bien documentado. En la clase externa deberá ir todo campo global a la base de datos Échale un vistazo a un ejemplo real de Google

public final class PersonContract {

    public PersonContract() {
    }

    /**
     * Clase interna que define el contenido de la tabla.
     *
     * Implementando BaseColumns, se añadirán dos entradas más a la tabla,
     * _ID y _COUNT.
     */
    public static abstract class PersonEntry implements BaseColumns{
        public static final String TABLE_NAME = "person";
        public static final String COLUMN_NAME_ENTRY_ID = "personID";
        public static final String COLUMN_NAME_FIRST_NAME = "firstname";
        public static final String COLUMN_NAME_SECOND_NAME = "secondname";
    }
}

Clase PersonDbBHelper

Esta clase hereda de SQLiteOpenHelper, que proporciona un conjunto de APIs útiles para el manejo de la base de datos. Como vemos también es una abstracción de la base de datos, lo cual permite tener el código mucho más ordenado. Si alguna vez necesitamos cambiar la estructura de la base de datos, solo habrá que tocar esta clase, y sus respectivas Contract.

public class PersonDbBHelper extends SQLiteOpenHelper {

    // De cambiar el esquema de la base de datos, hay que incrementar la versión
    public static final int DATABASE_VERSION = 1;
    public static final String DATABASE_NAME = "Persons.db";

    private static final String TEXT_TYPE = " TEXT";
    private static final String COMMA_SEP = ",";

    private static final String SQL_CREATE_ENTRIES =
            "CREATE TABLE " + PersonContract.PersonEntry.TABLE_NAME + " (" +
                    PersonContract.PersonEntry._ID + " INTEGER PRIMARY KEY," + // Heredado de BaseColumns
                    PersonContract.PersonEntry.COLUMN_NAME_ENTRY_ID + TEXT_TYPE + COMMA_SEP +
                    PersonContract.PersonEntry.COLUMN_NAME_FIRST_NAME + TEXT_TYPE + COMMA_SEP +
                    PersonContract.PersonEntry.COLUMN_NAME_SECOND_NAME + TEXT_TYPE  +
                    " )";

    private static final String SQL_DELETE_ENTRIES =
            "DROP TABLE IF EXISTS " + PersonContract.PersonEntry.TABLE_NAME;


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

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

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        /*
         * De cambiar el esquema en la base de datos, aquí habría que hacer
         * las modificaciones necesarias para actualizar de un esquema a otro,
         * para este ejemplo, simplemente la borraremos y la volveremos a crear
         */
        db.execSQL(SQL_DELETE_ENTRIES);
        onCreate(db);
    }

    @Override
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // Proceso inverso a actualizar
        onUpgrade(db, oldVersion, newVersion);
    }
}