Scopri RxJava: la libreria di programmazione reattiva mancante per Android
Pubblicato: 2022-03-11Se sei uno sviluppatore Android, è probabile che tu abbia sentito parlare di RxJava. È una delle librerie più discusse per abilitare la programmazione reattiva nello sviluppo di Android. È pubblicizzato come il framework di riferimento per semplificare le attività simultanee/asincrone inerenti alla programmazione mobile.
Ma... cos'è RxJava e in che modo "semplifica" le cose?
Sebbene ci siano molte risorse già disponibili online che spiegano cos'è RxJava, in questo articolo il mio obiettivo è darti un'introduzione di base a RxJava e in particolare come si adatta allo sviluppo di Android. Darò anche alcuni esempi concreti e suggerimenti su come integrarlo in un progetto nuovo o esistente.
Perché considerare RxJava?
In sostanza, RxJava semplifica lo sviluppo perché aumenta il livello di astrazione attorno al threading. Cioè, come sviluppatore non devi preoccuparti troppo dei dettagli su come eseguire operazioni che dovrebbero verificarsi su thread diversi. Ciò è particolarmente interessante poiché il threading è difficile da correggere e, se non implementato correttamente, può causare il debug e la correzione di alcuni dei bug più difficili.
Certo, questo non significa che RxJava sia a prova di proiettile quando si tratta di threading ed è comunque importante capire cosa sta succedendo dietro le quinte; tuttavia, RxJava può sicuramente semplificarti la vita.
Diamo un'occhiata a un esempio.
Chiamata di rete - RxJava vs AsyncTask
Supponiamo di voler ottenere dati sulla rete e di conseguenza aggiornare l'interfaccia utente. Un modo per farlo è (1) creare una sottoclasse AsyncTask
interna nel nostro Activity
/ Fragment
, (2) eseguire l'operazione di rete in background e (3) prendere il risultato di tale operazione e aggiornare l'interfaccia utente nel thread principale .
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() }
Per quanto innocuo possa sembrare, questo approccio presenta alcuni problemi e limitazioni. Vale a dire, le perdite di memoria/contesto vengono facilmente create poiché NetworkRequestTask
è una classe interna e quindi contiene un riferimento implicito alla classe esterna. Inoltre, cosa succede se vogliamo concatenare un'altra lunga operazione dopo la chiamata di rete? Dovremmo annidare due AsyncTask
che possono ridurre significativamente la leggibilità.
Al contrario, un approccio RxJava per eseguire una chiamata di rete potrebbe assomigliare a questo:
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(); }
Utilizzando questo approccio, risolviamo il problema (delle potenziali perdite di memoria causate da un thread in esecuzione che contiene un riferimento al contesto esterno) mantenendo un riferimento all'oggetto Subscription
restituito. Questo oggetto Subscription
viene quindi collegato al metodo #onDestroy()
dell'oggetto Activity
/ Fragment
per garantire che l'operazione Action1#call
non venga eseguita quando l' Activity
/ Fragment
deve essere distrutto.
Inoltre, si noti che il tipo restituito di #getObservableUser(...)
(cioè un Observable<User>
) è concatenato con ulteriori chiamate ad esso. Attraverso questa API fluida, siamo in grado di risolvere il secondo problema dell'utilizzo di AsyncTask
, ovvero che consente ulteriori chiamate di rete/concatenamento di operazioni lunghe. Abbastanza pulito, eh?
Immergiamoci in alcuni concetti di RxJava.
Osservabile, osservatore e operatore: le 3 O di RxJava Core
Nel mondo RxJava, tutto può essere modellato come stream. Un flusso emette elementi nel tempo e ciascuna emissione può essere consumata/osservata.
Se ci pensi, uno stream non è un concetto nuovo: gli eventi di clic possono essere uno stream, gli aggiornamenti di posizione possono essere uno stream, le notifiche push possono essere uno stream e così via.
L'astrazione del flusso è implementata attraverso 3 costrutti di base che mi piace chiamare "le 3 O"; vale a dire: l' osservabile , l' osservatore e l' operatore . L' Osservabile emette oggetti (il flusso); e l' Osservatore consuma quegli oggetti. Le emissioni degli oggetti osservabili possono essere ulteriormente modificate, trasformate e manipolate concatenando le chiamate dell'operatore .
Osservabile
An Observable è l'astrazione del flusso in RxJava. È simile a un Iteratore in quanto, data una sequenza, scorre e produce quegli elementi in modo ordinato. Un consumatore può quindi consumare quegli articoli attraverso la stessa interfaccia, indipendentemente dalla sequenza sottostante.
Supponiamo di voler emettere i numeri 1, 2, 3, in quest'ordine. Per farlo, possiamo usare il metodo 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(); } });
Richiamando subscriber.onNext(Integer)
viene emesso un elemento nel flusso e, al termine dell'emissione del flusso, viene richiamato subscriber.onCompleted()
.
Questo approccio alla creazione di un osservabile è abbastanza dettagliato. Per questo motivo, esistono metodi convenienti per la creazione di istanze osservabili che dovrebbero essere preferiti in quasi tutti i casi.
Il modo più semplice per creare un Observable è usare Observable#just(...)
. Come suggerisce il nome del metodo, emette semplicemente gli elementi che gli passi come argomenti del metodo.
Observable.just(1, 2, 3); // 1, 2, 3 will be emitted, respectively
Osservatore
Il prossimo componente del flusso osservabile è l'Observer (o gli osservatori) a cui si è iscritto. Gli osservatori vengono avvisati ogni volta che accade qualcosa di "interessante" nel flusso. Gli osservatori vengono avvisati tramite i seguenti eventi:
-
Observer#onNext(T)
: richiamato quando un elemento viene emesso dal flusso -
Observable#onError(Throwable)
- richiamato quando si è verificato un errore all'interno del flusso -
Observable#onCompleted()
- richiamato quando il flusso ha terminato l'emissione di elementi.
Per iscriversi a uno stream, chiama semplicemente Observable<T>#subscribe(...)
e passa un'istanza di 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); } });
Il codice sopra emetterà quanto segue in Logcat:
In onNext(): 1 In onNext(): 2 In onNext(): 3 In onNext(): 4 In onCompleted()
Potrebbero esserci anche alcuni casi in cui non siamo più interessati alle emissioni di un osservabile. Ciò è particolarmente rilevante in Android quando, ad esempio, è necessario recuperare in memoria Activity
/ Fragment
.
Per interrompere l'osservazione degli elementi, dobbiamo semplicemente chiamare Subscription#unsubscribe()
sull'oggetto Subscription restituito.
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();
Come visto nel frammento di codice sopra, al momento della sottoscrizione a un Observable, manteniamo il riferimento all'oggetto Subscription restituito e in seguito invochiamo subscription#unsubscribe()
quando necessario. In Android, è meglio invocarlo all'interno di Activity#onDestroy()
o Fragment#onDestroy()
.
Operatore
Gli elementi emessi da un Osservabile possono essere trasformati, modificati e filtrati tramite gli Operatori prima di notificare gli oggetti Observer sottoscritti. Alcune delle operazioni più comuni che si trovano nella programmazione funzionale (come mappare, filtrare, ridurre, ecc.) possono essere applicate anche a un flusso osservabile. Diamo un'occhiata alla mappa come esempio:
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) { // ... } });
Il frammento di codice sopra prenderebbe ciascuna emissione dall'Osservabile e moltiplicherà ciascuna per 3, producendo rispettivamente il flusso 3, 6, 9, 12, 15. L'applicazione di un operatore in genere restituisce un altro osservabile come risultato, il che è conveniente in quanto ciò ci consente di concatenare più operazioni per ottenere il risultato desiderato.
Dato lo stream sopra, supponiamo di voler ricevere solo numeri pari. Ciò può essere ottenuto concatenando un'operazione di 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) { // ... } });
Ci sono molti operatori integrati nel set di strumenti RxJava che modificano il flusso Observable; se riesci a pensare a un modo per modificare il flusso, è probabile che ci sia un operatore per questo. A differenza della maggior parte della documentazione tecnica, leggere i documenti RxJava/ReactiveX è abbastanza semplice e mirato. Ciascun operatore nella documentazione viene fornito con una visualizzazione su come l'operatore influisce sul flusso. Queste visualizzazioni sono chiamate "diagrammi di marmo".
Ecco come un ipotetico Operatore chiamato flip potrebbe essere modellato attraverso un diagramma marmoreo:
Multithreading con RxJava
Il controllo del thread all'interno del quale si verificano le operazioni nella catena Observable viene eseguito specificando lo Scheduler all'interno del quale deve verificarsi un operatore. In sostanza, puoi pensare a un'utilità di pianificazione come a un pool di thread che, quando specificato, un operatore utilizzerà e verrà eseguito. Per impostazione predefinita, se non viene fornito tale Scheduler, la catena Observable funzionerà sullo stesso thread in cui viene chiamato Observable#subscribe(...)
. In caso contrario, è possibile specificare uno Scheduler tramite Observable#subscribeOn(Scheduler)
e/o Observable#observeOn(Scheduler)
in cui l'operazione pianificata avverrà su un thread scelto dallo Scheduler.
La differenza fondamentale tra i due metodi è che Observable#subscribeOn(Scheduler)
indica all'Osservabile di origine su quale Scheduler deve essere eseguito. La catena continuerà a essere eseguita sul thread dall'utilità di pianificazione specificata in Observable#subscribeOn(Scheduler)
finché non viene effettuata una chiamata a Observable#observeOn(Scheduler)
con un'utilità di pianificazione diversa. Quando viene effettuata una tale chiamata, tutti gli osservatori da lì in poi (cioè, le operazioni successive lungo la catena) riceveranno notifiche in un thread preso dall'utilità di pianificazione observeOn
.
Ecco un diagramma a marmo che mostra come questi metodi influiscono su dove vengono eseguite le operazioni:
Nel contesto di Android, se un'operazione dell'interfaccia utente deve essere eseguita a seguito di un'operazione lunga, vorremmo che l'operazione avvenisse sul thread dell'interfaccia utente. A tale scopo, possiamo utilizzare AndroidScheduler#mainThread()
, uno degli Scheduler forniti nella libreria RxAndroid.

RxJava su Android
Ora che abbiamo alcune delle nozioni di base alle nostre spalle, potresti chiederti: qual è il modo migliore per integrare RxJava in un'applicazione Android? Come puoi immaginare, ci sono molti casi d'uso per RxJava ma, in questo esempio, diamo un'occhiata a un caso specifico: l'utilizzo di oggetti osservabili come parte dello stack di rete.
In questo esempio, esamineremo Retrofit, un client HTTP open source da Square che ha collegamenti integrati con RxJava per interagire con l'API di GitHub. Nello specifico, creeremo una semplice app che presenta tutti i repository speciali per un utente dotato di un nome utente GitHub. Se vuoi andare avanti, il codice sorgente è disponibile qui.
Crea un nuovo progetto Android
- Inizia creando un nuovo progetto Android e nominandolo GitHubRxJava .
- Nella schermata Dispositivi Android di destinazione , mantieni il telefono e il tablet selezionati e imposta il livello SDK minimo di 17. Sentiti libero di impostarlo su un livello API inferiore/superiore ma, per questo esempio, il livello API 17 sarà sufficiente.
- Seleziona Svuota attività nel prompt successivo.
- Nell'ultimo passaggio, mantieni il nome dell'attività come MainActivity e genera un file di layout activity_main .
Configurazione del progetto
Includi RxJava, RxAndroid e la libreria Retrofit in app/build.gradle
. Si noti che l'inclusione di RxAndroid include implicitamente anche RxJava. È consigliabile, tuttavia, includere sempre queste due librerie in modo esplicito poiché RxAndroid non contiene sempre la versione più aggiornata di RxJava. L'inclusione esplicita dell'ultima versione di RxJava garantisce l'utilizzo della versione più aggiornata.
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 }
Crea oggetto dati
Crea la classe dell'oggetto dati GitHubRepo
. Questa classe incapsula un repository in GitHub (la risposta di rete contiene più dati ma siamo interessati solo a un sottoinsieme di questi).
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; } }
Retrofit di configurazione
- Crea l'interfaccia
GitHubService
. Passeremo questa interfaccia in Retrofit e Retrofit creerà un'implementazione diGitHubService
.
public interface GitHubService { @GET("users/{user}/starred") Observable<List<GitHubRepo>> getStarredRepositories(@Path("user") String userName); }
Crea la classe
GitHubClient
. Questo sarà l'oggetto con cui interagiremo per effettuare chiamate di rete dal livello dell'interfaccia utente.Quando si costruisce un'implementazione di
GitHubService
tramite Retrofit, è necessario passare unRxJavaCallAdapterFactory
come adattatore di chiamata in modo che le chiamate di rete possano restituire oggetti osservabili (il passaggio di un adattatore di chiamata è necessario per qualsiasi chiamata di rete che restituisce un risultato diverso daCall
).Dobbiamo anche passare a
GsonConverterFactory
in modo da poter utilizzare Gson come un modo per eseguire il marshalling di oggetti JSON su oggetti 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); } }
Layout di configurazione
Quindi, crea una semplice interfaccia utente che visualizzi i repository recuperati con un nome utente GitHub di input. Crea activity_home.xml
- il layout per la nostra attività - con qualcosa di simile al seguente:
<?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>
Crea item_github_repo.xml
- il layout dell'elemento ListView
per l'oggetto repository GitHub - con qualcosa di simile al seguente:
<?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>
Incolla tutto insieme
Crea un ListAdapter
incaricato di associare gli oggetti GitHubRepo
agli elementi ListView
. Il processo consiste essenzialmente nel gonfiare item_github_repo.xml
in una View
se non viene fornita alcuna View
riciclata; in caso contrario, una View
riciclata viene riutilizzata per evitare di gonfiare eccessivamente troppi oggetti della 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); } } }
Incolla tutto insieme in MainActivity
. Questa è essenzialmente l' Activity
che viene visualizzata al primo avvio dell'app. Qui, chiediamo all'utente di inserire il proprio nome utente GitHub e, infine, visualizzare tutti i repository speciali con quel nome utente.
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); } }); } }
Esegui l'app
L'esecuzione dell'app dovrebbe presentare una schermata con una casella di input per immettere un nome utente GitHub. La ricerca dovrebbe quindi presentare l'elenco di tutti i repository speciali.
Conclusione
Spero che questo serva come un'utile introduzione a RxJava e una panoramica delle sue capacità di base. Ci sono un sacco di concetti potenti in RxJava e ti esorto a esplorarli scavando più a fondo nel wiki RxJava ben documentato.
Sentiti libero di lasciare qualsiasi domanda o commento nella casella dei commenti qui sotto. Puoi anche seguirmi su Twitter su @arriolachris, dove twitto molto su RxJava e tutto ciò che riguarda Android.
Se desideri una risorsa di apprendimento completa su RxJava, puoi dare un'occhiata all'ebook che ho scritto con Angus Huang su Leanpub.