Desarrollo rápido en Android con anotaciones

Si llevas un tiempo programando en Java recordarás esas decenas de líneas de código de inicialización y aquellos enormes XML de configuración con los que teníamos que lidiar en frameworks como Spring, JPA o JSF antes de que se popularizaran las anotaciones. En Android esa tendencia todavía no ha impuesto, pero os alegrará saber que sí existen librerías externas con las que reducir enormemente el código auxiliar y centrarnos en lo que realmente importa.

La opción más interesante es AndroidAnnotations, ya que es muy completa, no impone penalización alguna en el rendimiento y no nos obliga a que nuestras actividades extiendan de una cierta clase, lo que al carecer Java de herencia múltiple complicaría ligeramente el uso de librerías como ActionBarSherlock. ¿Suena bien? Entonces genera un proyecto con AndroidAnnotations en AndroidKickstarR y sigue leyendo; te aseguro que no te vas a arrepentir.

Asociar un layout a una actividad o fragmento

Sin AndroidAnnotations:

public class Actividad extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.actividad);
    }
}

Con AndroidAnnotations podemos usar la anotación @EActivity (de enhanced activity o actividad mejorada) y pasarle el identificador del layout:

@EActivity(R.layout.actividad)
public class MyActivity extends Activity {}

De forma parecida a las actividades, con la anotación @EFragment podemos obviar el método onCreateView del fragmento. Sin AndroidAnnotations:

public class Fragmento extends Fragment {
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragmento, container, false);
        return view;
    }
}

Con AndroidAnnotations:

@EFragment(R.layout.fragmento)
public class Fragmento extends Fragment {
}

Inyección de vistas

Sin AndroidAnnotations:

public class Actividad extends Activity {
	Button btnGuardar;
	EditText txtNombre;

	@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.actividad);
		
		btnGuardar = (Button) findViewById(R.id.btnGuardar);
		txtNombre = (EditText) findViewById(R.id.txtNombre);
    }
}

Con AndroidAnnotations usaríamos la anotación @ViewById pasando como parámetro el identificador de la vista

@EActivity(R.layout.actividad)
public class Actividad extends Activity {
	@ViewById(R.id.btnGuardar)
	Button btnGuardar;
	
	@ViewById(R.id.txtNombre)
	EditText txtNombre;
}

Si el nombre de la variable es igual al identificador de la vista el parámetro se puede obviar:

@EActivity(R.layout.actividad)
public class Actividad extends Activity {
	@ViewById Button btnGuardar;
	@ViewById EditText txtNombre;
}

También podemos inyectar fragmentos con @FragmentById

@EActivity(R.layout.actividad)
public class Actividad extends Activity {
	@ViewById Button btnGuardar;
	@ViewById EditText txtNombre;
	@FragmentById Fragmento fragmento;
}

Manipular las vistas

Si usamos @ViewById para inyectar la vistas, por la manera que tiene de funcionar AndroidAnnotations, no tendremos todavía las instancias asociadas en onCreate, por lo que si tenemos que hacer algo con las vistas tendremos que anotar uno o varios métodos con @AfterViews. Esto, que a alguno le puede parecer un incordio, a mí personalmente me gusta, porque prefiero extraer el resto del código a otros métodos siguiendo el Principio de Responsabilidad Única.

De esta forma un código como

public class Actividad extends Activity {
	TextView txtVersion;
	TextView txtFecha;

	@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.actividad);
		
		txtVersion = (TextView) findViewById(R.id.txtVersion);
		txtFecha = (TextView) findViewById(R.id.txtFecha);
		
		establecerVersion();
		establecerFecha();
    }
	
	void establecerVersion() {
		try {
			PackageInfo infoPaquete = getPackageManager().getPackageInfo(getPackageName(), 0);
			txtVersion.setText(infoPaquete.versionName);
		} catch(NameNotFoundException e) {
			e.printStackTrace();
		}
	}
	
	void establecerFecha() {
		DateFormat formato = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss");
		Date fecha = new Date();
		String strFecha = formato.format(fecha);
		txtFecha.setText(strFecha);
	}
}

se convertiría en lo siguiente

@EActivity(R.layout.actividad)
public class Actividad extends Activity {
	@ViewById TextView txtVersion;
	@ViewById TextView txtFecha;
	
	@AfterViews
	void establecerVersion() {
		try {
			PackageInfo infoPaquete = getPackageManager().getPackageInfo(getPackageName(), 0);
			txtVersion.setText(infoPaquete.versionName);
		} catch(NameNotFoundException e) {
			e.printStackTrace();
		}
	}
	
	@AfterViews
	void establecerFecha() {
		DateFormat formato = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss");
		Date fecha = new Date();
		String strFecha = formato.format(fecha);
		txtFecha.setText(strFecha);
	}
}

Eventos

Android cuenta con un atributo XML android:onClick con el que es muy rápido y sencillo asociar un listener a un evento clic. El equivalente en AndroidAnnotations es la anotación @Click, a la que se le pasa el identificador de la vista a la que se asocia el listener (se puede obviar en caso de que el nombre del método coincida con el identificador de la vista o que sea igual pero con el sufijo Clicked).

@EActivity(R.layout.actividad)
public class Actividad extends Activity {
	@Click(R.id.button1)
	void mostrarVersion() {
		// ...
	}

	@Click
	void mostrarFecha() {
		// ...
	}

	@Click
	void guardarClicked() {
		// ...
	}
}

En este caso la ventaja de las anotaciones no es tan obvia; quizás el poder ver la asociación entre la vista y el método en un golpe de vista. Tiene mayor interés el que podamos asociar listeners para unos pocos eventos más de forma igual de sencilla, con @LongClick para pulsaciones largas o @TextChange para cambios de texto.

Menús

Con la anotación @OptionsMenu podemos inflar el XML de menú, ahorrándonos sobreescribir onCreateOptionsMenu. Con @OptionsItem asociamos las distintas opciones de menú con distintos listeners, pasando el id de la opción a la anotación (también se puede obviar el id si coincide con el nombre del método, o si es igual pero con el sufijo Selected).

Versión sin anotaciones:

public class Actividad extends Activity {
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.actividad);
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		getMenuInflater().inflate(R.menu.actividad, menu);
		return true;
	}

	
	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		switch (item.getItemId()) {
			case R.id.actualizar:
				actualizar();
				return true;
			case R.id.acercaDe:
				acercaDeSelected();
				return true;
			default:
			  return false;
		}
	}

	
	void actualizar() {
		// ...
	}	

	
	void acercaDeSelected() {
		// ...
	}	
}

Versión con anotaciones:

@EActivity(R.layout.actividad)
@OptionsMenu(R.menu.actividad)
public class Actividad extends Activity {
	@OptionsItem(R.id.actualizar)
	void actualizar() {
		// ...
	}	

	@OptionsItem
	void acercaDeSelected() {
		// ...
	}	
}

Inyección de recursos

Para cada tipo de recurso existe una anotación que permite inyectar dicho recurso en nuestra clase pasándole simplemente el identificador (o sin pasarle nada si el nombre del recurso coincide con el de la variable): AnimationRes, BooleanRes, ColorRes, ColorStateListRes, DimensionPixelOffsetRes, DimensionPixelSizeRes, DimensionRes, DrawableRes, HtmlRes, IntArrayRes, IntegerRes, LayoutRes, MovieRes, StringArrayRes, StringRes, TextArrayRes, TextRes.

De esta forma en lugar de:

String nombreApp = getResources().getString(R.string.nombreApp);

simplemente podríamos escribir:

@StringRes String nombreApp;

Además, en el caso concreto de que queramos obtener una cadena HTML de un archivo de recurso y establecerla como texto en un TextView, podemos llevar a cabo ambos pasos con una simple anotación @FromHtml.

Hilos de ejecución

¿Harto de usar AsyncTask o similares para ejecutar un cierto código en un hilo distinto del principal o de interfaz de usuario?

void ejecutarTareaSegundoPlano(URL urls) {
	new AsyncTask<URL, Integer, Long> {
		protected Long doInBackground(URL... urls) {
			int numUrls = urls.length;
			Long tamanyoDescarga = 0;
			for (int i = 0; i < numUrls; i++) {
				tamanyoDescarga += ClienteHttp.descargar(urls[i]);
				publishProgress((int) ((i / (float) numUrls) * 100));
			}
			return totalSize;
		}

		protected void onProgressUpdate(Integer... progreso) {
			actualizarProgreso(progreso[0]);
		}

		protected void onPostExecute(Long tamanyoDescarga) {
			mostrarDialogo("Descargados " + tamanyoDescarga[0] + " bytes");
		}
	}.execute();
}

¿Qué tal si pudieras usar la anotación @Background para indicar que un método debe ejecutarse en un hilo en segundo plano y @UiThread para que se ejecute en el hilo de interfaz de usuario? De esta forma el ejemplo anterior se reduciría a:

@Background
void descargarArchivos(URL urls) {
	int numUrls = urls.length;
	Long tamanyoDescarga = 0;
	for (int i = 0; i < numUrls; i++) {
		tamanyoDescarga += ClienteHttp.descargar(urls[i]);
		actualizarEstadoDescarga((int) ((i / (float) numUrls) * 100));
	}
	mostrarDialogoFinDescarga(tamanyoDescarga);
}

@UiThread
void actualizarEstadoDescarga(int progreso) {
	actualizarProgreso(progreso);
}

@UiThread
void mostrarDialogoFinDescarga(Long tamanyoDescarga) {
	mostrarDialogo("Descargados " + tamanyoDescarga + " bytes");
}

Guardar y recuperar el estado

Esta es otra de esas tareas repetitivas que suponen gran cantidad de código. Comparemos una implementación típica con su correspondiente versión con anotaciones.

public class Actividad extends Activity {
    Jugador jugador;
    int puntuacion;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        if(savedInstanceState != null) { 
            jugador = (Jugador)savedInstanceState.getSerializable("jugador");
			puntuacion = savedInstanceState.getInt("puntuacion");
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putSerializable("jugador", jugador);
        outState.putInt("puntuacion", puntuacion);
    }
}
@EActivity(R.layout.actividad)
public class Actividad extends Activity {
    @InstanceState
    Jugador jugador;

    @InstanceState
    int puntuacion;
}

Otras funcionalidades de interés

  • @Trace nos permite trazar la ejecución de un método escribiendo una entrada en el log cuando se entra y se sale del método.
  • @HierarchyViewerSupport permite utilizar la herramienta Hierarchy Viewer aunque no dispongamos de un teléfono con permisos de root o de desarrollo
  • @RoboGuice nos permite integrar AndroidAnnotation con el framework de inyección de dependencias del mismo nombre (basado en Guice, de Google)
  • @OrmLiteDao nos permite integrar AndroidAnnotation con el framework de mapeo objeto-relacional OrmLite
  • @EView y @EViewGroup facilitan la creación de vistas personalizadas
  • @SystemService nos permite inyectar un servicio del sistema


Comentarios
  1. Donnie Emanuel

    Muy interesante, realmente una herramienta muy potente.

    Responder

  2. Willian Andres

    Amigo disculpa, yo quiero utilizar en mi proyecto, la anotacion de @ViewById pero me esale error, y el pluging me propone crear una nueva anotación en donde me sale wizard para hacerlo.
    Preg.1¿Tengo que desacrgarlos?
    Preg.2¿Tengo que configurar algo de eclipse?
    De antetodo gracias por tu ayuda.

    Responder

  3. Muy buen articulo. Creo que tenes la mejor documentacion de AndroidAnnotation en español. Es formidable utilizarlo, y sobre todo para deshacerse de codigo redundante. Y claro es mejor que genere subclases en vez de que tengamos que adaptar los nuestros de la forma como lo hacen en RobotGuice.

    Responder

Deja un comentario