El patrón ViewHolder, qué es y cómo utilizarlo

Uno de los elementos básicos de cualquier aplicación son las listas. Si nos fijamos, están prácticamente en cada pantalla y realmente son la mejor, si no la única, manera de mostrar una gran cantidad de datos de forma ordenada y clara.

En Android las listas se suelen crear mediante Adapters a los que añadimos la información y, si queremos personalizar la lista mínimamente, también el diseño con el que se mostrarán.
Pero esto que parece sencillo, se complica cuando queremos cargar listas muy largas, ya que nuestros dispositivos pueden alcanzar fácilmente ciertos límites de procesamiento y de memoria si no utilizamos ciertos patrones y buenas prácticas.

Uno de estos es el patrón ViewHolder, con el que podemos conseguir un procesamiento un 15% más rápido de las listas mejorando así la fluidez y, por consiguiente, la experiencia de usuario.

Después de que un adaptador cree o reutilice una vista, busca el lugar dentro del layout donde tiene que mostrar la información con el método findViewById(). Con el objeto View Holder conseguimos que esa referencia se establezca cuando se crea la vista y se guarde para no tener que volver a buscar.
Básicamente lo que se consigue con este patrón es evitar utilizar el método findViewById() cada vez que se tenga que mostrar un nuevo elemento de la lista.

Otro punto a tener en cuenta es que Google, desde Android 5.0, incluyó una nueva clase llamada RecyclerView, que viene a ser el sucesor a largo plazo del ListView, y su adaptador incluye por defecto este patrón. Así que parece interesante ir echándole un vistazo.
Y como siempre, la mejor manera de verlo es con un ejemplo, así que allá voy:

Ejemplo de patrón ViewHolder:

Imaginemos que queremos mostrar el nombre, el dni y la foto de 200 contactos. Cada contacto se mostrará en una vista general, con 3 vistas más pequeñas dentro (como en la imagen).

Lista que crearemos con el patrón VIewHolder

Para que se cargue la información, debemos pasarla cuando especifiquemos qué adaptador vamos a utilizar. En este caso puede ser una lista con objetos “Persona” de este estilo:


public class Persona {
	private String nombre;
	private String apellido;
	private Bitmap foto;

	public Persona (String nombre, String apellido) {
		this.nombre = nombre;
		this.apellido = apellido;
	}

	public String getNombre() {
		return nombre;
	}

	public String getApellido() {
		return apellido;
	}

	public Bitmap getFoto() {
		return foto;
	}

	public void setNombre(String nombre) {
		this.nombre = nombre;
	}

	public void setApellido(String apellido) {
		this.apellido = apellido;
	}

	public void setFoto(Bitmap foto) {
		this.foto = foto;
	}
}

Creamos la clase AdaptadorContactos. Hereda de ArrayAdapter y nos permite personalizar el adaptador modificando el método “getView()”. Suponemos que ya hemos creado un layout con el diseño y que lo hemos llamado “item_contacto.xml”.:


public class AdaptadorContactos extends ArrayAdapter {

    private Context context;
    private Persona[] personas;

    public AdaptadorContactos (Activity context, Persona[] personas) {
        super();
        this.context = context;
        this.personas = personas;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
       
        ViewHolder holder;
       
        if (convertView == null) {
            //La vista no está creada, así que la crea. Cuando vuelva a comprobar
            //si existe, reutilizará el objeto convertView para ahorrarse la creación de un nuevo objeto.
            convertView = mInflater.inflate(R.layout.item_contacto, null);
            holder = new ViewHolder();
            //Creamos un objeto de la clase ViewHolder y hacemos que cada atributo referencie
            //a un elemento del laout. Esta referencia se mantiene y cuando reutilicemos la vista
            //convertView ya no tendrá que llamar al método findViewById()
            holder.nombreTextView = (TextView) convertView.findViewById(R.id.contacto_nombre);
            holder.apellidoTextView = (TextView) convertView.findViewById(R.id.contacto_apellido);
            holder.fotoImageView = (ImageView) convertView.findViewById(R.id.contacto_foto);
            convertView.setTag(holder);
        }
        else {
            holder = (ViewHolder) convertView.getTag();
        }
           
        Persona persona = personas[position];
           
        holder.nombreTextView.setText(persona.getNombre());
        holder.apellidoTextView.setText(persona.getApellido());
        holder.fotoImageView.setImageBitmap(persona.getFoto()); 
        //Esta es la forma más sencilla de cargar una imagen, 
        //pero no sirve para cargarlas desde un servidor o para
        //procesar grandes cantidades. Ver nota al final.

        return convertView;
    }

    private static class ViewHolder {
    private TextView nombreTextView, apellidoTextView;
    private ImageView fotoImageView;
    }
}

Ahora solo queda añadir el adaptador con los datos desde la Activity o Fragment como haríamos normalmente y ya hemos terminado.

Con esto conseguimos mejorar la fluidez de nuestra aplicación y seguro que ayudará a conseguir alguna estrellita más en Google Play…

Nota sobre las imágenes: En este caso, las fotos se cargan en el UI Thread (hilo de interfaz de usuario) y esto va a provocar errores si queremos cargar las fotos desde un servidor, ya que todas las conexiones se deben hacer en otro hilo diferente. Podemos solucionar esto, además de añadir un montón de funcionalidades, utilizando librerías de terceros como Picasso o Universal Image Loader

TO DO list:

  • Explicación sobre cómo cargar correctamente imágenes desde un servidor.
  • Tutorial básico sobre Universal Image Loader

Fuentes (también para ampliar info):

Vogella.com

Lucasr.org

Java Code Geeks

dzone.com