Aplicación Android invocando un Servicio Rest WCF Parte 2

viernes, 3 de febrero de 2012

En este post continuaremos el ejemplo empezado en la parte 1.

Vamos a crear una aplicación Android que invocará al servicio  Rest creado en el anterior post.
El IDE que vamos a utilizar es Eclipse. Antes de empezar debemos configurar Eclipse para poder desarrollar aplicaciones Android como se explica aqui http://developer.android.com/sdk/installing.html.

Lo primero es crear un proyecto de Android en Eclipse. Indicamos el nombre del proyecto y del paquete, dejamos marcada la casilla de 'Create Activity'



Lo primero en lo que nos tenemos que fijar es en la estructura del proyecto Android.
La plantilla nos ha creado una clase AndroidClient que hereda de Activity, es la clase base de una ventana en Android.

En Android el diseño de las ventanas se pueden crear mediante código o se pueden utilizar ficheros xml. Yo prefiero esta segunda forma porque se separan conceptos y las modificaciones son más ágiles.

En nuestro ejemplo vamos a tener una ventana de tipo lista, hay dos formas de hacerlo:
  • Una clase que hereda de Activity y un xml que representa al diseño. En este xml es donde indicaríamos que la ventana contiene un LisView.
  • Una Clase que hereda de ListActivity, que es un Activity con un ListView ya agregado, como nuestra ventana no va a tener nada más, no es necesario definir el xml de la vista.
Vamos a utilizar la segunda opción .
Cambiamos el código generado por defecto en nuestra Activity para que herede de ListActivity y eliminamos la instrucción SetContentView que aparece en el método OnCreate de la ventana, es el método con el que se indica al Activity su diseño de ventana en xml. Podemos eliminar el fichero main.xml que nos ha generado la plantilla porque no va a ser necesario.

De momento la clase Activity se nos queda así:

package xurxo.AndroidClient;


import android.app.ListActivity;
import android.os.Bundle;

public class AndroidClientActivity extends ListActivity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        
    }
}

Ahora tenemos que crear la vista en xml que define cada uno de los items del ListView, existe uno por defecto en Android pero vamos a crear uno nuevo .  Desde el menú File->New->Other creamos un Android XML File, indicamos que es un fichero de tipo Layout y le asignamos el nombre.

Nuestro xml para cada item contiene varios texview con información del elemento de cada linea.

itemlistview.xml

 
  
  
   
   

       
   
   
   
  
 

Fijaros que en el xml cada control tiene un Id con un nombre, Android asignará un número único a cada control y desde código podremos acceder a un control a través de su Id.

Para rellenar un ListView se utilizan Adapters, son objetos que hacen de puente entre el ListView y los datos. Un Adapter es el encargado de rellenar un ListView. Vamos a crear una clase Adapter personalizada que herede de ArrayAdapter y vamos a hacerla lo más estándar posible para que nos pueda servir en próximos ejemplos. Para conseguirlo vamos a recibir en el constructor el contexto, el xml para cada item, el array de objetos a representar y a través de dos arrays más indicaremos qué propiedades de cada objeto hay que leer y en qué ids de controles hay que asignarle su valor.
Tenemos un método GetView que hay que sobreescribir, será invocado cada vez que tengamos que rellenar un item del listView, a través del parámetro position sabemos la posición del objeto a rellenar y por reflexión para cada elemento del array de propiedades leemos el valor y se lo asignamos al control del array de Ids de la misma posición. De esta forma podremos utilizar este Adapter en ejemplos donde hay que rellenar 2 controles o 7 dentro de cada item; este Adapter es reutilizable, vamos a ver el código para entenderlo mejor:

package xurxo.AndroidClient;

import java.util.List;

import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

/**
 * Adapter estándar para rellenar un listview
 * @author Eljo
 *
 */
public class ObjectAdapter extends ArrayAdapter<Object>
{
    private  List<Object> _items;
    private  String[] _from;
    private  int[] _to;
    private static final String TAG = "ListObjectAdapter";
    private int _textViewResourceId;

    /**
     * Contructor del Adapter
     * @param context contexto donde se aloja el ListView, por ejemplo un Activity
     * @param textViewResourceId Id del itemListview
     * @param items ArraList con los objetos a rellenar en el ListView
     * @param from Array de String con los nombres de los getter que hay que leer de cada objeto del ArraList
     * @param to Array de int con los Ids de los controles que hay que rellenar de cada item del ListView
     */
    public ObjectAdapter(Context context, int textViewResourceId, List<Object> items, String[] from, int[] to) 
    {
            super(context, textViewResourceId,items);
            
            //Almacenamos los parametros
            _textViewResourceId = textViewResourceId;
            _items = items;
            _from = from;
            _to = to;
    }


 /**
     * Es invocado para cada elemento a crear en el ListView. Inyecta el diseño del xml y rellena los controles
     */
    @Override
    public View getView(int position, View convertView, ViewGroup parent) 
    {
            View v = convertView;
            if (v == null) 
            {
             //creamos el inflater para inyectar xml
                LayoutInflater vi = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                
                //inyectamos
                v = vi.inflate(_textViewResourceId, null);
            }
            
            //recuperamos del ArrayList el objeto a rellenar segun la posición
            Object o = _items.get(position);
            
            if (o != null) 
            {   
             
             try        
             {
              //recorremos el array que contiene los getter a leer y para cada uno
              //leemos y asignamos su valor en su homologo en el array de ids de controles a rellenar
              for (int i = 0; i < _from.length; ++i) 
              {                   
               TextView txt =(TextView) v.findViewById (_to[i]);
               txt.setText((String) o.getClass().getMethod(_from[i]).invoke(o, (Object[]) null));
              }
             }
             catch (Exception ex)
             {
              Log.e(TAG, "Error al leer la informacion " + ex.getMessage() );
             }
      }
            
            return v;
    }

Creamos una clase POJO que representa a un cliente:
package xurxo.AndroidClient;

public class Customer
{
 private String _CodCliente ="";
 private String _RazonSocial="";
 private String _Direccion="";
 private String _Telefono="";
 
 public Customer()
 {  
 }
 
 public Customer(String CodCliente, String RazonSocial, String Direccion, String Telefono)
 {
  _CodCliente =CodCliente;
  _RazonSocial=RazonSocial;
  _Direccion=Direccion;
  _Telefono=Telefono;   
 }
 
 public String GetCodCliente()
 {
  return _CodCliente;
 }
 
 public void SetCodCliente(String value)
 {
  _RazonSocial = value;
 }

 public String GetRazonSocial()
 {
  return _RazonSocial;
 }
 
 public void SetRazonSocial(String value)
 {
  _RazonSocial = value;
 }
 
 public String GetDireccion()
 {
  return _Direccion;
 }
 
 public void SetDireccion(String value)
 {
  _Direccion = value;
 }
 
 public String GetTelefono()
 {
  return _Telefono;
 }
 
 public void SetTelefono(String value)
 {
  _Telefono = value;
 }
}

Para la comunicación a través de la red vamos a crear una clase llamada HttpClientHelper que va a ser la encargada de comunicarse con el servicio web creado en WCF y devolvemos un JSONArray. En una aplicación real para un cliente, sería interesante que nos devolviera un array de objetos Customer ya convertidos y nosotros desde fuera no nos tenemos que enterar si la información la obtuvo en JSON ó XML desde el web service, pero para poder hacer más sencillo el ejemplo vamos a devolver un JSONArray directamente.
En la versión 4 de Android es obligatorio que la comunicación a través de la red esté en un hilo distinto del principal, si no recibiremos un NetworkOnMainThreadException.
Para crear la comunicación en un hilo distinto tenemos varias posibilidades, pero vamos a utilizar la clase AsyncTask de Android, porque después de realizar la tarea tenemos que actualizar la ventana, que es un objecto del hilo principal. Esto utilizando hilos de java no es posible. En una aplicación real para un cliente, la mejor opción sería utilizar los servicios de Android junto con hijos de Java y cachear la información en una base de datos local como SQLlite y así poder consultar los datos de forma desconectada.
Creamos una clase anidada a nuestra Activity que hereda de AsyncTask, invocará a HttpClientHelper y convertirá el JSONArray en un ArrayList.

Así nos queda la clase Activity con la tarea Asyncrona anidada.

package xurxo.AndroidClient;

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

import org.json.JSONArray;

import android.app.ListActivity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.widget.ListView;
import android.widget.Toast;

/**
 * Activity principal de la aplicación
 * @author Eljo
 *
 */
public class AndroidClientActivity extends ListActivity {
 ListView _lv;
 //Getter a leer de cada objeto a rellenar en el ListView
 final String[] FROM = { "GetCodCliente", "GetRazonSocial","GetDireccion","GetTelefono" }; //
 
 //Ids de los controles del ItemListView.xml que hay que rellenar
 final int[] TO = { R.id.CodCliente, R.id.RazonSocial, R.id.Direccion,R.id.Telefono };
 
 
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        _lv = this.getListView();
      
        //invocamos la tarea asincrona de llamar al web service y rellenar el LitView
        new HttpClientCustomer().execute(this);
    }
    
    /**
     * Tarea asincrona que inicia la invocación del web service, convierte el JSONArray en un ArrayList y asigna 
     * el Adapter al ListView
     * @author Eljo
     *
     */
    public class HttpClientCustomer extends AsyncTask<object, object, object>{
     /**
      * Se invoca cuando la tarea asincrona es executada
      */
     @Override
     protected Object doInBackground(Object... arg0) {

       try
       {
        //inicio de la invocación del web service
        JSONArray Clientes = HttpClientHelper.GET("getclientes");
        
        //Conversión en ArrayList
        List<object><p>
CustomerList= new ArrayList<object><p>
();
        
        for (int i = 0; i < Clientes.length(); ++i) 
        {      
          
          CustomerList.add(new Customer(
            Clientes.getJSONObject(i).getString("CodCliente")=="null" ? "" : Clientes.getJSONObject(i).getString("CodCliente") ,
            Clientes.getJSONObject(i).getString("RazonSocial")=="null" ? "" : Clientes.getJSONObject(i).getString("RazonSocial"),
            Clientes.getJSONObject(i).getString("Direccion")=="null" ? "" : Clientes.getJSONObject(i).getString("Direccion"),
            Clientes.getJSONObject(i).getString("Telefono")=="null" ? "" : Clientes.getJSONObject(i).getString("Telefono")));
        }     
        
        //Devolvemos el ArrayList
        return CustomerList; 
       }
       catch (Exception ex)
       {
        //Como desde este metodo no se puede lanzar una excepción, lo que devolvemos es la propia
        //Excepcion
        return ex;
       }
       
      }
     
      /**
       * Se invoca cuando la tarea asincrona ha terminado
       */
       protected void onPostExecute(Object result) 
       {    
            //Si el resultado es un ArrayList... no hay error
         if (result instanceof ArrayList) 
         {
         //asignamos el Adapter 
     List<object><p>
L = (List<object><p>
) result;
         _lv.setAdapter(new ObjectAdapter(_lv.getContext(),R.layout.itemlistview, L,FROM,TO));
                  // ... process result ...
              } 
         else 
         {
          //Si el resultado es una Excepción..hay error
          if (result instanceof Exception) 
          {
           //mostramos el error en pantalla
                   Exception ex = (Exception) result;
                   Toast.makeText(_lv.getContext(), "Error al crear la actividad. Error: " + ex.getMessage(), Toast.LENGTH_LONG).show();
                  // ... process error ...
          } 
              };     
       }

    }
}

Así nos queda la clase HttpClientHelper.

package xurxo.AndroidClient;

import java.io.BufferedReader;

import java.io.InputStream;
import java.io.InputStreamReader;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONArray;

import android.util.Log;

/**
 * Clase encargada de comunicarse con el servicio web y devolver eel resultado en JSONArray
 * @author Eljo
 *
 */
public class HttpClientHelper {
 
 //Tag para identificar en el Log
 private static final String TAG = "HttpClientHelper";
 
 //Función que realiza una acción de tipo GET contra el servicio web 
 public static JSONArray GET(String OperationName) throws Exception
 {
  BufferedReader reader =null;
  StringBuilder sb =null;
  try 
  {
  DefaultHttpClient httpClient = new DefaultHttpClient();
  HttpGet request = new HttpGet("http://xxxxxxxx:xxxx/WCFServiceJSON/Service.svc/" + OperationName);                  
  HttpResponse response = httpClient.execute(request);          
  HttpEntity responseEntity = response.getEntity();          
  
  InputStream stream = responseEntity.getContent();    
  
  reader = new BufferedReader(new InputStreamReader(stream));
  sb = new StringBuilder();
  
   String line = reader.readLine();
   
   while (line != null) 
   {
    sb.append(line);
    line = reader.readLine();
   }
  }
  catch (Exception ex) 
  {
   //Procesamos el error
   Log.e(TAG, "Error al crear la actividad. Error: " + ex.getMessage() );
   throw ex;
  }  
  finally 
  {
   reader.close();
  }
  return new JSONArray(sb.toString());  
   
 }
}

Como podéis ver en esta clase, la dirección del servicio web esta escrita en código directamente. Una buena práctica en una aplicación real es almacenar esta dirección en las preferencias de la aplicación y dar la posibilidad al usuario de modificar esta dirección a través de un menú.

Os dejo la imagen del diagrama de clases y de cómo nos ha quedado la aplicación. Tiene el estilo por defecto de Android, para otro Post dejamos cómo aplicar Themes y Styles.



16 comentarios:

  1. Hola , e seguido los paso, el servicio ya se ejecuta, pero para aplicarlo al app me sale un error "FATAL EXCEPTON : MAIN " de ahí ya me e perdido un poco, realice unos cambios porque también por código algunas cosas acá me salia error.
    ¿A que podria deberse ese erro ?

    ResponderEliminar
    Respuestas
    1. Muchas gracias por leer el post.
      Es complicado que te pueda decir porque te puede dar error si has hecho cambios.
      El código lo fui haciendo a medida que escribía el artículo, lo que se me ocurre es ver como puedo ponerlo para descargar.
      Saludos

      Eliminar
    2. ok gracias, estaré pendiente

      Eliminar
    3. Hola nuevamente, realiza nuevamente el proyecto, para ver el error que me salia al comienzo y es
      al cuando se hace la conversión al arraylist:
      //Conversión en ArrayList
      List<.object><.p> CustomerList= new ArrayList<.object><.p>();
      bueno en esta parte se me a complicado un poco, y claro lo mismo en la sección de asignar el adapter.
      Saludos.

      Eliminar
  2. Buenas noches realice este tutorial paso a paso, a excepcion que mi servicio es algo sencillo y solo devuelve un string dice "Respuesta servicio", pero el error que me sale es por decirlo asi, toda la metadata, codigos y ejemplos que le salen a uno normalmente cuando lo consume desde un explorador, es decir lo unico que estoy haciendo es observar el servicio y no consumirlo, que debo hacer???
    Para que me funcione correctamente??? Muchas gracias y quedo a la espera de sus comentarios.

    ResponderEliminar
  3. Hola, bueno al final de unos días logre correr la aplicación , me a servido mucho tu guía, espero continúes realizando temas sobre esto. Saludos

    ResponderEliminar
  4. hola gracias por el aporte muy interesante, me presenta el siguiente error

    The type Object is not generic; it cannot be parameterized with arguments p

    me podria colaborar con dicho error... Gracias

    ResponderEliminar
    Respuestas
    1. Por algún motivo yo hice unos cambio en esa parte
      //Conversión en ArrayList
      List<.Object>
      CustomerList= new ArrayList<.Object>();
      lo mismo en
      //asignamos el Adapter
      List<.Object>
      L = (List<.Object>) result;
      pd: puse puntos porque me daba error, asi que omitelas nada mas
      y dio me dio el mismo resultado del ejemplo.
      la razon no sabria explicarte bien.

      Eliminar
    2. Hola, gracias por leerlo

      Me alegro que resulte interesante. Estoy buscando la mejor forma de poder colgar los ejemplos de código para que los podáis descargar. Se admiten ideas ya que nunca lo he hecho.

      Saludos

      Eliminar
  5. Esta bueno el tutorial!
    A espera del código....

    ResponderEliminar
  6. Buenas, muy buen articulo, quisiera saber si has tenido la oportunidad de probar trayendo imagenes desde la base de datos.


    Saludos

    ResponderEliminar
  7. Muy buenas Jorge, me parece genial el tutorial, estoy super interesado en el c´dogo final, no soy un entendido de Java, y me gustaría tomar tu codigo como referencia para el aprendizaje, ¿ pudieras hacérmelo llegar o ponerte en contacto conmigo por favor ? my skype es avazquez_wasser gracias de antemano

    ResponderEliminar
    Respuestas
    1. Muchas gracias Alberto

      He actualizado los post 1 y 2 de este ejemplo de android y wcf con un enlace al final para el código fuente en Github

      Saludos

      Eliminar
  8. Un millón de gracias Jorge, muy, pero que muy amable!!!

    ResponderEliminar
  9. Hola, gran tutorial, Estoy probando el código, y la parte 1 del tutorial me funciona, ya que puedo acceder a la hoja json por medio del navegador.

    La parte dos la compila bien pero no muestra ningun dato en el listview.

    ¿Por que ocurrirá esto?

    ResponderEliminar
  10. Hola 'Anónimo' así de primeras es difícil que te pueda dar una respuesta, pueden ser muchos los motivos. Depura el código, fíjate si te salen errores en Logcat a ver si das con ello.

    Saludos

    ResponderEliminar