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

в 16:15, , рубрики: android, crossplatform, iOS, java, kotlin, multiplatform, reactive, reactive extensions, reactive programming, Reactive Streams, Блог компании Badoo, Разработка под android, разработка под iOS

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

Многие сегодня любят реактивное программирование. В нём масса плюсов: и отсутствие так называемого "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.

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

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

Автор: Аркадий Иванов

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js