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

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

Розробляючи складний додаток під Android з безліччю мережевих з'єднань, взаємодією з користувачем і анімацією — означає писати код, який повний вкладених зворотних викликів. І в міру розвитку проекту такий код стає не тільки громіздким та важко розуміються, але також складним у розвитку, підтримці і схильний безліччю невловимим помилок.

ReactiveX або функціональне реактивне програмування пропонує альтернативний підхід, який дозволяє значно скоротити код додатка і створювати витончені розуміються додатки для управління асинхронними завданнями і подіями. У реактивному програмуванні споживач реагує на дані, як вони прийдуть і поширює зміни події в зареєстрованих спостерігачів.

RxJava — реалізація ReactiveX з відкритим вихідним кодом на Java. Базовими будівельними блоками реактивного коду є Observables і Subscribers. Детальніше з базовою основою можна ознайомитися в статті Грокаем* RxJava, частина перша: основи.

RxAndroid — розширення до RxJava, яке дозволяє планувальником запускати код в основному і додаткових потоках Android програми і забезпечує передачу результатів створених додаткових потоках в основне для агрегації та взаємодії з інтерфейсом користувача.
З метою більш повного розуміння основних принципів реактивного програмування розглянемо практичний приклад для платформи Android. І почнемо з налаштування оточення для розробки.

2. Підготовка оточення

Підключаємо основні бібліотеки і прописуємо залежності в секції dependencies{} конфігураційного файлу побудува.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:
чоловічий {
...
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:
Вами<List<String>> queryURLs(String url) {
WebParsing webParsing = new WebParsing();
return Вами.create(
new Вами.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());
}

Вами<String> queryTitle(String url) {
WebParsing webParsing = new WebParsing();
return Вами.create(new Вами.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> subscriber) {
subscriber.onNext(webParsing.getTitle(url));
subscriber.onCompleted();
}
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
}

Перший Вами буде породжувати список URL посилань, знайдених на сайті, другий буде породжувати Title. Розберемо приклад першого методу докладно і порядково:
  1. Вами<List > queryURLs(String url) — рядок оголошує Вами метод, який приймає у вигляді вхідного параметра посилання на сайт для парсингу і повертає результат парсинга у вигляді списку посилань <List> з вказаного сайту;
    WebParsing webParsing = new WebParsing() — створює змінну для доступу до наших функцій парсингу;
    return Вами.create — створює Вами, повертає список посилань;
    new Вами.OnSubscribe<List>() — рядок оголошує інтерфейс OnSubscribe з одним методом (див. нижче), який викликається при підписці;
    public void call(Subscriber<? super List> subscriber) — перевантажує метод call, який буде викликатися після підписки Subscriber;
    subscriber.onNext(webParsing.getURLs(url)) — викликає метод onNext для передачі даних Subscriber всякий раз, коли породжуються дані. Цей метод приймає як параметр об'єкт, що випускається Вами;
    subscriber.onCompleted() — Вами викликає метод onCompleted() після того, як викликає onNext востаннє, якщо не було виявлено ніяких помилок;
    subscribeOn(Schedulers.io()) — метод subscribeOn підписує всіх Вами вище по ланцюжку на планувальник 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. Нарощуємо реактивність — використання операторів

Вами може трансформувати вихідні дані за допомогою операторів і вони можуть бути використані в проміжку між Вами і 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) {
Вами.from(urls)
.subscribe(new Action1<String>() {
@Override
public void call(String url) {
String string = (String) textView.getText();
textView.setText(string + url + "\n\n");
}
});
}
});
}

Виглядає не зовсім гарно і трохи заплутано, тому застосуємо наступний оператор flatMap(), який приймає на вхід дані, випромінювані одним Вами, і повертає дані, випромінювані іншим Вами, підміняючи таким чином один Вами на інший:
public void example2(final TextView textView, String url) {
queryURLs(url)
.flatMap(new Func1<List<String>, Вами<String>>() {
@Override
public Вами<String> call(List<String> urls) {
return Вами.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 приклад 3(final TextView textView, String url) {
queryURLs(url)
.flatMap(new Func1<List<String>, Вами<String>>() {
@Override
public Вами<String> call(List<String> urls) {
return Вами.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 -> Вами.from(urls))
.map(url1 -> textView.getText() + url1 + "\n\n")
.subscribe(url1 -> {
textView.setText(url1);
});

або ще простіше:
queryURLs(url)
.flatMap(Вами::from)
.map(url1 -> textView.getText() + url1 + "\n\n")
.subscribe(textView::setText);

Порівняємо конструкцію вище з отриманим кодом і відчутний міць і простоту лямбда-виразів.

7. Збільшуємо потужності

На наступному кроці ускладнимо нашу обробку і скористаємося оператором flatMap(), щоб підключити другий підготовлений метод queryTitle () повертає спостерігача. Цей метод повертає Title сайту за посиланням на сайт. Створимо приклад, в якому будемо формувати і виводити список заголовків сайтів по посиланнях, знайденим на веб-сторінці, т. е. замість отриманого списку посилань на сайти в попередньому прикладі виведемо заголовків (Title) цих сайтів:
public void приклад 4(final TextView textView, String url) {
queryURLs(url)
.flatMap(new Func1<List<String>, Вами<String>>() {
@Override
public Вами<String> call(List<String> urls) {
return Вами.from(urls);
}
})
.flatMap(new Func1<String, Вами<String>>() {
@Override
public Вами<String> call(String url) {
return queryTitle(url);
}
})
.subscribe(new Action1<String>() {
@Override
public void call(String title) {
textView.setText(title);
}
});
}

або у скороченому вигляді:
queryURLs(url)
.flatMap(Вами::from)
.flatMap(this::queryTitle)
.subscribe(textView::setText);

додаємо map() для формування списку заголовків:
queryURLs(url)
.flatMap(Вами::from)
.flatMap(this::queryTitle)
.map(url1 -> textView.getText() + url1 + "\n\n")
.subscribe(textView::setText);

з допомогою оператора filter() отфильтровываем порожні рядки зі значенням null:
queryURLs(url)
.flatMap(Вами::from)
.flatMap(this::queryTitle)
.filter(title -> title != null)
.map(url1 -> textView.getText() + url1 + "\n\n")
.subscribe(textView::setText);

з допомогою оператора take() візьмемо тільки перші 7 заголовків:
queryURLs(url)
.flatMap(Вами::from)
.flatMap(this::queryTitle)
.filter(title -> title != null)
.take(7)
.map(url1 -> textView.getText() + url1 + "\n\n")
.subscribe(textView::setText);

Останній приклад показав, що об'єднання безлічі методів плюс використання великої кількості доступних операторів плюс лямбда-вирази і ми отримуємо буквально з декількох рядків потужний обробник потоків різних даних.

Всі приклади, наведені в статті викладені тут.

Джерела:

  1. Офіційна документація
  2. Грокаем* RxJava, частина перша: основи
  3. Getting Started With ReactiveX on Android
  4. RxJava — Tutorial
  5. Getting Started with RxJava and Android
  6. Reactive Programming with RxJava in Android
  7. Party tricks with RxJava, RxAndroid & Retrolambda
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.