Reaktive — мультиплатформенная библиотека для реактивного Kotlin

ppi6gn_y792ru1s4hyxybxyr4-s.jpeg

Многие сегодня любят реактивное программирование. В нём масса плюсов: и отсутствие так называемого «callback hell», и встроенный механизм обработки ошибок, и функциональный стиль программирования, который уменьшает вероятность багов. Значительно проще писать многопоточный код и легче управлять потоками данных (объединять, разделять и преобразовывать).

Для многих языков программирования существует своя реактивная библиотека: RxJava для JVM, RxJS — для JavaScript, RxSwift — для iOS, Rx.NET и т. д.

Но что мы имеем для Kotlin? Было бы логично предположить, что RxKotlin. И, действительно, такая библиотека существует, но это всего лишь набор расширений (extensions) для RxJava2, так называемый «сахар».

А в идеале хотелось бы иметь решение, соответствующее следующим критериям:

  • мультиплатформенность — чтобы иметь возможность писать мультиплатформенные библиотеки с использованием реактивного программирования и распространять их внутри компании;
  • Null safety — система типов Kotlin защищает нас от «ошибки на миллиард долларов», так что значения null должны быть допустимы (например, Observable);
  • ковариантность и контравариантность — ещё одна очень полезная особенность Kotlin, дающая возможность, например, безопасно привести тип Observable к Observable.


Мы в Badoo решили не ждать у моря погоды и сделали такую библиотеку. Как вы уже могли догадаться, назвали мы её Reaktive и выложили на GitHub.

В этой статье мы подробнее рассмотрим ожидания от реактивного программирования на Kotlin и увидим, насколько им соответствуют возможности Reaktive.


Мультиплатформенность


Первое естественное преимущество наиболее важно. В настоящее время наши iOS-, Android- и Mobile Web-команды существуют отдельно. Требования общие, дизайн одинаковый, но свою работу каждая команда делает сама по себе.

Kotlin позволяет писать мультиплатформенный код, но про реактивное программирование придётся забыть. А хотелось бы иметь возможность писать общие библиотеки с использованием реактивного программирования и распространять их внутри компании или выкладывать на GitHub. Потенциально такой подход может существенно сократить время разработки и уменьшить общее количество кода.

Null safety


Это скорее про недостаток Java и RxJava2. Если вкратце, то null использовать нельзя. Давайте попробуем разобраться почему. Взгляните на этот Java-интерфейс:

public interface UserDataSource {
    Single load();
}


Может ли результат быть null? Чтобы исключить неясности, в RxJava2 запрещено использовать null. А если всё же надо, то есть Maybe и Optional. Но в Kotlin таких проблем нет. Можно сказать, что Single и Single — это разные типы, и все проблемы всплывают ещё на этапе компиляции.

Ковариантность и контравариантность


Это отличительная особенность Kotlin, то, чего очень не хватает в Java. Подробно об этом можно почитать в руководстве. Приведу лишь пару интересных примеров того, какие проблемы возникают при использовании RxJava в Kotlin.

Ковариантность:

fun bar(source: Observable) {
}

fun foo(source: Observable) {
    bar(source) // Ошибка компиляции
}


Поскольку Observable — это интерфейс Java, то такой код не скомпилируется. Это потому что generic-типы в Java инвариантны. Можно, конечно, использовать out, но тогда применение операторов вроде scan опять приведёт к ошибке компиляции:

fun bar(source: Observable) {
    source.scan { a, b -> "$a,$b" } // Ошибка компиляции
}

fun foo(source: Observable) {
    bar(source)
}


Оператор scan отличается тем, что его generic тип «T» является сразу и входным, и выходным. Если бы Observable был интерфейсом Kotlin, то можно было бы его тип T обозначить как out и это решило бы проблему:

interface Observable {
    …
}


А вот пример с контравариантностью:

fun bar(consumer: Consumer) {
}

fun foo(consumer: Consumer) {
    bar(consumer) // Ошибка компиляции
}


По той же причине, что и в предыдущем примере (generic-типы в Java инвариантны), этот пример не компилируется. Добавление in решит проблему, но опять же не на сто процентов:

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
}


Ну и по традиции в Kotlin эта проблема решается использованием in в интерфейсе:

interface Consumer {
    fun accept(value: T)
}


Таким образом, вариантность и контравариантность generic типов являются третьим естественным преимуществом библиотеки Reaktive.
Переходим к главному — описанию библиотеки Reaktive.

Вот несколько её особенностей:

  1. Она мультиплатформенная, а это значит, что можно, наконец, писать общий код. Мы в Badoo считаем это одним из самых важных преимуществ.
  2. Написана на Kotlin, что даёт нам описанные выше преимущества: нет ограничений на null, вариантность/контравариантность. Это увеличивает гибкость и обеспечивает безопасность во время компиляции.
  3. Нет зависимости от других библиотек, таких как RxJava, RxSwift и т. д., а значит, нет необходимости приводить функционал библиотеки к общему знаменателю.
  4. Чистый API. Например, интерфейс ObservableSource в Reaktive называется просто Observable, а все операторы — это extension-функции, расположенные в отдельных файлах. Нет God-классов по 15 000 строк. Это даёт возможность легко наращивать функциональность, не внося изменения в имеющиеся интерфейсы и классы.
  5. Поддержка планировщиков (schedulers) (используются привычные операторы subscribeOn и observeOn).
  6. Совместимость с RxJava2 (interoperability), обеспечивающая конвертацию источников между Reaktive и RxJava2 и возможность переиспользовать планировщики из RxJava2.
  7. Соответствие ReactiveX.


Хотелось бы чуть больше рассказать о преимуществах, которые мы получили за счёт того, что библиотеки на Kotlin.

  1. В Reaktive значения null разрешены, потому что в Kotlin это безопасно. Вот несколько интересных примеров:
    • 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) // ошибка компиляции, несоответствие типов

    Вариантность — тоже большое преимущество. Например, в интерфейсе Observable тип T объявлен как out, что даёт возможность написать примерно следующее:
    fun foo() {
        val source: Observable = observableOf("Hello")
        bar(source) // ошибки нет
    }
    
    fun bar(source: Observable) {
    }
    


Так выглядит библиотека на сегодняшний день:

  • статус на момент написания статьи: альфа (возможны некоторые изменения в публичном 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. Делитесь опытом использования и задавайте вопросы. Будем благодарны за любой фидбек.

© Habrahabr.ru