
Many today love reactive programming. It has a lot of advantages: the absence of the so-called "
callback hell ", the built-in error handling mechanism, and the functional programming style, which reduces the likelihood of bugs. Much easier to write multi-threaded code and easier to manage data streams (merge, split and convert).
For many programming languages, there is a 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 is 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:
- multiplatform - to be able to write multiplatform libraries using reactive programming and distribute them within the company;
- Null safety - the Kotlin type system protects us from a “ billion dollar error ”, so null values must be valid (for example,
Observable<String?>
);
- Covariance and contravariance is another very useful feature of Kotlin, which makes it possible, for example, to safely reduce the type
Observable<String>
to Observable<CharSequence>
.
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 uploaded it to
GitHub .
In this article we will take a closer look at the expectations from reactive programming at Kotlin and see how they fit the capabilities of Reaktive.
Three Natural Reaktive Benefits
Multiplatform
The first
natural advantage is most important. Currently, our iOS, Android, and Mobile Web teams exist separately. The requirements are common, the design is the same, but each team does its own work.
Kotlin allows you to write a multiplatform code, but you will 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
This is more about the lack 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<User> load(); }
Can the result be null? To eliminate ambiguities, null is not allowed in RxJava2. And if you still need, that is Maybe and Optional. But in Kotlin there are no such problems. We can say that Single and Single <User?> Are different types, and all the problems emerge at the compilation stage.
Covariance and contravariance
This is a distinctive feature of Kotlin, something that is very lacking in Java. Details about this can be found in the
manual . I will give just a couple of interesting examples of what problems arise when using RxJava in Kotlin.
Covariance :
fun bar(source: Observable<CharSequence>) { } fun foo(source: Observable<String>) { bar(source)
Since
Observable
is a Java interface, such code will not compile. This is because generic types in Java are invariant. You can, of course, use out, but then using statements like scan will again lead to a compilation error:
fun bar(source: Observable<out CharSequence>) { source.scan { a, b -> "$a,$b" }
The scan operator is different in that its generic type "T" is immediately both input and output. If Observable were a Kotlin interface, then its type T could be designated as out and this would solve the problem:
interface Observable<out T> { … }
Here is an example with contravariance:
fun bar(consumer: Consumer<String>) { } fun foo(consumer: Consumer<CharSequence>) { bar(consumer)
For the same reason as in the previous example (generic types in Java are invariant), this example is not compiled. Adding in will solve the problem, but again not one hundred percent:
fun bar(consumer: Consumer<in String>) { if (consumer is Subject) { val value: String = consumer.value
Well, according to the tradition in Kotlin, this problem is solved by using in in the interface:
interface Consumer<in T> { fun accept(value: T) }
Thus, the variance and contravariance of generic types are the third
natural advantage of the Reaktive library.
Kotlin + Reactive = Reaktive
We turn to the main thing - the description of the Reaktive library.
Here are some of its features:
- It is multiplatform, which means that you can finally write common code. We at Badoo consider this one of the most important benefits.
- Written in Kotlin, which gives us the advantages described above: there are no restrictions on null, variation / 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 reduce the functionality of the library to a common denominator.
- Pure API. For example, the
ObservableSource
interface in Reaktive is simply called Observable
, 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 the functionality without making changes to existing interfaces and classes.
- Scheduler support (the familiar
subscribeOn
and observeOn
operators are observeOn
).
- Compatibility with RxJava2 (interoperability), which provides source conversion between Reaktive and RxJava2 and the ability to reuse schedulers from RxJava2.
- ReactiveX compliance.
I would like to tell a little more about the benefits that we have received due to the fact that the libraries at Kotlin.
- In Reaktive, null values are allowed, because in Kotlin it is safe. Here are some interesting examples:
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) // ,
Variance is also a big advantage. For example, in the Observable
interface, type T is declared as out
, which makes it possible to write something like the following:
fun foo() { val source: Observable<String> = observableOf("Hello") bar(source)
This is how the library looks 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
and Completable
;
- a sufficiently 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 need and the realization of this feature.
What are our plans for the near future:- start using Reaktive in our products (we are currently considering the possibilities);
- JavaScript support (pull request already on the review);
- iOS support;
- Publish artifacts to 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 will find everything you need on
GitHub . Share your experiences and ask questions. We will be grateful for any feedback.