Android VIPER на реактивной тяге
Чем больше строк кода написано, тем реже хочется дублировать код, а чем больше проектов реализовано, тем чаще обходишь старые, хоть и зачастую любимые, грабли, и начинаешь все больше интересоваться архитектурными решениями.
Думаю, достаточно не привычно рядом с Android встретить горячо любимый iOS-разработчиками архитектурный шаблон VIPER, мы тоже первое время пропускали мимо ушей разговоры из соседнего отдела по этому поводу, пока вдруг не обнаружили, что стали невольно использовать такой шаблон в своих Android приложениях.
Как такое могло произойти? Да очень просто. Поиски изящных архитектурных решений начались еще за долго до Android приложений, и одним из моих любимых и незаменимых правил всегда было — разделение проекта на три слабосвязанных слоя: Data, Domain, Presentation. И вот, в очередной раз изучая просторы Интернета на предмет новых веяний в архитектурных шаблонах для Android приложений, я наткнулась на великолепное решение: Android Clean Architecture, здесь, по моему скромному мнению, было все прекрасно: разбиение на любимые слои, Dependency Injection, реализация presentation layer как MVP, знакомый и часто используемый для data layer шаблон Repository.
Но помимо давно любимых и знакомых приемов в проектировании было место и открытиям. Именно этот проект познакомил меня с понятием Interactor (объект содержащий бизнес логику для работы с одной или несколькими сущностями), а так же именно здесь мне открылась мощь реактивного программирования.
Реактивное программирование и в частности rxJava достаточно популярная тема докладов и статей за прошедший год, поэтому вы без труда сможете ознакомится с этой технологией (если конечно, вы еще с ней не знакомы), а мы продолжим историю о VIPER.
Знакомство с Android Clean Architecture привело к тому, что любой новый проект, а так же рефакторинг уже существующих сводился к трехслойности, rxJava и MVP, а в качестве domain layer стали использоваться Interactors. Оставался открытым вопрос о правильной реализации переходов между экранами и здесь все чаще стало звучать понятие Router. Сначала Router был одинок и жил в главной Activity, но потом в приложении появились новые экраны и Router стал очень громоздким, а потом появилась еще одна Activity со своими Fragments и тут пришлось подумать о навигации всерьез. Вся основная логика, в том числе переключение между экранами, содержится в Presenter, соответственно Presenter-у необходимо знать о Router, который в свою очередь должен иметь доступ к Activity для переключения между экранами, таким образом Router должен быть для каждой Activity свой и передаваться в Presenter при создании.
И вот как-то в очередной раз глядя на проект пришло понимание, что у нас получился V.I. P.E.R — View, Interactor, Presenter, Entity and Router.
Думаю, вы заметили на схеме Observable, — именно здесь скрывается вся мощь реактивной тяги. Слой данных не просто извлекает из удаленного или локального хранилища данные в необходимом для нас представлении, он передает в Interactor всю последовательность действий завернутую в Observable, который в свою очередь может продолжить эту последовательность по своему усмотрению исходя из реализуемой задачи.
А сейчас разберем небольшой пример реализации VIPER для Android (исходники):
Предположим, что перед нами стоит задача разработать приложение, которое раз в три секунды запрашивает у «не очень гибкого» сервера список сообщений и отображает последнее для каждого отправителя, а так же оповещает пользователя о появлении новых. По тапу на последнее сообщение появляется список всех сообщений для выбранного отправителя, но сообщения все так же продолжают раз в 3 секунды синхронизироваться с сервером. Так же из главного экрана мы можем попасть в список контактов, и просмотреть все сообщения для одного из них.
И так, приступим, у нас есть три экрана: чаты (последние сообщения от каждого контакта), список сообщений от конкретного контакта и список контактов. Накидаем схему классов:
Экранами являются фрагменты, переходы между которыми регулируются Activity, реализующей интерфейс Router. Каждый фрагмент имеет свой Presenter и реализует необходимый для взаимодействия с ним интерфейс. Для облегчения создания нового Presenter и фрагмента, были созданы базовые абстрактные классы: BasePresenter и BaseFragment.
BasePresenter — содержит ссылки на интерфейс View и Router, а так же имеет два абстрактных метода onStart и onStop, повторяющих жизненный цикл фрагмента.
public abstract class BasePresenter {
private View view;
private Router router;
public abstract void onStart();
public abstract void onStop();
public View getView() {
return view;
}
public void setView(View view) {
this.view = view;
}
public Router getRouter() {
return router;
}
public void setRouter(Router router) {
this.router = router;
}
}
BaseFragment — осуществляет основную работу с BasePresenter: инициализирует и передает интерфейс взаимодействия в onActivityCreated, вызывает в соответствующих методах onStart и onStop.
Любое Android приложение начинается с Activity, у нас будет только одно MainAcivity в котором переключаются фрагменты.
Как уже было сказано выше Router живет в Activity, в конкретном примере MainActivity просто реализует его интерфейс, соответственно для каждой Activity свой Router, который управляет навигацией между фрагментами внутри нее, следовательно каждый фрагмент в такой Activity должен иметь Presenter, использующий один и тот же Router: так и появился BaseMainPresenter, который должен наследовать каждый Presenter работающий в MainActivity.
public abstract class BaseMainPresenter
extends BasePresenter {
}
При смене фрагментов в MainActivity меняется состояние Toolbar и FloatingActionButton, поэтому каждый фрагмент должен уметь сообщать необходимые ему параметры состояния в Activity. Для реализации такого интерфейса взаимодействия используется BaseMainFragment:
public abstract class BaseMainFragment extends BaseFragment implements BaseMainView {
public abstract String getTitle(); //заголовок в Toolbar
@DrawableRes
public abstract int getFabButtonIcon(); //иконка FloatingActionButton
//событие по клику на FloatingActionButton
public abstract View.OnClickListener getFabButtonAction();
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
MainActivity mainActivity = (MainActivity) getActivity();
//Передаем презентеру роутер
getPresenter().setRouter(mainActivity);
//Сообщаем MainActivity что необходимо обновить Toolbar и FloatingActionButton
mainActivity.resolveToolbar(this);
mainActivity.resolveFab(this);
}
@Override
public void onDestroyView() {
super.onDestroyView();
//Очищаем роутер у презентера
getPresenter().setRouter(null);
}
….
}
BaseMainView — еще одна базовая сущность для создания фрагментов в MainActivity, это интерфейс взаимодействия, о котором знает каждый Presenter в MainActivity. BaseMainView позволяет отображать сообщение об ошибке и отображать оповещения, этот интерфейс реализует BaseMainFragment:
...
@Override
public void showError(@StringRes int message) {
Toast.makeText(getContext(), getString(message), Toast.LENGTH_LONG).show();
}
@Override
public void showNewMessagesNotification() {
Snackbar.make(getView(), R.string.new_message_comming, Snackbar.LENGTH_LONG).show();
}
...
Имея заготовки в виде таких базовых классов значительно ускоряется процесс создания новых фрагментов для MainActivity.
Router
А вот какой получился MainRouter:
public interface MainRouter {
void showMessages(Contact contact);
void openContacts();
}
Interactor
Каждый Presenter использует один или несколько Interactor для работы с данными. Interactor имеет лишь два публичных метода execute и unsubscribe, то есть Interactor можно запустить на исполнение и отписаться от запущенного процесса:
public abstract class Interactor {
private final CompositeSubscription subscription = new CompositeSubscription();
protected final Scheduler jobScheduler;
private final Scheduler uiScheduler;
public Interactor(Scheduler jobScheduler, Scheduler uiScheduler) {
this.jobScheduler = jobScheduler;
this.uiScheduler = uiScheduler;
}
protected abstract Observable buildObservable(ParameterType parameter);
public void execute(ParameterType parameter, Subscriber subscriber) {
subscription.add(buildObservable(parameter)
.subscribeOn(jobScheduler)
.observeOn(uiScheduler)
.subscribe(subscriber));
}
public void unsubscribe() {
subscription.clear();
}
}
Entity
Для доступа к данным Interactor использует один или несколько DataProvider и формирует rx.Observable для последующего исполнения.
Постановка задачи для рассматриваемого примера включала в себя необходимость осуществления периодического запроса к серверу, что без труда удалось реализовать при помощи RX:
public static long PERIOD_UPDATE_IN_SECOND = 3;
@Override
public Observable> getAllMessages(Scheduler scheduler) {
return Observable
.interval(0, PERIOD_UPDATE_IN_SECOND, TimeUnit.SECONDS, scheduler)
.flatMap(this::getMessages);
}
Приведенный выше пример кода каждые три секунды выполняет запрос на получения списка сообщений и отправляет оповещение подписчику.
Заключение
Архитектура — скелет приложения, и если забыть о ней можно в итоге получить урода. Четкое разделение ответственности между слоями и типами классов облегчает поддержку, тестирование, ввод нового человека на проект занимает меньше времени и настраивает на единообразный стиль в программировании. Базовые классы помогают избежать дублирования кода, а rx не думать об асинхронности. Идеальная архитектура как и идеальный код величины практически не достижимые, но стремиться к ним — значит профессионально расти.
P.S. Есть идеи продолжить цикл статей, рассказав подробнее об интересных случаях в реализации:
presentation layer — сохранение состояния во фрагменте, композитные view;
domain layer — Interactor для нескольких подписчиков;
data layer — организация кэширования.
Если заинтересовало, ставьте плюсик:)