Статьи

Android, Rx и Kotlin: тематическое исследование

 

   

 

Существует бесчисленное множество введений в Rx и немало обсуждается Rx на Android, поэтому вместо того, чтобы написать еще одно, я решил придумать простое упражнение и пройти весь процесс его реализации с использованием Rx и Kotlin. Я уверен, что вы сможете легко следовать этой статье, даже если вы не знакомы с Kotlin, потому что синтаксис удивительно похож на тот, который вы написали бы, если бы использовали Java 8 (только с меньшим количеством точек с запятой).

Активность

Это показано в верхней части этой статьи. Это довольно просто, охватывая некоторые важные основы, и, как мы выясним в дальнейшем, дьявол кроется в деталях. Вот простая функциональная спецификация этой деятельности:

  • Пользователь вводит псевдоним в EditText. Операция спрашивает сервер, известно ли такое имя, и если это так, возвращает запись пользователя.
  • На этом этапе пользователь может «Добавить» этого человека в друзья, что инициирует еще один вызов на сервер, чтобы убедиться, что добавление разрешено. Сервер возвращает простое «ОК» или «Ошибка», если этого человека нельзя добавить в друзья.

Несколько дополнительных деталей:

  • Мы хотим отображать индикатор выполнения во время всех вызовов сервера.
  • Поскольку мы не хотим слишком беспокоить сервер, мы начинаем отправлять запросы только на имена, содержащие три или более символов (мы добавим это условие позже).

Без Rx

Реализация этого действия с «обычными» практиками Android проста:

  • Добавьте слушателя в EditText, чтобы мы получали уведомление каждый раз, когда вводится символ.
  • Отправьте сетевой запрос с помощью AsyncTask.
  • Когда будет получен результат, обновите пользовательский интерфейс (в основном потоке, а не в каком-либо другом потоке AsyncTask).
  • Включите кнопку «Добавить друга» и прослушайте ее, вызывая сервер, если нажата, чтобы убедиться, что друга можно добавить (еще один AsyncTask + главная тема после пост-танца).

Эта реализация проста, но она очень разбросана. Скорее всего, вы получите множество пустых методов, добавленных просто для того, чтобы удовлетворить требования слушателя, сохранить состояние в полях для связи между различными асинхронными задачами и убедиться, что ваша модель многопоточности является надежной. Другими словами, множество «котлов», грязная смесь безголовой и графической логики и графического кода обновления распространяются повсюду.

Существует лучший способ.

Котлин и Android

Я обнаружил, что Kotlin очень хорошо подходит для Android даже в первые годы, но недавно команда Kotlin по-настоящему усилила поддержку Android и добавила специальные функции для платформы в свои инструменты, что делает Kotlin еще более идеальным партнером. для Android.

Kotlin M11 был выпущен около недели назад, и он добавил функциональность, которая делает привлекательность Kotlin абсолютно неотразимой для Android: автоматически связанные ресурсы. Вот как это работает.

Предположим, вы определили следующее представление в макете activity_search.xml:

<Button ...
    android:id="@+id/addFriendButton"
>

Все, что вам нужно сделать, это добавить специальный вид импорта в ваш источник:

import kotlinx.android.synthetic.activity_search.addFriendButton
и идентификатор addFriendButton становится волшебным образом доступным везде в вашем источнике с правильным типом. Это в основном устаревает ButterKnife / KotterKnife (ну, не совсем, есть еще OnClick, что довольно мило. Кроме того, Джейк говорит, что у него есть что-то в работе). И если вы нажмете Ctrl-b на таком идентификаторе, Android Studio перенесет вас прямо в файл макета, где определено это представление. Очень аккуратный.

Сервер

Для этой статьи я просто издеваюсь над сервером. Вот его определение:

trait Server {
    fun findUser(name: String) : Observable<JsonObject>
    fun addFriend(user: User) : Observable<JsonObject>
}

Если вы думаете, что это определение во многом похоже на   интерфейс сервиса Retrofit , то это потому, что так оно и есть. Если вы не используете Retrofit, вы должны. Прямо сейчас я буду издеваться над этим сервером, возвращая набор жестко закодированных ответов, а также переводя каждый вызов в спящий режим на одну секунду, чтобы имитировать задержку (и поэтому мы можем видеть индикатор выполнения на экране). Обратите внимание, что каждый вызов этого интерфейса возвращает Observable, поэтому они соответствуют нашей реализации Rx.

В этом примере я жестко закодировал сервер, чтобы узнать о двух друзьях («cedric» и «jon»), но в качестве друга можно добавить только «cedric».

Rx мышление

Переключение на образ мышления Rx требует, чтобы вы начали думать с точки зрения источников событий (наблюдаемых) и слушателей (подписчиков). Если эта идея не звучит, это роман, потому что это не так. Эта модель уже защищалась в книге «Шаблоны проектирования» в 1994 году и даже в ранних версиях Java двадцать лет назад (и, без сомнения, вы можете найти ее следы в литературе до этого). Однако в этой идее Rx вводит новые концепции, которые мы рассмотрим в этой серии.

Итак, давайте переосмыслим нашу деятельность в терминах Rx: каковы источники событий (с этого момента я буду использовать имя «наблюдаемый») в этой деятельности?

Я могу сосчитать четыре наблюдаемые:

  1. Во-первых, у нас есть EditText: всякий раз, когда набирается новый символ, он генерирует событие, которое содержит весь набранный текст. Мы можем создать новое имя, если у нас более трех символов.
  2. Далее следует имя наблюдаемого, которое вызывает сервер и испускает объект JsonObject, который он получает в ответ.
  3. Далее в цепочке у нас есть «пользовательская» наблюдаемая, которая отображает JsonObject в экземпляр пользователя с именем и идентификатором этого человека.
  4. Наконец, кнопка «Добавить друга» — еще одна наблюдаемая: если пользователь нажимает эту кнопку, мы делаем еще один вызов на сервер с имеющимся у нас пользователем и обновляем наш пользовательский интерфейс на основе результатов.

Существуют различные способы, с помощью которых мы можем разбить эту проблему на наблюдаемые, и конечный результат зависит от того, как вы хотите обрабатывать различные задачи, какие наблюдаемые объекты вы хотите использовать повторно, предпочитаемую модель потоков и т. Д.

Реализация

Наблюдаемый «EditText»

Давайте начнем с нашего EditText. Его реализация довольно проста:

WidgetObservable.text(editText)
    .doOnNext { e: OnTextChangeEvent ->
        addFriendButton.setEnabled(false)
        loading.setVisibility(View.INVISIBLE)
    }
    .map { e: OnTextChangeEvent -> e.text().toString() }
    .filter { s: String -> s.length() >= 3 }
    .subscribe { s: String -> mNameObservable.onNext(s) }

Давайте пройдемся по каждой строке по очереди:

  • WidgetObservable превращает обычный вид в наблюдаемый. Это оболочка от RxAndroid, которая вызывает всех подписчиков с полным текстом всякий раз, когда набирается новый символ.
  • Далее мы сбрасываем интерфейс в его состояние по умолчанию.
  • Строка карты преобразует OnTextChangeEvent в строку. Это чисто вспомогательный шаг, поэтому следующие операторы в цепочке могут иметь дело с контекстом EditText в виде строки, вместо того, чтобы неоднократно извлекать его из события (e.text (). ToString () везде).
  • Далее, если длина текста меньше трех символов, мы остановимся прямо здесь.
  • Наконец, мы подписываемся на значение, которое мы получили, если мы достигли этого: строка длиной не менее трех символов. Мы передаем эту строку нашему названию заметному.

Наблюдаемое «имя»

val mNameObservable: BehaviorSubject<String> = BehaviorSubject.create()
BehaviorSubject — это особый вид Observable, в который вы можете отправлять события после его создания. Мне нравится думать об этом как о шине событий, за исключением того, что она сфокусирована на очень специфических видах событий (в отличие от шины событий, которая используется для публикации почти всего). Использование Subject здесь позволяет мне создавать thisObservable на ранней стадии и публиковать в нем только новые события по мере их поступления, что мы и сделали с фрагментом кода выше.

Давайте посмотрим, как мы сейчас используем этот Observable, просто подписавшись на него, что означает получение имен, которые содержат три или более символов. Все, что мы делаем, это вызываем сервер и передаем результат наблюдаемому пользователю:

mNameObservable
    .subscribe{ s: String ->
        mServer.findUser(s).subscribe { jo: JsonObject ->
            mUserObservable.onNext(jo)
        }
    }

Мы еще не закончили: у нас фактически есть еще один подписчик на этот Observable:

mNameObservable
    .subscribe { s: String ->
        loading.setVisibility(View.VISIBLE)
    }

Нам нужно сообщить пользователю, что мы только что выполнили сетевой вызов, поэтому мы показываем индикатор выполнения (это неоптимальная реализация, эту логику следует выполнять на уровне сервера, но мы сохраним это на потом).

Обратите внимание, что я намеренно скрываю очень важную часть этой логики, чтобы сосредоточиться на теме, и это также объясняет, почему у нас есть две отдельные подписки. Я объясняю почему в конце этой статьи.

Наблюдаемый «Пользователь»

Далее у нас есть User Observable, который получает уведомление, когда сервер отправляет нам ответ на запрос «Существует ли пользователь с именем« foo »?»:

mUserObservable
    .map { jo: JsonObject ->
        if (mServer.isOk(jo)) {
            User(jo.get("id").getAsString(), jo.get("id").getAsString())
        } else {
            null
        }
    }
    .subscribe { user: User? ->
        addFriendButton.setEnabled(user != null)
        loading.setVisibility(View.INVISIBLE)
        mUser = user
    }

Этот Observable делает две вещи: он обновляет наш пользовательский интерфейс и отображает ответ JsonObject на наш dataUser класса. Если вызов был успешным, мы присваиваем это значение полю mUser.

Наблюдаемая «Добавить друга»

Наконец, у нас есть кнопка «Добавить друга», которая включается только тогда, когда у нас есть действительный пользователь. Если пользователь нажимает эту кнопку, мы выполняем еще один вызов на сервер, чтобы запросить добавление этого человека в друзья, а затем обновляем пользовательский интерфейс в зависимости от ответа:

ViewObservable.clicks(addFriendButton)
    .subscribe { e: OnClickEvent ->
        mServer.addFriend(mUser!!)
            .subscribe { jo: JsonObject ->
                val toastText: String
                if (mServer.isOk(jo)) {
                    toastText = "Friend added id: " + jo.get("id").getAsString()
                    editText.setText("")
                } else {
                    toastText = "ERROR: Friend not added"
                }
                Toast.makeText(this, toastText, Toast.LENGTH_LONG).show();
            }
    }

Шаг назад

Это совсем другая реализация того, как вы будете писать код с помощью обычных вызовов Android, но, в конце концов, он не только компактен, но и разделяет всю нашу логику на четыре совершенно разных компонента, которые очень четко взаимодействуют друг с другом. Это макроуровень. На микроуровне эти четыре компонента не просто самодостаточны, они также легко конфигурируются благодаря операторам, операциям, которые вы можете вставить между Observable и вашим подписчиком, и которые преобразуют данные так, чтобы вам было легче их обрабатывать. У меня есть только один такой пример в приведенном выше коде (преобразование anOnTextChangeEvent в строку), но вы поняли идею.

Еще одно преимущество, которое должно быть сразу очевидно для вас, даже если вы еще не купитесь на весь сдвиг парадигмы Rx, заключается в том, что благодаря Rx у нас теперь есть универсальный язык для наблюдаемых и наблюдателей. Я уверен, что прямо сейчас ваша кодовая база Android содержит множество таких интерфейсов, все с едва различимыми именами и определениями методов, и все они нуждаются в вставке некоторых адаптеров, прежде чем они смогут общаться друг с другом. Если вы когда-нибудь почувствовали необходимость написать интерфейс с методом onSomethingHappened (), Rx станет незамедлительным улучшением.

операторы

Я едва касался операторов, и эта тема сама по себе достойна целой книги, но я хотел бы потратить несколько минут, чтобы привести быстрый пример силы операторов.

Возвращаясь к нашей деятельности, помните, что как только пользователь вводит более трех символов, мы отправляем запрос в сеть для каждого набранного символа. Это немного расточительно: многие пользователи телефонов очень быстро набирают текст, и если они набирают буквы в быстрой последовательности, мы могли бы сэкономить нашему серверу некоторую дополнительную работу. Например, как насчет того, чтобы определить, что мы вызываем сервер только тогда, когда пользователь перестал печатать?

Как мы определяем «перестал печатать»? Допустим, мы решим, что пользователь прекратил печатать, если два нажатия клавиш разделены более чем на 500 мс. Таким образом, быстрый ввод «cedric» приведет к одному вызову сервера вместо четырех без этой функции. Как мы можем реализовать это?

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

Как оказалось, у наблюдаемых есть оператор, который делает это для нас, который называется debounce (), поэтому наш код становится:

WidgetObservable.text(editText)
    .doOnNext {(e: OnTextChangeEvent) ->
        addFriendButton.setEnabled(false)
        loading.setVisibility(View.INVISIBLE)
    }
    .map { e: OnTextChangeEvent -> e.text().toString() }
    .filter { s: String -> s.length() >= 3 }
    .debounce(500, TimeUnit.MILLISECONDS)
    .subscribe { s: String -> mNameObservable.onNext(s) }

Я знаю, что вы думаете: «Вы только что выбрали проблему, которая может быть решена оператором, который уже существует». Ну, вроде как, но это не моя точка зрения. Прежде всего, это не выдуманная проблема, и я уверен, что многие разработчики, которые пишут код пользовательского интерфейса, согласятся, что это довольно часто.

Однако моя точка зрения более общая: оператор debounce () не имеет ничего общего с Android. Он определен в Observable, который является базовой библиотекой, которую мы используем. Этот оператор обычно полезен для любого источника, генерирующего события. Эти события могут быть графическими по своему характеру (как в данном случае) или любого другого типа, такого как поток сетевых запросов, координаты курсора мыши или сбор данных с датчика геолокации. debounce () представляет общую необходимость избавления от избыточности в потоках.

Мы не только можем повторно использовать существующую реализацию без необходимости переписывать ее самостоятельно, мы сохраняем локальность нашей реализации (при традиционном подходе вы, возможно, загрязнили бы ваш класс парой полей), и мы поддерживаем возможности компоновки: наши наблюдаемые вычисления. Например, вам нужно убедиться, что пользователь не добавляет себя перед вызовом сервера? Легко:

WidgetObservable.text(editText)
    // ...
    .filter { s: String -> s.length() >= 3 }
    .filter { s: String -> s != myName }

Завершение

На этом этапе вам, вероятно, стоит взглянуть на  полный исходный код,  чтобы вы могли отойти назад и посмотреть, как все эти части сочетаются друг с другом. Для меня очевидно, что представление о декомпозиции вашей проблемы с точки зрения наблюдаемых, которые испускают значения, наблюдаемые подписчиками, является чрезвычайно мощным и приводит к более чистому, более самодостаточному и компонуемому коду. Версия этого класса, не относящаяся к Rx, кажется мне невероятно грязной (гораздо больше полей, содержащих состояние, добавлены методы только для удовлетворения слегка несовместимых интерфейсов слушателей и полного отсутствия компоновки).

Сказав это, наша работа здесь не закончена:

  • Модель потоков все неправильно. Я намеренно пропустил обсуждение всех потоков в этой статье, чтобы оставаться в теме, но код, как он есть сейчас, крайне неоптимален. Это работает, но вы не должны отправлять его в его текущей форме. В следующей части я покажу, как Rx делает тривиально правильно настроить многопоточную модель Android.
  • Существует много кода пользовательского интерфейса, пронизанного бизнес-логикой. Это грязно, запутывает, как работает наш пользовательский интерфейс, и его сложно поддерживать. Мы также должны это исправить.

Оставайтесь в курсе!