martes, 27 de agosto de 2013

Como customizamos nuestras vistas con el BaseAdapter?

Hola a todos, en este post vamos a ver el tema de como generar nuestras propias vistas, implementando la clase BaseAdapter, por ejemplo para poder modificar la forma en la que se ve un ListView?





Bueno vamos a arrancar con algunas buenas prácticas para el uso de estos Adapters ;) la forma estándar por ejemplo para mostrar un ListView es usando un ListAdapter, ahora el tema esta en que tenemos varias formas de armar estos ListAdapter.


ArrayAdapter


Bueno normalmente cuando comenzamos a trabajar con ListView, y vemos los primeros tutoriales, generalmente nos dicen que la forma más sencilla de implementar este es usando un ArrayAdapter, lo que más adelante nos puede traer algún que otro dolor de cabeza :P Esto es, porque es tan sencillo que tiene muchas limitaciones, entonces para hacer pruebas todo funciona lindo :D pero cuando lo vamos a implementar en una app real, seguramente terminaremos insultando un poco :P


Entre estas limitaciones tenemos:
  • Solo podemos mostrar texto
  • Nos obliga a usar una lista de items de tipo CharSequence, o items que tengan implementados el método toString()
  • Nos obliga a utilizar un layout que solo contenga TextView
  • Nos bloquea cualquier actualización de la UI desde procesos que estén corriendo en background, esto es para asegurarse de ser Thread Safe pero obviamente nos quita mucha funcionalidad :P


Entonces? Este es el primer consejo, usar BaseAdapter :D


Así que generamos la clase que va a controlar la vista del ListView que extienda de BaseAdapter, ahora solo debemos implementar un par de métodos para que esto funcione, y un constructor. En este ejemplo, la clase Noticia es solo una clase POJO ;)


public class NoticiasAdapter extends BaseAdapter {


   private List<Noticia> noticias;
   private Context context;


   public NoticiasAdapter(Context context, List<Noticia> noticias){
       this.noticias = noticias;
       this.context = context;
   }


   @Override
   public int getCount() {
       return noticias.size();
   }


   @Override
   public Object getItem(int position) {
       return noticias.get(position);
   }


   @Override
   public long getItemId(int position) {
       return noticias.get(position).getIdNoticia();
   }


   @Override
   public View getView(int position, View convertView, ViewGroup parent) {


       // Este método, es el que debemos, implementar para nuestra vista ;)
      
       return null;
   }
}


Thread Safe


Como dije antes, el ArrayAdapter para asegurarse de ser Thread Safe directamente nos bloquea, eso esta bien pero existen otras maneras que seguramente nos resultaran más utiles, para asegurarnos que nuestro adapter sea utilizado por un unico hilo, el principal que maneja la UI.


Por ejemplo, podemos forzar de manera sencilla a que falle de manera rápida:


public void updateNoticias(List<Noticia> noticias) {
   ThreadPreconditions.checkOnMainThread();
   this.noticias = noticias;
   notifyDataSetChanged();
}


Y este es el resto del ejemplo de implementación:


public class ThreadPreconditions {
   public static void checkOnMainThread() {
       if (BuildConfig.DEBUG) {
           if (Thread.currentThread() != Looper.getMainLooper().getThread()) {
               throw new IllegalStateException("Este metodo deberia ser llamado por el hilo principal");
           }
       }
   }
}


Ahora lo más importante a implementar, el método getView() este podría ser un ejemplo:


@Override
public View getView(int position, View convertView, ViewGroup parent) {


   View rootView = LayoutInflater.from(context)
     .inflate(R.layout.noticias, parent, false);


   ImageView fotoView = (ImageView) rootView.findViewById(R.id.foto);
   TextView noticiaView = (TextView) rootView.findViewById(R.id.noticia);


   Noticia noticia = getItem(position);
   fotoView.setText(noticia.getFoto());
   noticiaView.setImageResource(noticia.getNoticia());


   return rootView;
}


También podríamos tener en cuenta que ListView recicla las vistas que no se muestran más, y nos las da de vuelta a través del objeto convertVIew, esto es un ejemplo de cómo podemos aprovecharlo:


@Override
public View getView(int position, View convertView, ViewGroup parent) {


   if (convertView == null) {
       convertView = LayoutInflater.from(context)
         .inflate(R.layout.noticias, parent, false);
   }


   ImageView fotoView = (ImageView) rootView.findViewById(R.id.foto);
   TextView noticiaView = (TextView) rootView.findViewById(R.id.noticia);


   Noticia noticia = getItem(position);
   fotoView.setText(noticia.getFoto());
   noticiaView.setImageResource(noticia.getNoticia());


   return rootView;
}


Un problemita con findViewById


Podemos tener un problema con este método, cada vez que el método getView es llamado, nos devuelve nuestro objetos por medio de findViewById, este es un ejemplo muy simplificado de cómo funciona:


public View findViewById(int id) {
   if (this.id == id) {
       return this;
   }
   for(View child : children) {
       View view = child.findViewById(id);
       if (view != null) {
           return view;
       }
   }
   return null;
}


Como se puede ver, este método navega a través de toda la jerarquía de la vista, hasta encontrar la vista que le pedimos y esto lo realiza cada vez que lo llamamos. Que esto sea un problema o no, va a depender mucho de nuestra aplicación y dispositivo en el que lo hagamos correr. Puede que no necesitemos la optimización, en caso contrario podemos utilizar el traceview y medir cuánto tiempo gastamos con el.


El patrón ViewHolder


Este patrón nos ayudará a limitar la cantidad de llamadas al método findViewById. La idea sería llamarlo una vez, y entonces guardar la vista hija a la que hace referencia en la instancia de ViewHolder que será asociada con el objeto convertView gracias al método View.setTag()


private static class ViewHolder {
   public final ImageView fotoView;
   public final TextView noticiaView;


   public ViewHolder(ImageView fotoView, TextView noticiaView) {
       this.fotoView = fotoView;
       this.noticiaView = noticiaView;
   }
}


@Override
public View getView(int position, View convertView, ViewGroup parent) {


   ImageView fotoView;
   TextView noticiaView;
   if (convertView == null) {
       convertView = LayoutInflater.from(context)
         .inflate(R.layout.noticias, parent, false);
       fotoView = (ImageView) convertView.findViewById(R.id.foto);
       noticiaView = (TextView) convertView.findViewById(R.id.noticia);
       convertView.setTag(new ViewHolder(fotoView, noticiaView));
   } else {
       ViewHolder viewHolder = (ViewHolder) convertView.getTag();
       fotoView = viewHolder.fotoView;
       noticiaView = viewHolder.noticiaView;
   }


   Foto foto = getItem(position);
   noticiaView.setText(foto.getNoticia());
   fotoView.setImageResource(foto.getFoto());


   return convertView;
}


Tag con el ID?


Una alternativa al patrón anterior, que podemos comenzar a usar desde la versión de la API 15 (Android 4) No es recomendable utilizarlo con versiones anteriores.


Ahora el ejemplo:


@Override
public View getView(int position, View convertView, ViewGroup parent) {


   ImageView fotoView;
   TextView noticiaView;
   if (convertView == null) {
       convertView = LayoutInflater.from(context)
         .inflate(R.layout.noticias, parent, false);
       fotoView = (ImageView) convertView.findViewById(R.id.foto);
       noticiaView = (TextView) convertView.findViewById(R.id.noticia);
       convertView.setTag(R.id.foto, fotoView);
       convertView.setTag(R.id.noticia, noticiaView);
   } else {
       fotoView = (ImageView) convertView.getTag(R.id.foto);
       noticiaView = (TextView) convertView.getTag(R.id.noticia);
   }


   Foto foto = getItem(position);
   noticiaView.setText(foto.getNoticia());
   fotoView.setImageResource(foto.getFoto());


   return convertView;
}



ViewGroup personalizado


Con esta tercera opción, obtendremos un mejor desacoplamiento ;)


@Override
public View getView(int position, View convertView, ViewGroup parent) {
   FotoNoticiaView fotoNoticiaView;
   if (convertView == null) {
       fotoNoticiaView = (FotoNoticiaView) LayoutInflater.from(context)
         .inflate(R.layout.foto_noticia, parent, false);
   } else {
       fotoNoticiaView = (FotoNoticiaView) convertView;
   }


   FotoNoticia fotoNoticia = getItem(position);


   fotoNoticiaView.update(fotoNoticia);


   return convertView;
}



Benoît Lubek en la red social G+, enlace que dejaré más abajo, sugirió otro enfoque que puede ser bastante interesante, por las dudas se los dejo acá :D


public class ViewHolder {
   // He añadido un tipo de retorno genéricos para reducir el ruido que genera el casting   
  // en el código del cliente
   @SuppressWarnings("unchecked")
   public static <T extends View> T get(View view, int id) {
       SparseArray<View> viewHolder = (SparseArray<View>) view.getTag();
       if (viewHolder == null) {
           viewHolder = new SparseArray<View>();
           view.setTag(viewHolder);
       }
       View childView = viewHolder.get(id);
       if (childView == null) {
           childView = view.findViewById(id);
           viewHolder.put(id, childView);
       }
       return (T) childView;
   }
}


Y esta sería la implementación del método getView:


@Override
public View getView(int position, View convertView, ViewGroup parent) {


   if (convertView == null) {
       convertView = LayoutInflater.from(context)
         .inflate(R.layout.foto_notocia, parent, false);
   }


   ImageView fotoView = ViewHolder.get(convertView, R.id.foto);
   TextView noticiaView = ViewHolder.get(convertView, R.id.noticia);


   FotoNoticia fotoNoticia = getItem(position);
   noticiaView.setText(fotoNoticia.getNoticia());
   fotoView.setImageResource(fotoNoticia.getFoto());


   return convertView;
}



Sitio Oficial:


Sitio no-oficial:



Esto fue todo por ahora, espero que les guste y sea de utilidad, realmente puede ser un poco complicado al principio, y nos den ganas de usar ArrayAdapter :D pero una vez que se acostumbran, se van a dar cuenta que es lo mejor y que es bastante sencillo implementarlo.

Saludos a todos, Gabriel