Functional Reactive Programming para Desenvolvedores Android — Part 3
Hello, em primeiro lugar parabéns por ter acompanhado os dois primeiros posts desta série onde comecei por dar uma introdução ao conceito e a teoria por de trás de reactive programming no primeiro post, seguido de um segundo post mais técnico focado na configuração da biblioteca RxJava e a criação de Observable simples para emitir um fluxo dados e um Observer(Subscriber) que fica a escuta e reage a medida que vai recebendo os dados.
Para este post irei falar sobre:
- Funções ou Operadores no RxJava e o que é possível fazer com eles.
Funções ou Operadores
Como expliquei no primeiro post reactive programming é como se fosse um upgrade ao Observer Pattern em que ao invés de ter um só componente que emite um fluxo de dados e outro que recebe e reage, existe um conjunto de funções(operadores) que permitem manipular o fluxo de dados de várias formas de acordo com a forma que queira que o fluxo esteja no momento da recepção pelo Observer.
De forma a entender o que é e como realmente acontecem as coisas ao pensar em termos de eventos, fluxo de dados e as funções aplicadas a este fluxo vou utilizar uma representação chamada Marble Diagrams para explicar.
Marble Diagrams
De forma muito simples, Marble Diagrams permitem representar fluxos de dados assíncronos.
Como mostra a figura abaixo, as duas linhas na horizontal representam o tempo. Neste tempo o fluxo de dados é emitido da esquerda para a direita e a pequena linha perpendicular a linha do tempo(as vezes um “x”) mais a direita representa o fim do fluxo.
O retângulo entre as duas linhas representa uma função, que será aplicada a cada elemento a ser emitido no fluxo de dados.
Para interpretar um Marble Diagram é preciso:
- Olhar para a primeira linha de tempo antes da função;
- Pegar nos elementos que estão a ser emitidos um de cada vez;
- Aplicar a função representada no retângulo
- E por fim colocar o elemento transformado na função no mesmo instante que este passou pela função na linha de tempo de saída.
Sendo assim, pegando o elemento número 2 da figura abaixo:
Este elemento é emitido pela fonte e passa por uma função(neste caso multiplicado o seu valor por 10 na função map) que o transforma em um novo número com o valor de 20.
Dica: Marble Diagrams são uma forma muito prática de entender o funcionamento de um operador sempre que a sua implementação ou documentação não for muito clara.
O site oficial da Reactive Streams e o RxMarbles contem muito boas representações que te vão ajudar na visualização do fluxo de saída depois de passar por um operador.
Operadores Interessantes(na minha Opinião Importantes)
Existe uma lista enorme de operadores e as melhores práticas para a sua aplicação no site da ReactiveX.
Para este post, irei focar-me apenas nos que considero importante e que tenho utilizado de forma repetitiva no desenvolvimento de apps para android.
map — O operador map como mostrei acima, é um dos mais simples pois o seu trabalho é de receber cada item a medida que estes são emitidos no fluxo de dados e o transformar em um outro Item(do mesmo tipo ou não) de acordo com a função definida.
Como mostra o exemplo abaixo, o Observable emite um nome mas utilizando a função map, a String “Dario” é transformada em “Hello I am Dario” e emitida para o subscriber. Super Simples =).
Observable.just("Dario").map(new Func1<String, String>() {
@Override
public String call(String name) {
return String.format("Hello I am %s", name);
}
});
flatMap — O Operador flat map opera de forma similar ao Operador Map transformando cada item emitido no fluxo de dados com a pequena diferença de retornar um novo fluxo de dados(Observable) ao invés de um item.
Apesar de existir um operador que ajuda no ordenamento dos items do fluxo de dados, como mostra o código abaixo, utilizei o operador flatMap para transformar uma lista de filmes em uma lista de filmes devidamente organizada em ordem alfabética simplesmente realizando a operação de ordenamento dentro do flatMap e emitir um um novo observable que emite a lista dos filmes ordenada pelos titulos.
Um caso de uso interessante para o flatMap pode ser encontrando em momentos que precisar de fazer duas chamadas a uma API em que uma depende da outra para começar.
Por exemplo, se precisar carregar uma lista de filmes favoritos e depois pegar os detalhes de cada filme, pode criar um Observable(call para carregar os id’s dos filmes favoritos da API) que irá enviar um fluxo de dados composto pelos Id’s dos filmes e utilizar o operador flatMap em cada elemento do fluxo para retornar um outro Observable(call para carregar os dados de um filme pelo Id) para compor único fluxo capaz de emitir apenas os filmes e os detalhes.
Atingir este objectivo de forma imperativa, implicaria escrever muito código utilizando callbacks para iniciar a segunda chamada assim que a primeira retornar os id’s dos filmes.
public Observable<List<Movie>> loadMovies() {
return service.loadMovies().flatMap(new Func1<List<Movie>, Observable<List<Movie>>>() {
@Override
public Observable<List<Movie>> call(List<Movie> movies) {
MovieUtils.sortByTitleAsc(movies);
return Observable.just(movies);
}
});
}
subscribeOn vs ObserveOn
Este dois operadores são as jóias da reactive programming pois facilitam muito na programação concorrente em que são utilizadas múltiplas threads para realização de diferentes tarefas em paralelo.
Voltando um pouco as bases do desenvolvimento para android, uma aplicação iniciada é executada na Main Thread(UI Thread) e esta é responsável por executar tarefas como gerir a interface gráfica, ficar a escuta de eventos da parte do utilizador dentre outras para garantir que a aplicação esta sempre responsiva para o utlizador.
Realizar tarefas com tempo indeterminado de fim como fazer a chamada a uma API na internet ou carregar os dados de uma base de dados local entre outros bloqueiam a Main Thread de continuar a fazer o seu trabalho e consequentemente a aplicação fica bloqueada e apresenta um erro ao utilizador.
Desde os primeiros dias do android, soluções do framework como AsyncTasks, Loaders permitiram a criação de uma nova Thread para executar estas tarefas e por fim enviar de volta a resposta a Main Thread.
O maior problema é que soluções como AsyncTask que dependem de uma Activity não são muito flexíveis principalmente se tiver em conta a escrita de um código que seja fácil de manter e testar(Quem já utilizou AsyncTasks e Loaders sabe o quão complicado é o código).
subscribeOn
Olhando para todos os exemplos escritos nesta série do posts, todos os Observables criados emitem o fluxo de dados na thread (Main thread) em que é invocado o método subscribe ao observable por parte de um subscriber.
Sendo assim, se qualquer um dos produtores do fluxo de dados realizar uma operação com tempo de fim indeterminado(Chamada a uma API), certamente irá encontrar os problemas relacionados ao bloqueio de uma thread como a Main Thread do Android.
Utilizando o operador subscribeOn, é possível especificar a Thread que será utilizada pelo produtor para emissão do fluxo de dados.
Este operador recebe como parâmetro um Scheduler que é responsável pela criação e gestão das threads como mostra o código abaixo em que a chamada a uma API para carregar os filmes em exibição será executado em uma nova thread criada pelo Schedulers.newThread.
public Observable<List<Movie>> getShowingMovies(){
return Observable.just(loadShowingMovies())
.subscribeOn(Schedulers.newThread());
}
public List<Movie> loadShowingMovies(){
List<Movie> showingMovies = new ArrayList<>();
//load from network and return list
return showingMovies;
}
ObserveOn
Depois de utilizar o operador subscribeOn para criar uma outra thread que será responsável por carregar os filmes em exibição, poderá ser necessário especificar em que thread é que o subscriber vai receber o fluxo de dados. Para o exemplo acima o mais provável que irá querer que o subscriber receba o fluxo de dados na Main Thread da aplicação mas caso fosse necessário poderia definir uma outra thread utilizando o operador observeOn.
Assim como o operador subscribeOn, o observeOn recebe como parâmetro um Scheduler que vai definir e gerir a thread em em que o subscriber irá receber o fluxo de dados.
Para definir que um subscriber receba o fluxo de dados na Main Thread do Android é preciso passar como parâmetro o scheduler AndroidSchedulers.MainThread que faz parte da biblioteca RxAndroid(Extensão da biblioteca RxJava especifica para Android) como mostra o código abaixo:
public Observable<List<Movie>> getShowingMovies(){
return Observable.just(loadShowingMovies())
.subscribeOn(Schedulers.newThread())
.//outros operadores flatMap, map, etc
.observeOn(AndroidSchedulers.mainThread());
}
public List<Movie> loadShowingMovies(){
List<Movie> showingMovies = new ArrayList<>();
//load from network and return list
return showingMovies;
}
Algumas coisas a ter em conta
- O operador subscribeOn só pode ser utilizado uma vez para cada observable e todos os operadores definidos logo a seguir no observable tirando o observeOn irão realizar o seu trabalho na thread que foi definida.
- Se definir várias vezes o operador onSusbscribeOn, só a primeira definição(a mais próxima da fonte do fluxo) será utilizada.
- O operador observeOn pode ser utilizado várias vezes e todos os operadores que forem definidos logo depois a sua utilização irão realizar a sua função na thread que este especifica.(Não é aconselhável pois há possibilidade de ter de lidar com backpressure em casos em que um observable em uma thread emite os dados mais rápido do que o observable que recebe o fluxo na outra thread pode processar)
Espero que a esta altura tenhas entendido o poder da Reactive Programming e como este pattern te pode te ajudar a melhorar no desenvolvimento de apps de qualidade.
Caso tenhas alguma duvida não hesite em adicionar um comentários para que possamos levar a conversa adiante :)
Se achaste este post interessante, deixe ficar um like clicando no coraçãozinho abaixo e partilhe com amigos!
Ate a próxima,
DM =)