Вторая часть Лекции 3 Курса по архитектуре клиент-серверных андроид-приложений.

Введение

RxJava

  1. Введение в RxJava
  2. Создание Observable
  3. Основные операторы
  4. Преобразование потоков данных

RxJava в Android

Дополнительно – проблема Backpressure

  1. Использование специальных операторов
  2. Отказ от использования Observable.create

Практические задания

Ссылки и полезные ресурсы

RxJava в Android

Поскольку RxJava – это фреймворк для Java, нет ничего удивительного в том, что он полностью поддерживается в Android. Мы уже убедились, что RxJava – это мощный инструмент для управления асинхронными потоками данных, разобрали основные операторы, и это все, разумеется, можно применить в разработке под Android.

Вероятно, самая главная причина широкой популярности RxJava в Android – это поддержка RxJava в Retrofit. Да, самая популярная библиотека для сетевых запросов позволяет не только возвращать данные с сервера, но и оборачивать их в Observable, чтобы удобно управлять этими данными и работать с асинхронностью.

Давайте разберем, что нужно сделать, чтобы использовать RxJava для сетевых запросов с помощью Retrofit. На самом деле, практически ничего! Нужно изменить сервис для сетевых запросов, чтобы он возвращал Observable:

И это все! Теперь мы можем асинхронно выполнять запросы, обрабатывать и получать данные, а также обрабатывать ошибки:

Что мы можем добавить еще? Разумеется, кэширование данных. Это очень легко исправить – преобразуем Observable так, чтобы он сохранял список элементов:

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

Конечно, сложность кода уже значительно выросла, но если вспомнить, что при этом мы реализовали и асинхронность запроса, и показ прогресса, и кэширование, и обработку ошибок, то нельзя не признать, что с RxJava все выглядит достаточно неплохо. К тому же обычно код для Observable распределяется так, чтобы UI-классы не работали с кэшированием, поэтому все становится еще проще. Но об этом в последующих лекциях.

Разумеется, нужно быть справедливым и сказать про недостатки, которые есть в RxJava применительно к Android. Основной недостаток – это наша любимая проблема с жизненным циклом. RxJava – это фреймворк для Java, которая ничего не знает про проблемы Android. Поэтому решений “из коробки” здесь нет. Но есть масса других возможностей.

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

Во-вторых, мы можем воспользоваться средствами самой RxJava. В первую очередь можно вспомнить, что метод subscribe у Observable возвращает объект Subscription, который позволяет отписываться от потока данных. Это не до конца решает проблему обработки жизненного цикла, но, по крайней мере, позволяет нам отписываться от запросов при выходе с экрана:

Также у Observable есть метод cache, который позволяет выполнять запрос только один раз. При повторной подписке будет возвращен старый результат. Но при этом нужно каким-то образом сохранять Observable (к примеру, в лоадере).

И в-третьих, есть некоторые библиотеки, которые созданы для решения проблемы жизненного цикла при использовании RxJava. К примеру, это библиотека RxLifecycle от Trello. То есть такие проблемы можно решить, и это ничуть не сложнее чем при использовании других средств для выполнения запросов.

Таким образом, RxJava для нас – это крайне удобный инструмент для управления запросами, многопоточностью и обработкой ошибок. Кроме того, нельзя забывать про то, что существует большое количество библиотек, которые позволяют использовать различные компоненты Android в реактивном стиле. Вероятно, самой популярной из таких библиотек является библиотека RxBindings, которая помогает работать с View в реактивном стиле.

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

 

Дополнительно – проблема Backpressure

Как мы уже поняли, RxJava – это очень мощный инструмент, но им в то же время надо пользоваться очень осторожно. Одна из таких предосторожностей – это отказ от использования Observable.create(). Мы уже видели, что это не делает код очевиднее, при этом нам приходится еще и следить за тем, что подписчик не отписался, иначе ему нельзя передавать данные.

Но тут есть и более серьезная проблема, которой подвержено немалое количество приложений. Эта проблема связана с тем, что Observable может отдавать данные быстрее, чем подписчик сможет их обрабатывать, и она известна под названием Backpressure.

Такая ситуация вполне реальна, ведь данные обычно грузятся в фоне, а подписчик работает в главном потоке приложения, который может быть заблокирован какой-то операцией. К тому же, код для работы вашего подписчика может сам по себе быть не быстрым, к примеру, перерисовывать UI.

Давайте приведем минимальный код, который позволит нам воспроизвести проблему Backpressure:

Казалось бы, что это самый обычный код, который вы можете встретить в любом приложении, и, тем не менее, он приводит к крашу (в данном случае в обработчик onError). Проблема заключается в том, что вызов System.out.println все-таки ощутимо медленнее, чем обычный вызов onNext у подписчика. И с такой проблемой вы можете столкнуться в любой ситуации. Проблема изначально заключается в методе Observable.create. И не зря его просят объявить как deprecated. Но раз такая проблема есть, давайте рассмотрим два различных способа, как защититься от нее.

1. Использование специальных операторов

Во-первых, есть большая группа операторов, позволяющих использовать только один элемент из всех, которые пришли за определенное время / количество элементов. К примеру, оператор sample позволяет получать только самый последний элемент из всех элементов во временном периоде. Схема его работы выглядит следующим образом:

Например, в следующем примере оператор sample позволит использовать только одно значение один раз в 10 микросекунд:

В качестве результата мы получим примерно такой вывод (который будет отличаться на разных запусках):

Такой оператор удобно использовать, если, к примеру, ваши данные обновляются очень часто, но при этом вам нужно только последнее значение. Вы не хотите слишком часто обновлять свой UI, поэтому можете настроить sample на секунду, к примеру.

Еще одним оператором из такой группы является оператор debounce, который отдает данные подписчику только в том случае, если уже достаточно долго (время ожидания является параметром) нет поступления новых данных. Схема оператора debounce:

Самый типичный пример использования оператора debounce – ввод данных пользователем. Если вы хотите подождать, пока пользователь закончит ввод данных, прежде чем выполнить запрос, стоит обратиться к оператору debounce.

Предыдущие операторы всегда отдавали только один элемент из группы, при этом остальные элементы пропадали. Такой подход удобен, но он не всегда применим. Иногда нам нужно обработать все поступающие данные. Логичным решением для такого случая будет использование какого-то буфера, который будет накапливать данные, а потом отдаст данные в подписчик, который обработает все данные в списке разом. Для этого существует оператор buffer, который собирает данные, пока не пройдет достаточного количества времени либо пока не наберется достаточного элемента (с этой возможностью нужно быть крайне аккуратным, чтобы не попасть в ситуацию, когда нужного количества элементов никогда не наберется). Схема оператора buffer:

Можно применить его и для решения проблемы из нашего примера:

Теперь мы выводим не каждый элемент, а порциями по 100 элементов – вероятность получения MissingBackpressureException стремится к нулю.

2. Отказ от использования Observable.create

На самом деле, проблеме Backpressure подвержен только способ создания Observable через метод create. Самое простое решение – отказаться от его использования. Сейчас это возможно. Рассмотрим несколько ситуаций, где вы можете заменить использование create с помощью других операторов.

Во-первых, если у вас есть несколько уже готовых элементов или их список, вы можете использовать методы just, from и другие.

Во-вторых, если у вас есть какой-то тяжелый код, возвращающий один элемент, используйте fromCallable:

И, наконец, самая главная причина использования метода create – “завернуть” какой-то асинхронный вызов в Observable, чтобы его дальше можно было использовать в реактивном стиле. Один из примеров – использование различных сенсоров. Корректная реализация такого подхода с использованием метода create выглядит следующим образом:

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

Кажется, что без create здесь не обойтись, но способ есть, он реализуется за счет операции fromEmitter и выглядит следующим образом:

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

Таким образом вы можете (и должны) полностью отказаться от метода Observable.create!

Пактические примеры

Скачайте проект Проект RxJavaSamples

1)Поток строк в поток чисел с предикатом

2)Получение уникальных данных из потока до выполнения условия

3)Сумма всех чисел в потоке данных

4)Переключение между потоками данных

5)Поток из наибольших общих делителей для элементов из входных потоков

6)Поток с долгим вычислением

Практическое задание

Скачать Проект PopularMovies

1) Условие задачи в классе ru.gdgkazan.popularmovies.screen.details.MovieDetailsActivity

2)Выполнить два запроса (на получение трейлеров и отзывов) параллельно

3)Отображать процесс загрузки данных

4)Сохранить данные в базу и использовать закэшированные данные в случае ошибки

5)Обрабатывать пересоздание Activity

Ссылки и полезные ресурсы

  1. Приложения из репозитория:
    1. RxJavaSamples – примеры кода с RxJava и практические задачки.
    2. PopularMovies – пример использования RxJava в Android для выполнения сетевых запросов и практическое задание.
    3. BackpressureProblem – демонстрация примеров MissingBackpressureException и способов решения этой проблемы.
  2. Хорошее введение в функциональное реактивное программирование.
  3. Документация по Rx.
  4. Вики по RxJava.
  5. Большой список различных статей по Rx и RxJava.
  6. Вводные статьи про RxJava и RxAndroid и их перевод на хабре.
  7. Вводные видео про RxJava.
  8. Документация по операторам из Rx.
  9. Интерактивный список операторов из Rx.
  10. Хорошая статья про использование RxJava для загрузки данных из нескольких источников.
  11. Доклад про использования RxJava в Яндекс.
  12. Книга RxJava Essentials.
  13. Документация про Subjects.
  14. Проблема Backpressure: описание, способы борьбы с ней и ответ на SO, статья про fromEmitter.

Продолжение:

Лекция 4 по архитектуре андроид приложения. Clean Arcitecture

Лекция 3 по архитектуре андроид-приложений. Часть 2. RxJava в Android обновлено: Июнь 29, 2017 автором: admin

Добавить комментарий