Статьи

Android, Rx и Kotlin: часть 3

Это третья часть моей серии 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 — это всегда интересное упражнение, которое часто приводит к более простым и элегантным решениям.