- PVSM.RU - https://www.pvsm.ru -

Основы реактивного программирования под Android на практическом примере

1. Введение в реактивное программирование

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

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

RxJava — реализация ReactiveX с открытым исходным кодом на Java. Базовыми строительными блоками реактивного кода являются Observables и Subscribers. Подробнее с базовой основой можно ознакомиться в статье Грокаем* RxJava, часть первая: основы [1].

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. Разберем пример перового метода подробно и построчно:

  1. Observable<List> queryURLs(String url) — строка объявляет метод, который порождает строку ссылки на сайт для парсинга и возвращающего список ссылок;
  2. WebParsing webParsing = new WebParsing() — создает переменную для доступа к нашим функциям парсинга;
  3. return Observable.create — создает Observable, возвращающего список ссылок;
  4. new Observable.OnSubscribe<List>() — интерфейс OnSubscribe создает подписчика;
  5. public void call(Subscriber<? super List> subscriber) — перегружает метод call, который будет вызываться после подписки Subscriber;
  6. subscriber.onNext(webParsing.getURLs(url)) — вызывает метод onNext для передачи данных Subscriber всякий раз, когда порождаются данные. Этот метод принимает в качестве параметра объект, испускаемый Observable;
  7. subscriber.onCompleted() — Observable вызывает метод onCompleted() после того, как вызывает onNext в последний раз, если не было обнаружено никаких ошибок;
  8. subscribeOn(Schedulers.io()) — метод subscribeOn запускает наш код в дополнительном потоке;
  9. 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 + "nn");
                   }
               }
           });
}

Обернем наш реализуемый пример в класс 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 для манипуляции с данными. Операторов [2] в 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 + "nn");
                               }
                           });
               }
           });
}

Выглядит не совсем красиво и немного запутанно, поэтому применим следующий оператор 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 + "nn");
                               }
                           });
}

На следующем шаге еще разгрузим наш 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 + "nn";
               }
           })
           .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 + "nn")
       .subscribe(url1 -> {
           textView.setText(url1);
       });

или еще проще:

queryURLs(url)
       .flatMap(Observable::from)
       .map(url1 -> textView.getText() + url1 + "nn")
       .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 + "nn")
       .subscribe(textView::setText);

с помощью оператора filter() отфильтровываем пустые строки со значением null:

queryURLs(url)
       .flatMap(Observable::from)
       .flatMap(this::queryTitle)
       .filter(title -> title != null)
       .map(url1 -> textView.getText() + url1 + "nn")
       .subscribe(textView::setText);

с помощью оператора take() возьмем только первые 7 заголовков:

queryURLs(url)
       .flatMap(Observable::from)
       .flatMap(this::queryTitle)
       .filter(title -> title != null)
       .take(7)
       .map(url1 -> textView.getText() + url1 + "nn")
       .subscribe(textView::setText);

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

Все примеры, приведенные в статье выложены здесь [3].

Источники:

  1. Официальная документация [4]
  2. Грокаем* RxJava, часть первая: основы [1]
  3. Getting Started With ReactiveX on Android [5]
  4. RxJava — Tutorial [6]
  5. Getting Started with RxJava and Android [7]
  6. Reactive Programming with RxJava in Android [8]
  7. Party tricks with RxJava, RxAndroid & Retrolambda [9]

Автор: vladv75

Источник [10]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/android-development/164112

Ссылки в тексте:

[1] Грокаем* RxJava, часть первая: основы: https://habrahabr.ru/post/265269/

[2] Операторов: https://github.com/ReactiveX/RxJava/wiki/Alphabetical-List-of-Observable-Operators

[3] здесь: https://github.com/vladv75/RxAndroidExample

[4] Официальная документация: http://reactivex.io/RxJava/javadoc/

[5] Getting Started With ReactiveX on Android: http://code.tutsplus.com/tutorials/getting-started-with-reactivex-on-android--cms-24387

[6] RxJava — Tutorial: http://www.vogella.com/tutorials/RxJava/article.html

[7] Getting Started with RxJava and Android: https://www.captechconsulting.com/blogs/getting-started-with-rxjava-and-android

[8] Reactive Programming with RxJava in Android: https://www.ykode.com/2015/02/20/android-frp-rxjava-retrolambda.html

[9] Party tricks with RxJava, RxAndroid & Retrolambda: https://medium.com/swlh/party-tricks-with-rxjava-rxandroid-retrolambda-1b06ed7cd29c#.yz1ea1hug

[10] Источник: https://habrahabr.ru/post/306746/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox