1. Введение в реактивное программирование
Разрабатывая сложное приложение под Android со множеством сетевых соединений, взаимодействием с пользователем и анимацией — означает писать код, который полон вложенных обратных вызовов. И по мере развития проекта такой код становится не только громоздким и трудно понимаемым, но также сложным в развитии, поддержке и подвержен множеством трудноуловимым ошибкам.
ReactiveX или функциональное реактивное программирование предлагает альтернативный подход, который позволяет значительно сократить код приложения и создавать изящные понимаемые приложения для управления асинхронными задачами и событиями. В реактивном программировании потребитель реагирует на данные, как они придут и распространяет изменения события в зарегистрированных наблюдателях.
RxJava — реализация ReactiveX с открытым исходным кодом на Java. Базовыми строительными блоками реактивного кода являются Observables и Subscribers. Подробнее с базовой основой можно ознакомиться в статье Грокаем* RxJava, часть первая: основы.
RxAndroid — расширение к RxJava, которое позволяет планировщику запускать код в основном и дополнительных потоках Android приложения и обеспечивает передачу результатов из созданных дополнительных потоках в основное для агрегации и взаимодействия с интерфейсом пользователя.
С целью более полного понимания основных принципов реактивного программирования рассмотрим практический пример для платформы Android. И начнем с настройки окружения для разработки.
2. Подготовка окружения
Подключаем основные библиотеки и прописываем зависимости в секции dependencies{} конфигурационного файла buil.gradle:
dependencies { compile 'io.reactivex:rxandroid:1.2.1' compile 'io.reactivex:rxjava:1.1.6' }
Подключаем поддержку лямбда-выражений — используем новые возможности языка Java 8 на платформе Android N. Чтобы использовать возможности языка Java 8 также необходимо подключить и новый компилятор Jack, для чего добавьте в файл build.gradle:
android { ... defaultConfig { ... jackOptions { enabled true } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } }
Примечание: Jack поддерживается только в Android Studio 2.1 и также необходимо выполнить обновление до JDK 8.
При внесении изменений в конфигурационном файле gradle появляется предупреждение о необходимости синхронизировать проект и, чтобы применить все изменения нажмите на ссылку Sync Now вверху-справа.
3. Создаем базовый пример
В связи с тем, что применение RxAndroid в большинстве случаев связано с проектами с много-поточной обработкой сетевых соединений — рассмотрим простой пример обработки результатов парсинга сайта.
Для отображения результатов создадим простой layout:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" ...> <ScrollView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/scrollView" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/textView" /> </ScrollView> </RelativeLayout>
Для парсинга создадим простой класс WebParsing с двумя методами getURLs и getTitle:
public class WebParsing { public List<String> getURLs(String url) { Document doc; List<String> stringList = new ArrayList<>(); try { doc = Jsoup.connect(url).get(); Elements select = doc.select("a"); for (Element element : select) { stringList.add(element.attr("href")); } } catch (IOException e) { e.printStackTrace(); return null; } return stringList; } }
public String getTitle(String url) { String title; try { Document doc = Jsoup.connect(url).get(); title = doc.title(); } catch (MalformedURLException mue) { mue.printStackTrace(); return null; } catch (HttpStatusException hse) { hse.printStackTrace(); return null; } catch (IOException e) { e.printStackTrace(); return null; } catch (IllegalArgumentException iae) { iae.printStackTrace(); return null; } return title; }
Метод getURLs просматривает содержимое сайта и возвращает список всех найденных ссылок, а метод getTitle возвращает Title сайта по ссылке.
4. Подключаем реактивность
Для того, чтобы использовать возможности RxAndroid на основе приведенных выше методов создадим два соответствующих Observables:
Observable<List<String>> queryURLs(String url) { WebParsing webParsing = new WebParsing(); return Observable.create( new Observable.OnSubscribe<List<String>>() { @Override public void call(Subscriber<? super List<String>> subscriber) { subscriber.onNext(webParsing.getURLs(url)); subscriber.onCompleted(); } }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); }
Observable<String> queryTitle(String url) { WebParsing webParsing = new WebParsing(); return Observable.create(new Observable.OnSubscribe<String>() { @Override public void call(Subscriber<? super String> subscriber) { subscriber.onNext(webParsing.getTitle(url)); subscriber.onCompleted(); } }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); }
Первый Observable будет порождать список URL ссылок, найденных на сайте, второй будет порождать Title. Разберем пример перового метода подробно и построчно:
- Observable<List > queryURLs(String url) — строка объявляет Observable метод, который принимает в виде входного параметра ссылку на сайт для парсинга и возвращает результат парсинга в виде списка ссылок <List> с указанного сайта;
WebParsing webParsing = new WebParsing() — создает переменную для доступа к нашим функциям парсинга;
return Observable.create — создает Observable, возвращающего список ссылок;
new Observable.OnSubscribe<List>() — строка объявляет интерфейс OnSubscribe с одним методом (см. ниже), который вызовется при подписке;
public void call(Subscriber<? super List> subscriber) — перегружает метод call, который будет вызываться после подписки Subscriber;
subscriber.onNext(webParsing.getURLs(url)) — вызывает метод onNext для передачи данных Subscriber всякий раз, когда порождаются данные. Этот метод принимает в качестве параметра объект, испускаемый Observable;
subscriber.onCompleted() — Observable вызывает метод onCompleted() после того, как вызывает onNext в последний раз, если не было обнаружено никаких ошибок;
subscribeOn(Schedulers.io()) — метод subscribeOn подписывает всех Observable выше по цепочке на планировщик Schedulers.io();
observeOn(AndroidSchedulers.mainThread()) — метод observeOn позволяет получить результат в основном потоке приложения.
5. Запускаем первое реактивное приложение
Итак, Observables созданы, реализуем простейший пример на основе первого выше метода, который будет выводить список ссылок сайта:
public void example0(final TextView textView, String url) { queryURLs(url) .subscribe(new Action1<List<String>>() { @Override public void call(List<String> urls) { for (String url: urls) { String string = (String) textView.getText(); textView.setText(string + url + "\n\n"); } } }); }
Обернем наш реализуемый пример в класс MainExample и вызовем в MainActivity:
public class MainActivity extends AppCompatActivity { TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView) findViewById(R.id.textView); MainExample mainExample = new MainExample(); mainExample.example0(textView, "https://yandex.ru/"); } }
6. Наращиваем реактивность — использование операторов
Observable может трансформировать выходные данные с помощью операторов и они могут быть использованы в промежутке между Observable и Subscriber для манипуляции с данными. Операторов в RxJava очень много, поэтому для начала рассмотрим наиболее востребованные.
И начнем с того, что избавимся от цикла в подписчике и заставим наблюдателя последовательно испускать данные полученного массива ссылок, и поможет в этом нам оператор from():
public void example1(final TextView textView, String url) { queryURLs(url) .subscribe(new Action1<List<String>>() { @Override public void call(List<String> urls) { Observable.from(urls) .subscribe(new Action1<String>() { @Override public void call(String url) { String string = (String) textView.getText(); textView.setText(string + url + "\n\n"); } }); } }); }
Выглядит не совсем красиво и немного запутанно, поэтому применим следующий оператор flatMap(), который принимает на вход данные, излучаемые одним Observable, и возвращает данные, излучаемые другим Observable, подменяя таким образом один Observable на другой:
public void example2(final TextView textView, String url) { queryURLs(url) .flatMap(new Func1<List<String>, Observable<String>>() { @Override public Observable<String> call(List<String> urls) { return Observable.from(urls); } }) .subscribe(new Action1<String>() { @Override public void call(String url) { String string = (String) textView.getText(); textView.setText(string + url + "\n\n"); } }); }
На следующем шаге еще разгрузим наш Subscriber и воспользуемся оператором map(), через который можно преобразовывать один элемент данных в другой. Оператор map() также может преобразовывать данные и порождать данные необходимого нам типа, отличного от исходного. В нашем случае наблюдатель будет формировать список строк, а подписчик только выведет их на экран:
public void example3(final TextView textView, String url) { queryURLs(url) .flatMap(new Func1<List<String>, Observable<String>>() { @Override public Observable<String> call(List<String> urls) { return Observable.from(urls); } }) .map(new Func1<String, String>() { @Override public String call(String url) { return textView.getText() + url + "\n\n"; } }) .subscribe(new Action1<String>() { @Override public void call(String url) { textView.setText(url); } }); }
Основные возможности мы рассмотрели и сейчас пришло время воспользоваться лямбдами, чтобы упростить наш код:
queryURLs(url) .flatMap(urls -> Observable.from(urls)) .map(url1 -> textView.getText() + url1 + "\n\n") .subscribe(url1 -> { textView.setText(url1); });
или еще проще:
queryURLs(url) .flatMap(Observable::from) .map(url1 -> textView.getText() + url1 + "\n\n") .subscribe(textView::setText);
Сравним конструкцию выше с получившимся кодом и ощутим мощь и простоту лямбда-выражений.
7. Увеличиваем мощности
На следующем шаге усложним нашу обработку и воспользуемся оператором flatMap(), чтобы подключить второй подготовленный метод queryTitle(), также возвращающий наблюдателя. Этот метод возвращает Title сайта по ссылке на сайт. Создадим пример, в котором будем формировать и выводить список заголовков сайтов по ссылкам, найденным на веб-странице, т.е. вместо полученного списка ссылок на сайты в предыдущем примере выведем заголовки (Title) этих сайтов:
public void example4(final TextView textView, String url) { queryURLs(url) .flatMap(new Func1<List<String>, Observable<String>>() { @Override public Observable<String> call(List<String> urls) { return Observable.from(urls); } }) .flatMap(new Func1<String, Observable<String>>() { @Override public Observable<String> call(String url) { return queryTitle(url); } }) .subscribe(new Action1<String>() { @Override public void call(String title) { textView.setText(title); } }); }
или в сокращенном виде:
queryURLs(url) .flatMap(Observable::from) .flatMap(this::queryTitle) .subscribe(textView::setText);
добавляем map() для формирования списка заголовков:
queryURLs(url) .flatMap(Observable::from) .flatMap(this::queryTitle) .map(url1 -> textView.getText() + url1 + "\n\n") .subscribe(textView::setText);
с помощью оператора filter() отфильтровываем пустые строки со значением null:
queryURLs(url) .flatMap(Observable::from) .flatMap(this::queryTitle) .filter(title -> title != null) .map(url1 -> textView.getText() + url1 + "\n\n") .subscribe(textView::setText);
с помощью оператора take() возьмем только первые 7 заголовков:
queryURLs(url) .flatMap(Observable::from) .flatMap(this::queryTitle) .filter(title -> title != null) .take(7) .map(url1 -> textView.getText() + url1 + "\n\n") .subscribe(textView::setText);
Последний пример показал, что объединение множества методов плюс использование большого количества доступных операторов плюс лямбда-выражения и мы получаем буквально из нескольких строк мощный обработчик потоков различных данных.
Все примеры, приведенные в статье выложены здесь.