Construindo um sistema de componentes reativos com o Kotlin



Olá pessoal! Meu nome é Anatoly Varivonchik, sou desenvolvedor Android do Badoo. Hoje vou compartilhar com vocês a tradução da segunda parte do artigo, do meu colega Zsolt Kocsi, sobre a implementação do MVI, que usamos diariamente no processo de desenvolvimento. A primeira parte está aqui .

O que queremos e como fazemos


Na primeira parte do artigo, apresentamos o Features , os elementos centrais do MVICore que podem ser reutilizados. Eles podem ter a estrutura mais simples e incluir apenas um Redutor , ou podem se tornar uma ferramenta totalmente funcional para gerenciar tarefas assíncronas, eventos e muito mais.

Cada recurso é rastreável - há a oportunidade de se inscrever em alterações em seu status e receber notificações sobre ele. Nesse caso, o recurso pode ser inscrito na fonte de entrada. E isso faz sentido, porque com a inclusão do Rx na base de código, já temos muitos objetos e assinaturas observáveis ​​em vários níveis.

É em conexão com o aumento do número de componentes reativos que é hora de refletir sobre o que temos e se é possível tornar o sistema ainda melhor.

Temos que responder a três perguntas:

  1. Quais elementos devem ser usados ​​ao adicionar novos componentes reativos?
  2. Qual é a maneira mais fácil de gerenciar suas assinaturas?
  3. É possível ignorar o gerenciamento do ciclo de vida / a necessidade de limpar assinaturas para evitar vazamentos de memória? Em outras palavras, podemos separar a associação de componentes do gerenciamento de assinaturas?

Nesta parte do artigo, examinaremos os princípios e benefícios da construção de um sistema usando componentes reativos e veremos como o Kotlin ajuda nisso.

Elementos principais


Quando começamos a trabalhar no design e na padronização de nossos Recursos , já tínhamos tentado muitas abordagens diferentes e decidimos que os Recursos teriam a forma de componentes reativos. Primeiro, nos concentramos nas principais interfaces. Antes de tudo, precisávamos determinar os tipos de dados de entrada e saída.

Nós argumentamos da seguinte forma:

  • Não vamos reinventar a roda - vamos ver quais interfaces já existem.
  • Como já estamos usando a biblioteca RxJava, faz sentido fazer referência às suas interfaces básicas.
  • O número de interfaces deve ser minimizado.

Como resultado, decidimos usar ObservableSource <T> para saída e Consumer <T> para entrada. Por que não Observável / Observador , você pergunta. Observable é uma classe abstrata da qual você precisa herdar e ObservableSource é a interface que você implementa que satisfaz totalmente a necessidade de implementar um protocolo reativo.

package io.reactivex; import io.reactivex.annotations.*; /** * Represents a basic, non-backpressured {@link Observable} source base interface, * consumable via an {@link Observer}. * * @param <T> the element type * @since 2.0 */ public interface ObservableSource<T> { /** * Subscribes the given Observer to this ObservableSource instance. * @param observer the Observer, not null * @throws NullPointerException if {@code observer} is null */ void subscribe(@NonNull Observer<? super T> observer); } 

O Observer , a primeira interface que vem à mente, implementa quatro métodos: onSubscribe, onNext, onError e onComplete. Em um esforço para simplificar o protocolo o máximo possível, preferimos o Consumidor <T> , que aceita novos elementos usando um único método. Se escolhermos o Observer , os métodos restantes geralmente serão redundantes ou funcionarão de maneira diferente (por exemplo, gostaríamos de apresentar erros como parte do estado, e não como exceções, e certamente não interromper o fluxo).

 /** * A functional interface (callback) that accepts a single value. * @param <T> the value type */ public interface Consumer<T> { /** * Consume the given value. * @param t the value * @throws Exception on error */ void accept(T t) throws Exception; } 

Portanto, temos duas interfaces, cada uma das quais contém um método. Agora podemos vinculá-los assinando Consumer <T> em ObservableSource <T> . Este último aceita apenas instâncias do Observer <T> , mas podemos agrupá-lo em um Observable <T> , que é inscrito no Consumidor <T> :

 val output: ObservableSource<String> = Observable.just("item1", "item2", "item3") val input: Consumer<String> = Consumer { System.out.println(it) } val disposable = Observable.wrap(output).subscribe(input) 

(Felizmente, a função .wrap (output) não cria um novo objeto se a saídafor um Observable <T> ).

Lembre-se de que o componente Feature da primeira parte do artigo usava dados de entrada do tipo Wish (correspondente a Intent de Model-View-Intent) e saída do tipo State e, portanto, pode estar nos dois lados do pacote configurável:

 // Wishes -> Feature val wishes: ObservableSource<Wish> = Observable.just(Wish.SomeWish) val feature: Consumer<Wish> = SomeFeature() val disposable = Observable.wrap(wishes).subscribe(feature) // Feature -> State consumer val feature: ObservableSource<State> = SomeFeature() val logger: Consumer<State> = Consumer { System.out.println(it) } val disposable = Observable.wrap(feature).subscribe(logger) 

Essa vinculação entre Consumidor e Produtor já parece bastante simples, mas existe uma maneira ainda mais fácil pela qual você não precisa criar assinaturas manualmente ou cancelá-las.

Apresentando o Binder .

Ligação esteróide


O MVICore contém uma classe chamada Binder que fornece uma API simples para gerenciar assinaturas Rx e possui vários recursos interessantes.

Por que é necessário?

  • Crie uma ligação assinando entrada para o fim de semana.
  • A capacidade de cancelar a inscrição no final do ciclo de vida (quando é um conceito abstrato e não tem nada a ver com o Android).
  • Bônus: O Fichário permite adicionar objetos intermediários, por exemplo, para registro ou depuração de viagens no tempo.

Em vez de assinar manualmente, você pode reescrever os exemplos acima da seguinte maneira:

 val binder = Binder() binder.bind(wishes to feature) binder.bind(feature to logger) 

Graças a Kotlin, tudo parece muito simples.

Esses exemplos funcionam se o tipo de entrada e saída for o mesmo. Mas e se não for? Ao implementar a função de extensão, podemos tornar a transformação automática:

 val output: ObservableSource<A> = TODO() val input: Consumer<B> = TODO() val transformer: (A) -> B = TODO() binder.bind(output to input using transformer) 

Preste atenção à sintaxe: ela parece quase como uma frase normal (e essa é outra razão pela qual eu amo Kotlin). Mas o Binder não é usado apenas como açúcar sintático - também é útil para resolver problemas com o ciclo de vida.

Criar Fichário


Criar uma instância não parece nada fácil:

 val binder = Binder() 

Mas, nesse caso, é necessário cancelar a assinatura manualmente e chamar binder.dispose() sempre que precisar excluir assinaturas. Há outra maneira: injetar a instância do ciclo de vida no construtor. Assim:

 val binder = Binder(lifecycle) 

Agora você não precisa se preocupar com as assinaturas - elas serão excluídas no final do ciclo de vida. Ao mesmo tempo, o ciclo de vida pode ser repetido várias vezes (como o ciclo de início e parada na interface do usuário do Android) - e o Binder cria e exclui assinaturas para você a cada vez.

E o que é um ciclo de vida?


A maioria dos desenvolvedores do Android, vendo a frase "ciclo de vida", representa os ciclos de atividade e fragmento. Sim, o Binder pode trabalhar com eles, cancelando a inscrição no final do ciclo.

Mas isso é apenas o começo, porque você não usa a interface do Android LifecycleOwner de forma alguma - o Binder possui uma própria e mais universal. É essencialmente um fluxo de sinal BEGIN / END:

 interface Lifecycle : ObservableSource<Lifecycle.Event> { enum class Event { BEGIN, END } // Remainder omitted } 

Você pode implementar esse fluxo usando Observable (pelo mapeamento) ou simplesmente usar a classe ManualLifecycle da biblioteca para ambientes que não sejam Rx (veja exatamente abaixo).

Como funciona o fichário ? Recebendo um sinal BEGIN, ele cria assinaturas para os componentes que você configurou anteriormente ( entrada / saída ) e, ao receber um sinal END, os exclui. O mais interessante é que você pode começar tudo de novo:

 val output: PublishSubject<String> = PublishSubject.create() val input: Consumer<String> = Consumer { System.out.println(it) } val lifecycle = ManualLifecycle() val binder = Binder(lifecycle) binder.bind(output to input) output.onNext("1") lifecycle.begin() output.onNext("2") output.onNext("3") lifecycle.end() output.onNext("4") lifecycle.begin() output.onNext("5") output.onNext("6") lifecycle.end() output.onNext("7") // will print: // 2 // 3 // 5 // 6 

Essa flexibilidade na reatribuição de assinaturas é especialmente útil ao trabalhar com o Android, quando pode haver vários ciclos de Start-Stop e Resume-Pause, além do habitual Create-Destroy.

Ciclos de vida do Android Binder


Existem três classes na biblioteca:

  • CreateDestroyBinderLifecycle ( androidLifecycle )
  • StartStopBinderLifecycle ( androidLifecycle )
  • ResumePauseBinderLifecycl e ( androidLifecycle )

androidLifecycle é o valor retornado pelo método getLifecycle() , ou seja, AppCompatActivity , AppCompatDialogFragment , etc. Tudo é muito simples:

 fun createBinderForActivity(activity: AppCompatActivity) = Binder(   CreateDestroyBinderLifecycle(activity.lifecycle) ) 

Ciclos de vida individuais


Não vamos parar por aí, porque não estamos ligados ao Android de forma alguma. Qual é o ciclo de vida de um fichário ? Literalmente, qualquer coisa: por exemplo, o tempo de reprodução de um diálogo ou o tempo de execução de alguma tarefa assíncrona. Você pode, por exemplo, vinculá-lo ao escopo de DI - e qualquer assinatura será excluída com ele. Total liberdade de ação.

  1. Deseja que as assinaturas sejam salvas antes que o Observable envie o item? Converta esse objeto em Ciclo de Vida e passe-o para o Fichário . Implemente o seguinte código na função de extensão e use-o posteriormente:

     fun Observable<T>.toBinderLifecycle() = Lifecycle.wrap(this   .first()   .map { END }   .startWith(BEGIN) ) 
  2. Deseja manter seus vínculos até que o Completable seja concluído? Sem problemas - isso é feito por analogia com o parágrafo anterior:

     fun Completable.toBinderLifecycle() = Lifecycle.wrap(   Observable.concat(       Observable.just(BEGIN),       this.andThen(Observable.just(END))   ) ) 
  3. Deseja outro código não Rx para decidir quando remover as assinaturas? Use o Ciclo de Vida Manual conforme descrito acima.

Em qualquer caso, você pode colocar um fluxo reativo no fluxo do elemento Lifecycle.Event ou usar ManualLifecycle se estiver trabalhando com código não Rx.

Visão Geral do Sistema


O fichário oculta os detalhes da criação e gerenciamento de assinaturas Rx. Tudo o que resta é uma breve visão geral generalizada: “O componente A interage com o componente B no escopo C”.

Suponha que tenhamos os seguintes componentes reativos para a tela atual:



Gostaríamos que os componentes fossem conectados na tela atual e sabemos que:

  • O UIEvent pode ser alimentado diretamente no AnalyticsTracker ;
  • O UIEvent pode ser transformado no Wish for Feature ;
  • O estado pode ser transformado em um ViewModel para uma View .

Isso pode ser expresso em algumas linhas:

 with(binder) {   bind(feature to view using stateToViewModelTransformer)   bind(view to feature using uiEventToWishTransformer)   bind(view to analyticsTracker) } 

Fazemos tais comprimentos para demonstrar a interconexão de componentes. E como nós desenvolvedores passamos mais tempo lendo o código do que escrevendo, uma breve visão geral é extremamente útil, especialmente à medida que o número de componentes aumenta.

Conclusão


Vimos como o Binder ajuda no gerenciamento de assinaturas Rx e como ele ajuda a obter uma visão geral de um sistema criado a partir de componentes reativos.

Nos artigos a seguir, descreveremos como separamos os componentes reativos da interface do usuário da lógica de negócios e como adicionar objetos intermediários usando o Binder (para registro e depuração de viagens no tempo). Não mude!

Enquanto isso, confira a biblioteca no GitHub .

Source: https://habr.com/ru/post/pt430550/


All Articles