company_banner

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



    Многие сегодня любят реактивное программирование. В нём масса плюсов: и отсутствие так называемого "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<User> и 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. Делитесь опытом использования и задавайте вопросы. Будем благодарны за любой фидбек.
    Badoo
    478.88
    Big Dating
    Share post

    Similar posts

    Comments 13

      +2

      В kotlinx.coroutines 1.2.0 планируют включить поддержку холодных стримов.


      Issue на GitHub. Документация. Статья.

        +2
        Здравствуйте! Flows основаны на корутинах, и пока не очень понятно, как это всё можно использовать в проде — реальный опыт мало у кого есть, насколько мне известно. Если кто-то знает успешные примеры — поделитесь, пожалуйста. У нас же давно возникла потребность делать Reaktive, еще до того, как появились какие-то слухи о Flows. Мы не стали менять традиции, в Reaktive есть стандартные наборы источников и планировщиков. Планировщики кстати тоже можно заменять на моки в тестах.
          +2

          С корутинами в проде все хорошо. Flow только-только появился и вообще еще в альфе, так что его действительно в проде не найдешь, впрочем, это относится и к Reaktive.


          Никак не хочу преуменьшить качество и полезность Reaktive, просто привел реальную альтернативу, т.к. в статье упомянули только обертку над RxJava

        +3
        скачал badoo/Reaktive и не нашел тестов и примерных приложений для десктопной явы. И даже в документации нет примеров. А так хотелось поиграться. С чего начать?
          +2
          Здравствуйте! Тесты у нас в планах, проект ещё молодой. До релиза обязательно напишем. Пример для большой Java (на Kotlin, разумеется) добавим, спасибо за идею! Пока можно посмотреть пример Android приложения, там тоже JVM.
          +2

          Есть ли в планах перевести MVICore на Reaktive и сделать его тоже мультиплатформенным?

            0
            Здравствуйте! Это очень хорошая идея и мы о ней, разумеется, думаем. Я не вижу каких-либо причин не перевести MVICore на Reaktive в будущем.
            0
            а чем Project Reactor не устроил?
              +1
              Здравствуйте! Project Reactor — это очень хороший проект. Но он нас не устраивает тем же, чем и RxJava — это Java-библиотека. У них было желание сделать её мультиплатформенной, но обсуждение недавно закрыли в пользу Kotlin Coroutines Flow.
              0
              Здравствуйте, а вы думали/используете ли как-то корутины под капотом? В том же дарте например местный rx тесто интегрирован с местными корутинами, что и зачастую удобнее с точки зрения кода (можно дождаться например какой-нить future внутри map функции, и для этого не нужны особые функции комбинаторы для разных вещей) и лучше с точки зрения перфоманса…
                0
                Здравствуйте! Как уже было упомянуто выше в комментариях, мы думали о корутинах. Но не используем. Причина в том, что на момент начала работы над Reaktive не было до конца понятно, как с их помощью можно реализовать холодные стримы. Недавно появилось превью Flows, но и по ним тоже есть вопросы. Классический подход на данный момент выглядит более привлекательным и надёжным.
                0
                Привет! Спасибо, что продвигаете kotlin и rx, я прям стал фанатом!
                По статье, кажется, есть пара опечаток в примерах про nullable-типы в Reaktive. Поправьте меня пожалуйста, если ошибаюсь.

                Например тут (явно две строчки идентичны):
                val o3: Observable<String?> = merge(o1, o2) // ошибки нет
                val o4: Observable<String?> = merge(o1, o2) // ошибка компиляции, несоответствие типов

                и тут (кажется для o2 тип должен быть String):
                val o1: Observable<String?> = observableOf(null)
                val o2: Observable<String?> = o1 // ошибка компиляции, несоответствие типов
                val o1: Observable<String?> = observableOf(null)
                val o2: Observable<String?> = o1.notNull() // ошибки нет, значения null отфильтрованы
                  0
                  Привет! Опечатки и вёрстку исправили! Спасибо! Кстати, мы скоро добавим поддержку Linux и сразу после этого iOS!

                Only users with full accounts can post comments. Log in, please.