Conozca RxJava: la biblioteca de programación reactiva que falta para Android
Publicado: 2022-03-11Si es un desarrollador de Android, es probable que haya oído hablar de RxJava. Es una de las bibliotecas más discutidas para habilitar la programación reactiva en el desarrollo de Android. Se promociona como el marco de referencia para simplificar las tareas de concurrencia/asincrónicas inherentes a la programación móvil.
Pero… ¿qué es RxJava y cómo “simplifica” las cosas?
Si bien ya hay muchos recursos disponibles en línea que explican qué es RxJava, en este artículo mi objetivo es brindarle una introducción básica a RxJava y específicamente cómo encaja en el desarrollo de Android. También daré algunos ejemplos concretos y sugerencias sobre cómo puede integrarlo en un proyecto nuevo o existente.
¿Por qué considerar RxJava?
En esencia, RxJava simplifica el desarrollo porque eleva el nivel de abstracción en torno a la creación de subprocesos. Es decir, como desarrollador, no tiene que preocuparse demasiado por los detalles de cómo realizar operaciones que deberían ocurrir en diferentes subprocesos. Esto es particularmente atractivo ya que es difícil hacer bien los subprocesos y, si no se implementa correctamente, puede causar algunos de los errores más difíciles de depurar y corregir.
Por supuesto, esto no significa que RxJava sea infalible cuando se trata de subprocesos y aún es importante comprender lo que sucede detrás de escena; sin embargo, RxJava definitivamente puede facilitarle la vida.
Veamos un ejemplo.
Llamada de red - RxJava vs AsyncTask
Digamos que queremos obtener datos a través de la red y, como resultado, actualizar la interfaz de usuario. Una forma de hacer esto es (1) crear una subclase AsyncTask
interna en nuestra Activity
/ Fragment
, (2) realizar la operación de red en segundo plano y (3) tomar el resultado de esa operación y actualizar la interfaz de usuario en el hilo principal .
public class NetworkRequestTask extends AsyncTask<Void, Void, User> { private final int userId; public NetworkRequestTask(int userId) { this.userId = userId; } @Override protected User doInBackground(Void... params) { return networkService.getUser(userId); } @Override protected void onPostExecute(User user) { nameTextView.setText(user.getName()); // ...set other views } } private void onButtonClicked(Button button) { new NetworkRequestTask(123).execute() }
Por inofensivo que parezca, este enfoque tiene algunos problemas y limitaciones. Es decir, las fugas de memoria/contexto se crean fácilmente ya que NetworkRequestTask
es una clase interna y, por lo tanto, contiene una referencia implícita a la clase externa. Además, ¿y si queremos encadenar otra operación larga después de la llamada de red? Tendríamos que anidar dos AsyncTask
s que pueden reducir significativamente la legibilidad.
Por el contrario, un enfoque de RxJava para realizar una llamada de red podría verse así:
private Subscription subscription; private void onButtonClicked(Button button) { subscription = networkService.getObservableUser(123) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1<User>() { @Override public void call(User user) { nameTextView.setText(user.getName()); // ... set other views } }); } @Override protected void onDestroy() { if (subscription != null && !subscription.isUnsubscribed()) { subscription.unsubscribe(); } super.onDestroy(); }
Con este enfoque, resolvemos el problema (de posibles fugas de memoria causadas por un subproceso en ejecución que contiene una referencia al contexto externo) al mantener una referencia al objeto de Subscription
devuelto. Luego, este objeto de Subscription
se vincula al método #onDestroy()
del objeto Activity
/ Fragment
para garantizar que la operación Action1#call
no se ejecute cuando sea necesario destruir la Activity
/ Fragment
.
Además, tenga en cuenta que el tipo de retorno de #getObservableUser(...)
(es decir, un Observable<User>
) está encadenado con más llamadas. A través de esta API fluida, podemos resolver el segundo problema del uso de AsyncTask
que es que permite un mayor encadenamiento de llamadas de red/operaciones largas. Bastante ordenado, ¿eh?
Profundicemos en algunos conceptos de RxJava.
Observable, observador y operador: las 3 O de RxJava Core
En el mundo de RxJava, todo se puede modelar como flujos. Una corriente emite elementos a lo largo del tiempo, y cada emisión se puede consumir/observar.
Si lo piensa, una transmisión no es un concepto nuevo: los eventos de clic pueden ser una transmisión, las actualizaciones de ubicación pueden ser una transmisión, las notificaciones automáticas pueden ser una transmisión, etc.
La abstracción de flujo se implementa a través de 3 construcciones centrales que me gusta llamar "las 3 O"; a saber: el O bservable, el O bservador y el O perador. El Observable emite elementos (la corriente); y el observador consume esos artículos. Las emisiones de los objetos observables se pueden modificar, transformar y manipular aún más mediante el encadenamiento de llamadas del operador .
Observable
Un Observable es la abstracción de flujo en RxJava. Es similar a un iterador en que, dada una secuencia, itera y produce esos elementos de forma ordenada. Luego, un consumidor puede consumir esos artículos a través de la misma interfaz, independientemente de la secuencia subyacente.
Digamos que queríamos emitir los números 1, 2, 3, en ese orden. Para hacerlo, podemos usar el método Observable<T>#create(OnSubscribe<T>)
.
Observable<Integer> observable = Observable.create(new Observable.OnSubscribe<Integer>() { @Override public void call(Subscriber<? super Integer> subscriber) { subscriber.onNext(1); subscriber.onNext(2); subscriber.onNext(3); subscriber.onCompleted(); } });
Invocar subscriber.onNext(Integer)
emite un elemento en el flujo y, cuando el flujo termina de emitirse, se invoca subscriber.onCompleted()
.
Este enfoque para crear un Observable es bastante detallado. Por esta razón, existen métodos convenientes para crear instancias de Observable que deberían preferirse en casi todos los casos.
La forma más sencilla de crear un Observable es usando Observable#just(...)
. Como sugiere el nombre del método, simplemente emite los elementos que le pasa como argumentos del método.
Observable.just(1, 2, 3); // 1, 2, 3 will be emitted, respectively
Observador
El siguiente componente del flujo Observable es el Observador (u Observadores) suscritos a él. Los observadores reciben una notificación cada vez que sucede algo "interesante" en la transmisión. Los observadores son notificados a través de los siguientes eventos:
-
Observer#onNext(T)
: se invoca cuando se emite un elemento desde la transmisión. -
Observable#onError(Throwable)
: se invoca cuando se produce un error en la secuencia -
Observable#onCompleted()
: se invoca cuando la secuencia termina de emitir elementos.
Para suscribirse a una transmisión, simplemente llame a Observable<T>#subscribe(...)
y pase una instancia de Observer.
Observable<Integer> observable = Observable.just(1, 2, 3); observable.subscribe(new Observer<Integer>() { @Override public void onCompleted() { Log.d("Test", "In onCompleted()"); } @Override public void onError(Throwable e) { Log.d("Test", "In onError()"); } @Override public void onNext(Integer integer) { Log.d("Test", "In onNext():" + integer); } });
El código anterior emitirá lo siguiente en Logcat:
In onNext(): 1 In onNext(): 2 In onNext(): 3 In onNext(): 4 In onCompleted()
También puede haber algunos casos en los que ya no estemos interesados en las emisiones de un Observable. Esto es particularmente relevante en Android cuando, por ejemplo, una Activity
/ Fragment
necesita recuperarse en la memoria.
Para dejar de observar elementos, simplemente necesitamos llamar a Subscription#unsubscribe()
en el objeto de suscripción devuelto.
Subscription subscription = someInfiniteObservable.subscribe(new Observer<Integer>() { @Override public void onCompleted() { // ... } @Override public void onError(Throwable e) { // ... } @Override public void onNext(Integer integer) { // ... } }); // Call unsubscribe when appropriate subscription.unsubscribe();
Como se ve en el fragmento de código anterior, al suscribirnos a un Observable, mantenemos la referencia al objeto de suscripción devuelto y luego invocamos subscription#unsubscribe()
cuando sea necesario. En Android, es mejor invocarlo dentro de Activity#onDestroy()
o Fragment#onDestroy()
.
Operador
Los elementos emitidos por un Observable se pueden transformar, modificar y filtrar a través de los Operadores antes de notificar a los objetos del Observador suscritos. Algunas de las operaciones más comunes que se encuentran en la programación funcional (como mapear, filtrar, reducir, etc.) también se pueden aplicar a un flujo Observable. Veamos el mapa como ejemplo:
Observable.just(1, 2, 3, 4, 5).map(new Func1<Integer, Integer>() { @Override public Integer call(Integer integer) { return integer * 3; } }).subscribe(new Observer<Integer>() { @Override public void onCompleted() { // ... } @Override public void onError(Throwable e) { // ... } @Override public void onNext(Integer integer) { // ... } });
El fragmento de código anterior tomaría cada emisión del Observable y multiplicaría cada una por 3, produciendo el flujo 3, 6, 9, 12, 15, respectivamente. La aplicación de un Operador generalmente devuelve otro Observable como resultado, lo cual es conveniente ya que nos permite encadenar múltiples operaciones para obtener el resultado deseado.
Dada la secuencia anterior, supongamos que solo queríamos recibir números pares. Esto se puede lograr encadenando una operación de filtro .
Observable.just(1, 2, 3, 4, 5).map(new Func1<Integer, Integer>() { @Override public Integer call(Integer integer) { return integer * 3; } }).filter(new Func1<Integer, Boolean>() { @Override public Boolean call(Integer integer) { return integer % 2 == 0; } }).subscribe(new Observer<Integer>() { @Override public void onCompleted() { // ... } @Override public void onError(Throwable e) { // ... } @Override public void onNext(Integer integer) { // ... } });
Hay muchos operadores integrados en el conjunto de herramientas RxJava que modifican el flujo Observable; si puede pensar en una forma de modificar la transmisión, es probable que haya un Operador para ello. A diferencia de la mayoría de la documentación técnica, leer los documentos de RxJava/ReactiveX es bastante simple y directo. Cada operador en la documentación viene con una visualización de cómo el Operador afecta la transmisión. Estas visualizaciones se denominan "diagramas de mármol".
Así es como se podría modelar un Operador hipotético llamado flip a través de un diagrama de mármol:
Multiproceso con RxJava
El control del hilo dentro del cual ocurren las operaciones en la cadena Observable se realiza especificando el Programador dentro del cual debe ocurrir un operador. Esencialmente, puede pensar en un Programador como un grupo de subprocesos que, cuando se especifica, un operador usará y ejecutará. De forma predeterminada, si no se proporciona dicho Programador, la cadena Observable operará en el mismo subproceso donde se llama a Observable#subscribe(...)
. De lo contrario, se puede especificar un Programador a través de Observable#subscribeOn(Scheduler)
y/u Observable#observeOn(Scheduler)
donde la operación programada ocurrirá en un subproceso elegido por el Programador.
La diferencia clave entre los dos métodos es que Observable#subscribeOn(Scheduler)
indica al Observable de origen en qué programador debe ejecutarse. La cadena continuará ejecutándose en el subproceso del Programador especificado en Observable#subscribeOn(Scheduler)
hasta que se realice una llamada a Observable#observeOn(Scheduler)
con un Programador diferente. Cuando se realiza una llamada de este tipo, todos los observadores a partir de ese momento (es decir, las operaciones subsiguientes en la cadena) recibirán notificaciones en un hilo tomado del programador de observeOn
.
Aquí hay un diagrama de mármol que demuestra cómo estos métodos afectan el lugar donde se ejecutan las operaciones:
En el contexto de Android, si es necesario realizar una operación de interfaz de usuario como resultado de una operación larga, nos gustaría que dicha operación se realice en el subproceso de interfaz de usuario. Para este propósito, podemos usar AndroidScheduler#mainThread()
, uno de los programadores provistos en la biblioteca RxAndroid.

RxJava en Android
Ahora que tenemos algunos de los conceptos básicos en nuestro haber, es posible que se pregunte: ¿cuál es la mejor manera de integrar RxJava en una aplicación de Android? Como puede imaginar, hay muchos casos de uso para RxJava pero, en este ejemplo, echemos un vistazo a un caso específico: usar objetos Observables como parte de la pila de red.
En este ejemplo, veremos Retrofit, un cliente HTTP de código abierto de Square que tiene enlaces integrados con RxJava para interactuar con la API de GitHub. Específicamente, crearemos una aplicación simple que presente todos los repositorios destacados para un usuario con un nombre de usuario de GitHub. Si desea avanzar, el código fuente está disponible aquí.
Crear un nuevo proyecto de Android
- Comience por crear un nuevo proyecto de Android y asígnele el nombre GitHubRxJava .
- En la pantalla Dispositivos Android de destino , mantenga seleccionados Teléfono y Tablet y establezca el nivel mínimo de SDK en 17. Siéntase libre de configurarlo en un nivel de API inferior/superior pero, para este ejemplo, el nivel de API 17 será suficiente.
- Seleccione Actividad vacía en el siguiente mensaje.
- En el último paso, mantenga el Nombre de la actividad como MainActivity y genere un archivo de diseño activity_main .
Configuración del proyecto
Incluya RxJava, RxAndroid y la biblioteca Retrofit en app/build.gradle
. Tenga en cuenta que incluir RxAndroid implícitamente también incluye RxJava. Sin embargo, es una buena práctica incluir siempre esas dos bibliotecas explícitamente, ya que RxAndroid no siempre contiene la versión más actualizada de RxJava. La inclusión explícita de la última versión de RxJava garantiza el uso de la versión más actualizada.
dependencies { compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0' compile 'com.squareup.retrofit2:converter-gson:2.1.0' compile 'com.squareup.retrofit2:retrofit:2.1.0' compile 'io.reactivex:rxandroid:1.2.0' compile 'io.reactivex:rxjava:1.1.8' // ...other dependencies }
Crear objeto de datos
Cree la clase de objeto de datos de GitHubRepo
. Esta clase encapsula un repositorio en GitHub (la respuesta de la red contiene más datos, pero solo nos interesa un subconjunto de eso).
public class GitHubRepo { public final int id; public final String name; public final String htmlUrl; public final String description; public final String language; public final int stargazersCount; public GitHubRepo(int id, String name, String htmlUrl, String description, String language, int stargazersCount) { this.id = id; this.name = name; this.htmlUrl = htmlUrl; this.description = description; this.language = language; this.stargazersCount = stargazersCount; } }
Actualización de configuración
- Cree la interfaz de
GitHubService
. Pasaremos esta interfaz a Retrofit y Retrofit creará una implementación deGitHubService
.
public interface GitHubService { @GET("users/{user}/starred") Observable<List<GitHubRepo>> getStarredRepositories(@Path("user") String userName); }
Cree la clase
GitHubClient
. Este será el objeto con el que interactuaremos para realizar llamadas de red desde el nivel de UI.Al construir una implementación de
GitHubService
a través de Retrofit, necesitamos pasar unRxJavaCallAdapterFactory
como el adaptador de llamada para que las llamadas de red puedan devolver objetos Observables (se necesita pasar un adaptador de llamada para cualquier llamada de red que devuelva un resultado que no seaCall
).También necesitamos pasar un
GsonConverterFactory
para que podamos usar Gson como una forma de ordenar objetos JSON a objetos Java.
public class GitHubClient { private static final String GITHUB_BASE_URL = "https://api.github.com/"; private static GitHubClient instance; private GitHubService gitHubService; private GitHubClient() { final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); final Retrofit retrofit = new Retrofit.Builder().baseUrl(GITHUB_BASE_URL) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create(gson)) .build(); gitHubService = retrofit.create(GitHubService.class); } public static GitHubClient getInstance() { if (instance == null) { instance = new GitHubClient(); } return instance; } public Observable<List<GitHubRepo>> getStarredRepos(@NonNull String userName) { return gitHubService.getStarredRepositories(userName); } }
Diseños de configuración
A continuación, cree una interfaz de usuario simple que muestre los repositorios recuperados con un nombre de usuario de GitHub de entrada. Cree activity_home.xml
, el diseño de nuestra actividad, con algo como lo siguiente:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ListView android: android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"/> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <EditText android: android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:hint="@string/username"/> <Button android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/search"/> </LinearLayout> </LinearLayout>
Cree item_github_repo.xml
, el diseño del elemento ListView
para el objeto del repositorio de GitHub, con algo como lo siguiente:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:andro xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="6dp"> <TextView android: android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="24sp" android:text tools:text="Cropper"/> <TextView android: android:layout_width="match_parent" android:layout_height="wrap_content" android:lines="2" android:ellipsize="end" android:textSize="16sp" android:layout_below="@+id/text_repo_name" tools:text="Android widget for cropping and rotating an image."/> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/text_repo_description" android:layout_alignParentLeft="true" android:textColor="?attr/colorPrimary" android:textSize="14sp" android:text tools:text="Language: Java"/> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/text_repo_description" android:layout_alignParentRight="true" android:textColor="?attr/colorAccent" android:textSize="14sp" android:text tools:text="Stars: 1953"/> </RelativeLayout>
Pegar todo junto
Cree un ListAdapter
que esté a cargo de enlazar objetos de GitHubRepo
en elementos de ListView
. El proceso consiste esencialmente en inflar item_github_repo.xml
en una View
si no se proporciona una View
reciclada; de lo contrario, se reutiliza una View
reciclada para evitar que se inflen demasiados objetos de View
.
public class GitHubRepoAdapter extends BaseAdapter { private List<GitHubRepo> gitHubRepos = new ArrayList<>(); @Override public int getCount() { return gitHubRepos.size(); } @Override public GitHubRepo getItem(int position) { if (position < 0 || position >= gitHubRepos.size()) { return null; } else { return gitHubRepos.get(position); } } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { final View view = (convertView != null ? convertView : createView(parent)); final GitHubRepoViewHolder viewHolder = (GitHubRepoViewHolder) view.getTag(); viewHolder.setGitHubRepo(getItem(position)); return view; } public void setGitHubRepos(@Nullable List<GitHubRepo> repos) { if (repos == null) { return; } gitHubRepos.clear(); gitHubRepos.addAll(repos); notifyDataSetChanged(); } private View createView(ViewGroup parent) { final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); final View view = inflater.inflate(R.layout.item_github_repo, parent, false); final GitHubRepoViewHolder viewHolder = new GitHubRepoViewHolder(view); view.setTag(viewHolder); return view; } private static class GitHubRepoViewHolder { private TextView textRepoName; private TextView textRepoDescription; private TextView textLanguage; private TextView textStars; public GitHubRepoViewHolder(View view) { textRepoName = (TextView) view.findViewById(R.id.text_repo_name); textRepoDescription = (TextView) view.findViewById(R.id.text_repo_description); textLanguage = (TextView) view.findViewById(R.id.text_language); textStars = (TextView) view.findViewById(R.id.text_stars); } public void setGitHubRepo(GitHubRepo gitHubRepo) { textRepoName.setText(gitHubRepo.name); textRepoDescription.setText(gitHubRepo.description); textLanguage.setText("Language: " + gitHubRepo.language); textStars.setText("Stars: " + gitHubRepo.stargazersCount); } } }
Pegue todo junto en MainActivity
. Esta es esencialmente la Activity
que se muestra cuando iniciamos la aplicación por primera vez. Aquí, le pedimos al usuario que ingrese su nombre de usuario de GitHub y, finalmente, muestre todos los repositorios destacados por ese nombre de usuario.
public class MainActivity extends AppCompatActivity { private static final String TAG = MainActivity.class.getSimpleName(); private GitHubRepoAdapter adapter = new GitHubRepoAdapter(); private Subscription subscription; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final ListView listView = (ListView) findViewById(R.id.list_view_repos); listView.setAdapter(adapter); final EditText editTextUsername = (EditText) findViewById(R.id.edit_text_username); final Button buttonSearch = (Button) findViewById(R.id.button_search); buttonSearch.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { final String username = editTextUsername.getText().toString(); if (!TextUtils.isEmpty(username)) { getStarredRepos(username); } } }); } @Override protected void onDestroy() { if (subscription != null && !subscription.isUnsubscribed()) { subscription.unsubscribe(); } super.onDestroy(); } private void getStarredRepos(String username) { subscription = GitHubClient.getInstance() .getStarredRepos(username) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Observer<List<GitHubRepo>>() { @Override public void onCompleted() { Log.d(TAG, "In onCompleted()"); } @Override public void onError(Throwable e) { e.printStackTrace(); Log.d(TAG, "In onError()"); } @Override public void onNext(List<GitHubRepo> gitHubRepos) { Log.d(TAG, "In onNext()"); adapter.setGitHubRepos(gitHubRepos); } }); } }
Ejecute la aplicación
Ejecutar la aplicación debería presentar una pantalla con un cuadro de entrada para ingresar un nombre de usuario de GitHub. La búsqueda debería presentar la lista de todos los repositorios destacados.
Conclusión
Espero que esto sirva como una introducción útil a RxJava y una descripción general de sus capacidades básicas. Hay una tonelada de conceptos poderosos en RxJava y lo insto a que los explore profundizando más en la bien documentada wiki de RxJava.
Siéntase libre de dejar cualquier pregunta o comentario en el cuadro de comentarios a continuación. También puede seguirme en Twitter en @arriolachris, donde tuiteo mucho sobre RxJava y todo lo relacionado con Android.
Si desea un recurso de aprendizaje integral sobre RxJava, puede consultar el libro electrónico que escribí con Angus Huang en Leanpub.