
Muitos hoje amam a programação reativa. Tem muitas vantagens: a falta do chamado "
inferno de retorno de chamada ", o mecanismo interno de tratamento de erros e um estilo de programação funcional que reduz a probabilidade de erros. Significativamente mais fácil escrever código multiencadeado e mais fácil gerenciar fluxos de dados (combinar, dividir e converter).
Muitas linguagens de programação têm sua própria biblioteca reativa: RxJava para JVM, RxJS para JavaScript, RxSwift para iOS, Rx.NET etc.
Mas o que temos para Kotlin? Seria lógico supor que RxKotlin. E, de fato, existe uma biblioteca desse tipo, mas é apenas um conjunto de extensões para o RxJava2, o chamado "açúcar".
E, idealmente, eu gostaria de ter uma solução que atenda aos seguintes critérios:
- multiplataforma - conseguir escrever bibliotecas multiplataforma usando programação reativa e distribuí-las na empresa;
- Segurança nula - o sistema do tipo Kotlin nos protege de " erros de bilhões de dólares ", portanto valores nulos devem ser válidos (por exemplo,
Observable<String?>
);
- covariância e contravariância é outro recurso muito útil do Kotlin, que possibilita, por exemplo, converter com segurança o tipo
Observable<String>
para Observable<CharSequence>
.
Nós no Badoo decidimos não esperar o tempo à beira-mar e criamos uma biblioteca desse tipo. Como você deve ter adivinhado, chamamos Reaktive e postamos no
GitHub .
Neste artigo, examinaremos mais de perto as expectativas de programação reativa de Kotlin e veremos como as capacidades do Reaktive as correspondem.
Três benefícios naturais de Reaktive
Multiplataforma
A primeira vantagem
natural é mais importante. Atualmente, nossas equipes de iOS, Android e Mobile Web existem separadamente. Os requisitos são gerais, o design é o mesmo, mas cada equipe faz seu próprio trabalho.
O Kotlin permite escrever código multiplataforma, mas você precisa esquecer a programação reativa. E eu gostaria de poder escrever bibliotecas compartilhadas usando programação reativa e distribuí-las na empresa ou fazer upload no GitHub. Potencialmente, essa abordagem pode reduzir significativamente o tempo de desenvolvimento e a quantidade total de código.
Segurança nula
É mais sobre a falha do Java e do RxJava2. Em resumo, nulo não pode ser usado. Vamos tentar descobrir o porquê. Dê uma olhada nesta interface Java:
public interface UserDataSource { Single<User> load(); }
O resultado pode ser nulo? Para evitar ambiguidade, nulo não é permitido no RxJava2. E se você ainda precisar, é Talvez e Opcional. Mas em Kotlin não existem tais problemas. Podemos dizer que
Single<User>
e
Single<User?>
tipos diferentes e todos os problemas aparecem no estágio de compilação.
Covariância e contravariância
Esta é uma característica distintiva do Kotlin, algo que falta muito em Java. Você pode ler mais sobre isso no
manual . Vou dar apenas alguns exemplos interessantes de quais problemas surgem ao usar o RxJava no Kotlin.
Covariância :
fun bar(source: Observable<CharSequence>) { } fun foo(source: Observable<String>) { bar(source)
Como
Observable
é uma interface Java, esse código não é compilado. Isso ocorre porque tipos genéricos em Java são invariantes. É claro que você pode usar, mas usar operadores como o scan novamente levará a um erro de compilação:
fun bar(source: Observable<out CharSequence>) { source.scan { a, b -> "$a,$b" }
A instrução de varredura é diferente, pois seu tipo genérico “T” é de entrada e saída. Se Observable fosse a interface Kotlin, seu tipo T poderia ser indicado como fora e isso resolveria o problema:
interface Observable<out T> { … }
E aqui está um exemplo com contravariância:
fun bar(consumer: Consumer<String>) { } fun foo(consumer: Consumer<CharSequence>) { bar(consumer)
Pelo mesmo motivo que no exemplo anterior (tipos genéricos em Java são invariáveis), este exemplo não é compilado. A adição resolverá o problema, mas novamente não cem por cento:
fun bar(consumer: Consumer<in String>) { if (consumer is Subject) { val value: String = consumer.value
Bem, por tradição, no Kotlin esse problema é resolvido usando in na interface:
interface Consumer<in T> { fun accept(value: T) }
Assim, variabilidade e contravariância de tipos genéricos são a terceira vantagem
natural da biblioteca Reaktive.
Kotlin + Reativo = Reaktive
Passamos à coisa principal - a descrição da biblioteca Reaktive.
Aqui estão alguns dos seus recursos:
- É multiplataforma, o que significa que você pode finalmente escrever código geral. No Badoo, consideramos esse um dos benefícios mais importantes.
- Está escrito em Kotlin, o que nos dá as vantagens descritas acima: não há restrições para nulo, variação / contravariância. Isso aumenta a flexibilidade e fornece segurança durante a compilação.
- Não há dependência de outras bibliotecas, como RxJava, RxSwift etc., o que significa que não há necessidade de levar a funcionalidade da biblioteca a um denominador comum.
- API pura. Por exemplo, a interface
ObservableSource
no Reaktive é chamada simplesmente Observable
, e todos os operadores são funções de extensão localizadas em arquivos separados. Não há classes de Deus de 15.000 linhas. Isso torna possível aumentar facilmente a funcionalidade sem fazer alterações nas interfaces e classes existentes.
- Suporte para agendadores (usando
observeOn
familiares subscribeOn
e observeOn
).
- Compatível com RxJava2 (interoperabilidade), fornecendo conversão de origem entre Reaktive e RxJava2 e a capacidade de reutilizar agendadores do RxJava2.
- Conformidade com o ReactiveX .
Gostaria de falar um pouco mais sobre os benefícios que recebemos devido ao fato de a biblioteca estar escrita em Kotlin.
- No Reaktive, valores nulos são permitidos, porque no Kotlin é seguro. Aqui estão alguns exemplos interessantes:
observableOf<String>(null) //
val o1: Observable<String?> = observableOf(null)
val o2: Observable<String> = o1 // ,
val o1: Observable<String?> = observableOf(null)
val o2: Observable<String> = o1.notNull() // , null
val o1: Observable<String> = observableOf("Hello")
val o2: Observable<String?> = o1 //
val o1: Observable<String?> = observableOf(null)
val o2: Observable<String> = observableOf("Hello")
val o3: Observable<String?> = merge(o1, o2) //
val o4: Observable<String> = merge(o1, o2) // ,
Variação também é uma grande vantagem. Por exemplo, na interface Observable
, o tipo T é declarado como out
, o que torna possível escrever algo como o seguinte:
fun foo() { val source: Observable<String> = observableOf("Hello") bar(source)
É assim que a biblioteca se parece hoje:- status no momento da redação: alfa (são possíveis algumas alterações na API pública);
- plataformas suportadas: JVM e Android;
- fontes suportadas:
Observable
, Maybe
, Single
e Completable
;
- um número bastante grande de operadores é suportado, incluindo map, filter, flatMap, concatMap, combineLatest, zip, merge e outros (a lista completa pode ser encontrada no GitHub );
- Os seguintes agendadores são suportados: computação, IO, trampolim e principal;
- assuntos: PublishSubject e BehaviorSubject;
- a contrapressão ainda não é suportada, mas estamos pensando na necessidade e na implementação desse recurso.
Quais são nossos planos para o futuro próximo:- começar a usar o Reaktive em nossos produtos (atualmente estamos considerando opções);
- Suporte a JavaScript (solicitação pull já em revisão);
- suporte para iOS
- publicar artefatos no JCenter (atualmente usando o serviço JitPack);
- Documentação
- aumento do número de operadores suportados;
- Testes
- mais plataformas - solicitações pull são bem-vindas!
Você pode experimentar a biblioteca agora e pode encontrar tudo o que precisa no
GitHub . Compartilhe sua experiência e faça perguntas. Ficaremos gratos por qualquer feedback.