3. Almacenamiento

Existen varias formas de almacenar información en la aplicación: preferencias compartidas, almacenamiento interno y externo, caché y bases de datos.

3.1. Preferencias compartidas (SharedPreferences)

Cuando se quiere guardar una pequeña colección de pares de clave-valor, pueden usarse las SharedPreferences. Para entender su funcionamiento, vamos a poner un ejemplo en el que añadimos un valor de clase String identificado con una clave (que siempre será String) en un "archivo" de nombre "grupo1":

SharedPreferences sharedPreferences = getSharedPreferences("grupo1", 0);
/* 0 es lo mismo que Context.MODE_PRIVATE que quiere decir que la información sólo es accesible para la app */
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("clave", "valor");
editor.commit();

En el caso de que el valor sea un número entero, usaremos el método putInt() en vez de putString().

  • Para ver la información guardada, tenemos varios métodos. Si queremos recoger el valor de una clave concreta:
SharedPreferences sharedPreferences = getSharedPreferences("grupo1", 0);
String valor = sharedPreferences.getString("clave", "valor si no encuentra");
  • Para recoger todas las claves y valores guardados en un "archivo" (el ejemplo que crea un TextView por cada par y los añade a un "layout" lineal):
public void viewShPref(LinearLayout layout, String group) {
    sharedPreferences = getSharedPreferences(group, 0);
    Map<String, ?> conjunto = sharedPreferences.getAll();
    for(Map.Entry<String, ?> entry : conjunto.entrySet()){
        textView = new TextView(this);
        textView.setText(entry.getKey() + ":" + entry.getValue());
        textView.setTextSize(16);
        textView.setGravity(Gravity.LEFT);
        textView.setPadding(2,0,0,10);
        textView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
        layout.addView(textView);
    }
}
  • Para borrar todos los pares de un archivo:
SharedPreferences sharedPreferences = getSharedPreferences("grupo1", 0);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.clear();
editor.commit();

3.2. Almacenamiento interno

Para cantidades más grandes de información que sólo son accesibles para la app, usamos el almacenamiento interno. Hay que recordar que siempre que se trabaja con archivos que pueden o no existir, hay que englobar el código en un bloque try-catch que recoja el posible error.

  • Para añadir información:
public void addIntStorage(String filename, String key, String value) {
    String sep = ":";
    try{
        FileOutputStream fos = openFileOutput(filename, Context.MODE_PRIVATE);
        fos.write(key.getBytes());
        fos.write(sep.getBytes());
        fos.write(value.getBytes());
        fos.close();
    }catch(IOException e){
        Toast.makeText(getApplicationContext() , e.toString(), Toast.LENGTH_SHORT).show();
    }
}
  • Para ver información:
public void viewIntSto(LinearLayout layout, String filename) {
    try{
        FileInputStream fis = openFileInput(filename);
        InputStreamReader isr = new InputStreamReader(fis);
        BufferedReader br = new BufferedReader(isr);
        String line;
        while((line = br.readLine()) != null){
            textView = new TextView(this);
            textView.setText(line);
            textView.setGravity(Gravity.LEFT);
            textView.setPadding(2,0,0,10);
            textView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
            layout.addView(textView);
        }
        br.close();
    }catch(IOException e){
        textView = new TextView(this);
        textView.setText(e.toString());
        layout.addView(textView);
    }
}

3.3. Caché

Si queremos usar la caché para guardar información, debemos recordar que hay que controlar el tamaño de ésta aunque Android pueda borrar el caché automáticamente cuando falta espacio. Por la misma razón, debemos asegurarnos de que el archivo de caché existe antes de buscar en él.

  • Para añadir información:
public void addCache(String filename, String key, String value) {
    String sep = ":";
    try{
        FileOutputStream fos = new FileOutputStream(new File(getCacheDir(), filename));
        OutputStreamWriter osr = new OutputStreamWriter(fos);
        BufferedWriter bw = new BufferedWriter(osr);
        bw.write(key + sep + value);
        bw.close();
    }catch(IOException e){
        Toast.makeText(getApplicationContext() , e.toString(), Toast.LENGTH_SHORT).show();
    }
}
  • Para ver información:
public void viewCache(LinearLayout layout, String filename) {
    try{
        FileInputStream fis = new FileInputStream(new File(getCacheDir(), filename));
        InputStreamReader isr = new InputStreamReader(fis);
        BufferedReader br = new BufferedReader(isr);
        String line;
        while((line = br.readLine()) != null){
            textView = new TextView(this);
            textView.setText(line);
            textView.setGravity(Gravity.LEFT);
            textView.setPadding(2,0,0,10);
            textView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
            layout.addView(textView);
        }
        br.close();
    }catch(IOException e){
        textView = new TextView(this);
        textView.setText(e.toString());
        layout.addView(textView);
    }
}

3.4. Almacenamiento externo

Para usar el almacenamiento externo (accesible fuera de la app) primero hay que pedir permiso al usuario con la siguiente etiqueta dentro del manifiesto:

 <manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
</manifest> 

También hay que asegurarse de que el almacenamiento es accesible (por ejemplo si el dispositivo está conectado a un ordenador, el almacenamiento externo es inaccesible). La siguiente función lo comprueba, devolviendo "0" si se puede leer y escribir, "1" si sólo se puede leer o "2" si no es accesible:

 public static int checkExtSto() {
    String estado = Environment.getExternalStorageState();
    if(Environment.MEDIA_MOUNTED.equals(estado)){
        return 0;
    }else if(Environment.MEDIA_MOUNTED_READ_ONLY.equals(estado)){
        return 1;
    }else{
        return 2;
    }
}
  • Para añadir información
    • En este ejemplo se busca o crea un archivo en la carpeta de descargas del dispositivo, que se encuentra en la variable Enviroment.DIRECTORY_DOWNLOADS. Usa el método context.getExternalFilesDir() (definiendo context como un objeto Context con valor this) para carpetas privadas de la app pero accesibles por el usuario (ruta Android/data/<<paquete>>). El argumento que toma es igual al del método del ejemplo y además se puede personalizar el nombre de la carpeta o que el archivo se añada en la carpeta raíz de la app (con el parámetro "null"). Por otro lado, en este caso la función que comprueba el almacenamiento está en la clase ViewActivity.
public void addExtStorage(String filename, String key, String value) {
    String sep = ":";
    if(ViewActivity.checkExtSto() == 0) {
        try{
            FileOutputStream fos = new FileOutputStream(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), filename));
            OutputStreamWriter osr = new OutputStreamWriter(fos);
            BufferedWriter bw = new BufferedWriter(osr);
            bw.write(key + sep + value);
            bw.close();
        }catch(IOException e){
            Toast.makeText(getApplicationContext() , e.toString(), Toast.LENGTH_SHORT).show();
        }
    }else{
        Toast.makeText(this, "Error con el almacenamiento", Toast.LENGTH_SHORT).show();
    }
}
  • Para leer información:
public void viewExtSto(LinearLayout layout, String filename) {
    if(checkExtSto() == 0 || checkExtSto() == 1) {
        try{
            FileInputStream fis = new FileInputStream(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), filename));
            InputStreamReader isr = new InputStreamReader(fis);
            BufferedReader br = new BufferedReader(isr);
            String line;
            while((line = br.readLine()) != null) {
                textView = new TextView(this);
                textView.setText(line);
                textView.setGravity(Gravity.LEFT);
                textView.setPadding(2,0,0,10);
                textView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
                layout.addView(textView);
            }
            br.close();
        }catch (IOException e) {
            textView = new TextView(this);
            textView.setText(e.toString());
            layout.addView(textView);
        }
    }
    else{
        Toast.makeText(this, "Error con el almacenamiento", Toast.LENGTH_SHORT).show();
    }
}

3.5. Bases de datos

Para el uso de bases de datos locales, Android proporciona Room, que se puede definir como una capa de abstracción sobre SQLite. Para usar Room se necesita que el proyecto esté basado en AndroidX (es una mejora de la librería de soporte tradicional de Android. Android Studio permite migrar el proyecto con la opción Refactor > Migrate to AndroidX).

Guia paso a paso (inglés)