Это третья часть моей серии Rx — Android — Kotlin ( часть 1 , часть 2 ).
Эта статья состоит из трех частей, очень похоже на магический трюк :
- Залог: мы реализуем простую операцию с графическим интерфейсом с Rx и Kotlin на Android.
- Поворот: мы создадим совершенно новый Rx-оператор.
- Престиж: мы будем использовать стандартную функцию Kotlin для окончательного переворота в элегантности кода.
Давайте начнем.
Залог
Мы хотим обнаружить тройные касания. Существуют различные способы определить, что такое тройное касание, поэтому давайте просто остановимся на простом: три касания считаются тройным касанием, если первый и последний касания разделены менее чем пятью сотнями миллисекунд.
В традиционной (не Rx) реализации у вас, вероятно, было бы два поля, поддерживающих метки времени двух нажатий назад [t-2] и одного нажатия назад [t-1]. Когда приходит новое касание, [t], вы сравниваете временные метки [t] и [t-2], и если оно меньше пятисот миллисекунд, у нас есть тройное касание. Затем вы перемещаете вниз временные метки: [t-2] получает [t-1], а [t-1] получает текущую временную метку. Повторение.
Этот подход является довольно стандартным, но он также уродлив: он переносит состояние в ваш класс и обеспечивает небольшую гибкость в плане фильтрации, преобразования и компоновки.
Давайте посмотрим на эту проблему с точки зрения Rx.
Поворот
Как я объяснил в первой статье этой серии, нам нужно изменить нашу точку зрения, чтобы выяснить, как реализовать это с помощью Rx. Сейчас мы ищем эмитентов и потребителей событий.
Излучатель довольно очевиден: это сигнал. На Android мы можем просто создать Observable из View с помощью удобного проекта RxAndroid . Как только мы получим события касания из представления, нам нужно использовать оператор, который будет передавать их по три штуки. Это иногда называют «скользящим окном». Например, если у вас есть Observable, который генерирует числа от 0 до 5, вы можете ожидать, что скользящее окно размера 3 получит следующие параметры:
[0, 1, 2] [1, 2, 3] [2, 3, 4] [3, 4, 5]
Любой список не третьего размера игнорируется. Затем, когда наш оператор получает уведомление, мы просто извлекаем временные метки первого и последнего индекса этого списка и видим, составляет ли разница менее пятисот миллисекунд.
Для изучения предположим, что такого оператора еще не существует, и мы реализуем его сами (как выясняется, это не так, перейдите к нижней части этой статьи, если хотите выяснить, ).
Реализация Rx-оператора
Я не хочу погружаться слишком глубоко во внутренности того, как работает Rx, поэтому я просто сосредоточусь на важных моментах здесь. По сути, оператор похож на Callable: он получает параметры и возвращает значение. Это значение — Subscriber, которому необходимо реализовать методы, с которыми вы, вероятно, уже знакомы после нескольких часов, проведенных на Rx: onNext (), onCompleted () и onError ().
Сейчас мы просто сосредоточимся на onNext (), чтобы эта статья была короткой и потому, что именно в этом заключается наша основная логика. Реализация довольно проста:
class SwSubscriber<T> (val child: Subscriber<in List<T>>, val size: Int) : Subscriber<T>(child) { val buffer = ArrayList<T>(size) override fun onNext(t: T) { buffer.add(t) if (buffer.size() == size) { child.onNext(ArrayList(buffer)) buffer.remove(0) } } override fun onCompleted() { /* ... */ } override fun onError(e: Throwable?) { /* ... */ } }
Мы поддерживаем буфер третьего размера и накапливаем в нем элементы из наблюдаемой. Когда этот буфер достигает размера три, мы его испускаем и сдвигаем буфер. Обратите внимание, что когда я вызываю onNext () в буфере, я передаю копию списка, а не сам список. Это всего лишь мера предосторожности против того факта, что подписчик может решить изменить список, который я им передаю, что может сломать нашего оператора.
Теперь, когда у нас есть подписчик, мы можем создать нашего оператора:
class SwOperator<T>(val n: Int) : Observable.Operator<List<T>, T> { override fun call(child: Subscriber<in List<T>>): Subscriber<in T>? = SwSubscriber(child, n) }
И вот как мы это используем:
Observable.just(1, 2, 3, 4, 5) .lift(SwOperator(3)) .subscribe { println("Received ${it}") }
… который печатает:
Received [1, 2, 3] Received [2, 3, 4] Received [3, 4, 5]
Престиж
Мы еще не закончили: вы могли заметить, что, поскольку наша новая функция, очевидно, не является частью класса Observable, нам нужно использовать вспомогательный метод с именем lift (), чтобы вставить наш код в композиционную цепочку. Конечно, мы всегда можем отправить запрос на извлечение проекта, чтобы добавить наш новый оператор, но нам не нужно ждать: мы можем определить метод расширения в Kotlin, чтобы сделать именно это:
fun <T> Observable<T>.slidingWindow(n: Int) = lift(SwOperator(n))
И теперь мы можем написать:
Observable.just(1, 2, 3, 4, 5) .slidingWindow(3)) .subscribe { println("Received ${it}")
Наш новый оператор выглядит так, как если бы он был частью библиотеки Rx.
Android
Давайте закроем цикл по первоначальной проблеме, чтобы мы могли перейти к более интересным вещам: теперь мы хотим использовать наше скользящее окно, чтобы сообщить о тройных нажатиях.
Используя RxAndroid, у нас есть удобный метод clicks (), который превращает касания из представления в поток событий. К сожалению, эти события являются простыми обертками, которые не несут с собой временную метку касания, но это на самом деле не проблема: нам просто нужно преобразовать наш поток событий в поток временных меток, и тогда мы можем добавить нашу логику:
ViewObservable.clicks(view) .map { System.currentTimeMillis() } .slidingWindow(3) .filter { it.get(2) - it.get(0) < 500 } .subscribe { println("Triple tap!") } )
Это еще один пример силы композиции: нет необходимости создавать наши собственные события или наши собственные обертки. Когда вы смотрите на проблемы в виде потоков данных, которые вы можете преобразовать по своему усмотрению, многие проблемы становятся тривиальными.
Rx и раздвижное окно
Обратите внимание, что Rx уже имеет функциональность, которую мы реализовали в этой статье: он называется buffer (), и вы передаете его не только размеру окна, но и тому, сколько элементов вы хотите пропустить. Буфер () ведет себя немного иначе, чем наша реализация, уведомляя подписчиков о окнах, которые меньше желаемого размера:
Observable.just(1, 2, 3, 4, 5) .buffer(3, 1) .subscribe { p("Received ${it}")
печатает:
[main] Received [1, 2, 3] [main] Received [2, 3, 4] [main] Received [3, 4, 5] [main] Received [4, 5] [main] Received [5]
Завершение
Концепция раздвижного окна очень распространена во всех областях. Обнаружение нескольких нажатий в пользовательском интерфейсе является примером, но вы можете также использовать это для определения ограничивающей рамки нескольких нажатий. Выйдя из графического домена, ваш поток может содержать географические местоположения, и вы можете использовать скользящее окно, чтобы определить мгновенную или среднюю скорость. Несколько лет назад я фактически поставил задачу кодирования, чтобы реализовать скользящее окно для регулирования вызовов к серверу . Излишне говорить, что я бы использовал Rx для этого сегодня.
Надеемся, что эта статья дала вам некоторое представление о преимуществах, которые Rx может принести вашему приложению, независимо от домена. Пересмотр проблем с философией Rx — это всегда интересное упражнение, которое часто приводит к более простым и элегантным решениям.