Android VIPER на реактивной тяге

c6e5b98b49164b1c9d1a779cda07833a.jpg

Чем больше строк кода написано, тем реже хочется дублировать код, а чем больше проектов реализовано, тем чаще обходишь старые, хоть и зачастую любимые, грабли, и начинаешь все больше интересоваться архитектурными решениями.
Думаю, достаточно не привычно рядом с 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.
b32c3bc22ccd4c12b8f3b6dc89de81a7.png
Думаю, вы заметили на схеме Observable, — именно здесь скрывается вся мощь реактивной тяги. Слой данных не просто извлекает из удаленного или локального хранилища данные в необходимом для нас представлении, он передает в Interactor всю последовательность действий завернутую в Observable, который в свою очередь может продолжить эту последовательность по своему усмотрению исходя из реализуемой задачи.

А сейчас разберем небольшой пример реализации VIPER для Android (исходники):
Предположим, что перед нами стоит задача разработать приложение, которое раз в три секунды запрашивает у «не очень гибкого» сервера список сообщений и отображает последнее для каждого отправителя, а так же оповещает пользователя о появлении новых. По тапу на последнее сообщение появляется список всех сообщений для выбранного отправителя, но сообщения все так же продолжают раз в 3 секунды синхронизироваться с сервером. Так же из главного экрана мы можем попасть в список контактов, и просмотреть все сообщения для одного из них.

И так, приступим, у нас есть три экрана: чаты (последние сообщения от каждого контакта), список сообщений от конкретного контакта и список контактов. Накидаем схему классов:

b3eb5f1e9968448fbfdacd4ce0ec4b3f.png

Экранами являются фрагменты, переходы между которыми регулируются 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 в котором переключаются фрагменты.

ca8e0dd9961c458d8fc34e45fef8a1b7.png

Как уже было сказано выше 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 — организация кэширования.
Если заинтересовало, ставьте плюсик:)

© Habrahabr.ru