Грокаем* RxJava, частина перша: основи

* від перекладача: я довго думав над тим, як перекласти на російську мову дієслово «to grok». З одного боку, це слово перекладається як «зрозуміти» або «усвідомити», а з іншого боку, при перекладі роману Роберта Хайнлайна «Чужинець у чужій країні» (в якому це слово вперше з'явилося на світ), перекладачі зробили з нього російське «грокать». Роман я не читав, тому вважав, що є у цього слова якісь смислові відтінки, які російськими аналогами не передавалися, а тому у своєму перекладі використовував ту ж саму кальку з англійської.

RxJava — це, зараз, одна з найгарячіших тем для обговорення у Android-програмістів. Єдина проблема полягає в тому, що зрозуміти самі її основи, якщо ви не стикалися з чимось подібним, може бути досить важко. Функціональне реактивне програмування досить складно зрозуміти, якщо ви прийшли з імперативного світу, але, як тільки ви розберетеся з ним, ви зрозумієте, наскільки це круто!
Я постараюся дати вам деяке загальне уявлення про RxJava. Завдання цього циклу статей полягає не в тому, щоб пояснити все аж до останньої коми (навряд чи я зміг би це зробити), але, скоріше в тому, щоб зацікавити вас RxJava, і тим, як вона працює.

Основи
Базовими будівельними блоками реактивного коду є
Observables
та
Subscribers
1.
Вами
є джерелом даних, а
Subscriber
— споживачем.
Породження даних через
Вами
завжди відбувається у відповідності з одним і тим же порядком дій:
Вами
«випромінює» деяка кількість даних (в тому числі,
Вами
може нічого і не випромінювати), і завершує свою роботу — небудь успішно, або з помилкою. Для кожного
Subscriber
, підписаного
Вами
, викликається метод
Subscriber.onNext()
для кожного елемента потоку даних, після якого може бути викликаний як
Subscriber.onComplete()
, так і
Subscriber.onError()
.
Все це дуже схоже на звичайний патерн «Спостерігач», але є одна важлива відмінність:
Observables
часто не починають породжувати дані до тих пір, поки хто-небудь явно не підписується на них2. Іншими словами: якщо дерево падає, а поряд нікого немає, значить звук його падіння не чути.

Здрастуй, світ!
Давайте розберемося з невеликим прикладом. Спочатку створимо простий
Observable
:

Observable<String> myObservable = Вами.create(
new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> sub) {
sub.onNext("Hello, world!");
sub.onCompleted();
}
}
);

Наш
Вами
породжує рядок «Hello, world!», і завершує свою роботу. Тепер створимо
Subscriber
для того, щоб прийняти дані і щось з ними зробити.

Subscriber<String> mySubscriber = new Subscriber<String>() {
@Override
public void onNext(String s) { System.out.println(s); }

@Override
public void onCompleted() { }

@Override
public void onError(Throwable e) { }
};

Все, що робить
Subscriber
— друкує рядки, передані йому
Вами
. Тепер, коли у нас є
myObservable
та
mySubscriber
, ми можемо зв'язати їх разом, скориставшись методом
subscribe()
:

myObservable.subscribe(mySubscriber);
// Виводить "Hello, world!"

Як тільки ми підписали
mySubscriber
на
myObservable
,
myObservable
викликає у
mySubscriber
методи
onNext()
та
onComplete()
, в результаті чого
mySubscriber
— виводить на консоль «Hello, world!», і завершує своє виконання.

Спрощуємо код
Взагалі кажучи, ми написали дуже багато коду для такої простої задачі, як висновок «Hello, world!» в консоль. Я спеціально написав цей код таким чином, щоб ви могли легко розібратися, що тут до чого. У RxJava є багато більш раціональних способів вирішити подібну задачу.
По-перше, давайте спростимо наш
Вами
. У RxJava існують методи створення
Вами
, придатних для вирішення найбільш типових завдань. В нашому випадку,
Observable.just()
породжує один елемент даних, а потім завершує своє виконання, так само як і наш перший варіант3:

Observable<String> myObservable = Вами.just("Hello, world!");

Далі, давайте спростимо наш
Subscriber
. Нас не цікавлять методи
onCompleted()
та
onError()
, так що ми можемо використовувати інший базовий клас для визначення того, що потрібно зробити в
onNext()
:

Action1<String> onNextAction = new Action1<String>() {
@Override
public void call(String s) {
System.out.println(s);
}
};

Action
може бути використаний для заміни будь-якої частини
Subscriber
:
Observable.subscribe()
може прийняти один, два або три
Action
-параметри, які будуть виконуватися замість
onNext()
,
onError()
та
onCompete()
. Тобто, ми можемо замінити наш
Subscriber
:

myObservable.subscribe(onNextAction, onErrorAction, onCompleteAction);

Але, так як нам не потрібні
onError()
та
onCompete()
ми можемо спростити код ще більше:

myObservable.subscribe(onNextAction);
// Виводить "Hello, world!"

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

Observable.just("Hello, world!")
.subscribe(new Action1<String>() {
@Override
public void call(String s) {
System.out.println(s);
}
});

Ну і, нарешті, ми можемо скористатися лямбдами з Java 8, щоб спростити код ще більше:

Observable.just("Hello, world!")
.subscribe(s -> System.out.println(s));

Якщо ви пишете під Android (і тому не можете використовувати Java 8), я дуже рекомендую retrolambda, яка допоможе спростити дуже багатослівний в деяких місцях код.

Трансформація
Давайте спробуємо щось нове.
Наприклад, я хочу додати підпис до «Hello, world!», виводиться в консоль. Як це зробити? По-перше, ми можемо змінити наш
Observable
:

Observable.just("Hello, world! -Dan")
.subscribe(s -> System.out.println(s));

Це може спрацювати, якщо ви маєте доступ до вихідного коду, в якому визначається ваш
Вами
, але це не завжди буде так — наприклад, коли ви використовуєте чиюсь бібліотеку. Інша проблема: що, якщо ми використовуємо наш
Вами
у багатьох місцях, але хочемо додавати підпис тільки у деяких випадках?
Можна спробувати переписати
Subscriber
:

Observable.just("Hello, world!")
.subscribe(s -> System.out.println(s + " -Dan"));

Такий варіант теж є невідповідним, але вже з інших причин: я хочу, щоб мої передплатники були настільки легковагими, наскільки це можливо, так як я можу запускати їх в головному потоці. На концептуальному рівні, передплатники повинні реагувати на поступають у них даних, а не редагувати.
Було б здорово, якби можна було змінити «Hello, world!» на деякому проміжному кроці.

Введення в оператори
І такий проміжний крок, призначений для трансформації даних, є. Ім'я йому — оператори, і вони можуть бути використані в проміжку між
Вами
та
Subscriber
для маніпуляції даними. Операторів у RxJava дуже багато, тому для початку краще буде зосередитися лише на деяких.
Для нашої конкретної ситуації найкраще підійшов би оператор
map()
, через який можна перетворювати один елемент даних в інший:

Observable.just("Hello, world!")
.map(new Func1<String, String>() {
@Override
public String call(String s) {
return s + " -Dan";
}
})
.subscribe(s -> System.out.println(s));

І знову можна вдатися до лямбдам:

Observable.just("Hello, world!")
.map(s -> s + " -Dan")
.subscribe(s -> System.out.println(s));

Круто, так? Наш оператор
map()
грубо кажучи, це
Вами
, який трансформує надходить в нього елемент даних. Ми можемо створити ланцюжок з такої кількості
map()
, яку вважатимемо за потрібне для того, щоб надати даними найбільш зручну і просту форму, щоб полегшити завдання нашого
Subscriber
.

Ще дещо про map()
Цікавим властивістю
map()
є те, що він не зобов'язаний породжувати дані того ж самого типу, що й вихідний
Observable
.
Припустимо, що наш
Subscriber
має виводити не породжує текст, а його хеш:

Observable.just("Hello, world!")
.map(new Func1<String, Integer>() {
@Override
public Integer call(String s) {
return s.hashCode();
}
})
.subscribe(i -> System.out.println(Integer.toString(i)));

Цікаво: ми почали з рядків, а наш
Subscriber
приймає Integer. До речі, ми знову забули про лямбдах:

Observable.just("Hello, world!")
.map(s -> s.hashCode())
.subscribe(i -> System.out.println(Integer.toString(i)));

Як я говорив раніше, ми хочемо, щоб наш
Subscriber
робив якомога менше роботи, тому давайте застосуємо ще один
map()
, щоб перетворити наш хеш назад у
String
:

Observable.just("Hello, world!")
.map(s -> s.hashCode())
.map(i -> Integer.toString(i))
.subscribe(s -> System.out.println(s));

Погляньте на це — наші
Вами
та
Subscriber
тепер виглядають так само, як і на самому початку! Ми просто додали кілька проміжних кроків, трансформуючих наші дані. Ми могли б навіть знову додати код, прибавляющий мій підпис до породжуваним рядками:

Observable.just("Hello, world!")
.map(s -> s + " -Dan")
.map(s -> s.hashCode())
.map(i -> Integer.toString(i))
.subscribe(s -> System.out.println(s));

І що далі?
Зараз ви, мабуть, думаєте: «Ну як завжди: показують простецький приклад, і кажуть, що технологія крута, тому що вона дозволяє вирішити цю задачу на дві строчки коду». Згоден, приклад і правда простий. Але з нього можна винести пару корисних ідей:

Ідея №1:
Вами
та
Subscriber
може робити все, що завгодно
Не обмежуйте свою уяву, можливо все, чого ви хочете.
Ваш
Вами
може бути запитом до бази даних, а
Subscriber
може відображати на екрані результати запиту.
Вами
може також бути кліком по екрану,
Subscriber
може містити в собі реакцію на цей клік.
Вами
може бути потоком байтів, прийнятих з мережі, тоді як
Subscriber
може писати ці дані на пристрій зберігання даних.
Це фреймворк загального призначення, здатний впоратися з будь-якою проблемою.

Ідея №2:
Вами
та
Subscriber
не залежать від проміжних кроків, що знаходяться між ними
Можна вставити скільки завгодно викликів
map()
в проміжку між
Вами
і підписаним на нього
Subscriber
. Система є легко компонуемой, і з її допомогою дуже легко управляти потоком даних. Якщо оператори працюють з коректними вхідними/вихідними даними, можна написати ланцюжок перетворень нескінченної довжини4.

Погляньте на ці ключові ідеї разом і ви побачите систему з великим потенціалом. Зараз, правда, у нас є тільки один оператор
map()
і з ним багато не напишеш. У другій частині цієї статті ми розглянемо велика кількість операторів, доступних вам з коробки, коли ви користуєтеся RxJava.

Продовження слід


1
Subscriber
імплементує інтерфейс
Observer
, і тому «базовим будівельним блоком» можна назвати, швидше, останній, але на практиці ви найчастіше будете використовувати
Subscriber
, тому що він має декілька додаткових корисних методів, в тому числі і
Subscriber.unsubscribe()
.
2 В RxJava є «гарячі» і «холодні»
Observables
. Гарячий
Вами
породжує дані постійно, навіть якщо на нього ніхто не підписаний. Холодний
Вами
, відповідно, породжує дані тільки якщо у нього є хоча б один передплатник (в статті використовуються саме холодні
Observables
). Для початкових стадій вивчення RxJava ця різниця не настільки важлива.
3 Строго кажучи,
Observable.just()
не є повним аналогом нашого початкового коду, але чому це так відбувається, я поясню тільки в третій частині статті.
4 Окей, не такий вже і нескінченною, так як в якийсь момент я упруся в обмеження, що накладаються залізом, але ви розумієте, що я хотів сказати.

Джерело: Хабрахабр

0 коментарів

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