Android — Что такое RxJava и с чем его едят

Содержание

RxJava примеры

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

Общая информация

Материал рассчитан на тех, кто уже имеет какой-то опыт работы с RxJava. Если же вы пока совсем ничего не знаете по этой теме, посмотрите два первых урока моего курса: Урок 1 и Урок 2. По ним вы сможете примерно понять механизмы RxJava. А в последующих уроках курса рассмотрены уже более продвинутые вещи.

В примерах использовалась RxJava версии 2. Но в целом будет работать и на первой версии.

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

Статья будет периодически пополняться новыми примерами.

Список примеров:

От списка к отдельным элементам

Используемые операторы: flatMap, fromIterable

У нас есть метод getUsers, который возвращает список пользователей.

Нам нужно от списка пользователей перейти к отдельным пользователям. Т.е. из Observable
> получить Observable , чтобы дальше работать с отдельными пользователями. Для этого можно использовать flatMap и fromIterable.

Метод fromIterable возьмет список и создаст из него Observable с отдельными элементами списка, т.е. из List мы получим Observable .

Оператор flatMap раскроет получившийся Observable и запостит его элементы далее в поток .

В итоге, в метод saveUser будут приходить отдельные объекты User.

Лямбда версия того же кода:

Использование одного из нескольких источников

Используемые операторы: concat, first, filter

Это вполне распространенный сценарий, когда нам необходимо получать данные. Мы смотрим сначала в кэш, если там пусто, то смотрим в БД, если и там пусто, то идем на сервер.

У нас есть три репозитория, которые возвращают нам список пользователей. Мы получаем от них три Observable.

Если данные в репозитории есть, то Observable отправит их нам в (onNext) и завершит работу (onCompleted). Если же данных нет, то Observable сразу вызовет onCompleted.

Соединяем три Observable с помощью concat.

Оператор concat будет последовательно получать данные из этих Observable и передавать их дальше. Т.е. сначала пойдут данные из cacheUsers, затем из dbUsers, затем из networkUsers. Если в каком-то из Observable нет данных, он будет просто пропущен.

Может так случиться, что во всех трех Observable будут данные. И мы получим их всех. Чтобы избежать этого и получить только одни данные, мы используем оператор first. Он пропустит только первые данные, а затем завершит всю цепочку.

Т.е. если в cacheUsers были данные, мы получим их, а остальное (dbUsers и networkUsers) будет проигнорировано.

Если в cacheUsers данных не было, но они были в dbUsers, то мы получим их, а networkUsers будет проигнорирован.

А если cacheUsers и dbUsers были пусты, то мы получим данные из networkUsers.

Если все три Observable ничего не вернули, то мы получим пустой список, который мы указали, как дефолтное значение, в операторе first.

Может быть так, что Observable в случае отсутствия данных выполняет не onCompleted, а onNext с пустым списком. Тогда concat из предыдущего примера вернет нам этот пустой список. Это неправильно. Нам надо игнорировать пустой список и смотреть следующий репозиторий.

В этом случае нам поможет оператор filter, который не пропустит пустые списки.

Данные второго Observable зависят от данных первого Observable

Используемые операторы: flatMap, fromIterable

В репозитарии есть два метода: получение списка пользователей и получение детальной информации по пользователю.

Необходимо для всех пользователей из списка getUsers получить детальные данные методом getUserDetails.

Первый flatMap разделит список, полученный из getUsers, на отдельных пользователей.

Далее следующий flatMap вызовет метод getUserDetails для каждого пользователя, и дальше в поток пойдут уже объекты UserDetails.

В методе subscribe мы получаем UserDetails и выполняем необходимую операцию.

Данные второго Observable зависят от данных первого Observable и соединяются друг с другом в общем Observable

Используемые операторы: flatMap, fromIterable

В репозитарии есть два метода: получение списка пользователей и получение детальной информации по пользователю.

Нам снова необходимо для всех пользователей из списка getUsers получить детальные данные методом getUserDetails. Но теперь нам надо еще и соединить вместе User и UserDetails. Т.е. у класса User есть метод:

и для каждого объекта User из списка getUsers нам надо передать в этот метод полученный UserDetails.

Это делается расширенной версией оператора flatMap

Первый flatMap разделит список, полученный из getUsers, на отдельных пользователей.

У следующего flatMap мы используем две функции. Первая вызовет метод getUserDetails для каждого объекта User и получит объект UserDetails. Оба этих объекта (User и UserDetails) пойдут во вторую функцию, и тем самым нам дается возможность соединить их. В нашем примере, мы вызываем метод user.setDetails и возвращаем этот же User объект. Он пойдет дальше в поток.

В методе subscribe мы получаем User (с присоединенным к нему UserDetails) и выполняем необходимую операцию.

Соединение данных из нескольких Observable в один общий

Используемые операторы: flatMap, fromIterable, zip

В репозитарии есть три метода: получение списка пользователей, получение детальной информации по пользователю и получение фото пользователя.

Нам необходимо для каждого пользователя из списка getUsers получить детальные данные и фото. Для хранения данных пользователя будем использовать класс UserData.

Сначала отдельно напишем метод getUserData, где будем для User получать UserDetails и UserPhoto и складывать их в UserData.

В оператор zip мы передаем два Observable, которые вернут UserDetails и UserPhoto для указанного пользователя. Получив эти данные, zip предоставит нам возможность соединить их в один общий объект UserData и создаст Observable с этим объектом.

В методе getUserData мы соединили два Observable в один. Теперь используем этот метод в итоговом Observable.

Первый flatMap разделит список, полученный из getUsers, на отдельных пользователей.

Далее следующий flatMap вызовет метод getUserData для каждого пользователя и дальше в поток пойдут уже объекты UserData содержащие UserDetails и UserPhoto.

В методе subscribe мы получаем UserData и выполняем необходимую операцию.

Обработка ввода текста в строку поиска

Используемые операторы: switchMap, debounce

У нас есть экран поиска пользователей. По мере ввода текста в строку поиска нам необходимо запускать метод, который вернет нам список пользователей, удовлетворяющих запросу.

Пусть это будет метод searchUsers:

На вход принимает строку и возвращает Observable со списком.

Чтобы по мере ввода символов в поле запроса мы могли получать полный текст запроса как Observable , мы будем использовать Subject .

А на EditText нам надо повесить TextWatcher, который будет ловить ввод новых символов и передавать полный текст запроса в subject.

Либо вы можете просто использовать библиотеку RxBinding.

В этом сценарии есть пара нюансов, которые нам надо учитывать.

Во-первых, если пользователь будет быстро набирать текст, то мы будем запускать поиск при вводе каждого нового символа и вполне может получиться так, что будет несколько запусков в секунду. Это не очень хорошая практика, особенно если поиск выполняется на сервере.

Лучше мы будем после каждого символа ждать. Если прошла, например, одна секунда, значит пользователь скорее всего закончил ввод, и мы действительно можем запускать поиск.

Второй нюанс обсудим после просмотра кода.

Обработка текста из subject будет выглядеть так:

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

В переводе на нормальный язык — debounce будет отсеивать передаваемые ему данные, если между ними пауза меньше, чем указанное время. Это как раз то, что нам нужно, чтобы отсеять излишние срабатывания поиска.

Далее мы используем switchMap. Это аналог flatMap, но работает чуть по-другому.

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

0 мс. Пользователь вводит в строку поиска символ А. Оператор debounce получает его и включает паузу.

1000 мс — debounce выждал паузу и передает символ дальше. Запускается поиск по строке А. Т.е. flatMap запускает searchUsers(“А”) и подписывается на его Observable, чтобы получить результат поиска и передать его дальше в поток.

1100 мс — пользователь вводит символ Б. Оператор debounce получает текст АБ и включает паузу.

2100 мс — debounce выждал паузу и передает текст АБ дальше. Запускается поиск по строке АБ. flatMap запускает searchUsers(“АБ”) и подписывается на его Observable, чтобы получить результат поиска и передать его дальше в поток.

2500 мс — отработал поиск по строке А и вернул результаты. flatMap получил их и отправил дальше — в метод showUsers, и мы отобразили их на экране.

3600 мс — отработал поиск по строке АБ и вернул результаты. flatMap получил их и отправил дальше — в метод showUsers, и мы отобразили их на экране.

Обратите внимание что с 2500 по 3600 мс экран будет отображать результаты поиска по строке А, хотя в строке поиска в этом время уже был текст АБ. Это неправильно.

Нам нужно игнорировать результаты прошлого поиска, как только мы запускаем новый поиск. Для этого мы используем switchMap, а не flatMap. Хронология тех же событий, но с оператором switchMap будет выглядеть так:

0 мс. Пользователь вводит в строку поиска символ А. Оператор debounce получает его и включает паузу.

1000 мс — debounce выждал паузу и передает символ дальше. Запускается поиск по строке А. Т.е. switchMap запускает searchUsers(“А”) и подписывается на его Observable, чтобы получить результат поиска и передать его дальше в поток.

1100 мс — пользователь вводит символ Б. Оператор debounce получает текст АБ и включает паузу.

2100 мс — debounce выждал паузу и передает текст АБ дальше. Запускается поиск по строке АБ. switchMap запускает searchUsers(“АБ”) и подписывается на его Observable, чтобы получить результат поиска и передать его дальше в поток. Но! При этом switchMap отпишется от прошлого Observable, который он получил от searchUsers(“А”).

2500 мс — отработал поиск по строке А и вернул результаты. Но switchMap уже отписался от этого поиска и результат просто никуда не пойдет.

3600 мс — отработал поиск по строке АБ и вернул результаты. Они пошли дальше в метод showUsers и мы отобразили их на экране.

Т.е. switchMap в отличие от flatMap будет отписываться от прошлых поисков при запуске нового.

Повтор при ошибке

Используемые операторы: retryWhen, take, delay, range, zip, just, error, flatMap

У нас есть метод getUsers, который возвращает список пользователей.

Мы можем настроить Observable так, чтобы при ошибке он перезапускался определенное количество раз и через определенное время.

Для этого используется оператор retryWhen

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

Временной интервал мы указали в операторе delay, а количество попыток — в take.

Описать принцип работы оператора retryWhen достаточно сложно. В моем курсе RxJava есть отдельный урок по retry операторам и там я все подробно объясняю.

Если вкратце, то throwableObservable, который мы получаем в функции в retryWhen, — это Observable, куда будут приходить ошибки из getUsers. От нас требуется вернуть, как результат работы функции, Observable, который будет использован, как триггер перезапуска метода getUsers.

В нашем примере мы берем throwableObservable, добавляем к нему take и delay и возвращаем как результат работы функции. Соответственно, первые три (оператор take) ошибки из getUsers будут отложенным (оператор delay) сигналом к перезапуску getUsers.

Но в этой схеме есть недостаток. Когда будет четвертая ошибка, мы не получим ее в onError. Вместо этого придет onComplete. И мы даже не узнаем, что что-то пошло не так.

Это исправляется следующим образом:

Содержимое функции в retryWhen стало сложнее. Но весь этот код просто прокидывает ошибку из throwableObservable на верхний уровень, когда количество попыток достигает установленного максимума. И в итоге мы получим эту ошибку в onError обработчике в subscribe.

Периодический повтор операции

Используемые операторы: repeatWhen, delay

У нас есть метод getUsers, который возвращает список пользователей.

Нам необходимо, чтобы данные загружались с сервера раз в минуту и сохранялись в БД. Для повтора операции мы можем использовать оператор repeatWhen

Механизм repeatWhen похож на рассмотренный в предыдущем примере retryWhen. В функции мы получаем objectObservable, который будет постить void, когда из getUsers придет onComplete. Из objectObservable мы можем сделать Observable, элементы которого будут триггером для повторного запуска getUsers. Мы добавляем оператор delay с минутной задержкой. Это значит, что через минуту после каждого onComplete, пришедшего из getUsers, метод getUsers будет перезапущен.

Если вам необходимо количественно ограничить количество повторов, то используйте take для objectObservable.

Если вам необходимо остановить повтор при получении каких-либо данных, то добавьте takeUntil

Как только придет пустой список, вся цепочка завершится

Либо в subscribe используйте полноценный DisposableObserver, в onNext проверяйте ваше условие и, если оно выполняется, вызывайте dispose().

Чтобы остановить всю цепочку извне, просто вызовите dispose для Disposable, полученного из subscribe.

Присоединяйтесь к нам в Telegram:

— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.

— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Kotlin, RxJava, Dagger, Тестирование

— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня

— новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме

Использование RxJava и Okhttp

Я хочу запросить URL-адрес, используя okhttp в другом потоке (например, поток IO) и получить Response в главном потоке Android, но я не знаю, как создать Observable .

Сначала добавьте RxAndroid в свои зависимости, а затем создайте свой Observable следующим образом:

Он запросит ваш url в другом потоке (io thread) и будет наблюдать за ним в основной нити Android.

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

Когда вы используете Observable.create , вы должны написать много кода шаблона, также вы должны обрабатывать подписку самостоятельно. Лучшей альтернативой является использование отсрочки . Форма документа:

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

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

Так, как заметил Марцин Козиньский , вам просто нужно сделать это:

Легче и безопаснее использовать Observable.defer() вместо Observable.create() :

Таким образом, вы можете отказаться от подписки и противодавления. Вот отличный пост Дэн Лью о create() и defer() .

Если вы хотите пройти маршрут Observable.create() он должен больше походить на эту библиотеку с isUnsubscribed() посыпаются повсюду. И я считаю, что это все еще не справляется с противодавлением.

Я понимаю, что этот пост немного стар, но есть новый и более удобный способ сделать это сейчас

Вопрос по rx-java, andro >

Я хочу запросить URL-адрес, используя okhttp в другой поток (например, поток ввода-вывода) и получить Response в основной теме Android, но я не знаю, как создать Observable .

Я понимаю, что этот пост немного стар, но теперь есть новый и более удобный способ сделать это

Это проще и безопаснее в использовании Observable.defer() вместо Observable.create() :

Таким образом, отмена подписки и обратное давление обрабатываются для вас. Вототличный пост Дэна Лью около create() а также defer() .

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

Я опоздал к обсуждению, но, если по какой-то причине код должен передавать тело ответа, то defer или же fromCallable не буду этого делать Вместо этого можно использовать using оператор.

  1. Первая лямбда выполняет ответпосле подписки.
  2. Вторая лямбда создает наблюдаемый тип, здесь с Single.just(. )
  3. Третья лямбда располагает ответом. С defer можно было бы использовать стиль «попробуй с ресурсами».
  4. Установить eager переключиться на false чтобы вызывающее устройство вызывалось после терминального события, то есть после того, как потребитель подписки был выполнен.
  5. Конечно, сделать это на другом пуле потоков
  6. Вот лямбда, которая будет потреблять тело ответа. Без eager установлен в false , код вызовет IOException с причиной «закрыт», потому что ответ будет уже закрыт до входа в эту лямбду.
  7. onError Лямбда должна обрабатывать исключения, особенно IOException что больше не может быть пойман с using оп, эратор, как это было возможно с попыткой defer .

Первое добавление RxAndroid к вашим зависимостям, затем создайте свой Observable как это:

Он запросит ваш URL в другом потоке (io thread) и увидит его в главном потоке android.

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

Когда вы используете Observable.create Вы должны написать много стандартного кода, также вы должны самостоятельно оформить подписку. Лучшей альтернативой является использованиеоткладывать, Форма документа:

не создавайте Observable, пока наблюдатель не подпишется, и создайте новую Observable для каждого наблюдателя

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

Таким образомМарчин Козиньски упомянуто, вам просто нужно сделать это:

Android programmers blog

Simply about difficult

Поиск по этому блогу

вторник, 14 ноября 2020 г.

Пишем код красиво с Rx Andro >

Давно хотел рассказать про RX но никак не выходило, то времени не хватало, то желания небыло, и так куча причин была… А тут выдалось свободное время, и желание тоже появилось, в общем как-то планеты сошлись все в одну линию и пришло осознание бытия, и что пора написать что-то новое.

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

Попался мне проект один, который был написан на Rx, я как нубас впал в ступор и начал истерить что это вообще какая-то не понятная хрень и в растроенных чувствах начал разбираться в ней. В итоге понял что эта непонятная хрень довольно таки удобная хрень, дак еще и полезная, так как она обсорбирует в себя кучу ошибок, и разных плюшек типа работы в UI треде или объеденение нескольких списков в один после запроса.

В этой статье я хочу рассказать про несколько вариантов использования Rx в проекте, в одном случае я приведу прям вот живой пример использования этой штуки, а в других случаях пройдусь объяснив как это можно реализовать у себя в проекте. Я лично использую Rx для спокойной и удобной работы в UI треде, как минимум не надо создавать там разные AsyncTask’и и писать кучу колбеков, достаточно написать один класс в котором будут нужные методы, а дальше просто вызывать этот класс в нужных классах, и получать нужные данные.

Начнем мы с того что разберем что такое Observable и Subscriber. Это два основных класса которые обеспечивают нас как программистов основными функциями Rx. Observable — у нас класс который хранит в себе данные, а Subscriber — класс передающий данные из Observable. Ну то есть Observable у нас хранит данные, а Subscriber — возвращает все что имеет в себе Observable. Как то так. В общем дальше на примерах будет понятней.

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

Давайте перейдем к практике. Вот у нас есть какая-то функция которая возвращает нам какой-то ArrayList, нам нужно его получить через Observable и передать в MainActivity. Что нам для этого нужно. Создать какой-то метод котоый будет возвращать Observable и дальше вернуть этот список в наш метод.

Цукерберг рекомендует:  Установка и настройка красивого меню P30 Bubblemenu для Joomla 1.5.xx

Как видно из кода, мы создали Observable.create, который передает какой-то observableEmitter, это объект самого ObservableEmitter который возвращает текущее состояние потока, и работает в то же время колбеком для нас. С помощью его мы можем вызывать такие методы как:

Из их названий я так думаю понятно что они занчат. onNext() вызывается когда выполнилась ваша функция, и он возвращает в Subscriber, то что вы передаете в него. onError() соответственно возвращает ошибку которую вы в него передадите, если ничего не передадите, то соответственно он ничего и не выведет. onComplete() вызывается когда все действия завершены и поток закрывается.

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

Это в принципе основной функционал который считается самым популярным в RX. Есть еще методы just, first, last, rand, from, они все нужны, но в редких случаях когда у вас идет работа с локальным списком, когда вы передаете список в Observable и вам нужно сделать какие-то манипуляции с ним внутри Observable. Тогда да, но я тут все это размазывал не из-за этих методов, так что их я оставлю на потом. Возможно в будущем расскажу о них…

А сейчас я расскажу как я парсил сайт с помощью JSOUP и RxAndroid. Это оказалось довольно просто, даже как-то подозрительно.

Для примера я взял сайт instructables.com. Там довольно простая верстка, и просто можно достать нужные названия классов для парсинга. В примере я всего лишь достану список статей из главной страницы об технологиях.

Начнем мы с того что создадим проект, и в нем у нас будут MainActivity.java и разметка activity_main.xml. Нам нужно будет подключить пару библиотек, и java 8 для работы ретролямбды, что бы не подключать библиотеку. Данный проект писался в Android Studio 3, они уже умеет в ретролямбду без ретролябмды, то есть фишки java 8 доступны без лишних библиотек, это кстати очень удобно, так как раньше это был страшный геморой, пока подключишь ее, свихнешься.

Библиотеки которые нам понадобятся:

Вроде бы для такого маленького проекта не нужно особо много библиотек, но у меня их довольно много, я буду создавать списки с помощью RecyclerView, буду использовать Rx, буду паристь страницы с помощью JSOUP и буду искать вюхи с помощью ButterKnife, а еще как же без Picasso, мне же надо с помощью чего-то отображать картинки…

А еще нам нужно добавить в тот же app/build.gradle использование java 8.

Таким образом мы говорим студии что у нас можно сокращать код и делать что-то на подобии -> в место длинного и не удобного new View.OnClickListener() < private void onClick >. Но это опять же по желанию, мне нравятся эти сокращения, может кому-то больше нравится длинные но понятные колбеки, без этих непонятных сокращений.

В общем с настройкой gradle мы справились, это уже великолепно. Далее нужно создать интерфейс который будет у нас хранить все методы которые нам нужны для работу. Так и красивей получается и удобней. У нас там будет один метод, но это ведь не важно, за то красиво!

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

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

Если у нас появятся какие то ошибки, то мы добавили в catch метод onError() который принимает ексепшн и отдает его в сабскрайбера. Ну и в конце в finnlay когда уже все действия у нас заканчиваются мы вызываем onComplete(), что бы убирать прогрессбар на пример, или что-то прятать по окончанию загрузки.

Давайте создадим еще адаптер, что бы в конце статьи уже собрать все в кучу и получить какой-то красивый результат. Адаптер у нас будет стандартный, по этому я на нем не буду задерживать внимания. Обычный RecyclerView.Adapter который отображает текст и картинку.

Как я и говорил все тривиально просто. Создали адаптер который принимает в конструкторе ArrayList и дальше сетит данные из списка в вьюхи.

И не забываем конечно же за разметочку для адаптера.

А что же делать дальше скажете вы? Да все просто, дальше мы открываем наш MainActivity, и прямо в onCreate() пишем вызов этого метода с определенными параметрами, которые я описывал выше.

Находим RecyclerView на нашей активити, задаем ему параметры, что бы он использовал LinearLayoutManager для отображения, и DividerItemDecoration для разделения айтемов друг от друга. Дальше вызываем наш RepositoryImpl() который вызывает наш метод, передаем в него ссылку откуда парсить наши статьи.

Метод subscribeOn(Schedulers.io()) — говорит о том что поток в котором будет выполняться функция будет задан Schedulers’ом.
Метод observeOn(AndroidSchedulers.mainThread()) — очевидно говорит что потом мы создаем поверх нашего UI треда. Но как бы мы не хотели повлиять на главный поток, он все равно не будет заблокирован пока действие не закончится, то что нам как раз и нужно, так как JSOUP требует выполнение действий в отдельном потоке.
Ну и конечно же subscribe() который возвращает нам наш Observer, который мы и хотим получить в итоге.

Дальше в методе onNext() мы получаем наш ArrayList, и выводим то что у нас в нем есть в адаптер. Если же у нас появтяся какие-то ошибки по ходу действия Observable — все ошибки вернутся в onError().

Осталось только создать RecyclerView в разметке. Кто не знает как вот пример.

И не забываем что у нас приложение все таки работает с интернетом, по этому ему нужно разрешить работу с интернетом в манифесте.

AndroidManifest.xml
И вот после этого всего, у вас по идее должно все скомпилироваться и работать как часы, без падений и ексепшинов. Как часы.

PS:

Rx еще часто используют для комбайна данных из запросов Retrofit на пример. Это довольно интересная тема, так как это очень спасает от костылей которые будут вручную эти данные добавлять и будут в ключат ьв себя кучу ошибок типа смешивания данных и путаницу в айдишниках, а так у нас допустим есть два запроса, как это было сказанно в статье с медиума что я выше дал, для получения фотографии и данных пользователя, и нам нужно эти данные объеденить в одну кучу что бы потом передать куда-нибудь в адаптер.

Если в кратце. Мы создаем интерфейс для работы с ретрофитом, как я описывал это в статье про работу с Retrofit .

И у нас как бы есть два запроса, которые мы дальше объеденяем в простую конструкцию

Где Observable.zip() метод который объеденяет наши два объекта service.getUserPhoto(id) и service.getPhotoMetadata(id). Дальше у нас идет колбек который возвращае (photo, metadata) и мы их объъеденяем в методе createPhotoWithData() который принимает их. Указываем работу в отдельном потоке, но при этом в UI, и дальше выводим то что у нас получилось в итоге в subscribe(), в метод showPhoto() который принимает photoWithData.

В общем если вам так не понятно то объясню на другом примере. Возможно так будет более понятно. Вот у нас есть два метода которые возвращают разные данные в списках.

Первый у нас возвращает слово one. Второй возвращает слово two. И преобразовываем мы это все в Observable что бы можно было его скормить для парсинга. Все очень просто, а дальше у нас уже знакомая конструкция сбора данных в одну кучу:

Реализация мгновенного поиска в Android с помощью RxJava

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

Мгновенный поиск

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

Но вот в чём проблема. Поскольку мне необходимо реализовать поиск прямо во время ввода, то всякий раз, когда срабатывает обработчик события onQueryTextChange() , я обращаюсь к API для получения первого набора результатов. Логи выглядят следующим образом:

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

Теперь, предположим, я хочу найти что-нибудь ещё. Я удаляю TEST и ввожу другие символы:

Происходит 20 вызовов API! Небольшая задержка сократит количество этих вызовов. Я также хочу избавиться от дубликатов, чтобы обрезанный текст не приводил к повторным запросам. Ещё я, вероятно, захочу отфильтровать некоторые элементы. Например, нужна ли возможность поиска без введённых символов или поиска по одному символу?

Реактивное программирование

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

ReactiveX — это API, который работает с асинхронными структурами и манипулирует потоками данных или событиями, используя сочетания паттернов Observer и Iterator, а также особенности функционального программирования.

Это определение не полностью объясняет сущность и сильные стороны ReactiveX. А если и объясняет, то только тем, кто уже знаком с принципами работы этого фреймворка. Я также видел такие диаграммы:

Диаграмма объясняет роль оператора, но не позволяет полностью разобраться в сути. Итак, давайте посмотрим, смогу ли я понятнее объяснить данную диаграмму на простом примере.

Давайте сначала подготовим наш проект. Вам понадобится новая библиотека в файле build.gradle вашего приложения:

Не забудьте синхронизировать зависимости проекта, чтобы загрузить библиотеку.

Теперь давайте рассмотрим новое решение. Используя старый метод, я обращался к API при вводе каждого нового символа. C помощью нового способа я собираюсь создать Observable :

Этот код выполняет абсолютно то же самое, что и старый код. Логи выглядят следующим образом:

Тем не менее, ключевое отличие использования нового приёма заключается в наличии реактивного потока — Observable . Обработчик текста (или обработчик запроса в данном случае) отправляет элементы в поток, используя метод onNext() . А у Observable есть подписчики, которые и обрабатывают эти элементы.

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

Я сократил методы, чтобы показать наиболее значимую часть. Теперь те же логи выглядит следующим образом:

Теперь давайте применим задержку на 250мс, ожидая большего количества контента:

И, наконец, удалим дубликаты потока, чтобы только первый уникальный запрос был обработан. Последующие идентичные запросы будут игнорироваться:

Прим. перев. В данном случае разумнее использовать оператор distinctUntilChanged() , потому что иначе в случае повторного поиска по какой-либо строке запрос просто проигнорируется. А при реализации такого поиска разумно обращать внимание только на последний успешный запрос и игнорировать новый в случае его идентичности с предыдущим.

В конце отфильтруем пустые запросы:

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

Существует ещё много различных операторов, которые вы можете добавить в этот пайплайн в зависимости от ваших целей. Я считаю, что они весьма полезны для работы с полями ввода, которые взаимодействуют с API. Полный код выглядит следующим образом:

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

Заключение

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

  • Grokking RxJava от Дэна Лью (это сайт, который помог меня двигаться в правильном направлении).
  • Сайт ReactiveX (я часто ссылаюсь на этот сайт при построении пайплайнов).

Существуют также дополнительные библиотеки для Android и Kotlin. Но я расскажу об этом в другой раз.

Реактивное программирование с RxJava и RxKotlin

Russian (Pусский) translation by Liliya (you can also view the original English article)

С тех пор, как стал официально поддерживаемым языком для разработки Android, Kotlin быстро завоевал популярность среди разработчиковAndroid, и Google сообщает о 6-кратном увеличении приложений, созданных с помощью Kotlin.

Если вы ранее использовали RxJava или RxAndro >Observers , Observables и потоков данных в Kotlin, прежде чем смотреть на то, как вы можете обрезать тонны кода шаблона из своих проектов, путем объединенияRxJava с функциями Kotlin.

Использование RxJava с Kotlin может помочь вам создавать высокореактивные приложения в меньшем количестве кодов, но язык программирования не идеален, поэтому я также буду обмениваться обходным решением проблемы конверсии SAM, с которой сталкиваются многие разработчики, когда они впервые начинают использовать RxJava 2.0 с Kotlin.

Чтобы обернуть все, мы создадим приложение, демонстрирующее, как вы можете использовать RxJava для решения некоторых проблем, с которыми вы сталкиваетесь в реальных проектах Android.

Если вы первый раз пробуете RxJava, то по пути я также буду предоставлять всю справочную информацию, необходимую для понимания основных концепций RxJava. Даже если вы никогда не экспериментировали с RxJava раньше, в конце этой статьи вы будете иметь четкое представление о том, как использовать эту библиотеку в своих проектах, и вы создали несколько рабочих приложений,используя RxJava, RxKotlin, RxAndroid и RxBinding.

Что такое RxJava, во всех случаях?

RxJava — это реализация библиотеки ReactiveX с открытым исходным кодом, которая помогает создавать приложения в стиле реактивного программирования. Хотя RxJava предназначен для обработки синхронных и асинхронных потоков данных, он не ограничивается «традиционными» типами данных. Определение «данных» RxJava довольно широко и включает такие вещи, как кеши, переменные, свойства и даже пользовательские события ввода, такие как щелчки и нажатия. Просто потому, что ваше приложение не имеет дело с огромными числами или выполняет сложные преобразования данных, это не значит, что он не может извлечь выгоду из RxJava!

За небольшим опытом использования приложений RxJava для Android вы можете ознакомиться с некоторыми другими моими сообщениями здесь, на Envato Tuts +.

Итак, как работает RxJava?

RxJava расширяет шаблон разработки программного обеспечения Observer, основанный на концепции наблюдателей и наблюдаемых. Чтобы создать базовый конвейер данных RxJava, вам необходимо:

  • Создать наблюдаемый.
  • Дайте наблюдаемым некоторые данные для выделения.
  • Создайте наблюдателя.
  • Подписывайте Наблюдателя в Наблюдаемого.

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

Если возникает исключение, тогда вызывается onError() , и Observable будет немедленно завершен без каких-либо дополнительных данных или вызова onComplete() .

Но RxJava — это не просто передача данных из Observable в Observer! RxJava имеет огромную коллекцию операторов, которую вы можете использовать для фильтрации, слияния и преобразования этих данных. Например, представьте себе, что в вашем приложении есть кнопка «Оплатить сейчас», которая определяет события onClick , и вы опасаетесь,что нетерпеливый пользователь может нажать кнопку несколько раз, в результате чего ваше приложение обработает несколько платежей.

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

Каковы преимущества использования RxJava?

Мы видели, как RxJava может помочь вам решить определенную проблему в конкретном приложении, но что он должен предлагать проектам Android в целом?

RxJava может упростить ваш код, предоставив вам способ написать то, что вы хотите достичь, вместо того, чтобы писать список инструкций, которые должны выполнять ваше приложение. Например, если вы хотите игнорировать все данные, которые происходят в течение одного и того же 500-миллисекундного периода, тогда вы должны написать:

Кроме того, поскольку RxJava обрабатывает почти все как данные, он предоставляет шаблон, который вы можете применить к широкому спектру событий: создать Observable, создать Observer, подписать Observer к наблюдаемому, проверить и повторить. Этот формульный подход дает гораздо более простой, удобочитаемый код.

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

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

В RxJava вы создаете и управляете дополнительными потоками, используя комбинацию операторов и планировщиков. Вы можете легко изменить поток, в котором выполняется работа, используя оператор subscribeOn плюс планировщик. Например, здесь мы планируем работу над новым потоком:

Вы можете указать, где результаты этой работы должны быть опубликованы, используя оператор observOn . Здесь мы публикуем результаты для всего важного основного потока пользовательского интерфейса Andro >AndroidSchedulers.mainThread , который доступен как часть библиотекиRxAndroid:

По сравнению с встроенными многопоточными решениями Android, подход RxJava гораздо более краток и понятен.

Опять же, вы можете узнать больше о том, как работает RxJava, и о преимуществах добавления этой библиотеки в ваш проект в статье«Начало работы с RxJava 2 для Android».

Должен ли я использовать RxJava или RxKotlin?

Поскольку Kotlin на 100% совместим с Java, вы можете использовать большинство библиотек Java в своих проектах Kotlin без каких-либо трудностей, а библиотека RxJava не является исключением.

Существует специальная библиотека RxKotlin, которая представляет собой обертку Kotlin вокруг обычной библиотеки RxJava. Эта оболочка предоставляет расширения, которые оптимизируют RxJava для среды Kotlinи могут дополнительно уменьшить количество кода шаблона, который вам нужно написать.

Поскольку вы можете использовать RxJava в Kotlin, не требуя RxKotlin, мы будем использовать RxJava в этой статье, если не указано иное.

Создание простых наблюдателей и наблюдаемых в Kotlin

Наблюдатели и наблюдаемые являются строительными блоками RxJava, поэтому давайте начнем с создания:

  • Простой Observable, который испускает короткий поток данных в ответ на событие нажатия кнопки.
  • Наблюдаемый, который реагирует на эти данные, печатая различные сообщения в Logcat Android Studio.

Создайте новый проект с настройками по вашему выбору, но при появлении запроса установите флажок Включить поддержку Kotlin. Затем откройте файл build.gradle вашего проекта и добавьте библиотеку RxJava в зависимости от проекта:

Затем откройте файл activity_main.xml вашего проекта и добавьте кнопку, которая начнет поток данных:

Существует несколько способов создания Observable, но одним из самых простых является использование оператора just() для преобразования объекта или списка объектов в Observable.

В следующем коде мы создаем Observable ( myObservable ) и даем ему элементы 1, 2, 3, 4 и 5 для выдиления. Мы также создаем Observer( myObserver ), подписываемся на myObservable , а затем сообщаем ему печатать сообщение Logcat каждый раз, когда он получает новую эмиссию.

Теперь вы можете проверить это приложение:

  • Установите свой проект на физическом Android-смартфоне или планшете или виртуальном устройстве Android (AVD).
  • Нажмите на Start RxJava stream
  • Откройте монитор Logcat для Android Studio, выбрав вкладку «Монитор Android» (где курсор расположен на следующем снимке экрана), а затем выберите вкладку «Logcat».

На этом этапе Observable начнет выдавать свои данные, и Observer будет печатать свои сообщения в Logcat. Вывод Logcat должен выглядеть примерно так:

Вы можете загрузить этот проект из GitHub, если хотите попробовать его сами.

Kotlin Extensions для RxJava

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

Чтобы использовать библиотеку RxKotlin, вам нужно добавить ее как зависимость от проекта:

В следующем примере мы используем функцию расширения RxKotlin toObservable() для преобразования List в наблюдаемый. Мы также используем функцию расширения subscribeBy() , поскольку она позволяет нам построить Observer с использованием именованных аргументов, что приводит к более четкому коду.

Вот результат, который вы должны увидеть:

Решение проблемы неоднозначности SAM RxJava

RxKotlin также обеспечивает важное обходное решение для проблемы с преобразованием SAM, которое может возникать при наличии нескольких перегрузок параметров SAM для данного метода Java. Эта двусмысленность SAM смущает компилятор Kotlin, поскольку он не может решить, какой интерфейс он должен преобразовать, и ваш проект не сможет получить результат.

Эта двусмысленность SAM является особой проблемой при использовании RxJava 2.0 с Kotlin, так как многие операторы RxJava принимают несколько SAM-совместимых типов.

Давайте рассмотрим проблему преобразования SAM в действии. В следующем коде мы используем оператор zip() для объединения вывода двух наблюдаемых:

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

Вот результат этого кода:

Заключение

В этом уроке я показал вам, как начать использовать библиотеку RxJava в проектах Kotlin, в том числе с использованием ряда дополнительных поддерживающих библиотек, таких как RxKotlin и RxBinding. Мы рассмотрели, как вы можете создавать простые наблюдатели и наблюдаемых в RxKotlin, чтобы оптимизировать RxJava для платформы Kotlin, используя функции расширения.

Цукерберг рекомендует:  Ищем работу 22 телеграм-канала и 3 чата в помощь

До сих пор мы использовали RxJava для создания простых Observables, которые излучают данные, и наблюдателей, которые печатают эти данные в Logcat Android Studio, но это не то, как вы будете использовать RxJava в реальном мире!

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

Javanese Online

Android — это плохой DI-контейнер

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

Наиболее приемлемым костылём может показаться использование Application , но он один на всех, поэтому Google рекомендует использовать статические синглтоны; нужно знать, что во время бэкапа Application будет предательски подменён, а после рестарта процесса все классы загрузятся заново и данные, хранившиеся в синглтонах, пропадут.

Такой подход становится феерической помехой повторному использованию кода: при добавлении новой функциональности она не делегируется объектам, которые передаются в конструктор, а реализуется прямо внутри фрагмента (активити, презентера, вьюмодели) в if-else/switch-case. Впоследствии такой класс становится типичным customizable object, подчиняющимся десяткам условий — а такой код крайне сложно поддерживать.

Ложку дёгтя добавляет жизненный цикл. Знание того, что Activity/Fragment — обычный Java-объект, оказывается немного неверно: после пересоздания ту же задачу выполняет уже другой объект, которому достаётся Bundle с сохранённым состоянием предшественника. Люди, не знающие Java, часто неверно интерпретируют пересоздание как «GC вычистил мне все ссылки!»

Иерархии типов

Нужно постоянно наследовать классы. Если какая-нибудь библиотека, например, хочет предоставить свои подтипы фрагментов, приходится учитывать, что есть нативные фрагменты ( android.app.Fragment ) и их бэкпорт ( android.support.v4.app.Fragment ), а также несколько особых случаев: DialogFragment , BottomSheetDialogFragment и т. п..

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

Чтобы поселить фактический сервис (просто объект, представляющий определённую функциональность) в Bound Service и передать его в Activity , нужно унаследовать как минимум Service и Binder (а также реализовать ServiceConnection ). Пример заворачивания объекта в сервис

Принеси с собой свои best practices

Как работать с асинхронностью?
AsyncTask
  • Невероятно странный интерфейс: метод execute() принимает дженерик, но задачу можно исполнить лишь один раз, поэтому проще передать параметр в конструктор; второй дженерик определяет тип промежуточных данных — он обычно не используется, поэтому имеет смысл завести для редкого случая отдельный класс — скажем, ProgressAsyncTask ; execute() и onPostExecute() принимают vararg, но в большинстве случаев передаётся ровно один объект; нельзя создавать и запускать AsyncTask из фона — onPreExecute() вызывается прямо из execute() , а в старых версиях статический инициализатор вызывает new Handler(без аргументов) , тем самым привязываясь к текущему Looper (если он есть); ничего не знает о жизненном цикле.
  • Loader полны багов, API монструозен. Считаются устаревшими начиная с Android Pie.
  • ThreadPoolExecutor — неплохой, хоть и старомодный, инструмент из Java, который, естественно, ничего не знает о жизненном цикле.
  • Корутины или монструозную RxJava несложно адаптировать для работы с Android (в частности, прерывать текущие задачи onDestroyView или onDestroy ), но их нужно приносить с собой, т. к. фреймворк их не содержит.
Работа с JSON

Я бы не стал использовать этот формат добровольно, но к этому принуждают многие публичные API.

  • org.json (AST), который есть в Android, хорош лишь в качестве академической реализации.
  • android.util.JsonReader (streaming) — копия com.google.gson.stream.JsonReader , немного оптимизированная для Android. Вообще, стриминг — достаточно низкоуровневый принцип, поверх которого обычно пишут более человечную реализацию. Но при этом JsonReader недостаточно низкоуровневый: он читает символы из Reader и возвращает строки в виде String , то есть доступа к нижележащему InputStream/byte[] он не имеет, что мешает выполнять некоторые оптимизации. Присутствует попытка убирать дубликаты строк с помощью libcore.internal.StringPool , но этот пул не умеет разрешать коллизии, поэтому после считывания нескольких сотен строк в пуле оседает от силы 30.
  • Практические реализации — Gson, Jackson, Moshi, Klaxon — нужно приносить с собой.

HttpURLConnection — это вообще издевательство. Конечно же, большинство использует OkHttp/Retrofit, пока Google пытается делать припарки библиотеке Volley.

Всё осложняется полнейшим отсутствием пакетного менеджера. В некоторых дистрибутивах Linux есть apt-get , apt или aptitude , на Mac OS можно установить brew . В Java-мире есть системы сборки Gradle, Maven и множество других. Все вышеперечисленные умеют скачивать пакеты, их зависимости, зависимости их зависимостей и т. д.. В Android нет ничего подобного — Gradle скачивает всё на многострадальный компьютер разработчика, ProGuard давится десятками библиотек, dx конвертирует их в Android-совместимый байт-код, программист загружает в маркет, пользователь — из маркета, рантайм верифицирует, загружает, компилирует все эти классы, собирает о них статистическую информацию для наиболее оптимального выполнения (profile-driven compilation). Всё это происходит снова и снова, даже если множество приложений используют одни и те же зависимости одинаковых версий, в том числе для таких крупных библиотек как AppCompat/Support, Google Mobile Services, ExoPlayer, Realm, FFMpeg. Последние две содержат нативный (машинный) код, что заставляет разработчиков собирать по несколько APK для разных архитектур (APK splits).

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

Context

Контекст — это god object. View , например, нужен не контекст, а тема и ресурсы. registerReceiver мог бы быть методом не Context , а Application (для локального броадкаста) и, допустим, AndroidSystem (выдуманный класс) для броадкаста по всей системе.

Особенно доставляет, что контексты бывают разные — приложения, сервиса, активити — и с ними нужно обходиться по-разному.

non-configuration instance

Retain-фрагменты переживают смену конфигурации. Слышали что-нибудь о retain-фрагментах без View ? Это костыль, который позволяет хранить объекты в таком фрагменте. В Activity есть похожий механизм — non-configuration instance (custom non-configuration instance у AppCompatActivity ). Отсюда вопрос: почему нельзя сделать Activity живучей, как retain-фрагмент, и почему у фрагмента, наоборот, нет non-configuration instance?

Ресурсы

У Android собственный механизм для доступа к ресурсам. Стандартные для Java ресурсы из classpath работают, но медленно и расточительно. Та же проблема затрагивает механизм ServiceLoader , т. к. META-INF/services — тоже ресурсы classpath.

XML — основной и официальный способ описания ресурсов — от вёрстки, меню, переводов и графики до конфигураций, примитивов и строк. Он хорошо подходит для случая, когда ОС/лончеру/шторке нужно достать ресурсы, не пробуждая приложение, но мешает всяческим попыткам написать лаконично, гибко и без дублирования. Присутствует (неудавшаяся) попытка исправить XML статической типизацией.

Каждый ресурс формально находится в определённом пакете (package) — изолированном пространстве имён. На практике таковых всего два — android и пакет текущего приложения; Android Gradle Plugin сливает ресурсы изо всех AAR-библиотек в пакет приложения, провоцируя потенциальные конфликты имён, а также страшные костыли с перегенерацией R.java .

AttributeSet — это интерфейс, но реализовывать его бесполезно: obtainStyledAttributes кастит его к XmlBlock.Parser — а это package-private класс. По сути, AttributeSet играет роль маркер-интерфейса, что есть антипаттерн.

Сама абстракция Drawable кажется мне очень удачной, а разнообразие коробочных реализаций радует глаз. Но атрибуты темы не работают на четвёрках, названия XML-тегов ( selector , shape ) отличаются от имён классов ( StateListDrawable , GradientDrawable ), возможность использовать в XML свои классы доступна аж с SDK 24, а Drawable paddings влияют на View paddings по-разному, в зависимости от версии Android.

В векторных картинках можно задавать путям цвета из темы (например, ?colorPrimary ). Но темы нельзя создавать из кода!

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

shrinkResources удаляет неиспользуемые ресурсы, но как-то несерьёзно; оставшиеся ресурсы не переименовываются.

Remote views нельзя создать из кода, только XML. Что вносит множество ограничений.

LayoutParams можно создать только из одного XML-тэга. с layout_gravity , в котором находится с layout_margin , который содержит вью с layout_width и layout_height — нельзя, получайте непереиспользуемый код.

В XML-вёрстку нельзя передавать параметры, будь это хоть LayoutInflater#inflate , хоть или (а вот в любом шаблонизаторе такая возможность есть).

В XML-вёрстке нельзя поставить точку останова (breakpoint), что делает отладку ошибок вроде Binary XML file line #0: Error inflating class . чрезвычайно увлекательной. LayoutInflater работает рекурсивно, поэтому многие стектрейсы не вмещаются и обрезаются: 86 more.

findViewById — нечто абсолютно противоположное лаконичности и типобезопасности. Количество различных костылей, с этим связанных, превышает все мыслимые пределы.

Идеологически TypedArray — это массив TypedValue . Но функциональность у них разная: TypedArray предоставляет человеческий интерфейс из методов getText, getBoolean, getInt, getFloat, getColor, . , а TypedValue ничего из этого не умеет. Зато у TypedValue есть поле changingConfigurations , а вот TypedArray#getChangingConfigurations возвращает одно общее значение для всех элементов (за O(n)) и доступно только для 21+. В итоге приходится использовать TypedArray даже для единственного атрибута и прибегать к помощи TypedValue , даже когда имеется «человеческий» TypedArray .

changingConfigurations у классов ColorStateList, Theme, TypedArray, Drawable, TypedValue расскажут, при смене каких конфигураций ресурс инвалидируется. Казалось бы, чтобы обновлять ресурсы вовремя, нужно придержать айдишники всех ресурсов, которые могут инвалидироваться в течение жизни данного компонента (т. е. (component.configChanges & resource.configChanges) != 0 ) и переопределить onConfigurationChanged данного Activity или View , где и можно перезагрузить протухшие ресурсы. Но метод Activity#getChangingConfigurations предназначен для другого и возвращает осмысленное значение, только когда активити уничтожается, а у вью вообще нет подобного метода. Чтобы узнать, какие конфигурации обрабатывает текущая активити, нужно спросить ActivityInfo у PackageManager . Хотя нужный объект ActivityInfo уже есть у Activity . В приватном поле.

По умолчанию все ресурсы библиотеки считаются публичными. Как только появляются ресурсы, явно отмеченные, как публичные, все остальные становятся приватными. Android Lint отмечает использование «приватных» ресурсов как warning. Не хватает возможностей:

  • писать модификаторы доступа «на месте», а не в отдельном файле;
  • генерировать настоящую ошибку компиляции при использовании чужих приватных ресурсов;
  • кроме private in module иметь private in file, например, для идентификаторов, которые используются для позиционирования в RelativeLayout ;
  • писать документацию к ресурсам, по аналогии с javadoc и KDoc.

В итоге неплохо подходят для переводов, а растровые картинки нормально чувствуют себя в drawable-*dpi. Всё остальное настолько убого, негибко и многословно, что напрашивается на перенос в код.

Parcel

Писать собственную сериализацию — это всегда весело и задорно. Parcel очень напоминает DataInput и DataOutput из JDK, а Parcelable — это подобие Externalizable , но реализациями этих интерфейсов они не являются, что заставляет писать платформозависимый код. Parcel слегка походит на бинарные протоколы сериализации вроде Protobuf, но, опять же, им не является. Надо было затаскивать Protobuf во фреймворк: тогда сериализуемые этим путём объекты удобно было бы ещё и передавать по сети.

Parcelable.Creator нужно держать в статическом поле с именем CREATOR , чтобы фреймворк мог найти его рефлекшеном. При этом, не найдя CREATOR , фреймворк пойдёт искать его у суперклассов, откладывая возникновение ошибок десериализации на потом.

Асинхронные события и фрагменты

У фрагментов может случиться state loss. Очевидно, выполнение транзакций с фрагментами после сохранения состояния — это ошибка. Но фреймворк никак не поможет её найти — в асинхронных стектрейсах вообще не будет вашего кода.

Даже если транзакция выполняется в ответ на действие пользователя (например, непосредственно в OnClickListener ), Activity вполне может быть на паузе в этот момент. Банальное нажатие кнопки «назад» может привести к падению, приложению при этом вообще не обязательно использовать фрагменты.

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

Обратная совместимость

APK, собранные с использованием gradle plugin 3 и build-tools 26, ломают лончер и файловый менеджер Cyanogenmod.

В AppCompat 26 сломали target у фрагментов в тех случаях, которые не описаны (и никогда не были описаны) в документации как недопустимые.

В javadoc не проставлены @since . (На сайте они откуда-то всё же берутся.)

Слабые контракты

Intent может содержать Action (строка), Uri, Extras (Bundle, т. е. Map ). Для популярных extras есть заранее заготовленные ключи, например, Intent.EXTRA_EMAIL . Но нет типов. Так, можно попытаться открыть почтовый клиент, передав в качестве темы письма картинку, или открыть браузер, не передавая адреса веб-страницы. Чтение интента в видимой всей системе активити — поле непаханное для крэшей. Некоторые популярные приложения валились у меня на глазах при попытке поделиться картинкой. И дело не только в кривизне рук разработчиков, но и в изначальной проблемности такого механизма.

Кроме как в Extras, Bundle используется ещё и в аргументах фрагмента ( Fragment.setArguments ). Как всегда, нет гарантий, что по нужному ключу передан объект нужного типа.

Положим, у вас есть фрагмент, который принимает какой-то объект (выдумаем, например, ParcelUser ) через аргументы. И вы осознаёте, что не нужно передавать объект целиком, достаточно передать идентификатор пользователя ( ParcelUuid ). Счастливого рефакторинга!

Профилирование

Взамен Android Monitor сделали полурабочий Android Profiler. Да, я посвятил открыванию хип-дампов целый тред.

Взамен Android Device Monitor не сделали ничего. Благо, его не удалили по-настоящему, а просто убрали из меню в IDE.

Единственный профайлер, способный предоставить полезную информацию, — это стороннее решение. Method tracing бесполезен, т. к. выбрасывает скомпилированный (и даже интринсифицированный!) код и использует интерпретатор. Systrace помогает найти медленные места, но очень приблизительно, т. к. фреймворковые методы инструментировать нельзя.

  • Fragment#isRemoving работает вдвойненеправильно.
  • DialogFragment неправильно работает c retainInstance.
  • SparseArray не проверяет границы переданного индекса при чтении.

Ясное дело, баги есть в любом софте. Только здесь понятие бага очень расплывчато.

Android — Что такое RxJava и с чем его едят?

Группа: Главные администраторы
Сообщений: 14349
Регистрация: 12.10.2007
Из: Twilight Zone
Пользователь №: 1

Разработка под Android*,
Разработка мобильных приложений*
В первой, второй и третьей частях я объяснил в общих чертах устройство RxJava. Вы можете подумать: «Прекрасно, но как всё это сделать полезным для меня, как для разработчика под Android?» В заключительной части статьи я приведу некоторую информацию, практичную именно для вас.

RxAndroid — это расширение RxJava, написанное специально для Android, которое включает в себя специальные обвязки вокруг RxJava, делающие вашу жизнь проще.

Во-первых, здесь есть класс AndroidSchedulers, предоставляющий готовые планировщики для потоков, специфичных для Android. Нужно запустить код на UI потоке? Без проблем — воспользуйтесь AndroidSchedulers.mainThread():

retrofitService.getImage(url)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(bitmap -> myImageView.setImageBitmap(bitmap));

Если у вас есть ваш собственный Handler, вы можете создать связанный с ним планировщик с помощью HandlerThreadScheduler 1 .

Во-вторых, у нас есть AndroidObservable, предоставляющий возможности по работе с жизненным циклом некоторых классов из Android SDK. В нем есть операторы bindActivity()() и bindFragment(), которые не только автоматически используют для наблюдения AndroidSchedulers.mainThread(), но ещё и перестанут порождать данные когда ваши Activity или Fragment начнут завершать свою работу (таким образом вы не попадёте впросак, попытавшись изменить их состояние тогда, когда делать этого уже нельзя).

AndroidObservable.bindActivity(this, retrofitService.getImage(url))
.subscribeOn(Schedulers.io())
.subscribe(bitmap -> myImageView.setImageBitmap(bitmap));

Ещё мне нравится AndroidObservable.fromBroadcast(), позволяющий вам создавать Observable, который работает как BroadcastReceiver. Вот так, например, можно получить уведомление в момент изменения состояния сети:

IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
AndroidObservable.fromBroadcast(context, filter)
.subscribe(intent -> handleConnectivityChange(intent));

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

ViewObservable.clicks(mCardNameEditText, false)
.subscribe(view -> handleClick(view));

Существует такая примечательная библиотека, поддерживающая RxJava, как Retrofit, популярный REST клиент для Android. Обычно, когда вы определяете в ней асинхронный метод, вы используете Callback:

@GET(«/user//photo»)
void getUserPhoto(@Path(«id») int id, Callback

Но, если вы пользуетесь RxJava, вы вместо этого можете возвращать нашего друга Observable:

getUserPhoto(@Path(«id») int id);

После этого вы можете использовать Observable как только вы пожелаете, можно будет не только получить из него данные, но и трансформировать их на лету!

Поддержка Observable, включенная в Retrofit, также упрощает комбинирование нескольких REST запросов вместе. Например, у нас есть два метода api, первый возвращает фото, а второй — его метаданные. Мы можем собрать результаты выполнения этих запросов вместе:

Observable.zip(
service.getUserPhoto(id),
service.getPhotoMetadata(id),
(photo, metadata) -> createPhotoWithData(photo, metadata))
.subscribe(photoWithData -> showPhoto(photoWithData));

Я показывал нечто похожее во второй части (используя flatMap()). Сейчас я хотел показать насколько легко собрать несколько REST запросов в один, воспользовавшись связкой RxJava+Retrofit.

Старый, медленный код

То, что Retrofit умеет возвращать Observables, здорово, но что если у вас есть другая библиотечка, которая ни сном ни духом про них не слышала? Или у вас есть какой-то старый код, который вы хотели бы изменить без особых трудозатрат так, чтобы он умел работать с Observable. Проще говоря, как вам соединить старый код с новым без того чтобы переписывать всё подряд?

Чаще всего вам будет достаточно использовать Observable.just() и Observable.from():

private Object oldMethod()

public Observable newMethod() <
return Observable.just(oldMethod());
>

Это сработает замечательно, если oldMethod() выполняется быстро, но что если это не так? Вы заблокируете весь поток, потому что сначала будет вызван oldMethod(), а уж потом его результат будет передан в Observable.just().

Чтобы обойти эту проблему, можно воспользоваться следующим трюком (которым я пользуюсь постоянно): обернуть медленный код в Observable.defer():

private Object slowBlockingMethod()

public Observable newMethod() <
return Observable.defer(() -> Observable.just(slowBlockingMethod()));
>

Теперь, полученный вами Observable не будет вызывать slowBlockingMethod() до тех пор, пока вы не подпишитесь на него.

Самую сложную часть я оставил напоследок. Как нам учитывать жизненный цикл Activity, работая с RxJava? Есть пара проблем, которые дают о себе знать снова и снова:

    Возобновление подписки после смены конфигурации.

Например, вы делаете REST запрос с Retrofit, и хотите отобразить его результаты в ListView. Что если во время выполнения запроса пользователь повернет телефон? Надо бы возобновить выполнение запроса, но как?
Утечки памяти, вызванные Observables, которые удерживают ссылку на Context.

Эта проблема вызывается созданием подписки, которая каким-то образом удерживает ссылку на Context (что не так уж и сложно, если вы работаете со Views!) Если Observable не завершит свою работу вовремя, в какой-то момент вы обнаружите, что вы никак не можете освободить большое количество памяти.

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

С первой проблемой можно справиться, используя встроенные в RxJava механизмы кеширования, которые позволяют подписываться на/отписываться от одного и того же Observable, без повторения его работы. В частности, cache() (или replay()) продолжат выполнявшийся ранее запрос, даже если вы успели отписаться. Это означает, что вы можете продолжить работу после пересоздания Activity:

request = service.getUserPhoto(id).cache();
Subscription sub = request.subscribe(photo -> handleUserPhoto(photo));

// . Когда Activity пересоздаётся.
sub.unsubscribe();

// . Как только Activity была пересоздана.
request.subscribe(photo -> handleUserPhoto(photo));

Заметьте, что мы используем тот же самый закешированный request в обоих случаях; таким образом, выполняемый им запрос будет выполнен только один раз. Где вы сохраните ваш request, решать вам, но, как и в случае со всеми решениями, связанными с жизненным циклом, это должно быть место, которое переживает изменения, порожденные жизненным циклом (retained fragment, синглетон, и т.д.)

Вторая проблема решается правильным отписыванием от подписок в соответствии с жизненным циклом. Общим решением является использование CompositeSubscription для хранения всех ваших подписок, и отписывание от них всех в onDestroy() или в onDestroyView():

private CompositeSubscription mCompositeSubscription = new CompositeSubscription();

private void doSomething() <
mCompositeSubscription.add(
AndroidObservable.bindActivity(this, Observable.just(«Hello, World!»))
.subscribe(s -> System.out.println(s)));
>

@Override
protected void onDestroy() <
super.onDestroy();

Чтобы упростить себе жизнь, вы можете создать базовую Activity/Fragment, содержащие в себе CompositeSubscription, через которую впоследствии вы будете сохранять все ваши подписки, и которая будет автоматически очищаться.

Внимание! Как только вы вызвали CompositeSubscription.unsubscribe(), этот экземпляр CompositeSubscription перестанет быть доступным для использования (то есть добавлять к нему подписки вы, конечно, сможете, но он будет тут же автоматом вызывать на них unsubscribe())! Если вы хотите в дальнейшем продолжать использовать CompositeSubscription, вам придётся создать новый экземпляр.

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

RxJava является относительно новой технологией, а для Android так и подавно, поэтому заключения для Android пока что не будет. RxAndroid находится в стадии активной разработки, а мы (Android-программисты) до сих пор пытаемся разобраться с тем, как делать хорошо, а как — плохо; общепризнанных примеров отличного применения связки RxJava+RxAndroid пока что нет, и я бьюсь об заклад, что спустя год некоторые из советов, которые я вам тут понадавал, будут считаться довольно эксцентричными.

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

Я благодарю Matthias Kay ещё раз за его неоценимую помощь в подготовке этой статьи, и призываю всех присоединиться к нему, чтобы сделать RxAndroid ещё круче!

1 AndroidSchedulers.mainThread() использует внутри себя HandlerThreadScheduler.

Прим. переводчика: я благодарю пользователя Artem_zin за помощь в переводе пары мест, вызвавших у меня сложности; если бы не он и его обширные знания RxJava, застрял бы я надолго.

Когда использовать RxJava в Android и когда использовать LiveData от Android Architectural Components?

Я не получаю причины использовать RxJava в Android и LiveData от архитектурных компонентов Android. Было бы очень полезно, если бы объяснения и различия между ними объяснялись вместе с примером примера в виде кода, который объясняет различия между ними.

Android LiveData — это вариант исходного шаблона наблюдателя с добавлением активных/неактивных переходов. Таким образом, он очень ограничительный по своему охвату.

Используя пример, описанный в Android LiveData, создается класс для контроля данных местоположения и регистрации и отмены регистрации на основе состояния приложения.

RxJava предоставляет операторы, которые гораздо более обобщены. Предположим, что это наблюдение предоставит данные о местоположении:

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

Цукерберг рекомендует:  C# game - C#, ошибка при запуске 2д игры.

Предположим также, что у вас есть наблюдаемое, которое испускает true, когда приложение активно:

Затем вы можете предоставить всю функциональность LiveData следующим

Оператор switchMap() будет либо предоставлять текущее местоположение как поток, либо ничего, если приложение неактивно. После того, как у вас будет liveLocation значение liveLocation , с ним можно многое сделать, используя операторы RxJava. Мой любимый пример:

Это действие будет выполняться только при изменении местоположения, и место будет интересным. Вы можете создавать аналогичные операции, которые объединяют операторы времени для определения скорости. Что еще более важно, вы можете предоставить подробный контроль того, происходят ли операции в основном потоке, или в фоновом потоке, или в нескольких потоках, с использованием операторов RxJava.

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

LiveData адресует только одну небольшую часть этого юниверса, что эквивалентно построению liveLocation .

Что касается исходного вопроса, то и RxJava, и LiveData прекрасно дополняют друг друга.

LiveData светит на уровне ViewModel, с его тесной интеграцией с жизненными LiveData Android и ViewModel . RxJava предоставляет больше возможностей в преобразованиях (как упоминает @Bob Dalgleish).

В настоящее время мы используем RxJava в слоях источника данных и репозитория, и он преобразуется в LiveData (используя LiveDataReactiveStreams ) в ViewModels (до LiveDataReactiveStreams данных в действия/фрагменты) — вполне доволен этим подходом.

Между LiveData и RxJava много различий:

  1. LiveData не является STREAM, тогда как в RxJava все (буквально все) является STREAM.
  2. LiveData — это наблюдаемый класс держателя данных. В отличие от обычной наблюдаемой, LiveData учитывает жизненный цикл, то есть он учитывает жизненный цикл других компонентов приложения, таких как действия, фрагменты или службы. Эта осведомленность гарантирует, что LiveData обновляет только те компоненты приложения, которые находятся в состоянии активного жизненного цикла.
  3. LiveData является синхронным, поэтому вы не можете асинхронно выполнять фрагмент кода (сетевой вызов, манипулирование базой данных и т.д.), Используя только LiveData, как и в RxJava.
  4. Лучшее, что вы можете сделать, чтобы использовать большую часть этого дуэта, это использовать RxJava для вашей бизнес-логики (сетевой вызов, манипулирование данными и т.д., Все, что происходит в репозитории и за его пределами) и использовать LiveData для уровня презентации. Таким образом, вы получаете возможности преобразования и потоковой передачи для своей бизнес-логики и операции с учетом жизненного цикла для вашего пользовательского интерфейса.
  5. LiveData и RxJava дополняют друг друга, если используются вместе. Я имею в виду, что все делайте с RxJava, а в конце, когда вы захотите обновить пользовательский интерфейс, сделайте что-то вроде приведенного ниже кода, чтобы изменить Observable на LiveData. Итак, ваш View (UI) наблюдает за LiveData в ViewModel, где ваши LiveData — это не что иное, как неизменяемый MutableLiveData (или MutableLiveData является изменяемым LiveData).
  6. Итак, вопрос в том, зачем вам вообще использовать LiveData? Как вы можете видеть ниже в коде, вы сохраняете свой ответ из RxJava в MutableLiveData (или LiveData), а ваши LiveData учитывают жизненный цикл, поэтому в некотором смысле ваши данные учитывают жизненный цикл. А теперь представьте себе возможность, когда ваши данные будут знать, когда и когда не обновлять пользовательский интерфейс.
  7. LiveData не имеет истории (только текущее состояние). Следовательно, вы не должны использовать LiveData для приложения чата.
  8. Когда вы используете LiveData с RxJava, вам не нужны такие вещи, как MediatorLiveData, SwitchMap и т.д. Они являются инструментами управления потоками, и RxJava во многих случаях лучше в этом.
  9. Посмотрите LiveData как вещь держателя данных и ничего больше. Мы также можем сказать, что LiveData — потребитель с учетом жизненного цикла.

Фактически, LiveData не является принципиально отличным инструментом от RxJava , поэтому почему он был представлен как компонент архитектуры, когда RxJava мог легко управлять жизненным циклом, сохраняя все подписки на наблюдаемые в объекте CompositeDispoable а затем onDestroy() их в onDestroy() Activity или onDestroyView() Fragment использующего только одну строку кода?

Я ответил на этот вопрос в полной мере путем создания поискового приложения фильм один раз RxJava, а затем с помощью LiveData здесь.

Короче говоря, да, это возможно, но для этого нужно сначала переопределить соответствующие методы жизненного цикла, помимо базовых знаний жизненного цикла. Это все еще может не иметь смысла для некоторых, но дело в том, что согласно одному из сеансов Jetpack в Google I/O 2020 многие разработчики находят сложным управление жизненным циклом. Ошибки сбоя, возникающие из-за отсутствия обработки зависимости от жизненного цикла, могут быть еще одним признаком того, что некоторые разработчики, даже не зная жизненного цикла, забывают заботиться об этом в каждой операции/фрагменте, который они используют в своем приложении. В больших приложениях это может стать проблемой, несмотря на негативное влияние, которое это может оказать на производительность.

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

** ОБНОВЛЕНИЕ ** Я добавил новую статью здесь, где я объяснил, как неправильное использование LiveData может привести к неожиданным результатам. RxJava может прийти на помощь в этих ситуациях

Как вы, возможно, знаете, в реактивной экосистеме у нас есть Observable, который испускает данные, и Observer, который подписывается (получает уведомление) об этом Observable излучении, нет ничего странного в том, как работает так называемый паттерн Observer. Наблюдаемый что-то «кричит», Наблюдатель получает уведомление, что Наблюдаемый что-то кричит в данный момент.

Думайте о LiveData как о Наблюдаемой, которая позволяет вам управлять Наблюдателями, которые находятся в active состоянии. Другими словами, LiveData является простой Observable, но также заботится о жизненном цикле.

Но давайте рассмотрим два кода, которые вы запрашиваете:

А) Это базовая реализация LiveData

1) вы обычно создаете экземпляр LiveData в ViewModel для поддержания изменения ориентации (у вас могут быть LiveData, предназначенные только для чтения, или MutableLiveData, доступные для записи, поэтому вы обычно выставляете их вне класса LiveData)

2) в методе OnCreate основного действия (не ViewModel) вы «подписываете» объект Observer (обычно это метод onChanged)

3) вы запускаете метод наблюдения, чтобы установить ссылку

Сначала ViewModel (владеет бизнес-логикой)

И это MainActivity (настолько глупо, насколько это возможно)

Б) Это базовая реализация RXJava

1) вы объявляете наблюдаемую

2) вы объявляете наблюдателя

3) вы подписываете Observable с Observer

В частности, LiveData используется с Lifecycle и часто с компонентами архитектуры ViewModel (как мы уже видели). Фактически, когда LiveData объединяется с ViewModel, вы можете обновлять в реальном времени каждое изменение в Observer, так что события управляются в реальном времени там, где это необходимо. Для использования LiveData настоятельно рекомендуется знать концепцию жизненного цикла и относительных объектов LifeCycleOwner/LifeCycle, также я бы посоветовал вам взглянуть на Transformations, если вы хотите реализовать LiveData в реальных сценариях жизни. Здесь вы можете найти несколько вариантов использования от большого общего программного обеспечения.

По сути, LiveData — это упрощенный RXJava , элегантный способ наблюдать за изменениями в нескольких компонентах без создания явных так называемых правил зависимости между компонентами, чтобы вы могли намного проще протестировать код и сделать его намного более читабельным. RXJava, позволяет вам делать вещи LiveData и многое другое. Из-за расширенных функциональных возможностей RXJava вы можете использовать LiveData для простых случаев или использовать всю мощь RXJava, продолжая использовать компоненты архитектуры Android в качестве ViewModel, конечно, это означает, что RXJava может быть гораздо более сложным, просто представьте, что у него сотни операторов вместо SwitchMap и Map LiveData (на данный момент).

RXJava версии 2 — это библиотека, которая произвела революцию в объектно-ориентированной парадигме, добавив так называемый функциональный способ управления потоком программы.

Информационный портал по безопасности

Информационный портал по безопасности » Операционные системы » Android » Многопоточное программирование в Android с использованием RxJava 2

Многопоточное программирование в Android с использованием RxJava 2

Автор: admin от 7-12-2020, 07:45, посмотрело: 788

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

Оригинал статьи написан 29 ноября 2020. Перевод вольный.

Почему реактивное программирование?

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

Никаких больше обратных вызовов

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

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

Простой контроль ошибок

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

Очень простое использование многопоточности

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

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

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

RxJava НЕ многопоточна по умолчанию

Да, вы прочли всё верно. RxJava по умолчанию не многопоточна в любом случае. Определение, данное для RxJava на официальном сайте, выглядит примерно следующим образом:

«Библиотека для составления асинхронных и основанных на событиях программ с использованием последовательностей (observable sequences) для виртуальной Java машины».

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

Если вы уже немного работали с RxJava, то её знаете базовые конструкции:

  • Наблюдаемый источник (source Observable), далее
  • несколько операторов (Operators), затем
  • целевой подписчик (Subscriber)

Если вы запустите данный пример кода, то ясно увидите, что все действия выполняются в основном потоке приложения (проследите за именами потоков в логе в консоли). Этот пример показывает, что по умолчанию поведение RxJava блокирующее. Всё выполняется в том же потоке, в котором вызван код.

Бонус: Интересно, что же делает doOnNext() ? Это не что иное, как side-effect оператор. Он помогает внедряться в цепочку объектов observable и выполнять грязные (impure) операции. Например, внедрять дополнительный код в цепочке вызовов для отладки. Прочитать больше можно здесь .

Простой пример

Для того, чтобы начать работать с многопоточностью с применением RxJava необходимо познакомиться с базовыми классами и методами, такими как Schedulers, observeOn/subscribeOn.

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

Здесь мы видим метод getBooks() , который осуществляет сетевой вызов и собирает список книг для нас. Сетевой вызов занимает время (несколько миллисекунд или секунд), поэтому мы используем subscribeOn() и указываем планировщик Schedulers.io() для выполнения операции в потоке ввода-вывода.

Также мы используем оператор observeOn() вместе с планировщиком AndroidSchedulers.mainThread() для того, чтобы обрабатывать результат в основном потоке и показать список книг в пользовательском интерфейсе приложения.

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

Подружимся с планировщиками (Schedulers)

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

Можете представлять планировщики как потоки или пулы потоков (коллекции потоков) для выполнения разного рода задач.

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

В RxJava доступны несколько типов планировщиков. Самая сложная часть — выбрать верный планировщик для вашей задачи. Задача никогда не будет выполняться оптимально, если вы не выберете верный планировщик. Давайте разберем каждый планировщик.

Schedulers.io()

Этот планировщик основывается на неограниченном пуле потоков и используется для интенсивной работы с вводом-выводом без использования ЦП, например, доступ к файловой системе, выполнение сетевых вызовов, доступ к базе данных и так далее. Количество потоков в этом планировщике неограничено и может расти по мере необходимости.

Schedulers.computation()

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

Так как этот планировщик подходит только для интенсивной работы с ЦП — количество его потоков ограничено. Сделано это для того, чтобы потоки не конкурировали за процессорное время и не простаивали.

Schedulers.newThread()

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

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

Schedulers.single()

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

Schedulers.from(Executor executor)

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

Допустим, вы хотите ограничить число параллельных сетевых вызовов, которые делает ваше приложение. Можно создать собственный планировщик, который будет работать на базе ограниченного в размерах пула потоков ( Scheduler.from(Executors.newFixedThreadPool(n)) ) и использовать его во всех местах, связанных с сетевыми вызовами.

AndroidSchedulers.mainThread()

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

По умолчанию этот планировщик ставит задания в очередь в Looper , связанный с основным потоком, но есть возможность переопределения: AndroidSchedulers.from(Looper looper) .

Заметка: Будьте осторожны в использовании планировщиков, основанных на неограниченных пулах потоков, таких как Schedulers.io() . Всегда есть риск бесконечного роста количества потоков.

Понимание subscribeOn() и observeOn()

Теперь, когда у вас есть представление о типах планировщиков, разберем subscribeOn() и observeOn() в деталях.

Вы должны глубоко разбираться в том, как эти два оператора работают по отдельности и вместе, чтобы профессионально работать с многопоточностью в RxJava.

subscribeOn()

Простыми словами, этот оператор говорит в какой поток наблюдаемый источник (source observable) будет передавать элементы. Вы должны уяснить важность слова «источник». Когда у вас цепь наблюдаемых элементов (observables), источник (source observable) — это всегда корневой элемент или верхняя часть цепи, откуда происходит создание событий.

Как вы уже видели, если не использовать subscribeOn() , то все события происходят в том потоке, в котором произошел вызов кода (в нашем случае — main поток).

Давайте перенаправим события в вычислительный поток с помощью subscribeOn() и планировщика Schedulers.computation() . Когда вы запустите нижеследующий пример кода, то увидите, что события происходят в одном из вычислительных потоков, доступных в пуле — RxComputThreadPool-1 .

В целях сокращения кода мы не будем полностью переопределять все методы DisposableSubscriber , так как нам не нужно переопределять onerror() и onComplete() . Воспользуемся doOnNext() и лямбдами.

Не важно в каком месте в цепочке вызовов вы используете subscribeOn() . Он работает только с наблюдаемым источником (source observable), и контролирует в какой поток наблюдаемый источник передает события.

В нижеследующем примере после observable-источника создаются другие объекты observable (методами map() и filter() ), а оператор subscribeOn() помещен в конце цепочки вызовов. Но как только вы запустите этот код, то заметите, что все события будут возникать в потоке, указанном в subscribeOn() . Это станет более понятным при добавлении observeOn() в цепь вызовов. И даже если мы разместим subscribeOn() ниже observeOn() , то логика работы не изменится. subscribeOn() работает только с наблюдаемым источником (source observable).

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

Если вы ответили Schedulers.io() , то вы правы! Даже если делать вызов многократно — сработает только первый subscribeOn() , вызванный после observable-источника.

Под капюшоном

Стоит потратить ещё немного времени на более подробное изучение рассмотренного примера. Почему срабатывает только планировщик Schedulers.io() ? Обычно все думают, что сработает Schedulers.newThread() , так как этот вызов находится в конце цепочки.

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

Для того, чтобы понять как всё работает — начнем разбирать всё с последней строки примера. В ней целевой подписчик (target subscriber), вызывает метод subscribe() у observable объекта o3 , который затем делает неявный вызов subscribe() у своего родительского observable объекта o2 . Реализация наблюдателя (observer), предоставляемая объектом o3 , умножает переданные числа на 10.

Процесс повторяется и o2 неявно вызывает subscribe() у объекта o1 , передавая реализацию наблюдателя, которая позволяет обрабатывать только четные числа. Теперь мы достигли корневого элемента ( o1 ), у которого нет родителя для последующего вызова subscribe() . На этом этапе завершается цепочка наблюдаемых (observable) элементов, после чего observable-источник начинает передавать (emit) элементы.

Теперь для вас должна быть понятна концепция работы подписок в RxJava. К настоящему времени у вас должно появиться понимание того, как формируются цепочки наблюдаемых (observable) объектов и как события распространяются, начиная с observable-источника.

observeOn()

Как мы уже видели, subscribeOn() указывает observable-источнику передавать элементы в определенный поток и этот поток будет отвечать за продвижение элементов вплоть до подписчика (Subscriber). Поэтому, по умолчанию, подписчик получает обработанные элементы в этом же потоке.

Но это может быть не то поведение, которого вы ожидаете. Предположим, вы хотите получить некие данные из сети и отобразить их в пользовательском интерфейсе.

Нужно выполнить две вещи:

  • Сделать сетевой вызов в неблокирующем потоке ввода-вывода
  • Получить результат в основном потоке приложения

У вас будет Observable , который осуществляет сетевой вызов в потоке ввода-вывода и передает результат подписчику. Если вы используете только subscribeOn(Schedulers.io()) , то целевой подписчик будет обрабатывать результат в том же потоке ввода-вывода. И нам не повезло, так как работать с пользовательским интерфейсом в Android можно только в основном потоке.

Теперь нам крайне необходимо переключить потоки и мы используем для этого observeOn() . Когда observeOn() встречается в цепочке вызовов, то передаваемые observable-источником элементы незамедлительно перебрасываются в поток, указанный в observeOn() .

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

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

В примере выше observable-источник передаёт элементы в цепочку обработчиков в потоке ввода вывода, так как мы использовали subscribeOn() вместе с Schedulers.io() . Далее мы хотим преобразовать каждый элемент, используя оператор map() , но сделать это нужно в вычислительном потоке. Для этого используем observeOn() вместе с Schedulers.computation() перед вызовом map() для переключения потока и передачи элементов в целевой вычислительный поток.

Следующим шагом отфильтруем некоторые элементы и по какой-то причине мы хотим выполнить эту операцию в новом потоке для каждого из элементов. Используем снова observeOn() , но уже в паре с Schedulers.newThread() перед вызовом оператора filter() для передачи каждого элемента в новый поток.

В итоге мы хотим, чтобы подписчик получил результат обработки в потоке пользовательского интерфейса. Для этого используем observeOn() вместе с планировщиком AndroidSchedulers.mainThread() .

Но что произойдет, если использовать observeOn() несколько раз последовательно? В примере ниже в каком потоке подписчик получит результат?

Если запустите пример, то увидите, что подписчик получит элементы в вычислительном потоке RxComputationThreadPool-1. Это значит, что сработал последний вызванный observeOn(). Интересно почему?

Под капюшоном

Возможно вы уже догадались. Как мы знаем, подписка (subscription) вызывается после обратного обхода всех Obsevable, но с передачей событий (emissions) всё происходит наоборот, то есть в обычном порядке, как записан код. Вызов происходит от observable-источника и далее вниз по цепочке вызова вплоть до подписчика.

Оператор observeOn() всегда работает в прямом порядке, поэтому последовательно происходит переключение потоков и последним происходит переключение на вычислительный поток (observeOn(Schedulers.computation())). Итак, когда нужно переключить поток для обработки данных в новом потоке, то просто сначала вызовите observeOn(), а далее обрабатывайте элементы. Синхронизация, исключение состояния гонки, всё это и многие другие сложности многопоточности RxJava обрабатывает за вас.

Резюме

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

Если понимание не пришло сразу, ничего страшного. Прочитайте статью ещё раз, поэкспериментируйте с примерами кода. Здесь достаточно много нюансов для понимания, не торопитесь.

Понравилась статья? Поделиться с друзьями:
Все языки программирования для начинающих