Reaktive - multi-platform library for reactive Kotlin
Many today love reactive programming. It has a lot of advantages: the lack of the so-called " callback hell ", and the built-in error handling mechanism, and a functional programming style that reduces the likelihood of bugs. Significantly easier to write multi-threaded code and easier to manage data streams (combine, split and convert).
Many programming languages have their own reactive library: RxJava for JVM, RxJS for JavaScript, RxSwift for iOS, Rx.NET, etc.
But what do we have for Kotlin? It would be logical to assume that RxKotlin. And, indeed, such a library exists, but it’s just a set of extensions for RxJava2, the so-called “sugar”.
And ideally, I would like to have a solution that meets the following criteria:
- multi-platform - to be able to write multi-platform libraries using reactive programming and distribute them within the company;
- Null safety - the Kotlin type system protects us from " billion dollar errors ", so null values must be valid (for example, );
Observable
- Covariant and contravariant - another very useful feature of Kotlin, enabling, for example, the type of lead is safe to .
Observable
Observable
We at Badoo decided not to wait for the weather by the sea and made such a library. As you might have guessed, we called it Reaktive and posted it on GitHub .
In this article, we’ll take a closer look at Kotlin’s reactive programming expectations and see how Reaktive’s capabilities match them.
Three Natural Reaktive Benefits
Multi-platform
The first natural advantage is most important. Our iOS, Android, and Mobile Web teams currently exist separately. The requirements are general, the design is the same, but each team does its own work.
Kotlin allows you to write multi-platform code, but you have to forget about reactive programming. And I would like to be able to write shared libraries using reactive programming and distribute them within the company or upload to GitHub. Potentially, this approach can significantly reduce development time and reduce the total amount of code.
Null safety
It is rather about the flaw of Java and RxJava2. In short, null cannot be used. Let's try to figure out why. Take a look at this Java interface:
public interface UserDataSource {
Single load();
}
Can the result be null? To avoid ambiguity, null is not allowed in RxJava2. And if you still need to, that is Maybe and Optional. But in Kotlin there are no such problems. We can say that and are different types, and all problems come up at the compilation stage.
Single
Single
Covariance and contravariance
This is a distinctive feature of Kotlin, something that is very lacking in Java. You can read more about this in the manual . I will give only a couple of interesting examples of what problems arise when using RxJava in Kotlin.
Covariance :
fun bar(source: Observable) {
}
fun foo(source: Observable) {
bar(source) // Ошибка компиляции
}
Since
Observable
this is a Java interface, such code does not compile. This is because generic types in Java are invariant. You can, of course, use out, but then using operators like scan will again lead to a compilation error:fun bar(source: Observable) {
source.scan { a, b -> "$a,$b" } // Ошибка компиляции
}
fun foo(source: Observable) {
bar(source)
}
The scan statement is different in that its generic type “T” is both input and output. If Observable was the Kotlin interface, then its type T could be denoted as out and this would solve the problem:
interface Observable {
…
}
And here is an example with contravariance:
fun bar(consumer: Consumer) {
}
fun foo(consumer: Consumer) {
bar(consumer) // Ошибка компиляции
}
For the same reason as in the previous example (generic types in Java are invariant), this example does not compile. Adding in will solve the problem, but again not one hundred percent:
fun bar(consumer: Consumer) {
if (consumer is Subject) {
val value: String = consumer.value // Ошибка компиляции
}
}
fun foo(consumer: Consumer) {
bar(consumer)
}
interface Subject : Consumer {
val value: T
}
Well, by tradition, in Kotlin this problem is solved by using in in the interface:
interface Consumer {
fun accept(value: T)
}
Thus, variability and contravariance of generic types are the third natural advantage of the Reaktive library.
Kotlin + Reactive = Reaktive
We pass to the main thing - the description of the Reaktive library.
Here are a few of its features:
- It is multi-platform, which means that you can finally write general code. At Badoo, we consider this one of the most important benefits.
- It is written in Kotlin, which gives us the advantages described above: there are no restrictions on null, variance / contravariance. This increases flexibility and provides security during compilation.
- There is no dependence on other libraries, such as RxJava, RxSwift, etc., which means that there is no need to bring the library functionality to a common denominator.
- Pure API. For example, the interface
ObservableSource
in Reaktive is called simplyObservable
, and all operators are extension functions located in separate files. There are no God classes of 15,000 lines. This makes it possible to easily increase functionality without making changes to existing interfaces and classes. - Support for schedulers (used familiar operators
subscribeOn
andobserveOn
). - Compatible with RxJava2 (interoperability), providing source conversion between Reaktive and RxJava2 and the ability to reuse schedulers from RxJava2.
- ReactiveX Compliance .
I would like to talk a little more about the benefits that we have received due to the fact that the library is written in Kotlin.
- In Reaktive, null values are allowed, because in Kotlin it is safe. Here are some interesting examples:
observableOf
(null) // ошибка компиляции val o1: Observable
= observableOf(null) val o2: Observable
= o1 // ошибка компиляции, несоответствие типов val o1: Observable
= observableOf(null) val o2: Observable
= o1.notNull() // ошибки нет, значения null отфильтрованы val o1: Observable
= observableOf("Hello") val o2: Observable
= o1 // ошибки нет val o1: Observable
= observableOf(null) val o2: Observable
= observableOf("Hello") val o3: Observable
= merge(o1, o2) // ошибки нет val o4: Observable
= merge(o1, o2) // ошибка компиляции, несоответствие типов
Variation is also a big advantage. For example, in the interface,Observable
type T is declared asout
, which makes it possible to write something like the following:fun foo() { val source: Observable
= observableOf("Hello") bar(source) // ошибки нет } fun bar(source: Observable ) { }
This is what the library looks like today:
- status at the time of writing: alpha (some changes in the public API are possible);
- supported platforms: JVM and Android;
- Supported sources:
Observable
,Maybe
,Single
andCompletable
; - a fairly large number of operators are supported, including map, filter, flatMap, concatMap, combineLatest, zip, merge and others (the full list can be found on GitHub );
- The following schedulers are supported: computation, IO, trampoline and main;
- subjects: PublishSubject and BehaviorSubject;
- backpressure is not yet supported, but we are thinking about the necessity and implementation of this feature.
What are our plans for the near future:
- start using Reaktive in our products (we are currently considering options);
- JavaScript support (pull request already in review);
- iOS support
- publishing artifacts in JCenter (currently using the JitPack service);
- documentation;
- increase in the number of supported operators;
- Tests
- more platforms - pull requests are welcome!
You can try the library now, you can find everything you need on GitHub . Share your experience and ask questions. We will be grateful for any feedback.