Cómo utilizar Annotations en Android

Hasta hace poco no le había prestado mucha atención a las annotations de Java, y ha sido ahora cuando he empezado a utilizarlas y darme cuenta de su enorme potencial. Estas añaden metadatos a métodos, variables, propiedades, etc... Y son estos metadatos y la forma de procesarlos los que nos darán esa utilidad.



Muchos ya estaréis cansados de ver annotations en código Java, el más común sin duda es @override, que indica que un método se sobreescribe. Pero, ¿y si queremos dar de alta nosotros uno? Es muy sencillo, y similar a crear una clase. Pero en este caso debemos indicar que es una annotation.



Con Eclipse debemos hacer botón derecho en el package que queremos incluirlo, New > Annotation. y con Android Studio, New -> Java Class > escribimos un nombre y seleccionamos Annotation. Yo crearé una nueva annotation en un package llamado annotations llamada ItemWidget y que tendrá el siguiente código:

/**
 * Define propiedades de un determinado Widget para poder instanciarlo
 */
@java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD})
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
public @interface ItemWidget {
    int identifier(); Class className();
}

Nuestra annotation a su vez tendrá dos annotations que le indican cual será su target o destino, en este caso los elementos tipo field, y como será almacenado este campo.

La estructura de nuestra annotation es muy simple, se declara como public @interface y se le da un nombre. Dentro de ella se declaran los parámetros (si los tuviera). En este caso voy a definir un dato tipo int llamado identifier que almacenará el identificador de un widget, y Class para saber que tipo de widget es y poder instanciarlo correctamente.

No es necesario en este caso, pero se pueden asignar valores por defecto a nuestros parámetros de la siguiente forma:

int identifier() default 0;

El siguiente paso es encontrarle una utilidad a nuestra annotation. En mi caso yo quiero instanciar automáticamente los widgets de cualquier Activity. Para ello voy a crear una nueva clase en el package views llamada LayoutActivity y que hereda de Activity. El resto de Activities heredarán de esta para que sea cual sea la Activity siempre llame a nuestro método que carga los widgets. Veamos como hacerlo:

/**
 * Clase base para inicializar por defecto todo lo que necesitemos en una {@link Activity}
 */
public class LayoutActivity extends Activity {

    /**
     * Identificador de la vista que utilizará la Activity
     * @return
     */
    protected int getLayout(){return 0;}

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if(getLayout() != 0){
            setContentView(getLayout());
        }

        for(Field field: ((Object) this).getClass().getFields()){
            if(field.isAnnotationPresent(ItemWidget.class)){
                ItemWidget itemWidget = field.getAnnotation(ItemWidget.class);
                try {
                    field.set(this, (itemWidget.className().cast(findViewById(itemWidget.identifier()))) );
                } catch (IllegalAccessException e) {
                }
            }
        }

    }

}

En esta clase he creado un getter para setear fácilmente el layout y que sea esta clase también la que setee la vista necesaria. La parte que nos ocupa en este artículo está en el onCreate, es ahí donde inicializaremos todos aquellos elementos que marquemos con nuestra annotation.

Para ello obtenemos los Fields de nuestra clase actual y los recorremos, verificando para cada uno de ellos si está presente la annotation ItemWidget. En caso de estar presente obtenemos el ItemWidget del field en el que estamos y con los parámetros identifier y className, instanciamos el widget correspondiente y lo seteamos en field.

Ya veis como con el uso combinado de Reflection y las Annotations podemos hacer métodos muy genéricos y fácilmente seteables. El último paso, es ver como útilizarlo, para ello en una de nuestras Activities, modificamos su herencia por LayoutActivity, y en el layout añadimos unos cuantos widgets de diferentes tipos, yo tengo un TextView, un CheckBox y un Button. Así:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">

    <TextView
            android:id="@+id/lblHello"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world" />

    <CheckBox android:id="@+id/chk01" android:text="Checkbox 01"
              android:layout_below="@+id/lblHello"
              android:layout_alignParentLeft="true"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"/>
    <Button android:id="@+id/btn02" android:text="botón"
            android:layout_below="@+id/chk01"
            android:layout_alignParentLeft="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

</RelativeLayout>

Y nuestra Activity quedará así:

public class MainActivity extends LayoutActivity {

    /**
     * La clase padre busca los elementos con la annotation {@link ItemWidget} y los inicializará
     */
    @ItemWidget(identifier = R.id.lblHello, className = TextView.class)
    public TextView lblHello;

    @ItemWidget(identifier = R.id.chk01, className = CheckBox.class)
    public CheckBox chk01;

    @ItemWidget(identifier = R.id.btn02, className = Button.class)
    public Button btn02;

    @Override
    protected int getLayout(){return R.layout.activity_main;}

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        /**
         * Aquí podemos ver como no es necesario inicializar {@link btn02} y {@link chk01} para comenzar a utilizarlos
         */
        btn02.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                chk01.setChecked(!chk01.isChecked());
            }
        });

    }


    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }
   
}

Como siempre hemos hecho, declaramos los Widgets de nuestra Activity, pero ahora añadimos nuestra annotation, a cada uno le indicamos el identificador que tiene su Widget relacionado y su tipo. Y ahora vereis que no es necesario inicializar ningún widget, podemos empezar a utilizarlo con total tranquilidad porque la clase de la que hereda se ha encargado de instanciarlo.

Si queréis ver el código completo de este ejemplo funcionando podéis revisarlo en nuestra cuenta de github.com en https://github.com/3pies/android.samples. Estáis invitados a hacer fork de este ejemplo y el resto, y cualquier modificación que creáis que puede ayudar a mejorar el código no dudéis en hacerla y enviarnos vuestros cambios.

Comments are closed.