Un mundo lleno de eventos
El trabajo en una empresa requiere la movilización de gran cantidad de personal para el desarrollo de una sola aplicación: el briefing con el cliente, el análisis de requisitos, la elaboración de la base de datos, los servicios web, picar código, picar código, picar código. Una de las tareas más duraderas del proceso de creación del hardware es la de escribir el código. Es por esto que cualquier cosa que pueda evitarnos picar más código y además, facilitar la labor es bienvenida.¿Por qué usar eventos? La modularidad
Muchas de las aplicaciones que desarrolla una empresa tienen las mismas funcionalidades, por lo que se puede aprovechar el código de unas a otras. Si ya está hecho, ¿para qué vamos a invertir tiempo en hacerlo otra vez? Y en estrecha relación con este objetivo se encuentra la idea de la modularidad. La modularidad consiste en codificar de manera que el sistema esté compuesto por módulos independientes que, aunque interactúen entre sí, dependan lo mínimo posible los unos de los otros. De esta manera, unos módulos se pueden aprovechar más adelante haciendo cambios mínimos. Una biblioteca que contribuye a esta modularidad —así como a mantener una comunicación más sencilla entre nuestras clases— es Otto con su clase Bus. Gracias a esta biblioteca, podemos mandar eventos entre clases sin que tengamos que forzar que solo una clase pueda recibir el evento. Imagina que tenemos la clase CocinaActivity. Para cocinar, CocinaActivity necesita que la clase HornilloFragment llame a sus métodos
abrirGas()
, producirChispa()
y prenderLlama()
. Una forma de hacer esto sería darle a CocinaActivity un objeto de la clase HornilloFragment y que esta primera los llamara. Ahora, en el futuro realizamos una aplicación donde podemos reutilizar CocinaActivity, pero por desgracia hay que cambiar todas las referencias a HornilloFragment porque en nuestro proyecto usamos la clase VitroceramicaFragment, y para cocinar, CocinaActivity solo tiene que llamar al método encender()
de esta.
Si, en lugar de haberle dado un objeto HornilloFragment, hubiéramos emitido un evento para que lo recogiera la clase correspondiente, ahora solo tendríamos que decirle a VitroceramicaFragment que estuviese pendiente de ese evento para encenderse en lugar de tener que cambiar todas las referencias a HornilloFragment que hay en la clase CocinaActivity.
CocinaActivity diría simplemente «necesito cocinar» y VitroceramicaFragment u HornilloFragment o cualquier otra clase futura, estaría pendiente de cuándo CocinaActivity dice eso para ejecutar los métodos que pertinentes. Incluso podría haber otras veinte clases más esperando a que CocinaActivity quiera cocinar para ejecutar código, por lo que podríamos hacer que esta única orden llegara a todo el que le interesa.
Pues básicamente, con Otto podemos publicar este mensaje emitido por CocinaActivity y recogerlo en cualquier otra clase, igual que un BroadcastReceiver pero sin IntentFilter.
Paso a paso
En este tutorial vamos a aprender a publicar eventos y a suscribirse a ellos, igual que se publica y te suscribes a un boletín de noticias. Así pues, estos son los puntos que cubriremos en esta explicación:
- Añadir la biblioteca de Otto a nuestras dependencias y crear una clase singleton
- Registrar una actividad para usar un bus
- Publicar y subscribirse a eventos
- Proyecto
- Uso de un evento más realista
- ¿Por qué hay que desregistrarlas clases?
- Subscribirse a eventos en una clase padre
- Producir un evento con
@Provide
1. Añadir la biblioteca a las dependencias y crear una clase singleton
Para usar Otto debemos compilar la siguiente biblioteca en las dependencias:
1 2 3 4 5 |
dependencies{ compile 'com.squareup:otto:1.3.8' } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class BusProvider { private BusProvider(){} private static Bus bus; public synchronized static Bus getBus(){ if (bus == null){ bus = new Bus(ThreadEnforcer.ANY); } return bus; } } |
getBus()
comprueba si el atributo bus
tiene valor. Si no tiene, lo inicializa y por último lo devuelve, así que, como podéis ver, cada vez que llamemos a este método, estaremos obteniendo el mismo objeto.
La instancia de la clase Bus que hemos creado permitirá que se envíen eventos tanto desde hilo principal (UIThread) como desde cualquier otro hilo que creemos. Si queremos obligarlo a que solo se ejecuten eventos en el hilo principal, Bus permite que en el constructor le pasemos el parámetro ThreadEnforcer(MAIN)
, pero esta solución fallará al intentar publicar un evento desde un hilo secundario que modifique las vistas, por lo que a continuación os dejo otra clase BusProvider que solo ejecuta eventos en el UIThread (user interface thread):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
public class BusProvider { private BusProvider(){} private static Bus bus; public synchronized static Bus getBus(){ if (bus == null){ bus = new MainThreadBus(); } return bus; } public static class MainThreadBus extends Bus { private final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); @Override public void post(final Object event) { if (Looper.myLooper() == Looper.getMainLooper()) { super.post(event); } else { mainThreadHandler.post(new Runnable() { @Override public void run() { post(event); } }); } } } } |
post()
haciendo que todo lo que se publique desde un hilo secundario se envíe al Looper del UIThread.
Si todo esto de los Threads te suena a chino, no te preocupes. Simplemente copia la clase y no te hagas más preguntas. Tu salud mental te lo agradecerá.
La tarea de crear un singleton se ve facilitada por la recién publicada versión 1.3.1 de Android Studio, que incorpora la opción de crear un singleton al pulsar el botón secundario sobre un directorio para crear una clase.
2. Publicar un evento
Un evento es cualquier objeto que queramos mandar de una clase a otra, ya sea un String, Integer o una clase que nosotros hayamos creado. Para publicar un evento solo necesitamos disponer de una instancia de la clase Bus. En las Activities obtenemos un objeto Bus en el
onCreate()
, en los Fragments lo hacemos en el onCreateView()
y en cualquier otra clase podemos hacerlo en el constructor. Y luego es tan sencillo como llamar al método bus.post()
pasándole el objeto que queremos publicar:
Así, por ejemplo, en la clase CocinaActivity publicamos un String:
CocinaActivity.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class CocinaActivity extends AppCompatActivity{ private Bus bus; @Override public void onCreate(){ this.bus = BusProvider.getBus(); } public void cocinar(){ bus.post("Quiero cocinar"); } } |
cocinar()
, se publicará un evento (en este caso un objeto String).
3. Subscribirse a un evento
Para recibir eventos es necesario registrar la clase en el Bus. Registrar es una forma de decirle al
bus
, «Hola, quiero enterarme de lo que dicen las otras clases», igual que registras en una página de noticias para recibir sus noticias. Si no queremos recibir eventos en esa clase, no registramos la clase, ya que esto es memoria desperdiciada.
En el caso de una Activity o de un Fragment, registramos la clase en el onResume()
y la desregistramos en el onPause()
. Más adelante se da una explicación sobre por qué hay que desregistrar la clase.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class VitroceramicaFragment extends Fragment{ private Bus bus; @Override public void onCreateView(){ this.bus = BusProvider.getBus(); […] } @Override public void onResume(){ super.onResume(); bus.register(this); } @Override public void onPause(){ super.onPause(); bus.unregister(this); } } |
@Subscribe
que reciba el mismo tipo de objeto publicado, es decir, un String:
1 2 3 4 5 6 7 8 |
@Subscribe public void onStringPosted(String mensaje){ if (mensaje.equals("Quiero cocinar")) Toast.makeText(this, "Cocina ha dicho " + mensaje, Toast.LENGTH_SHORT).show(); encender(); } |
@Subscribe
y cuyo único parámetro formal sea del tipo String, recibirá dicha objeto cuando sea publicado. En este insulso ejemplo, VitroceramicaFragment muestra con un Toast el objeto String que le ha mandado CocinaActivity. Si hubiera cuarenta clases con un método que se suscribe a recibir un String, las 40 lo recibirán. El nombre del método con la anotación @Subscribe
es indiferente. Lo único importante es que el parámetro formal sea del mismo tipo que el publicado.
4. Código
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout 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" android:gravity="center_horizontal" android:orientation="vertical"> <LinearLayout android:layout_width="wrap_content" android:layout_height="0dp" android:layout_weight="1" android:orientation="vertical"> <TextView android:text="Cocina" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="40sp"/> <FrameLayout android:id="@+id/contenedorFragment" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout> <Button android:id="@+id/botonCocinar" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="COCINAR" android:layout_gravity="center"/> </LinearLayout> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#55ba78" android:gravity="center"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Vitrocerámica" android:layout_gravity="center" android:textSize="30sp"/> <TextView android:id="@+id/estado" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:textSize="25sp" android:textStyle="italic"/> </LinearLayout> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
package tutoriales.codictados.pruebaotto; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.widget.Button; import com.squareup.otto.Bus; import com.squareup.otto.Subscribe; public class CocinaActivity extends AppCompatActivity { private Bus bus; private VitroceramicaFragment vitroceramicaFragment; private Button botonEncender; private boolean puedoCocinar; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); this.bus = BusProvider.getBus(); this.botonEncender = (Button) findViewById(R.id.botonCocinar); vitroceramicaFragment = new VitroceramicaFragment(); getSupportFragmentManager().beginTransaction() .add(R.id.contenedorFragment,vitroceramicaFragment) .commit(); botonEncender.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (!puedoCocinar) bus.post("Quiero cocinar"); else bus.post("No quiero cocinar más"); } }); } @Override protected void onResume() { super.onResume(); bus.register(this); } @Override protected void onPause() { super.onPause(); bus.unregister(this); } @Subscribe public void onBooleanEvent (Boolean respuesta){ if (respuesta) { puedoCocinar = true; botonEncender.setText("NO COCINAR"); } else { puedoCocinar = false; botonEncender.setText("COCINAR"); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
package tutoriales.codictados.pruebaotto; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import com.squareup.otto.Bus; import com.squareup.otto.Subscribe; /** * Created by Juan José Melero on 12/08/2015. */ public class VitroceramicaFragment extends Fragment { private Bus bus; private boolean encendido; private TextView estado; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); bus = BusProvider.getBus(); } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View parent = inflater.inflate(R.layout.vitroceramica_fragment_layout, container, false); this.estado = (TextView) parent.findViewById(R.id.estado); estado.setText("Apagada"); return parent; } @Override public void onResume() { super.onResume(); bus.register(this); } @Override public void onPause() { super.onPause(); bus.unregister(this); } @Subscribe public void onStringEvent(String mensaje){ if (mensaje.equals("Quiero cocinar")){ estado.setText("Encendida"); encendido = true; bus.post(encendido); } else if (mensaje.equals("No quiero cocinar más")){ estado.setText("Apagada"); encendido = false; bus.post(encendido); } } } |
5. Uso de un evento más realista
Naturalmente, nunca vamos a publicar un String o un Integer. De forma profesional, cuando queremos lanzar un evento, se crea una clase concreta con el sufijo «Event», que contenga los objetos que necesitamos pasar y se publica siempre una nueva instancia. Ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class InfoUsuarioCambiadaEvent { private Usuario usuario; private String password; private Bitmap imagen; public InfoUsuarioCambiadaEvent(Usuario usuario, String password, Bitmap imagen) { this.imagen = imagen; this.password = password; this.usuario = usuario; } /* AQUÍ VAN TODOS LOS GETTERS Y SETTERS*/ } |
1 2 3 |
bus.post(new InfoUsuarioModificadaEvent(usuario, password, foto)); |
1 2 3 4 5 6 7 8 |
@Subscribe public void onInfoUsuarioModificadaEvent (InfoUsuarioModificadaEvent event){ if (event.getUsuario != null && event.getPassword() != null && event.getImagen() != null){ //Hacer algo } } |
6. ¿Por qué hay que desregistrar las clases?
No es obligatorio, pero sí que es importante desregistrar las clases porque, si no, se estaría produciendo una pequeña fuga de memoria. El Bus tiene una lista de clases que se registran. Si registramos una Activity al iniciarse y no la desregistramos al finalizar, cuando vuelva a iniciarse, se volverá a registrar y la lista será cada vez mayor.
7. Subscribirse a eventos en una clase padre
El propio Jake Wharton, autor de esta biblioteca, explica en una publicación de GitHub (https://github.com/square/otto/issues/26) que los método con anotaciones
@Subscribe
en una clase padre no instanciada directamente no funcionarán, pero que esto se debe a las propias limitaciones de Java. Por ejemplo, tenemos una clase MyActivity que hereda de MyBaseActivity y en esta última hacemos un método con una anotación @Subscribe
. Si instanciamos MyActivity, ese método que hemos puesto en la clase padre no funcionará aunque registremos MyActivity en el Bus. SI por el contrario instanciamos MyBaseActivity, sí. Aunque es preferible no hacerlo así. si queremos sortear este problema y hacer que funcionen los métodos @Subscribe
en una clase padre no instanciada, la solución consiste en declara el método como un atributo de la clase de tipo Object, registrarlo en el bus en el onResume()
y desregistrarlo en el onPause()
de la siguiente manera:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public Object miMetodo = new Object(){ @Subscribe public void onCualquierEvento(CualquierEvento evento){ //Codigo } }; @Override protected void onResume(){ super.onResume(); bus.register(miMetodo); } @Override public void onPause(){ super.onPause(); bus.unregister(miMetodo); } |
8. La anotación @Produce
Otra forma adicional de publicar eventos es utilizando la anotación
@Produce
al principio de un método que devuelva algo. Este método será llamado inmediatamente al ser creada la Activity y se publicará el objeto que devuelva dicho método. Ejemplo:
1 2 3 4 5 6 |
@Produce public String produceEvent(){ return "hello"; } |