Faceți cunoștință cu RxJava: Biblioteca de programare reactivă lipsă pentru Android

Publicat: 2022-03-11

Dacă sunteți un dezvoltator Android, sunt șanse să fi auzit de RxJava. Este una dintre cele mai discutate biblioteci pentru activarea programării reactive în dezvoltarea Android. Este prezentat ca cadrul de bază pentru simplificarea sarcinilor simultane/asincrone inerente programării mobile.

Dar... ce este RxJava și cum „simplifica” lucrurile?

Programare reactivă funcțională pentru Android: o introducere în RxJava

Descurcă Android-ul de prea multe fire Java cu RxJava.
Tweet

Deși există o mulțime de resurse deja disponibile online care explică ce este RxJava, în acest articol scopul meu este să vă ofer o introducere de bază despre RxJava și în special cum se încadrează în dezvoltarea Android. De asemenea, voi da câteva exemple concrete și sugestii despre cum îl puteți integra într-un proiect nou sau existent.

De ce să luați în considerare RxJava?

În esență, RxJava simplifică dezvoltarea deoarece ridică nivelul de abstractizare în jurul threading-ului. Adică, ca dezvoltator, nu trebuie să vă faceți griji prea mult cu privire la detaliile modului de efectuare a operațiunilor care ar trebui să apară pe diferite fire. Acest lucru este deosebit de atractiv, deoarece threadingul este dificil de corectat și, dacă nu este implementat corect, poate cauza unele dintre cele mai dificile erori de depanare și remediate.

Desigur, acest lucru nu înseamnă că RxJava este antiglonț când vine vorba de threading și este totuși important să înțelegem ce se întâmplă în culise; cu toate acestea, RxJava vă poate face viața mai ușoară.

Să ne uităm la un exemplu.

Apel de rețea - RxJava vs AsyncTask

Să presupunem că vrem să obținem date prin rețea și ca rezultat să actualizăm interfața de utilizare. O modalitate de a face acest lucru este să (1) să creați o subclasă internă AsyncTask în Activity / Fragment nostru, (2) să efectuați operația de rețea în fundal și (3) să luați rezultatul acelei operațiuni și să actualizați interfața de utilizare în firul 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() }

Oricât de inofensiv ar părea, această abordare are unele probleme și limitări. Și anume, scurgerile de memorie/context sunt create cu ușurință, deoarece NetworkRequestTask este o clasă interioară și, astfel, deține o referință implicită la clasa exterioară. De asemenea, ce se întâmplă dacă vrem să înlănțuim o altă operațiune lungă după apelul de rețea? Ar trebui să punem două AsyncTask -uri care pot reduce semnificativ lizibilitatea.

În schimb, o abordare RxJava pentru efectuarea unui apel de rețea ar putea arăta cam așa:

 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(); }

Folosind această abordare, rezolvăm problema (a potențialelor scurgeri de memorie cauzate de un fir de execuție care conține o referință la contextul exterior) prin păstrarea unei referințe la obiectul Subscription returnat. Acest obiect Subscription este apoi legat de metoda #onDestroy() a obiectului Activity / Fragment pentru a garanta că operația Action1#call nu se execută atunci când Activity / Fragment trebuie distrus.

De asemenea, observați că tipul returnat de #getObservableUser(...) (adică un Observable<User> ) este înlănțuit cu apeluri suplimentare către acesta. Prin acest API fluid, putem rezolva a doua problemă a utilizării unui AsyncTask și anume că permite înlănțuirea în continuare a apelurilor în rețea/operațiunilor lungi. Destul de îngrijit, nu?

Să ne aprofundăm în câteva concepte RxJava.

Observabil, Observator și Operator - Cele 3 O ale RxJava Core

În lumea RxJava, totul poate fi modelat ca fluxuri. Un flux emite element(e) în timp și fiecare emisie poate fi consumată/observată.

Dacă vă gândiți bine, un flux nu este un concept nou: evenimentele de clic pot fi un flux, actualizările de locație pot fi un flux, notificările push pot fi un flux și așa mai departe.

În lumea RxJava, totul poate fi modelat ca fluxuri.

Abstracția fluxului este implementată prin intermediul a 3 constructe de bază pe care îmi place să le numesc „cele 3 O”; și anume: observabilul , observatorul și operatorul . Observabilul emite elemente (fluxul); iar Observatorul consumă acele articole. Emisiile de la obiectele observabile pot fi modificate, transformate și manipulate în continuare prin înlănțuirea apelurilor operatorului .

Observabil

Un observabil este abstractizarea fluxului în RxJava. Este similar cu un Iterator prin faptul că, dată fiind o secvență, iterează și produce acele elemente într-un mod ordonat. Un consumator poate consuma apoi acele articole prin aceeași interfață, indiferent de secvența subiacentă.

Să presupunem că am vrut să emitem numerele 1, 2, 3, în această ordine. Pentru a face acest lucru, putem folosi metoda 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(); } });

Invocarea subscriber.onNext(Integer) emite un element în flux și, atunci când fluxul este emis, subscriber.onCompleted() este apoi invocat.

Această abordare a creării unui observabil este destul de pronunțată. Din acest motiv, există metode convenabile pentru crearea instanțelor observabile care ar trebui să fie preferate în aproape toate cazurile.

Cel mai simplu mod de a crea un Observable este folosirea Observable#just(...) . După cum sugerează numele metodei, emite doar elementele pe care le treceți în el ca argumente de metodă.

 Observable.just(1, 2, 3); // 1, 2, 3 will be emitted, respectively

Observator

Următoarea componentă a fluxului Observable este observatorul (sau observatorii) abonați la acesta. Observatorii sunt notificați ori de câte ori se întâmplă ceva „interesant” în flux. Observatorii sunt anunțați prin următoarele evenimente:

  • Observer#onNext(T) - invocat atunci când un element este emis din flux
  • Observable#onError(Throwable) - invocat atunci când a apărut o eroare în flux
  • Observable#onCompleted() - invocat atunci când fluxul a terminat de emite elemente.

Pentru a vă abona la un flux, pur și simplu apelați Observable<T>#subscribe(...) și transmiteți o instanță 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); } });

Codul de mai sus va emite următoarele în Logcat:

 In onNext(): 1 In onNext(): 2 In onNext(): 3 In onNext(): 4 In onCompleted()

Pot exista, de asemenea, unele cazuri în care nu mai suntem interesați de emisiile unui observabil. Acest lucru este deosebit de relevant în Android atunci când, de exemplu, o Activity / Fragment trebuie recuperată în memorie.

Pentru a opri observarea articolelor, trebuie pur și simplu să apelăm Subscription#unsubscribe() pe obiectul Subscription returnat.

 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();

După cum se vede în fragmentul de cod de mai sus, la abonarea la un Observable, păstrăm referința la obiectul Subscription returnat și mai târziu invocăm subscription#unsubscribe() atunci când este necesar. În Android, aceasta este cel mai bine invocată în Activity#onDestroy() sau Fragment#onDestroy() .

Operator

Elementele emise de un Observabil pot fi transformate, modificate și filtrate prin Operatori înainte de a notifica obiectul (obiectele) Observer abonat(e). Unele dintre cele mai comune operațiuni găsite în programarea funcțională (cum ar fi harta, filtrarea, reducerea etc.) pot fi aplicate și unui flux Observable. Să ne uităm la harta ca exemplu:

 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) { // ... } });

Fragmentul de cod de mai sus ar lua fiecare emisie din Observable și ar înmulți fiecare cu 3, producând fluxul 3, 6, 9, 12, respectiv 15. Aplicarea unui operator returnează, de obicei, un alt observabil ca rezultat, ceea ce este convenabil deoarece acest lucru ne permite să înlănțuim mai multe operații pentru a obține rezultatul dorit.

Având în vedere fluxul de mai sus, să spunem că am vrut să primim doar numere pare. Acest lucru poate fi realizat prin înlănțuirea unei operațiuni de filtrare .

 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) { // ... } });

Există mulți operatori încorporați în setul de instrumente RxJava care modifică fluxul Observable; dacă vă puteți gândi la o modalitate de a modifica fluxul, sunt șanse să existe un operator pentru el. Spre deosebire de majoritatea documentației tehnice, citirea documentelor RxJava/ReactiveX este destul de simplă și la obiect. Fiecare operator din documentație vine împreună cu o vizualizare a modului în care operatorul afectează fluxul. Aceste vizualizări sunt numite „diagrame de marmură”.

Iată cum ar putea fi modelat un operator ipotetic numit flip printr-o diagramă de marmură:

Exemplu de modul în care un operator ipotetic numit flip ar putea fi modelat printr-o diagramă de marmură.

Multithreading cu RxJava

Controlul firului în care au loc operațiunile în lanțul Observable se face prin specificarea Scheduler-ului în care ar trebui să apară un operator. În esență, vă puteți gândi la un Scheduler ca la un pool de fire pe care, atunci când este specificat, un operator îl va folosi și pe care îl va rula. În mod implicit, dacă nu este furnizat un astfel de planificator, lanțul Observable va funcționa pe același fir în care este apelat Observable#subscribe(...) . În caz contrar, un planificator poate fi specificat prin Observable#subscribeOn(Scheduler) și/sau Observable#observeOn(Scheduler) în care operația programată va avea loc pe un fir ales de Scheduler.

Diferența cheie dintre cele două metode este că Observable#subscribeOn(Scheduler) indică sursei Observable pe ce Scheduler ar trebui să ruleze. Lanțul va continua să ruleze pe firul de execuție din Scheduler specificat în Observable#subscribeOn(Scheduler) până când se efectuează un apel către Observable#observeOn(Scheduler) cu un Scheduler diferit. Când se efectuează un astfel de apel, toți observatorii de acolo încolo (adică operațiunile ulterioare din lanț) vor primi notificări într-un fir preluat din observeOn de planificare.

Iată o diagramă de marmură care demonstrează modul în care aceste metode afectează locul în care sunt executate operațiunile:

O diagramă de marmură care demonstrează modul în care aceste metode afectează locul în care se desfășoară operațiunile.

În contextul Android, dacă o operațiune de UI trebuie să aibă loc ca urmare a unei operații lungi, am dori ca operația să aibă loc pe firul de execuție UI. În acest scop, putem folosi AndroidScheduler#mainThread() , unul dintre Schedulers-urile furnizate în biblioteca RxAndroid.

RxJava pe Android

Acum, că avem câteva dintre elementele de bază sub centură, s-ar putea să vă întrebați — care este cea mai bună modalitate de a integra RxJava într-o aplicație Android? După cum vă puteți imagina, există multe cazuri de utilizare pentru RxJava, dar, în acest exemplu, să aruncăm o privire la un caz specific: utilizarea obiectelor Observable ca parte a stivei de rețea.

În acest exemplu, ne vom uita la Retrofit, un client HTTP deschis de Square care are legături încorporate cu RxJava pentru a interacționa cu API-ul GitHub. Mai exact, vom crea o aplicație simplă care prezintă toate depozitele marcate cu stea pentru un utilizator cu un nume de utilizator GitHub. Dacă doriți să treceți mai departe, codul sursă este disponibil aici.

Creați un nou proiect Android

  • Începeți prin a crea un nou proiect Android și numiți-l GitHubRxJava .

Captură de ecran: creați un nou proiect Android

  • În ecranul Dispozitive Android țintă , păstrați Telefon și tabletă selectate și setați nivelul minim SDK de 17. Simțiți-vă liber să îl setați la un nivel API mai mic/mai ridicat, dar, pentru acest exemplu, nivelul API 17 va fi suficient.

Captură de ecran: ecranul dispozitivelor Android vizate

  • Selectați Activitate goală în următoarea solicitare.

Captură de ecran: adăugați o activitate pe ecranul mobil

  • În ultimul pas, păstrați Numele activității ca MainActivity și generați un fișier de aspect activity_main .

Captură de ecran: personalizați ecranul Activitate

Stabilirea proiectului

Includeți RxJava, RxAndroid și biblioteca Retrofit în app/build.gradle . Rețineți că includerea RxAndroid include implicit și RxJava. Cu toate acestea, este cea mai bună practică să includeți întotdeauna aceste două biblioteci în mod explicit, deoarece RxAndroid nu conține întotdeauna cea mai actualizată versiune a RxJava. Includerea explicită a celei mai recente versiuni de RxJava garantează utilizarea celei mai recente versiuni.

 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ți obiect de date

Creați clasa de obiecte de date GitHubRepo . Această clasă încapsulează un depozit în GitHub (răspunsul rețelei conține mai multe date, dar ne interesează doar un subset al acestora).

 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; } }

Configurare modernizare

  • Creați interfața GitHubService . Vom trece această interfață în Retrofit și Retrofit va crea o implementare a GitHubService .
 public interface GitHubService { @GET("users/{user}/starred") Observable<List<GitHubRepo>> getStarredRepositories(@Path("user") String userName); }
  • Creați clasa GitHubClient . Acesta va fi obiectul cu care vom interacționa pentru a efectua apeluri de rețea de la nivelul UI.

    • Când construim o implementare a GitHubService prin Retrofit, trebuie să transmitem un RxJavaCallAdapterFactory ca adaptor de apel, astfel încât apelurile de rețea să poată returna obiecte Observable (trecerea unui adaptor de apel este necesară pentru orice apel de rețea care returnează un rezultat altul decât un Call ).

    • De asemenea, trebuie să transmitem un GsonConverterFactory , astfel încât să putem folosi Gson ca o modalitate de a distribui obiecte JSON la obiecte 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); } }

Configurați aspecte

Apoi, creați o interfață de utilizare simplă care afișează repozițiile preluate, cu un nume de utilizator GitHub introdus. Creați activity_home.xml - aspectul pentru activitatea noastră - cu ceva de genul următor:

 <?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ți item_github_repo.xml - aspectul articolului ListView pentru obiectul depozit GitHub - cu ceva de genul următor:

 <?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>

Lipiți totul împreună

Creați un ListAdapter care se ocupă de legarea obiectelor GitHubRepo în elemente ListView . Procesul implică în esență umflarea item_github_repo.xml într-o View dacă nu este furnizată nicio View reciclată; în caz contrar, o View reciclată este reutilizată pentru a preveni supraumflarea prea multor obiecte 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); } } }

Lipiți totul împreună în MainActivity . Aceasta este în esență Activity care este afișată atunci când lansăm prima aplicație. Aici, îi cerem utilizatorului să introducă numele de utilizator GitHub și, în cele din urmă, să afișeze toate depozitele marcate cu stea după acel nume de utilizator.

 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); } }); } }

Rulați aplicația

Rularea aplicației ar trebui să prezinte un ecran cu o casetă de introducere pentru a introduce un nume de utilizator GitHub. Căutarea ar trebui să prezinte apoi lista tuturor depozitelor marcate cu stea.

Captură de ecran a aplicației care arată o listă cu toate depozitele marcate cu stea.

Concluzie

Sper că aceasta va servi ca o introducere utilă la RxJava și o prezentare generală a capacităților sale de bază. Există o mulțime de concepte puternice în RxJava și vă îndemn să le explorați săpat mai adânc în wiki-ul RxJava bine documentat.

Simțiți-vă liber să lăsați orice întrebări sau comentarii în caseta de comentarii de mai jos. De asemenea, mă puteți urmări pe Twitter la @arriolachris unde scriu pe Twitter multe despre RxJava și despre toate lucrurile Android.

Dacă doriți o resursă de învățare cuprinzătoare pe RxJava, puteți consulta cartea electronică pe care am scris-o împreună cu Angus Huang pe Leanpub.

Înrudit : Zece funcții Kotlin pentru a stimula dezvoltarea Android