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

Всем привет! Меня зовут Анатолий Варивончик, я Android-разработчик Badoo. Сегодня я поделюсь с вами переводом второй части статьи моего коллеги Zsolt Kocsi о реализации MVI, которую мы ежедневно используем в процессе разработки. Первая часть здесь [1].
В первой части статьи мы познакомились с Features, центральными элементами MVICore [2], которые можно переиспользовать. Они могут обладать максимально простой структурой и включать всего один Reducer, а могут стать полнофункциональным средством для управления асинхронными задачами, событиями и многим другим.
Каждая Feature отслеживаема — есть возможность подписаться на изменения её состояния и получать уведомления об этом. При этом Feature можно подписать на источник ввода. И в этом есть смысл, ведь с включением Rx в кодовую базу у нас и так появилось множество наблюдаемых объектов и подписок на самых разных уровнях.
Именно в связи с увеличением количества реактивных компонентов пришло время поразмышлять о том, что мы имеем и можно ли сделать систему ещё лучше.
Нам предстоит ответить на три вопроса:
В этой части статьи мы рассмотрим основы и преимущества построения системы при помощи реактивных компонентов и увидим, как Kotlin помогает в этом.
К тому моменту, когда мы подошли к работе над дизайном и стандартизацией наших Features, мы уже перепробовали множество различных подходов и решили, что Features будут выполнены в форме реактивных компонентов. Сначала мы сосредоточились на главных интерфейсах. Прежде всего нам нужно было определиться с типами входных и выходных данных.
Мы рассуждали следующим образом:
В результате мы решили использовать ObservableSource<Т> для вывода и Consumer<Т> для ввода. Почему не Observable/Observer, спросите вы. Observable — абстрактный класс, от которого вам нужно отнаследоваться, а ObservableSource — реализуемый вами интерфейс, полностью удовлетворяющий потребность в реализации реактивного протокола.
package io.reactivex;
import io.reactivex.annotations.*;
/**
* Represents a basic, non-backpressured {@link Observable} source base interface,
* consumable via an {@link Observer}.
*
* @param <T> the element type
* @since 2.0
*/
public interface ObservableSource<T> {
/**
* Subscribes the given Observer to this ObservableSource instance.
* @param observer the Observer, not null
* @throws NullPointerException if {@code observer} is null
*/
void subscribe(@NonNull Observer<? super T> observer);
}
Observer, первый приходящий на ум интерфейс, реализует четыре метода: onSubscribe, onNext, onError и onComplete. Стремясь максимально упростить протокол, мы предпочли ему Consumer<Т>, который принимает новые элементы с помощью одного-единственного метода. Если бы мы выбрали Observer, то оставшиеся методы чаще всего были бы избыточными либо работали бы иначе (например, нам хотелось представить ошибки как часть состояния (State), а не как исключения, и уж точно не прерывать поток).
/**
* A functional interface (callback) that accepts a single value.
* @param <T> the value type
*/
public interface Consumer<T> {
/**
* Consume the given value.
* @param t the value
* @throws Exception on error
*/
void accept(T t) throws Exception;
}
Итак, у нас есть два интерфейса, каждый из которых содержит по одному методу. Теперь мы можем связать их, подписав Consumer<Т> на ObservableSource<Т>. Последний принимает только экземпляры Observer<Т>, но мы можем обернуть его в Observable<Т>, который подписан на Consumer<Т>:
val output: ObservableSource<String> = Observable.just("item1", "item2", "item3")
val input: Consumer<String> = Consumer { System.out.println(it) }
val disposable = Observable.wrap(output).subscribe(input)
(К счастью, функция .wrap(output) не создаёт новый объект если output уже является Observable<Т> ).
Возможно, вы помните, что компонент Feature из первой части статьи использовал входные данные типа Wish (соответствует Intent из Model-View-Intent) и выходные данные типа State, а потому может находиться с обеих сторон связки:
// Wishes -> Feature
val wishes: ObservableSource<Wish> = Observable.just(Wish.SomeWish)
val feature: Consumer<Wish> = SomeFeature()
val disposable = Observable.wrap(wishes).subscribe(feature)
// Feature -> State consumer
val feature: ObservableSource<State> = SomeFeature()
val logger: Consumer<State> = Consumer { System.out.println(it) }
val disposable = Observable.wrap(feature).subscribe(logger)
Такое связывание Consumer и Producer уже выглядит достаточно просто, но существует ещё более лёгкий способ, при котором не нужно ни создавать подписки вручную, ни отменять их.
Представляем Binder.
MVICore [2] содержит класс под названием Binder, который предоставляет простой API для управления Rx-подписками и обладает целым рядом крутых возможностей.
Зачем он нужен?
Вместо того чтобы подписываться вручную, можно переписать приведённые выше примеры следующим образом:
val binder = Binder()
binder.bind(wishes to feature)
binder.bind(feature to logger)
Благодаря Kotlin всё выглядит очень просто.
Эти примеры работают, если тип входных и выходных данных совпадает. Но что, если это не так? Реализовав функцию расширения, мы можем сделать трансформацию автоматической:
val output: ObservableSource<A> = TODO()
val input: Consumer<B> = TODO()
val transformer: (A) -> B = TODO()
binder.bind(output to input using transformer)
Обратите внимание на синтаксис: читается почти как обычное предложение (и это ещё одна причина, почему я люблю Kotlin). Но Binder используется не только как синтаксический сахар — он также пригодится нам для решения проблем с жизненным циклом.
Создание экземпляра выглядит проще некуда:
val binder = Binder()
Но в этом случае нужно отписываться вручную, и вам придётся вызывать binder.dispose() всякий раз, когда будет необходимо удалить подписки. Есть и другой способ: ввести экземпляр жизненного цикла в конструктор. Вот так:
val binder = Binder(lifecycle)
Теперь вам не нужно волноваться о подписках — они будут удаляться в конце жизненного цикла. При этом жизненный цикл может повторяться многократно (как, например, цикл запуска и остановки в Android UI) — и Binder будет каждый раз создавать и удалять подписки за вас.
Большинство Android-разработчиков, видя словосочетание «жизненный цикл», представляют цикл Activity и Fragment. Да, Binder может работать и с ними, отписываясь по завершении цикла.
Но это только начало, ведь вы никак не задействуете андроидовский интерфейс LifecycleOwner — у Binder есть свой, более универсальный. Он, по сути, представляет собой поток сигналов BEGIN/END:
interface Lifecycle : ObservableSource<Lifecycle.Event> {
enum class Event {
BEGIN,
END
}
// Remainder omitted
}
Вы можете либо реализовать этот поток при помощи Observable (путём маппинга), либо просто использовать класс ManualLifecycle из библиотеки для не Rx-сред (как именно, увидите чуть ниже).
Как при этом действует Binder? Получая сигнал BEGIN, он создаёт подписки для ранее сконфигурированных вами компонентов (input/output), а получая сигнал END, удаляет их. Самое интересное — что можно всё начинать заново:
val output: PublishSubject<String> = PublishSubject.create()
val input: Consumer<String> = Consumer { System.out.println(it) }
val lifecycle = ManualLifecycle()
val binder = Binder(lifecycle)
binder.bind(output to input)
output.onNext("1")
lifecycle.begin()
output.onNext("2")
output.onNext("3")
lifecycle.end()
output.onNext("4")
lifecycle.begin()
output.onNext("5")
output.onNext("6")
lifecycle.end()
output.onNext("7")
// will print:
// 2
// 3
// 5
// 6
Эта гибкость в переназначении подписок особенно полезна при работе с Android, когда может быть сразу несколько циклов Start-Stop и Resume-Pause, помимо обычного Create-Destroy.
В библиотеке представлены три класса:
androidLifecycle — это возвращаемое методом getLifecycle() значение, то есть AppCompatActivity, AppCompatDialogFragment и т. д. Всё очень просто:
fun createBinderForActivity(activity: AppCompatActivity) = Binder(
CreateDestroyBinderLifecycle(activity.lifecycle)
)
Давайте не будем на этом останавливаться, ведь мы никак не привязаны к Android. Что такое жизненный цикл Binder? Буквально что угодно: например, время воспроизведения диалога или время выполнения какой-нибудь асинхронной задачи. Можно, скажем, привязать его к области видимости DI — и тогда любая подписка будет удаляться вместе с ней. Полная свобода действий.
fun Observable<T>.toBinderLifecycle() = Lifecycle.wrap(this
.first()
.map { END }
.startWith(BEGIN)
)
fun Completable.toBinderLifecycle() = Lifecycle.wrap(
Observable.concat(
Observable.just(BEGIN),
this.andThen(Observable.just(END))
)
)
В любом случае вы можете либо проложить реактивный поток к потоку элементов Lifecycle.Event, либо использовать ManualLifecycle, если вы работаете с не Rx-кодом.
Binder прячет подробности создания и управления Rx-подписками. Остаётся только сжатый, обобщённый обзор: «Компонент A взаимодействует с компонентом B в области видимости C».
Предположим, что для текущего экрана у нас есть следующие реактивные компоненты:

Мы хотели бы, чтобы компоненты были связаны в пределах текущего экрана, и знаем, что:
Это можно выразить в паре строк:
with(binder) {
bind(feature to view using stateToViewModelTransformer)
bind(view to feature using uiEventToWishTransformer)
bind(view to analyticsTracker)
}
Мы делаем такие выжимки для наглядной демонстрации взаимосвязи компонентов. И поскольку мы, разработчики, проводим больше времени за чтением кода, чем за его написанием, подобный краткий обзор крайне полезен, особенно по мере увеличения числа компонентов.
Мы увидели, как Binder помогает в управлении Rx-подписками и как он помогает получить обзор системы, построенной из реактивных компонентов.
В следующих статьях мы расскажем, как мы отделяем реактивные UI-компоненты от бизнес-логики и как с помощью Binder добавлять промежуточные объекты (для ведения лога и time travel debugging). Не переключайтесь!
А пока познакомьтесь с библиотекой на GitHub [2].
Автор: ANublo
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/299746
Ссылки в тексте:
[1] здесь: https://habr.com/company/badoo/blog/429728/
[2] MVICore: https://github.com/badoo/MVICore
[3] Источник: https://habr.com/post/430550/?utm_campaign=430550
Нажмите здесь для печати.