Многие сегодня любят реактивное программирование. В нём масса плюсов: и отсутствие так называемого "callback hell", и встроенный механизм обработки ошибок, и функциональный стиль программирования, который уменьшает вероятность багов. Значительно проще писать многопоточный код и легче управлять потоками данных (объединять, разделять и преобразовывать).
Для многих языков программирования существует своя реактивная библиотека: RxJava для JVM, RxJS — для JavaScript, RxSwift — для iOS, Rx.NET и т. д.
Но что мы имеем для Kotlin? Было бы логично предположить, что RxKotlin. И, действительно, такая библиотека существует, но это всего лишь набор расширений (extensions) для RxJava2, так называемый «сахар».
А в идеале хотелось бы иметь решение, соответствующее следующим критериям:
- мультиплатформенность — чтобы иметь возможность писать мультиплатформенные библиотеки с использованием реактивного программирования и распространять их внутри компании;
- Null safety — система типов Kotlin защищает нас от «ошибки на миллиард долларов», так что значения null должны быть допустимы (например,
Observable<String?>
); - ковариантность и контравариантность — ещё одна очень полезная особенность Kotlin, дающая возможность, например, безопасно привести тип
Observable<String>
кObservable<CharSequence>
.
Мы в Badoo решили не ждать у моря погоды и сделали такую библиотеку. Как вы уже могли догадаться, назвали мы её Reaktive и выложили на GitHub.
В этой статье мы подробнее рассмотрим ожидания от реактивного программирования на Kotlin и увидим, насколько им соответствуют возможности Reaktive.
Три естественных преимущества Reaktive
Мультиплатформенность
Первое естественное преимущество наиболее важно. В настоящее время наши iOS-, Android- и Mobile Web-команды существуют отдельно. Требования общие, дизайн одинаковый, но свою работу каждая команда делает сама по себе.
Kotlin позволяет писать мультиплатформенный код, но про реактивное программирование придётся забыть. А хотелось бы иметь возможность писать общие библиотеки с использованием реактивного программирования и распространять их внутри компании или выкладывать на GitHub. Потенциально такой подход может существенно сократить время разработки и уменьшить общее количество кода.
Null safety
Это скорее про недостаток Java и RxJava2. Если вкратце, то null использовать нельзя. Давайте попробуем разобраться почему. Взгляните на этот Java-интерфейс:
public interface UserDataSource {
Single<User> load();
}
Может ли результат быть null? Чтобы исключить неясности, в RxJava2 запрещено использовать null. А если всё же надо, то есть Maybe и Optional. Но в Kotlin таких проблем нет. Можно сказать, что Single и Single<User?> — это разные типы, и все проблемы всплывают ещё на этапе компиляции.
Ковариантность и контравариантность
Это отличительная особенность Kotlin, то, чего очень не хватает в Java. Подробно об этом можно почитать в руководстве. Приведу лишь пару интересных примеров того, какие проблемы возникают при использовании RxJava в Kotlin.
Ковариантность:
fun bar(source: Observable<CharSequence>) {
}
fun foo(source: Observable<String>) {
bar(source) // Ошибка компиляции
}
Поскольку Observable
— это интерфейс Java, то такой код не скомпилируется. Это потому что generic-типы в Java инвариантны. Можно, конечно, использовать out, но тогда применение операторов вроде scan опять приведёт к ошибке компиляции:
fun bar(source: Observable<out CharSequence>) {
source.scan { a, b -> "$a,$b" } // Ошибка компиляции
}
fun foo(source: Observable<String>) {
bar(source)
}
Оператор scan отличается тем, что его generic тип «T» является сразу и входным, и выходным. Если бы Observable был интерфейсом Kotlin, то можно было бы его тип T обозначить как out и это решило бы проблему:
interface Observable<out T> {
…
}
А вот пример с контравариантностью:
fun bar(consumer: Consumer<String>) {
}
fun foo(consumer: Consumer<CharSequence>) {
bar(consumer) // Ошибка компиляции
}
По той же причине, что и в предыдущем примере (generic-типы в Java инвариантны), этот пример не компилируется. Добавление in решит проблему, но опять же не на сто процентов:
fun bar(consumer: Consumer<in String>) {
if (consumer is Subject) {
val value: String = consumer.value // Ошибка компиляции
}
}
fun foo(consumer: Consumer<CharSequence>) {
bar(consumer)
}
interface Subject<T> : Consumer<T> {
val value: T
}
Ну и по традиции в Kotlin эта проблема решается использованием in в интерфейсе:
interface Consumer<in T> {
fun accept(value: T)
}
Таким образом, вариантность и контравариантность generic типов являются третьим естественным преимуществом библиотеки Reaktive.
Kotlin + Reactive = Reaktive
Переходим к главному — описанию библиотеки Reaktive.
Вот несколько её особенностей:
- Она мультиплатформенная, а это значит, что можно, наконец, писать общий код. Мы в Badoo считаем это одним из самых важных преимуществ.
- Написана на Kotlin, что даёт нам описанные выше преимущества: нет ограничений на null, вариантность/контравариантность. Это увеличивает гибкость и обеспечивает безопасность во время компиляции.
- Нет зависимости от других библиотек, таких как RxJava, RxSwift и т. д., а значит, нет необходимости приводить функционал библиотеки к общему знаменателю.
- Чистый API. Например, интерфейс
ObservableSource
в Reaktive называется простоObservable
, а все операторы — это extension-функции, расположенные в отдельных файлах. Нет God-классов по 15 000 строк. Это даёт возможность легко наращивать функциональность, не внося изменения в имеющиеся интерфейсы и классы. - Поддержка планировщиков (schedulers) (используются привычные операторы
subscribeOn
иobserveOn
). - Совместимость с RxJava2 (interoperability), обеспечивающая конвертацию источников между Reaktive и RxJava2 и возможность переиспользовать планировщики из RxJava2.
- Соответствие ReactiveX.
Хотелось бы чуть больше рассказать о преимуществах, которые мы получили за счёт того, что библиотеки на Kotlin.
- В Reaktive значения null разрешены, потому что в Kotlin это безопасно. Вот несколько интересных примеров:
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) // ошибка компиляции, несоответствие типов
Вариантность — тоже большое преимущество. Например, в интерфейсе
Observable
тип T объявлен какout
, что даёт возможность написать примерно следующее:fun foo() { val source: Observable<String> = observableOf("Hello") bar(source) // ошибки нет } fun bar(source: Observable<CharSequence>) { }
Так выглядит библиотека на сегодняшний день:
- статус на момент написания статьи: альфа (возможны некоторые изменения в публичном API);
- поддерживаемые платформы: JVM и Android;
- поддерживаемые источники:
Observable
,Maybe
,Single
иCompletable
; - поддерживается достаточно большое количество операторов, среди которых map, filter, flatMap, concatMap, combineLatest, zip, merge и другие (полный список можно найти на GitHub);
- поддерживаются следующие планировщики: computation, IO, trampoline и main;
- subjects: PublishSubject и BehaviorSubject;
- backpressure пока не поддерживается, но мы думаем над необходимостью и реализацией этой возможности.
Что у нас в планах на ближайшее будущее:
- начать использовать Reaktive в наших продуктах (в данный момент мы обдумываем возможности);
- поддержка JavaScript (pull request уже на ревью);
- поддержка iOS;
- публикация артефактов в JCenter (в данный момент используется сервис JitPack);
- документация;
- увеличение количества поддерживаемых операторов;
- тесты;
- больше платформ — pull request’ы приветствуются!
Попробовать библиотеку можно уже сейчас, всё необходимое вы найдёте на GitHub. Делитесь опытом использования и задавайте вопросы. Будем благодарны за любой фидбек.
Автор: Аркадий Иванов