jueves, 4 de abril de 2013

Android: Consultar datos de una web ( tareas asíncronas)


Muchas veces necesitamos que nuestra aplicación se nutra de información online. Para ello, o tenemos un servicio web al que invocar que nos devuelva esa información, o tendremos que leer documentos de un determinado directorio web.

En el día de hoy nos centraremos en la consulta de información disponible en la web (ya sea una página o un documento xml) y dedicaremos el tema de las invocaciones a servicios web lo veremos en próximos capítulos, junto con una introducción que estoy preparando de cómo crearlos.

Configurando el archivo AndroidManifest

Antes de hacer nada, es importante modificar el archivo AndroidManifest.xml para que nos permita debuggear.

1

Después le añadiremos permisos de usuario, cosa que haremos la pestaña Permissions, haciendo clic en Add y seleccionando Uses Permissions. En el desplegable Name seleccionaremos android.permission.INTERNET.



Savaremos y ya tendremos lista nuestra configuración de acceso a internet.

Creando nuestra interfaz

Para esta aplicación partiremos de un proyecto creado por defecto. En su layout eliminaremos el TextView que añade por defecto y añadiremos los siguientes componentes:
  • EditText (con id edtUrl) para introducir la url a consultar.
  • Button (con id btnGo) para indicar que queremos leer la url indicada anteriormente.
  • TextView  (con id txtCarga) que nos mostrará el resultado.
  • ProgressBar (con idpbLoading y visibility a invisible) para indicar que se está ejecutando la tarea.


Una vez puesto bonito en la pantalla, debe quedar algo más o menos así (el ProgressBar empleado es el “circulito” y no se vé porque está a invisible):



El código en XML de la pantalla es el siguiente:

<RelativeLayout 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:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <EditText
        android:id="@+id/edtUrl"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_toLeftOf="@+id/btnGo"
        android:ems="10" />

    <Button
        android:id="@+id/btnGo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@+id/edtUrl"
        android:layout_alignParentRight="true"
        android:layout_alignTop="@+id/edtUrl"
        android:text="Ir" />
   
    <TextView
        android:id="@+id/txtCarga"
        android:layout_width="fill_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/edtUrl"
        android:text="Small Text"
        android:textAppearance="?android:attr/textAppearanceSmall" />

    <ProgressBar
        android:id="@+id/pbLoading"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:visibility="invisible" />
</RelativeLayout>

Manos al codigo

Con nuestra interfaz ya lista, abriremos el archivo MainActivity.java y añadiremos en el método onCreate la invocación a cargaObjetosPantalla.

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

En esta función, definiremos los campos de la pantalla y su asignación y le crearemos un evento al clic de botón, que simplemente invocará a la función cargaWeb.


       private void cargaObjetosPantalla()
       {
             //cargamos los componentes
             edtUrl = (EditText)findViewById(R.id.edtUrl);
             btnGo = (Button)findViewById(R.id.btnGo);
             txtCarga = (TextView)findViewById(R.id.txtCarga);
             pbLoading = (ProgressBar)findViewById(R.id.pbLoading);

             //asignamos valores
             edtUrl.setText("http://www.android.com/");
            
             //creamos eventos
             btnGo.setOnClickListener(new OnClickListener() {
                    public void onClick(View v) {
                           if (!pbLoading.isShown())
                                  cargaWeb();
                    }
             });
       }

Antes de implementar la función cargaWeb, debemos tener presente que la carga de un archivo o el acceso web suele ser bastante lento respecto al resto de la ejecución. Es decir, si nosotros realizamos un evento determinado e invocamos un código que tarda en responder, Android nos devolverá un error NetworkOnMainThreadException, que en castellano viene a decirnos que la rutina invocada no ha finalizado mientras que la padre sí.

Para evitar esto haremos uso de las tareas asíncronas, de forma que nosotros crearemos una clase que herede de AsyncTask y la invocaremos. Esta creará su propio hilo de ejecución y nos olvidaremos de ella.

Tareas Asincronas

Las tareas asíncronas heredan de la clase generica AsyncTask y se basan en tres tipos:
  • Params: tipo de los parámetros de entrada
  • Progress: tipo de datos para informar del progreso (no lo vamos a usar)
  • Result: tipo del parámetro de resultado


private class MiClaseAsincrona extends AsyncTask< Params, Progress, Result>

Nosotros llamaremos a nuestra clase AsyncLoadWeb recibiremos una URL, para el progreso indicaremos el tipo int aunque no lo vamos a usar, y devolveremos el String leído.

public class AsyncLoadWeb extends AsyncTask<URL, Integer, String>
{

}

Como nosotros queremos trabajar con objetos de la pantalla, lo que haremos es indicar en el constructor qué queremos recibir y crearemos sus respectivos campos. En este caso hablamos del txtCarga (al que llamaremos texto) y pbLoading (al que llamaremos loading). De igual forma, definiremos el objeto String res donde almacenaremos el resultado de la consulta.

       //aqui almacenaremos el resulado
       private String res;

       //recibiremos objetos de la pantalla para interactuar con ellos
       private TextView texto;
       private ProgressBar loading;
      
       public AsyncLoadWeb(TextView texto, ProgressBar loading){
             super();
             this.texto = texto;
             this.loading = loading;
       }

El método principal de la clase, el que ejecuta el código de forma asíncrona respecto a la invocación, se llama doInBackground. Nosotros en cambio invocamos a un método execute, que previa ejecución de doInBackground realiza un onPreExecute y tras su finalización un onPostExecute.
Si escribimos el código siguiendo el que será el orden de ejecución, deberemos crear primero la función onPreExecute, que simplemente pondrá a visible el ProgressBar de la pantalla.
protected void onPreExecute() {
       super.onPreExecute();
       loading.setVisibility(0); //VISIBLE
}
En doInBackground haremos la invocación a la web, y nos ayudaremos de una función auxiliar (readStream) para ir leyendo lo que vamos recibiendo.

//ejecución de la operación en background
protected String doInBackground(URL... urls)
{
       URL url = urls[0];
       HttpURLConnection urlConnection;
      
       try {
             urlConnection = (HttpURLConnection)
                           url.openConnection();
      
             try {
                    InputStream in =
                                  new BufferedInputStream(
                                               urlConnection.getInputStream());
                    res = readStream(in);
             } catch (Exception e) {
                    res = "Ha ocurrido un error " +
                                  "en doInBackground (try 2): " +
                                  e.toString();
             } finally {
                    urlConnection.disconnect();
             }
      
       } catch (IOException e1) {
             res = "Ha ocurrido un error " +
                           "en doInBackground (try 1): " +
                           e1.toString();
       }
       return res;
}

//leemos el stream
private String readStream(InputStream in) {
       InputStreamReader isw = new InputStreamReader(in);
       String res = "";
       int paso = 0; //auxiliar
       try {
             int data = isw.read();
             while (data != -1) {
                    char current = (char) data;
               data = isw.read();
               res += current;
               paso++; //auxiliar
        }
       } catch (IOException e) {
             res = "Ha ocurrido un error" +
                           " en readStream (paso "
                           + Integer.toString(paso)+"): " +
                           e.toString();
       }
       return res;
}

El resultado de doInBackground será el objeto que recibirá la función onPostExecute, y aquí simplemente mostraremos el texto leído (o el error encontrado) e indicaremos el ProgressBar a invisible.

//este metodo se ejecuta cuando ha finalizado doInBackground()
protected void onPostExecute(String result) {
       texto.setText(res);
       loading.setVisibility(4); //INVISIBLE
}

Y con esto ya tenemos lista la clase AsyncLoadWeb.

Invocación de la tarea asíncrona

Como habíamos comentado, en MainActivity disponemos de una función para invocar a esta clase asíncrona. La función cargaWeb creará la URL desde el EditText, la una instancia de la clase AsyncLoadWeb con los parámetros txtCarga y bpLoading y realizará la invocación de su método execute.

private void cargaWeb()
{
       AsyncLoadWeb loadweb = new AsyncLoadWeb(this.txtCarga, this.pbLoading);
       URL url = null;
       try {
             url = new URL(edtUrl.getText().toString());
       } catch (MalformedURLException e) {
             e.printStackTrace();
       }
       loadweb.execute(url);
}

¡Y con esto, ya podemos probar!


Código de ejemplo aquí