viernes, 4 de septiembre de 2015

¿Qué es el ViewHolder Pattern?

Supongamos una situación en la que deseamos realizar una aplicación de mensajería, y la primer vista es una lista con los últimos mensajes, que presenta algunos datos como imagen de perfil, nombre, mensaje y hora de recepción.
Tenemos una clase que modela esto de la siguiente forma:
/**
 * POJO class representing a Message.
 */
public class MessageContent {
    private Drawable image;
    private String name;
    private String message;
    private String hour;

    /** Default Constructor **/
    public MessageContent() {}

    /**  Attribute setters && getters **/
}
Y en nuestro Adapter propio tenemos este método definido de la siguiente manera:
@Override
public View getView(int position, View view, ViewGroup parent) {
    final MessageContent content = (MessageContent) getItem(position);   

    if(null == view) {
        view = LayoutInflater
                .from(mContext)
                .inflate(R.layout.message_item, parent, false);
    }

    final ImageView profile = (ImageView) view.findViewById(R.id.profile_picture);
    final TextView name = (TextView) view.findViewById(R.id.contact_name); 
    final TextView message = (TextView) view.findViewById(R.id.message); 
    final TextView hour = (TextView) view.findViewById(R.id.message_hour); 

    profile.setImageDrawable(content.getImage());
    name.setText(content.getName());
    message.setText(content.getMessage());
    hour.setText(content.getHour());

    return view;
}
Aquí tenemos un problema grave que afecta el rendimiento de nuestro ListView. Y el hecho se da en las múltiples veces que el método findViewById va a ser invocado.
Este método de la clase View genera un árbol que se recorre de forma recursiva en búsqueda del id solicitado. Al finalizar la búsqueda nos devuelve una instancia de Object la cual casteamos al tipo de objeto que necesitamos.
En nuestro caso, nuestro item contiene 4 vistas, con lo cual el método findViewById se va a llamar cuatro veces. Además de eso, este método se ejecuta dentro de getView. GetView tiene la particularidad de que se va a llamar tantas veces como items haya en la lista o si hay items que necesitan ser actualizados. Esto hace que si la lista que tenemos sea muy grande, afecte el rendimiento del scroll del ListView, alterando la experiencia de los usuarios de nuestra app.
Para evitar que surja este problema, se ideo el patrón ViewHolder, que consiste básicamente en cachear las vistas que necesita nuestro item en una clase estática dentro de nuestra clase Adapter.
Retomando el ejemplo anterior, la aplicación de ViewHolder sería la siguiente:
/**
* ViewHolder class used for caching the views needed by our adapter.
*/
protected static class ViewHolder {
    private ImageView profile;
    private TextView  name;
    private TextView message;
    private TextView hour;

    /**
    * Constructor with parameters.
    *
    * @param view - The root view that contains all the views needed.
    */
    public ViewHolder(@NonNull final View view) {
        profile = (ImageView) view.findViewById(R.id.profile_picture);
        name = (TextView) view.findViewById(R.id.contact_name);
        message = (TextView) view.findViewById(R.id.message);
        hour = (TextView) view.findViewById(R.id.message_hour);
    }
}
Bien! Ahora que tenemos el ViewHolder definido es hora de utilizarlo, modificando getView para que nos quede de la siguiente forma:
@Override
public View getView(int position, View view, ViewGroup parent) {
    final MessageContent content = (MessageContent) getItem(position);
    final ViewHolder viewHolder;

    if(null != view) {
        viewHolder = (ViewHolder) view.getTag();
    } else {
        view = LayoutInflater
                .from(mContext)
                .inflate(R.layout.message_item, parent, false);

        viewHolder = new ViewHolder(view);
        convertView.setTag(viewHolder);
    }

    viewHolder.profile.setImageDrawable(content.getImage());
    viewHolder.name.setText(content.getName());
    viewHolder.message.setText(content.getMessage());
    viewHolder.hour.setText(content.getHour());

    return convertView;
}

Conclusión:

Como habrán visto el ViewHolder es un patrón bastante sencillo de implementar.

Es altamente recomendable utilizarlo en casos donde el layout de nuestro ítem contenga muchas vistas. Esto nos ayudará a mejorar la performance y la experiencia de nuestros usuarios.


Si deseas aprender un poco más al respecto, he dejado el código del post en un repositorio en Github.

Puedes visualizarlo accediendo a este link.

3 comentarios:

  1. muy buen articulo, espero sigas escribiendo más sobre android, saludos.

    ResponderEliminar
  2. Muchas Gracias! Espero te haya servido. Tan pronto como pueda seguire publicando más sobre Android! Saludos.

    ResponderEliminar
  3. Muy bien explicado y justo lo que necesitaba!
    Gracias!

    ResponderEliminar

Jonatan E. Salas

Android Software Engineer en SAM Sistemas. Estudiante de Ingenieria en sistemas en la UTN BA.