Conheça o RxJava: a biblioteca de programação reativa ausente para Android

Publicados: 2022-03-11

Se você é um desenvolvedor Android, provavelmente já ouviu falar do RxJava. É uma das bibliotecas mais discutidas para habilitar a Programação Reativa no desenvolvimento Android. É apresentado como a estrutura ideal para simplificar tarefas de simultaneidade/assíncronas inerentes à programação móvel.

Mas… o que é RxJava e como ele “simplifica” as coisas?

Programação reativa funcional para Android: uma introdução ao RxJava

Desembaraçar seu Android de muitos threads Java com RxJava.
Tweet

Embora existam muitos recursos já disponíveis online explicando o que é RxJava, neste artigo meu objetivo é fornecer uma introdução básica ao RxJava e especificamente como ele se encaixa no desenvolvimento Android. Também darei alguns exemplos concretos e sugestões de como você pode integrá-lo em um projeto novo ou existente.

Por que considerar o RxJava?

Em sua essência, o RxJava simplifica o desenvolvimento porque aumenta o nível de abstração em torno do encadeamento. Ou seja, como desenvolvedor você não precisa se preocupar muito com os detalhes de como realizar as operações que devem ocorrer em diferentes threads. Isso é particularmente atraente, pois o encadeamento é difícil de acertar e, se não for implementado corretamente, pode causar alguns dos bugs mais difíceis de depurar e corrigir.

Concedido, isso não significa que RxJava é à prova de balas quando se trata de threading e ainda é importante entender o que está acontecendo nos bastidores; no entanto, o RxJava pode definitivamente tornar sua vida mais fácil.

Vejamos um exemplo.

Chamada de rede - RxJava vs AsyncTask

Digamos que queremos obter dados pela rede e atualizar a interface do usuário como resultado. Uma maneira de fazer isso é (1) criar uma subclasse AsyncTask interna em nossa Activity / Fragment , (2) executar a operação de rede em segundo plano e (3) obter o resultado dessa operação e atualizar a interface do usuário no thread 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 mais inofensiva que possa parecer, essa abordagem tem alguns problemas e limitações. Ou seja, vazamentos de memória/contexto são facilmente criados, pois NetworkRequestTask é uma classe interna e, portanto, contém uma referência implícita à classe externa. Além disso, e se quisermos encadear outra operação longa após a chamada de rede? Teríamos que aninhar dois AsyncTask que podem reduzir significativamente a legibilidade.

Em contraste, uma abordagem RxJava para realizar uma chamada de rede pode ser algo assim:

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

Usando essa abordagem, resolvemos o problema (de possíveis vazamentos de memória causados ​​por um thread em execução mantendo uma referência ao contexto externo) mantendo uma referência ao objeto Subscription retornado. Este objeto Subscription é então vinculado ao método #onDestroy() do objeto Activity / Fragment para garantir que a operação Action1#call não seja executada quando a Activity / Fragment precisar ser destruída.

Além disso, observe que o tipo de retorno de #getObservableUser(...) (ou seja, um Observable<User> ) está encadeado com outras chamadas para ele. Por meio dessa API fluida, podemos resolver o segundo problema de usar um AsyncTask , que permite mais chamadas de rede/encadeamento de operações longas. Bem legal, hein?

Vamos nos aprofundar em alguns conceitos do RxJava.

Observable, Observer e Operator - Os 3 O's do RxJava Core

No mundo RxJava, tudo pode ser modelado como streams. Um fluxo emite itens ao longo do tempo e cada emissão pode ser consumida/observada.

Se você pensar bem, um fluxo não é um conceito novo: eventos de clique podem ser um fluxo, atualizações de localização podem ser um fluxo, notificações push podem ser um fluxo e assim por diante.

No mundo RxJava, tudo pode ser modelado como streams.

A abstração de fluxo é implementada através de 3 construções centrais que eu gosto de chamar de “os 3 O's”; a saber: o observável , o observador e o operador . O Observable emite itens (o fluxo); e o Observador consome esses itens. Emissões de objetos observáveis ​​podem ainda ser modificadas, transformadas e manipuladas encadeando chamadas de operador .

Observável

Um Observable é a abstração de fluxo em RxJava. É semelhante a um Iterator , pois, dada uma sequência, ele itera e produz esses itens de maneira ordenada. Um consumidor pode consumir esses itens por meio da mesma interface, independentemente da sequência subjacente.

Digamos que queiramos emitir os números 1, 2, 3, nessa ordem. Para fazer isso, podemos usar o 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 um item no stream e, quando o stream termina de emitir, subscriber.onCompleted() é então invocado.

Essa abordagem para criar um Observable é bastante detalhada. Por esse motivo, existem métodos convenientes para criar instâncias observáveis ​​que devem ser preferidas em quase todos os casos.

A maneira mais simples de criar um Observable é usando Observable#just(...) . Como o nome do método sugere, ele apenas emite o(s) item(ns) que você passa para ele como argumentos de método.

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

Observador

O próximo componente do fluxo Observable é o Observer (ou Observers) inscrito nele. Os observadores são notificados sempre que algo “interessante” acontece no fluxo. Os observadores são notificados através dos seguintes eventos:

  • Observer#onNext(T) - invocado quando um item é emitido do fluxo
  • Observable#onError(Throwable) - invocado quando ocorre um erro no fluxo
  • Observable#onCompleted() - invocado quando o fluxo termina de emitir itens.

Para assinar um stream, basta chamar Observable<T>#subscribe(...) e passar uma instância 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); } });

O código acima emitirá o seguinte no Logcat:

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

Também pode haver alguns casos em que não estamos mais interessados ​​nas emissões de um Observável. Isso é particularmente relevante no Android quando, por exemplo, uma Activity / Fragment precisa ser recuperada na memória.

Para parar de observar itens, simplesmente precisamos chamar Subscription#unsubscribe() no objeto Subscription retornado.

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

Conforme visto no trecho de código acima, ao assinar um Observable, mantemos a referência ao objeto Subscription retornado e depois invocamos subscription#unsubscribe() quando necessário. No Android, isso é melhor invocado em Activity#onDestroy() ou Fragment#onDestroy() .

Operador

Itens emitidos por um Observable podem ser transformados, modificados e filtrados por meio de Operadores antes de notificar o(s) objeto(s) Observador(es) inscrito(s). Algumas das operações mais comuns encontradas na programação funcional (como mapear, filtrar, reduzir, etc.) também podem ser aplicadas a um fluxo observável. Vejamos o mapa como exemplo:

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

O trecho de código acima pegaria cada emissão do Observable e multiplicaria cada uma por 3, produzindo o fluxo 3, 6, 9, 12, 15, respectivamente. A aplicação de um operador normalmente retorna outro observável como resultado, o que é conveniente, pois isso nos permite encadear várias operações para obter um resultado desejado.

Dado o fluxo acima, digamos que queremos receber apenas números pares. Isso pode ser alcançado encadeando uma operação 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) { // ... } });

Existem muitos operadores integrados ao conjunto de ferramentas RxJava que modificam o fluxo Observable; se você puder pensar em uma maneira de modificar o fluxo, é provável que exista um operador para isso. Ao contrário da maioria das documentações técnicas, ler os documentos RxJava/ReactiveX é bastante simples e direto ao ponto. Cada operador na documentação vem com uma visualização de como o Operador afeta o fluxo. Essas visualizações são chamadas de “diagramas de mármore”.

Veja como um operador hipotético chamado flip pode ser modelado por meio de um diagrama de mármore:

Exemplo de como um operador hipotético chamado flip pode ser modelado através de um diagrama de mármore.

Multithreading com RxJava

O controle do encadeamento no qual as operações ocorrem na cadeia Observable é feito especificando o Agendador no qual um operador deve ocorrer. Essencialmente, você pode pensar em um Scheduler como um pool de threads que, quando especificado, um operador usará e executará. Por padrão, se nenhum Scheduler for fornecido, a cadeia Observable operará no mesmo thread em que Observable#subscribe(...) é chamado. Caso contrário, um Scheduler pode ser especificado via Observable#subscribeOn(Scheduler) e/ou Observable#observeOn(Scheduler) em que a operação agendada ocorrerá em um thread escolhido pelo Scheduler.

A principal diferença entre os dois métodos é que Observable#subscribeOn(Scheduler) instrui a fonte Observable em qual Agendador ele deve ser executado. A cadeia continuará a ser executada no encadeamento do Agendador especificado em Observable#subscribeOn(Scheduler) até que uma chamada para Observable#observeOn(Scheduler) seja feita com um Agendador diferente. Quando tal chamada é feita, todos os observadores de lá em diante (ou seja, operações subsequentes na cadeia) receberão notificações em um encadeamento retirado do agendador observeOn .

Aqui está um diagrama de mármore que demonstra como esses métodos afetam onde as operações são executadas:

Um diagrama de mármore que demonstra como esses métodos afetam onde as operações são executadas.

No contexto do Android, se uma operação de interface do usuário precisar ocorrer como resultado de uma operação longa, gostaríamos que essa operação ocorresse no encadeamento da interface do usuário. Para isso, podemos usar AndroidScheduler#mainThread() , um dos Schedulers fornecidos na biblioteca RxAndroid.

RxJava no Android

Agora que temos algumas noções básicas em nosso currículo, você pode estar se perguntando - qual é a melhor maneira de integrar o RxJava em um aplicativo Android? Como você pode imaginar, existem muitos casos de uso para RxJava, mas, neste exemplo, vamos dar uma olhada em um caso específico: usar objetos Observable como parte da pilha de rede.

Neste exemplo, veremos o Retrofit, um cliente HTTP de código aberto da Square que possui ligações internas com RxJava para interagir com a API do GitHub. Especificamente, criaremos um aplicativo simples que apresenta todos os repositórios com estrela para um usuário com um nome de usuário do GitHub. Se você quiser avançar, o código-fonte está disponível aqui.

Criar um novo projeto Android

  • Comece criando um novo projeto Android e nomeando-o GitHubRxJava .

Captura de tela: Criar um novo projeto Android

  • Na tela Target Android Devices , mantenha Phone and Tablet selecionados e defina o nível mínimo de SDK de 17. Sinta-se à vontade para defini-lo para um nível de API inferior/superior, mas, para este exemplo, o nível de API 17 será suficiente.

Captura de tela: tela de dispositivos Android de destino

  • Selecione Atividade vazia no próximo prompt.

Captura de tela: Adicionar uma atividade à tela do celular

  • Na última etapa, mantenha o nome da atividade como MainActivity e gere um arquivo de layout activity_main .

Captura de tela: personalize a tela Atividade

Configuração do projeto

Inclua RxJava, RxAndroid e a biblioteca Retrofit em app/build.gradle . Observe que incluir RxAndroid implicitamente também inclui RxJava. No entanto, é uma prática recomendada sempre incluir essas duas bibliotecas explicitamente, pois o RxAndroid nem sempre contém a versão mais atualizada do RxJava. Incluir explicitamente a versão mais recente do RxJava garante o uso da versão mais atualizada.

 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 }

Criar objeto de dados

Crie a classe de objeto de dados GitHubRepo . Essa classe encapsula um repositório no GitHub (a resposta da rede contém mais dados, mas estamos interessados ​​apenas em um subconjunto disso).

 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 de configuração

  • Crie a interface GitHubService . Passaremos essa interface para o Retrofit e o Retrofit criará uma implementação do GitHubService .
 public interface GitHubService { @GET("users/{user}/starred") Observable<List<GitHubRepo>> getStarredRepositories(@Path("user") String userName); }
  • Crie a classe GitHubClient . Este será o objeto com o qual interagiremos para fazer chamadas de rede no nível da interface do usuário.

    • Ao construir uma implementação do GitHubService por meio do Retrofit, precisamos passar um RxJavaCallAdapterFactory como o adaptador de chamada para que as chamadas de rede possam retornar objetos Observable (é necessário passar um adaptador de chamada para qualquer chamada de rede que retorne um resultado diferente de Call ).

    • Também precisamos passar um GsonConverterFactory para que possamos usar o Gson como uma maneira de empacotar objetos JSON para 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); } }

Layouts de configuração

Em seguida, crie uma interface do usuário simples que exiba os repositórios recuperados com um nome de usuário do GitHub de entrada. Crie activity_home.xml - o layout para nossa atividade - com algo como o seguinte:

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

Crie item_github_repo.xml - o layout de item ListView para o objeto de repositório GitHub - com algo como o seguinte:

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

Cola tudo junto

Crie um ListAdapter responsável por vincular objetos GitHubRepo em itens ListView . O processo envolve essencialmente inflar item_github_repo.xml em uma View se nenhuma View reciclada for fornecida; caso contrário, uma View reciclada é reutilizada para evitar a superinflação de muitos objetos 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); } } }

Cole tudo em MainActivity . Esta é essencialmente a Activity que é exibida quando iniciamos o aplicativo pela primeira vez. Aqui, pedimos ao usuário que insira seu nome de usuário do GitHub e, finalmente, exiba todos os repositórios com estrela por esse nome de usuário.

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

Execute o aplicativo

A execução do aplicativo deve apresentar uma tela com uma caixa de entrada para inserir um nome de usuário do GitHub. A pesquisa deve apresentar a lista de todos os repositórios com estrela.

Captura de tela do aplicativo mostrando uma lista de todos os repositórios com estrela.

Conclusão

Espero que isso sirva como uma introdução útil ao RxJava e uma visão geral de seus recursos básicos. Há uma tonelada de conceitos poderosos em RxJava e exorto você a explorá-los cavando mais profundamente no bem documentado wiki RxJava.

Sinta-se à vontade para deixar qualquer dúvida ou comentário na caixa de comentários abaixo. Você também pode me seguir no Twitter em @arriolachris, onde eu tweeto muito sobre RxJava e tudo relacionado ao Android.

Se você quiser um recurso de aprendizado abrangente em RxJava, confira o ebook que escrevi com Angus Huang no Leanpub.

Relacionado: Dez recursos Kotlin para impulsionar o desenvolvimento do Android