Я пришел в Tinkoff пару лет назад, на новый проект Клиенты и проекты, который тогда только запускался.
Сейчас уже не помню своих ощущений от новой тогда для меня архитектуры. Но точно помню: было непривычно, что Rx используется еще где-то, за пределами обычных походов в сеть и в базу. Сейчас, когда эта архитектура уже прошла некоторый эволюционный путь развития, хочется наконец рассказать о том, что было и к чему пришло.   По моему мнению, все популярные ныне архитектуры — MVP, MVVM и даже MVI — уже давно на арене и не всегда заслуженно. Разве у них нет недостатков? Я вижу их немало. Мы у себя решили, что хватит это терпеть, и (пере)изобрели новую, максимально асинхронную архитектуру. Тезисно опишу, что мне не нравится в текущих архитектурах. Некоторые пункты могут быть спорными. Возможно, вы с таким никогда не сталкивались, пишете идеально и вообще джедай программирования. Тогда простите меня, грешного.
Итак, моя боль — это: Изначально мы писали на MVP, но немного мутированной. Это была какая-то смесь MVP и MVI. Были сущности из MVP в виде презентера и интерфейса View: Уже тут можно заметить подвох: View здесь очень далека от канонов MVP. В презентере был метод: Снаружи передавалась реализация интерфейса, которая реактивно подписывалась на изменения UI. И это уже попахивает MVI! Дальше — больше. В Presenter’e создавались и подписывались на изменения View разные интеракторы, но они не вызывали методы UI напрямую, а возвращали некоторый глобальный State, в котором были все возможные состояния экрана: Активити была наследником интерфейса SomeViewStateMachine: Когда пользователь нажимал на что-то на экране, в презентер приходило событие и он создавал новую модель, которую отрисовывал специальный класс: Согласитесь, какой-то странный MVP, да и от MVI далеко. Ищем вдохновение.  Общаясь о своих проблемах с другими разработчиками, наш (тогда еще) лид Сергей Боиштян узнал про Redux. Посмотрев доклад Дорфмана про все архитектуры и поигравшись с Redux, мы решили с её помощью модернизировать нашу архитектуру.
Но сначала давайте взглянем на архитектуру поближе и рассмотрим ее плюсы и минусы. Action
Описывает действие. ActionCreator
Он как системный аналитик: форматирует, дополняет ТЗ заказчика так, чтобы его понимали программисты.
Когда пользователь кликает на экран, ActionsCreator формирует Action, который идет в middleware (какая-то бизнес-логика). Бизнес-логика отдает нам новые данные, которые получает и отрисовывает определенный Reducer. Если вы еще раз посмотрите на картинку, то можете заметить такой объект, как Store. Store хранит в себе Reducer’ы. То есть мы видим, что фронтендеры — братья по несчастью — догадались, что можно один большой объект распилить на много маленьких, каждый из которых будет отвечать за свою часть экрана. И это просто замечательная мысль! Примеры кода простых ActionCreator’ов (осторожно, JavaScript!): Reducer Короче говоря, Reducer знает, как декомпозированно обновлять экран/view. Плюсы: Минусы:  Да, нам показалось, что разделение на ActionCreator и Reducer не самый удачный вариант связки модели и экрана, потому что писать instanceof(is) — плохой подход. И тут-то мы и изобрели НАШУ архитектуру!  Что такое Action и ActionCreator в контексте EBA: Да, половина архитектуры — это typealias’ы и интерфейс. Простота равно изящность!  Action нужен для того, чтобы что-то вызвать без передачи каких-то данных. Так как ActionCreator возвращает Observable, нам пришлось обернуть Action в еще одну лямбду для передачи каких-то данных. Так и получился ActionMapper — типизированный Action, через который мы можем передавать то, что нам нужно для обновления экрана/view.

Основные постулаты:
 С первым пунктом все ясно: чтобы не было ада из непонятных перекрестных обновлений, мы договорились, что один ActionCreator может обновлять только свою часть экрана. Если это список — он обновляет только список, если кнопка — только ее. Но, спрашивается, чем нам Dagger не угодил? Рассказываю.
Типичная история, когда на проекте есть абстрактный Сергей aka даггер-мастер aka «А что эта аннотация делает?». Получается так, что, если ты экспериментировал с даггером, приходится объяснять каждый раз каждому новому (да и не только новому) разработчику. А может, ты и сам уже забыл, что эта аннотация делает, и идешь гуглить. Все это сильно усложняет процесс создания фичи, не привнося особого удобства. Поэтому мы решили, что будем создавать необходимые нам вещи руками, так это будет быстрее собираться, ведь нет никакой кодогенерации. Да, мы потратим лишних пять минут на написание руками всех зависимостей, но сэкономим много времени на компиляции. Да, мы не везде отказались от даггера, он используется на глобальном уровне, создает какие-то общие вещи, но и их — для большей оптимизации — мы пишем на Java, чтобы не привлекать kapt. Схема архитектуры:  Component это аналог того самого компонента из Dagger’a, только без Dagger’a. Его задача — создать Binder. Binder связывает воедино ActionCreator’ы. Из View в Binder приходят Events о том, что произошло, а из Binder’a во View отправляются Actions, которые обновляют экран. ActionCreator  Теперь давайте разберемся, что это за штука такая — ActionCreator. В самом простом случае он просто однонаправленно обрабатывает действие. Допустим, есть такой сценарий: пользователь кликнул на кнопку «Создать задачу». Должен открыться другой экран, где мы будем ее описывать, без всяких дополнительных запросов. Для этого мы просто подписываемся на кнопку с помощью RxBinding от нашего любимого Джейка и ждем, когда пользователь на нее кликнет. Как только произойдет клик — Binder отправит Event в конкретный ActionCreator, который вызовет наш Action, который откроет нам новый экран. Заметьте, тут не было никаких switch. Дальше я покажу в коде, почему именно так.
Если нам вдруг надо сходить в сеть или в базу, мы делаем эти запросы тут же, но через интеракторы, которые мы передали в конструктор ActionCreator’a по интерфейсу их вызова: Под словами «по интерфейсу их вызова» я имел в виду как раз то, как объявляется getItems (здесь ViewTyped — это наш интерфейс для работы со списками). Кстати, этот ActionCreator у нас переиспользуется в восьми разных частях приложения, потому что он написан максимально универсально.  Так как события имеют реактивную природу, мы можем собирать цепочку, добавляя туда другие операторы, например startWith(showLoadingAction), чтобы показать загрузку, и onErrorReturn(errorAction), чтобы показать стейт экрана с ошибкой.
И все это реактивно! Давайте уже наконец рассмотрим архитектуру на примере кода. Для начала я выбрал один из самых простых экранов — о приложении, потому что это статичный экран.
Рассмотрим создание компонента: Аргументы компонента — Action’ы/ActionMapper’ы — помогают связать View с ActionCreator’ами. В ActionMapper’e setVersionName мы передаем версию проекта и присваиваем тексту на экране это значение. В openPdfAction — пару из ссылки на документ и имени для открытия следующего экрана, где пользователь может прочитать этот документ.  Вот так выглядит сам компонент: Напомню, что: Окей, идем дальше. Давайте рассмотрим AboutEventsBinder подробнее. ActionOnEvent — это очередной typealias, чтобы не писать каждый раз. В AboutEventsBinder мы передаем ActionCreator’ы и, вызывая их, связываем с конкретным событием. Но, чтобы понять, как всё это связывается, давайте рассмотрим базовый класс — BaseEventsBinder. Видим знакомый метод bindInternal, который мы переопределили в наследнике. Теперь рассмотрим метод bind. Вся магия заключена тут. Мы принимаем наследника интерфейса BaseEvents, передаем его в bindInternal для связи Events и Actions. Один раз говорим, что всё, что бы ни пришло, исполняем на ui-потоке и подписываемся. Также видим интересный хак — takeUntil.  Определив в BaseEvents поле unbindEvent для контроля отписки, мы обязаны реализовывать ее во всех наследниках. Это замечательное поле позволяет отписываться от цепочки автоматически, как только данный эвент выполнится. Это же просто великолепно! Теперь можно не следить и не париться по поводу жизненного цикла и спать спокойно. Вернемся к компоненту. И уже тут виден способ переиспользования. Мы написали один класс, который умеет открывать экран просмотра pdf, и нам без разницы — с каким url. Никакой дубликации кода. Код ActionCreator’а тоже максимально простой, тут мы просто производим некоторые манипуляции со строкой.  Снова вернемся к компоненту и рассмотрим следующий ActionCreator: Однажды нам стало лень писать одинаковые и простые по своей сути ActionCreator’ы. Мы воспользовались мощью котлина и написали extension’ы. Например, в этом случае нам нужно было просто передать в ActionMapper статическую строку. Бывают случаи, когда нам вообще ничего передавать не надо, а только вызвать какой-то Action — например, чтобы открыть следующий экран: Итак, с компонентом покончено, возвращаемся во фрагмент: Здесь мы видим создание класса, отвечающего за прием событий от пользователя. А unbind и bind — это просто события жизненного цикла экрана, которые мы забираем с помощью библиотеки Navi от Trello. В интерфейсе Events описываются события конкретного экрана, плюс он обязан наследовать BaseEvents. Ниже всегда следует реализация интерфейса. В данном случае эвенты получились один в один с теми, что приходят с экрана, но бывает, что нужно смержить два события.  Например, события загрузки экрана при открытии и повторной загрузке в случае ошибки должны быть объединены в один — просто загрузку экрана. Возвращаемся во фрагмент и соединяем все воедино! Просим у компонента создать и вернуть нам binder, далее вызываем на нем метод bind, куда передаем объект, наблюдающий за событиями экрана. На этой архитектуре мы пишем проект уже около двух лет. И нет предела счастью менеджеров в скорости деливеринга фич! Они не успевают новую придумать, как мы уже заканчиваем старую. Архитектура очень гибкая и позволяет переиспользовать много кода.
Минусом данной архитектуры можно назвать несохранение состояния. У нас нет целой модели, описывающей состояние экрана, как в MVI, но мы с этим справляемся. Как — смотрите ниже. Думаю, всем знакома проблема аналитики: никто не любит ее писать, потому что она лезет через все слои и уродует вызовы. Некоторое время назад и нам пришлось с этим столкнуться. Но благодаря нашей архитектуре получилась очень красивая реализация. Итак, какая у меня была идея: аналитика обычно уходит в ответ на действия пользователя. А у нас как раз есть класс, который аккумулирует действия пользователя. Окей, приступим. Шаг 1. Немного меняем базовый класс BaseEventsBinder, обернув events в trackAnalytics: Шаг 2. Создаем стабовую реализацию переменной trackAnalytics, чтобы поддержать обратную совместимость и не сломать наследников, которым пока не нужна аналитика: Шаг 3. Пишем реализацию интерфейса TrackAnalytics для нужного экрана — допустим, для экрана списка проектов: Тут мы опять используем мощь котлина в виде делегатов. У нас уже есть созданный нами наследник интерфейса — в данном случае ProjectsEvents. Но для некоторых событий надо переопределить то, как идут события, и добавить вокруг них обвязку с отсылкой аналитики. На деле trackEvent это просто doOnNext: Шаг 4. Осталось передать это в Binder. Так как мы его конструируем в компоненте, у нас есть возможность, если вдруг понадобится, докинуть в конструктор дополнительные зависимости. Теперь конструктор ProjectsEventsBinder будет выглядеть так: Другие примеры можете посмотреть на GitHub . Никак. Мы блочим ориентацию. Но также используем arguments/intent и сохраняем туда переменную OPENED_FROM_BACKSTACK. И при конструировании Binder’а смотрим на нее. Если она false — грузим данные из сети. Если true — из кэша. Это позволяет быстро пересоздать экран. Для всех, кто против блокирования ориентации: попробуйте провести тест и залогировать в аналитику, как часто ваши пользователи переворачивают телефон и сколько находятся в другой ориентации. Результаты могут удивить. Не советую, но если не жалко времени на компиляцию — можно создавать Component и через даггер. Но мы не пробовали. Все то же самое можно написать и на Java, просто выглядеть будет не так красиво. Если вам понравится статья — следующая часть будет про то, как на такой архитектуре писать тесты (тут-то и станет ясно, зачем столько интерфейсов). Спойлер — писать легко и можно писать на все слои, кроме компонента, но его и не нужно тестировать, он просто создает объект binder’а. Спасибо коллегам из команды мобильной разработки Тинькофф Бизнес за помощь в составлении статьи.