Por que programação reativa?

A seguir os slides utilizados na apresentação que Felipe Costa e eu fizemos na Android Dev Conference 2017. O conteúdo apresentado possui conceitos básicos de programação reativa e motivação para uso com base em exemplos práticos em Kotlin.

Introdução

Analisando a premissa mais básica de um programa: Programas pegam entrada e produzem saída. A saída é o resultado de fazer algo com a entrada. Entrada, transforma, saída, pronto.


Isso é fácil de visualizar numa função de contar elementos de uma lista: Pegue a lista, conte a quantidade de elementos e retorne o resultado obtido.


Esse padrão também é fácil de perceber no UNIX, como este simples comando no terminal onde são abertos para visualização no diretório atual os elementos que contenham a palavra key.


Mas é um pouco mais difícil de perceber isso quando falamos de um app Android com UI, diferentes funcionalidades, tarefas periódicas, etc.

A entrada são todas as fontes de ação no seu app.


Todos eles alimentam o app que combina todos eles de alguma forma e produz um resultado. Mas diferente do clássico design de entrada/saída, esse tipo de ciclo é contínuo enquanto o app estiver aberto. Ele sempre consome entradas e produz saídas de forma imperativa.


Então, vamos analisar como programamos utilizando programação imperativa…

Apesar deste programa estar descrito de forma sequencial, a execução ocorre de forma diferente como apresentado na linha do tempo.


Eventos continuam a surgir de forma linear, sem saber o que aconteceu anteriormente. Mas para gerar uma saída, é necessário combinar a informação atual com o que aconteceu previamente de forma relevante para saída atual.


A forma que nós usamos para guardar essas informações do passado é chamada de estado. Estado é que tornar o nosso trabalho de desenvolvedores MUITO difícil às vezes.


Gerenciamento de estado é exponencialmente complexo.


Mas deve haver outra forma…

O que realmente queremos fazer é remover a necessidade de gerenciar os estados dentro de um programa e simplesmente declarar como o fluxo do programa deve ocorrer. Isso é Programação Reativa.


Um pouco de contexto…

Programação reativa no contexto de desenvolvimento de software foi citado pela primeira vez por Gérard Berry no artigo “Real time programming : special purpose or general purpose languages”.

Rx Microsoft foi lançado em novembro de 2009 para .NET & Silverlight. Hoje, porém, já existem Rx para várias linguagens como: Java, Javascript, Ruby, Kotlin. Mais informações nesse link.

Rx é o que muitos costumam chamar de “programação funcional reativa”. De acordo com a documentação: “Rx pode ser chamado reativo, e pode ser chamado funcional, mas não funcional reativo”.

Entretanto, Funcional Reativo opera sobre valores que mudam constantemente ao longo do tempo, enquanto Rx opera em valores discretos emitidos ao longo de um tempo.


O que é programação reativa?

Uma definição poderia ser:

"Programação reativa é programar com fluxos de dados assíncronos"

Mas há outras maneiras para fazer isso usando broadcasts, MVP, Event Bus, Callbacks.

Porém em todos esses casos precisamos guardar estado. E como já vimos controle de estado poder levar ao caos e aumenta em complexidade exponencialmente.

Mas então o que é programação reativa?

É um paradigma de programação orientado a fluxo de dados e propagação de mudança.

Temos aqui duas palavras chaves: PROPAGAÇÃO DE MUDANÇA e FLUXO DE DADOS.


Propagação de mudança pode ser percebida em uma planilha onde declaramos uma fórmula.

Com programação imperativa, o valor seria atribuído apenas uma vez e se houvesse mudança no valor de beta ou gama alfa não seria alterado.


Porém na programação reativa, ao mudar o valor de beta para 30, o valor de alfa é “automagicamente” mudado para 50.

Pensando como um fluxo, é como se o valor de beta fluísse para alfa, propagando a mudança pela planilha.


Propagação de mudança remete a um padrão muito conhecido: O padrão do Observador.

Esse padrão define uma dependência de um pra muitos, onde você tem um Sujeito e uma interface Observador, a qual pode ser implementada por muitos.


Nesse exemplo temos um Sujeito, o qual possui um estado ou informação que os Observadores estão interessados. Eles então se registram para ouvir mudanças de estado ou notificações de um Sujeito.


O Sujeito podem então notificar Observadores registrados quando necessário, como, por exemplo, na ocorrência de uma mudança de estado.


Os Observadores podem, então, agir de acordo com as mudanças que ocorreram da maneira como foram programados.


O Sujeito também pode ser chamado Observável (Observable) e pode ser definido como um recipiente que emite sinais para observadores ao longo do tempo.


Os sinais emitidos pelos Observáveis podem ser considerados um fluxo de dados (Data Stream). Outros exemplos de fluxo de dados são, por exemplo: partes de um arquivo sendo baixado, posições do cursor do mouse, um campo de texto sendo editado, etc


Exemplo de fluxo de dados

Um observável é criado com os elementos do tipo texto "Alfa", "Beta" e "Gama". Enquanto não há chamada de registro (subscribe), os eventos não são emitidos.

Com a chamada ao subscribe, os eventos são emitidos em ordem e impresso no console.

Quando todos os itens forem emitidos com sucesso o fluxo de dados termina e o onComplete é chamado imprimindo no console uma mensagem. Assim como em caso de erro, onError, é impresso no console uma message de erro — quando o fluxo termina em erro, onComplete não é executado.


Composição de Fluxos de Dados

Fluxo de dados podem ser compostos através da aplicação de uma transformação de um observável para outro.


Consideremos um exemplo de uma transformação que realiza o mapeamento dos valores emitidos multiplicando cada elemento por dois. Dessa forma, os valores emitidos (1, 2, 5, 8) passam a ser outros depois da transformação (2, 4, 10, 16).


Da mesma maneira com o operador de filtro (filter), somente serão emitidos os elementos pares neste caso.


Para exemplificar esses conceitos separamos dois exemplos: Double Tap e Up and Down.

Double tap


Consideremos agora um exemplo de duplo toque em Android. Isso poderia ser feito através da API de Gestures, mas sem essa possibilidade, como poderia ser implementada?


Através do controle de estado do tempo do último click. Mas como seria esse exemplo em programação reativa?


Através de composição de fluxo de dados!

A partir do fluxo de dados de eventos de clicks do usuário, um buffer é criado gerando uma lista dentro de um intervalo de 700 milisegundos. O tamanho da lista é extraída, e então, esse valor é mapeado para o texto que representa a quantidade de clicks realizado pelo usuário. Por fim, esse texto é exibido para o usuário.

Para melhor visualização de como esse fluxo funciona ao longo do tempo, veja o gif que preparamos abaixo!

Note que não houve qualquer necessidade do controle de estado e fluxo ficou mais claro!


Up and Down

Nessa situação, um contador bem simples é implementado como exemplo. Ao tocar o botão de up,o valor do campo de texto é incrementado. O inverso é válido para o botão de down. Como escrever esse programa?


Através do artifício de controle de estado. O contador de toques registra a quantidade de toques e a cada interação do usuário, ele é incrementado e decremetnado. E com programação reativa?


Dois fluxos de dados são criados a partir dos eventos de toques nos botões. Cada evento é mapeado para o valor de 1 ou -1.

Depois disso, esses fluxos são mergeados gerando um único fluxo de valores inteiros (1, 1, -1, 1, etc) sendo o primeiro valor zero o primeiro emitido com startWith. Cada valor, então, é agregado (scan) em um valor que representada um somatório de todos elementos emitodos no fluxo. Após serem mapeados para texto, são exibidos para o usuário.

Separamos mais um gif representando o que acontece no fluxo ao longo do tempo.


Callback Hell

O exemplo ao lado mostra como o uso de callbacks pode deixar o código menos legível. Neste caso é usado um callback de OnClickListener para favoritar um personagem. Em caso de sucesso a informação é salva em disco localmente, e a tela é atualizada para o usuário, se tudo ocorrer como esperado. Todavia, se um erro acontecer no processo é mostrado uma mensagem com o erro.


Mas como poderia ser feito com programação reativa?
Resposta: Com Streams!

No caso ao lado, foi usado a extensão rxbinding para transformar os clicks em um stream e mapeia a chamada para a requisição web. A requisição por sua vez devolve um Observable que é mapeado para um stream que salva em disco. Em caso de erro, nos dois casos o stream é encerrado e retorna um erro. Caso o fluxo finalize sem erro a tela é atualizada para o usuário.


Concorrência

Até agora vimos fluxo de dados sem se preocupar em que thread o fluxo de dados irá ser executado. Para auxiliar nessa tarefa, vamos usar schedulers.


Através do uso dos operadores subscribeOn e observeOn, declaramos onde o fluxo irá começar, no caso em uma nova thread, e a partir de onde ele irá mudar para a main thread.


Conclusão

Nessa apresentação foi abordado definição de Programação Reativa e motivação pra uso. Há ainda muitos tópicos em torno desse tema que poderiam ser abordado. Mas antes de encerrar, algumas armadilhas precisam ser citadas, como backpressure, problemas de memória e como usar logs. Mas esse assunto fica para um próximo post.

[][]

Slides

Vídeo da apresentação

Em breve

Referências

Alguns links para usadas na construção da apresentação: