jueves, 8 de mayo de 2008

Android 101: los recursos, ese gran desconocido (I)

Bien, hoy vamos a hablar de recursos, sobre cómo incluirlos en nuestro código de manera que queden correctamente agrupados en la aplicación.

Tenemos tres alternativas para hacerlo:

  1. Meter lo que queramos en la carpeta assets(1)
  2. Meter los fichero de manera lógica en la carpeta res.
  3. Si los archivos son creados partiendo de la ejecución de la aplicación crearlos en un directorio asignado para la aplicación específica bajo la ruta /data/app/<paquete>/files (en el caso del post anterior la ruta sería /data/app/mi.paquete.test1/files).
Pero para ambas, lo que nos puede interesar, más que la manera de guardarlas es cómo acceder a ellas y, para eso (acabo de caer en ello y esta es la razón por la que ahora mismo pongo un (I) en el post, para no hacerlo eterno) hace falta que entendais qué es el contexto de la aplicación, es decir Context.

Parece una tontería que lo explique pero el Contexto encapsula todo el entorno de la aplicación para que, en teoría, sólo sea accesible desde dentro de ella.

A primera vista parece muy simple, pero esta clase abstracta (realmente de las instancias de su clase hija, ApplicationContext) es la que nos va a permitir acceder a la información de la aplicación y a sus recursos.

Considerando que existen realmente 5 bloques en toda aplicación android (Application, Activity, IntentReceiver, Service y Provider(2)) sería lógico que fuera accesible desde cualquiera de ellos, pero esto no es así en la realidad. Sólo es accesible directamente desde Application, Activity y Service, teniendo que recurrir a sucios trucos para poder utilizarla desde IntentReceiver por ejemplo.

Como curiosidad diré que si por ejemplo gestionais el evento de por ejemplo un botón dentro de una Actividad, dicho contexto no será accesible. hay dos alternativas:
  • Crear una variable accesible desde cualquier punto en la construcción de la instancia de la actividad.
  • Utilizar la variable de clase que incluyen los bloques constructores: <NombreActividad>.this. Suena como que a cualquier purista se le revolverían las tripas con esto, pero yo personalmente me quedo con ésta última.
Bueno, volvamos al código de la actividad, que así se verá todo mejor, en este apartado (el primero de dos) vamos a hablar de los facilones, de assets y de los archivos creado dinámicamente:
  • Si hablamos de assets podemos decir realmente que su función es poder meter lo que nos dé la real gana en la aplicación.
    • Accedemos a su gestor mediante context.getAssets(), que devuelve la instancia de la clase AssetManager que se corresponde con la aplicación.
    • Para listar el contenido de la carpeta context.getAssets(). list( "/" );(3)
    • Para extraer su contenido context.getAssets. open("archivoPrueba.txt"); [devuelve un InputStream]

  • Si estamos hablando de escritura y lectura sobre archivos, la única manera de hacerlo es mediante el espacio incluido como contexto de la aplicación mediante:
    • Context.openFileInput("archivoPrueba2.txt");
    • Context.openfileOutput("archivoPrueba3.txt",modo);(4)
Brevemente vamos a hacer una prueba con ambos.
Sobre la aplicación especificada en el post anterior ( es decir, sobre una aplicación vacía) vamos a acceder a los archivos siguiendo los pasos a continuación:
  1. Utilizaremos droiddraw para crear un layout con un campo de texto y tres botones (por ahora da igual como se disponga)
  2. Modificamos sus propiedades (pestaña properties):(5)
    1. id = @+id/btn1, text= abrir asset (extraera el contenido del asset y lo cargara en el campo de texto)
    2. id= @+id/btn2, text= guardar (guardara el campo de texto en un archivo en el contexto de la aplicacion).
    3. id=@+id/btn3, text = abrir (abrira el archivo del contexto si existe y los cargara).
    4. id=@+id/texto, text = vacio.
  3. Aplicamos los cambios, extraemos el código (generate) y lo reemplazamos por el anterior de res/layout/main.xml (que es el layout que se carga en la actividad)

    <?xml version="1.0" encoding="utf-8"?>
    <AbsoluteLayout
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    xmlns:android="http://schemas.android.com/apk/res/android"
    >
    <Button
    android:id="@+id/btn1"
    android:layout_width="83px"
    android:layout_height="wrap_content"
    android:text="Abrir asset"
    android:layout_x="10px"
    android:layout_y="352px"
    >
    </Button>
    <Button
    android:id="@+id/btn2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="guardar "
    android:layout_x="100px"
    android:layout_y="352px"
    >
    </Button>
    <Button
    android:id="@+id/btn3"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Abrir"
    android:layout_x="180px"
    android:layout_y="352px"
    >
    </Button>
    <EditText
    android:id="@+id/texto"
    android:layout_width="197px"
    android:layout_height="217px"
    android:text=""
    android:layout_x="20px"
    android:layout_y="22px"
    >
    </EditText>
    </AbsoluteLayout>

  4. Introducimos el siguiente código en la actividad (Actividad.java):
    package mi.paquete.test1;

    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import android.util.Log;
    import android.app.Activity;
    import android.os.Bundle;
    import android.view.View;
    import android.view.View.OnClickListener;
    import android.widget.Button;
    import android.widget.EditText;

    public class Actividad extends Activity {
    private static final String TAG = "Actividad";
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle icicle) {
    super.onCreate(icicle);
    setContentView(R.layout.main);
    //Accedemos al los objetos creados a través de su identificador
    final EditText texto = (EditText) this.findViewById(R.id.texto);
    Button btn1 = (Button) this.findViewById(R.id.btn1);
    Button btn2 = (Button) this.findViewById(R.id.btn2);
    Button btn3 = (Button) this.findViewById(R.id.btn3);
    btn1.setOnClickListener(new OnClickListener(){
    //Accion a realizar en el click del boton 1
    public void onClick(View vista) {
    InputStream is;
    try {
    is = Actividad.this.getAssets().open("archivoprueba.txt");
    int tam = is.available();
    byte [] buffer = new byte[tam];
    is.read(buffer);
    texto.setText(new String(buffer));
    } catch (IOException e) {
    Log.d(TAG,"Error en la lectura",e);
    }
    }
    });
    btn2.setOnClickListener(new OnClickListener(){
    //Accion a realizar en el click del boton 2
    public void onClick(View vista) {
    try {
    FileOutputStream fos =Actividad.this.openFileOutput("archivoprueba2.txt", MODE_WORLD_READABLEMODE_WORLD_WRITEABLE);
    fos.write(texto.getText().toString().getBytes());
    fos.close();
    } catch (IOException e) {
    Log.d(TAG,"Error en la escritura",e);
    }
    }
    });
    btn3.setOnClickListener(new OnClickListener(){
    //Accion a realizar en el click del boton 3
    public void onClick(View vista) {
    try {
    FileInputStream fis = Actividad.this.openFileInput("archivoprueba2.txt");
    int tam = fis.available();
    byte [] buffer = new byte[tam];
    fis.read(buffer);
    texto.setText(new String(buffer));
    fis.close();
    } catch (IOException e) {
    Log.d(TAG,"Error en la lectura",e);
    }

    }
    });
    }
    }

  5. Introducimos el archivo archivoprueba.txt en la carpeta assets con el contenido que queramos:
  6. Ejecutamos et voilá


PD: no sé si alguien me leerá, pero si me estoy yendo por las ramas, o si hay algo que no se entiende, por favor, postead, sólo intento escribir lo que sé sobre esto y que todos aprendamos (iaiaoooo...).



Notas:
(1) : de acuerdo con lo definido por la documentacion de la sdk, se considera un asset como un conjunto de datos asociados con una aplicacion (como blob), que serán organizados en la jerarquia de directorios tal y como están en el apk final, agrupados como un único fichero zip.
(2): aunque parece que me he saltado un paso al no explicar los bloques aún, es porque cada uno tiene una funcionalidad muy específica que requiere haberse adentrado un poquito más en la información y, de momento, con tener una actividad (es decir, una clase que visualice la información en la pantalla que es la que inluye el proyecto por defecto) vamos que chutamos. Pero será lo próximo que caiga como post, palabra.
(3): existe un bug en este punto (a fecha de hoy en revisión) por el que si se realiza list("") sería accesible el asset del framework y se solaparían las referencias si correspondiera al mismo nombre dos archivos, uno en la aplicación y otro en el del framework. (ver issue 373 y comentarios)
(4): los modos son MODE_APPEND para concatenar, MODE_WORLD_WRITEABLE para otorgar permisos de escritura, MODE_WORLD_READABLE para otorgar permisos de lectura y MODE_PRIVATE, el más restrictivo y el inlcuido por defecto.
(5): @+id indica que R.java (la clase que asocia a los recursos con identificadores) contendrá dentro del layout de main un índice para ese objeto (para evitar indexarlos todos). Si no lo ponemos no aparecerá aquí:

public final class R {
//...
public static final class id {
public static final int btn1=0x7f050000;
public static final int btn2=0x7f050001;
public static final int btn3=0x7f050002;
public static final int texto=0x7f050003;
}
public static final class layout {
public static final int main=0x7f030000;
}
//...
}

De aquí se deriva la conclusión que no puede duplicarse el id para NINGÚN elemento dentro de la misma aplicación, aunque estén en distintos layouts

2 comentarios:

Anónimo dijo...

Bueeeeno, te comentaré pq lo has dicho en la posdata, pero tb pq no puedo dejar ningún post a 0 comentarios, es superior a mí xD

En fin, +o- pondré el comentario por puntos:
- Android = Java (y eso es bueno pq básicamente sólo sé Java xD). Si lo vi antes ni me acordaba.
- Las ventanas/formularios (lo que vea el usuario en algo que no sea consola) son ventanas propias de Java o son HTML's? Es que al ver la definición en XML y el uso de tags me ha sonado a HTML, lo que pasa que entre lo que has dicho de cambiar propiedades y los eventos Listener... para mí que son ventanas propias.
- El contexto es todopoderoso, es como el genio de la lámpara, tú le pides y él te da. Supongo que eso vendrá por la ancestral filosofía de la encapsulación y abstracción no? Que tú puedes tener tus ficheros de configuración en cualquier sitio de la aplicación, pero dentro del código no te interesa saber dónde están realmente, con preguntarle al Context ya basta.
- Última question: ¿Android = Struts? Quicir, tiene filosofía por capas 'establecida' (que si la clase Action, Delegate, los beans...) o eso va al gusto del chef? Pq del tema acceso a bases de datos tp has comentado nada.

Fin del tostón.

PD: Al final me vas a hacer interesarme por Android malvada xDDD

Del dijo...

Perdón por la tardanza:

A ver, a ver:
- android == Java a nivel de codificación, salvo contados detalles.
- Las ventanas se crean algo así como los frames heredando de una superclase llamada Activity. Supongo que lo que han querido hacer con el xml es ahorrarse el código autogenerado para colocar los botones que normalmente te insertan los IDE's de java.
- Sííííí, el context es el oráculo de nuestro tiempo, todo lo sabe y lo que no sabe no existe, es el dueño de todos los Manager (WindowsManager, AssetsManager,...), es el puto amo.
- No, no es como Struts, el tema de acceso a la base de datos está en las clases que heredan de provider, sería un acceso desde el lado del servidor, sin interfaz cliente que se precie.


Es un placer que leas y preguntes Auron, (tralará)
Salu2!!