«

»

dic 17 2012

Trabajando con Parcelables

Hola a todos! hoy me estreno en androcode.es con un artículo sobre tipos parcelables, si no sabes lo que son, hoy aprenderás para qué sirven y cómo implementarlos.

Normalmente cuando queremos pasar un objeto entre actividades tenemos varias opciones, unas más elegantes y otras menos.

Tal vez lo primero que se nos ocurra sea guardar el objeto en un campo static de una de nuestras clases, y recuperarlo en la nueva actividad, pero este tipo de prácticas no están muy bien vistas ya que puedes incurrir en problemas de concurrencia, leaks de memoria, etc.

Puede que lo siguiente que se te ocurra sea implementar en el objeto la interfaz serializable. Esta solución no es mala para objetos pequeños pero en la práctica es muy lenta y si vamos a serializar un objeto complejo y grande, no es la mejor solución. Por eso el equipo de android decidió inventarse los tipos parcelables, que en la práctica es como escribir los tipos en un Bundle y recuperarlos después. De echo el objeto Bundle implementa la interfaz Parcelable y cuando haces intent.putExtra(“key”,”hello world”), estás escribiendo un Bundle asociado al intent, y cuando haces getIntent().getExtras() para leer, es un Bundle de donde lees.

Pero vamos a ver esto con más detalle y vamos a ver como podemos implementar la interfaz Parcelable en nuestros objetos para pasarlos entre actividades o servicios de una manera rápida.


Puedes descargarte el código de ejemplo de github

Lo primero es crearnos una clase que implemente la interfaz Parcelable

package com.example.parcelable;

import android.os.Parcel;
import android.os.Parcelable;

public class Estudiante implements Parcelable {

	@Override
	public int describeContents() {
		return 0;
	}

	@Override
	public void writeToParcel(Parcel dest, int flags) {

	}

}

Esto nos creará un par de métodos en la clase, describeContents, que en los ejemplos siempre ponen return 0; así que de momento no nos preocupamos, y el método writeToParcel que recibe un objeto Parcel de destino y un flag que puede ser 0 o PARCELABLE_WRITE_RETURN_VALUE.

Yo recomiendo añadir otro constructor al objeto que reciba un Parcel para recrearlo a partir de ahí y un método readFromParcel(Parcel in) para rellenar los campos del objeto y tenerlo más ordenado.

Además vamos a añadirle unos campos a la clase, porque una clase sin campos no vale para mucho ¿no?

package com.example.parcelable;

import java.util.List;

import android.os.Parcel;
import android.os.Parcelable;

public class Estudiante implements Parcelable {
	int fechaNacimiento;
	String nombreCompleto;
	boolean esHijoUnico;
	float[] notas;
	List<Estudiante> amigos;

	public Estudiante() {
		notas = new float[3];
		amigos = new ArrayList<Estudiante>();
	}

	public Estudiante(Parcel in) {
		notas = new float[3];
		amigos = new ArrayList<Estudiante>();
		readFromParcel(in);
	}

	@Override
	public int describeContents() {
		return 0;
	}

	@Override
	public void writeToParcel(Parcel dest, int flags) {

	}

	private void readFromParcel(Parcel in) {

	}

}

Cómo veis, he añadido unos campos de distintos tipos, incluidos un array de floats y una lista tipada (para que veáis que se pueden meter distintos tipos en un Parcel).

Ahora vamos a ver la chicha, cómo guardar el objeto en un Parcel y como recuperarlo después.

Básicamente el objeto Parcel nos expone unos métodos para escribir nuestros campos según su tipo para no equivocarnos, por lo que rellenamos nuestro método writeToParcel de la forma siguiente:

@Override
public void writeToParcel(Parcel dest, int flags) {
	dest.writeInt(fechaNacimiento);
	dest.writeString(nombreCompleto);
	dest.writeBooleanArray(new boolean[]{esHijoUnico});
	dest.writeFloatArray(notas);
	dest.writeTypedList(amigos);
}

Mientras escribía esto he descubierto que no hay un método Parcel.writeBoolean(boolean val) (gracias Google!) por lo que podéis o bien escribirlo en un array de booleans con un único valor, o escribir un byte y luego parsearlo como boolean.

Ahora vamos a ver como recuperaríamos nuestro objeto de un Parcel, así que rellenamos el método readFromParcel de esta manera:

private void readFromParcel(Parcel in) {
	fechaNacimiento = in.readInt();
	nombreCompleto = in.readString();

	boolean[] temp = new boolean[1];
	in.readBooleanArray(temp);
	esHijoUnico = temp[0];

	in.readFloatArray(notas);
	in.readTypedList(amigos, CREATOR);
}

¡Ostras! Para leer una lista tipada necesito un objeto Parcelable.Creator, ¿qué es eso? Digamos que es un objeto que genera objetos del tipo que le digas a partir de un Parcel (vamos, lo que veníamos haciendo) pero es necesario para recrear los objetos en otros Parcelables. Es muy sencillo, sólo hay que añadir esto:

public static final Parcelable.Creator<Estudiante> CREATOR
	= new Parcelable.Creator<Estudiante>() {
		public Estudiante createFromParcel(Parcel in) {
			return new Estudiante(in);
		}

		public Estudiante[] newArray(int size) {
			return new Estudiante[size];
		}
	};

Ahora veis como añadir un constructor que recibe un Parcel era buena idea para tener todo limpito. Además, nota importante, cuando leemos del Parcel no hay pares clave-valor, así que hay que leerlo en el mismo orden en que lo escribimos.

¡Pues ya tenemos nuestro objeto implementado!  Vamos a ver si funciona. Creamos un par de activities, en el primero creamos un par de estudiantes, uno amigo del otro (¡no es un amistad mutua así que sin referencias cruzadas por favor!) y lo metemos en los extras de un intent que viajarán a la segunda activity donde rescatamos nuestro estudiante y escribimos en el LogCat toda la info que hemos recibido de él y de su amigo no correspondido:

MainActivity.java

Estudiante otroEstudiante = new Estudiante();
otroEstudiante.nombreCompleto = "Perico Palotes";
otroEstudiante.esHijoUnico = false;
otroEstudiante.fechaNacimiento = 1990;
otroEstudiante.notas = new float[]{7.8f, 4.9f, 10.0f};
otroEstudiante.amigos = new ArrayList<Estudiante>();

Estudiante estudiante = new Estudiante();
estudiante.nombreCompleto = "Fernando F. Gallego";
estudiante.esHijoUnico = true;
estudiante.fechaNacimiento = 1983;
estudiante.notas = new float[]{9.5f, 8.6f, 4.6f};
estudiante.amigos = new ArrayList<Estudiante>();

estudiante.amigos.add(otroEstudiante);

Intent intent = new Intent(this, DestActivity.class);
intent.putExtra("estudiante", estudiante);
startActivity(intent);

DestActivity.java

Estudiante estudiante = getIntent().getParcelableExtra("estudiante");

Log.i("estudiante", estudiante.nombreCompleto);
Log.i("estudiante", String.valueOf(estudiante.esHijoUnico));
Log.i("estudiante", String.valueOf(estudiante.fechaNacimiento));
Log.i("estudiante", Arrays.toString(estudiante.notas));
Estudiante amigo = estudiante.amigos.get(0);
Log.i("amigo", amigo.nombreCompleto);
Log.i("amigo", String.valueOf(amigo.esHijoUnico));
Log.i("amigo", String.valueOf(amigo.fechaNacimiento));
Log.i("amigo", Arrays.toString(amigo.notas));

Y esto es lo que veremos en nuestro querido LogCat

I/estudiante(6739): Fernando F. Gallego
I/estudiante(6739): true
I/estudiante(6739): 1983
I/estudiante(6739): [9.5, 8.6, 4.6]
I/amigo(6739): Perico Palotes
I/amigo(6739): false
I/amigo(6739): 1990
I/amigo(6739): [7.8, 4.9, 10.0]

Puedes descargarte el código de ejemplo de github

Acerca del autor

Fernando F. Gallego

Desarrollador de junaio Augmented Reality para Android desde Munich. Me encanta cacharrear y probar ideas y conceptos nuevos para aprender. Tengo tantos pet projects que podría montar un zoo de aplicaciones. Además me encanta el sushi, los gintonics y los smartphones. Puedes encontrarme en twitter como @ferdy182

  • http://hellogreenrules.blogspot.com.es/ Roger

    Perfecto! Me vino de lujo. Muchas gracias por le tutorial y enhorabuena.
    Saludos!

    PD: A favoritos lo tengo ;)

  • Pingback: Objetos parcelable | Biljet app()

  • ezequiel

    Hola , yo tengo una duda , sería posible tener Una clase que contenga objetos de una clase de otro tipo.

    Ejemplo:

    public Class Persona implements Parcelable{

    Telefono tlf;

    }

    En este caso , ¿habría que hacer lo mismo que explicas en el tutorial en la clase teléfono?

    • http://twitter.com/ferdy182 Fernando F. Gallego (@ferdy182)

      Hola! si, tendrías que hacer parcelable tanto Telefono como Persona y escribirla con el método que indicas. Saludos

      • carlos

        Yo tampoco compreendi lo que hago si tuver un objeto de otra clase. Puedes ayudarnos?

        • http://twitter.com/ferdy182 Fernandroid (@ferdy182)

          Cuando guardas un objeto en un Parcelable, el sistema guarda en valor de todos los campos de la clase para luego poder reconstruirla, normalmente son tipos básicos, Strings, integets, floats, etc. Esos tipos el sistema sabe cómo guardarlos, pero si tienes clases propias, éstas también tienen que implementar la interfaz Parcelable y sus métodos, ya que el sistema no sabe como guardarlos, por eso debes indicar tú mismo cómo debe escribirlos al través del método writeToParcel y cómo leerlos con el método readFromParcel.

        • carlos

          yo he criado un Vector de otra Clase…

          Class A implements Parcelable{
          Vector vecB;
          }

  • ezequiel

    ¿Llamando a la función dest.writeParcelable(obj , flags); ?

  • diego

    sos un crack

    • http://twitter.com/ferdy182 Fernando F. Gallego (@ferdy182)

      Gracias!

  • Pingback: 10 herramientas fuera de Eclipse para trabajar con Android (1/2) | Androcode()

  • AleV

    Super Util!!!

  • Chema

    Muy util!!! y mejor explicado.

    Gracias y enhorabuena por el blog

  • Marc

    Muy instructivo el post, gracias!
    Con el uso de parcelable, entiendo que se crea un objeto nuevo con los mismos atributos.
    Me gustaría saber si hay forma de utilizar el mismo objeto en distintos activities. Es decir, que se usen sus getters y setters en distintas clases.
    Gracias

  • https://plus.google.com/109651943815661818987 Andres Felipe Aguiar Monsalve

    Buen día, qué harías si la clase EStudiante tiene varios tipos String (p.e Nombre, Apellido…) Mi pregunta es porque veo que sólo lees del parcel un dato por tipo, es decir: in.readString();
    Gracias.

    • Slimp

      Por si alguien sigue con la misma duda, si se puede leer mas de un dato del mismo tipo: ej.

      id = parcel.readLong();
      nombre = parcel.readString();
      direccion = parcel.readString();
      servido = parcel.readString();

  • https://www.facebook.com/laurasofia.brugesholguin Laura Brugés

    Muchísimas gracias, me fue extremadamente útil y muy clara tu explicación! :)

  • http://gravatar.com/xajiar javi

    ¿Y si un estudiante tuviera una foto? Es decir, que existiera un campo del tipo Drawable en la clase Estudiante. ¿Se puede pasar una imagen?

    • http://twitter.com/ferdy182 Fernando F. Gallego (@ferdy182)

      Si tienes una imagen siempre puedes obtenerla como un bitmap, y la clase Bitmap de Android implementa la interfaz parcelable así que lo puedes pasar en un intent como extra sin problemas y recuperarlo con un getParcelableExtra

  • Pingback: Android: ciclo de vida y persistencia | JHCernuda()

  • AbelardoLG

    Hola, Fernando:

    Ya sé que hace años que se escribió este post y no sé si aún sigue activo pero por intentarlo…pregunto:

    Tengo una clase getter/setter llamada “Movie”. Es la clase a la cual implemento la interfaz Parcelable. Por lo tanto, ahí tengo:
    - el CREATOR;
    - los métodos que leen y que escriben;
    - el describeContents..

    En mi MainActivity tengo el método:
    public void onPictureClick(int param)

    que uso para capturar, como su nombre indica, el click sobre un item. Dentro de este método soy capaz de visualizar la película sobre la que ha hecho click de manera correcta. A continuación de mostrar los datos de la película (para verificar que está bien), meto el objeto ‘movie’ en el intent y lo envío a otra clase llamada ‘detailActivity’ para mostrar los detalles de la película elegida. Hasta aquí todo bien.

    Pero…en la clase DetailActivity no se muestran los datos de la película NI tampoco el Log que tengo en el método:

    ‘public Movie(Parcel in)’

    ¿Qué puede estar pasando?

    He seguido escrupulosamente tu tutorial (muy simple y muy claro, mi enhorabuena por esta entrada) pero nada… ¿?

    Agradezco cualquier comentario que me conduzca a la solución de este problema.

    Saludos cordiales.

    ACTUALIZACIÓN:
    Decir que SÍ se muestra el log que tengo en el método:
    writeToParcel
    con los valores correctos de la película elegida.