company_banner

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

  • Tutorial


Чем больше строк кода написано, тем реже хочется дублировать код, а чем больше проектов реализовано, тем чаще обходишь старые, хоть и зачастую любимые, грабли, и начинаешь все больше интересоваться архитектурными решениями.

Думаю, достаточно не привычно рядом с 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<View, Router> {
   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<View extends BaseMainView> 
                                                          extends BasePresenter<View, MainRouter> {
}


При смене фрагментов в 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<ResultType, ParameterType> {

   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<ResultType> buildObservable(ParameterType parameter);

   public void execute(ParameterType parameter, Subscriber<ResultType> 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<List<Message>> 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 — организация кэширования.
Если заинтересовало, ставьте плюсик :)
Rambler&Co 87,83
Компания
Поделиться публикацией

Вакансии компании Rambler&Co

Комментарии 35
  • 0
    Сразу две статьи про архитектуру Android приложений. Будет чем занять себя на выходные.
    • 0
      Moxy довольно легко расширяется до VIPE®. Правда, Router там особо не нужен, но может и он войдёт.
      Разница между статьями в том, что эта статья – про архитектуру. А статья про Moxy – больше про то, как используя Moxy построить приложение, подходящее под паттерны MVP & Co. Так что эта статья вам очень пригодится ;)
    • 0
      Молодцы! Давно ждал когда кто-нибудь выложит хоть какой-то материал по VIPER в Android.
      P.S. Статейку бы подкрепить полезными ссылками по теме.
      • +1
        Вот зачем? давайте тогда уже MVVM — это лучше будет чем вайпер. На сколько я знаю VIPER мало где прижился и кто его использует…
        • 0
          MVVM — это вырожденная форма MVP, где VM несколько интерактивней работает с View нежели Presenter, поэтому если для какой-то View необходима большая отзывчивость, например форма с валидацией полей, то Presenter можно заменить View Model, VIPER же, это несколько более широкое понятие, где помимо MVP есть Interactor и Router.
          • 0
            Почему мало где прижился?
          • 0
            Очень приятно, что статья оказалась полезной :) про реализацию VIPER в Android действительно очень мало материала, но если внимательней посмотреть, например, на android clean architecture, то можно увидеть что, там реализуется этот паттерн, правда Router называется Navigator :)

            А полезные ссылки были набросаны по ходу статьи) Но наверно вы правы лучше вынести в отдельный блок, спасибо.
          • 0
            А почему Router в Activity? Мне кажется он должен быть в Presenter.
            Странно, что View может управлять переходами.
            • +1
              Router и так находится в Presenter, просто в данном конкретном случае Router осуществляет навигацию между фрагментами в Activity, соответственно ему необходимо знать об Activity для добавления фрагментов в back stack, поэтому для удобства интерфейс Roter имплементит MainActivity, но конечно это может быть и отдельный класс, у нас во многих проектах — это, как раз, отдельный класс.
              • 0
                А как вы боритесь с жизненным циклом Fragment/Activity?
                • 0
                  У Presentor есть соответсвующие методы, в данном случае onStart, onStop достаточно, но можно расширить.
                  • 0
                    1. Вы каждый раз пересоздаете Presenter?
                    2. Почему только Start/Stop?
                    • 0
                      1. Presenter создаётся в onActivityCreated во фрагменте, так как в этот момент уже произошло onCreateView и мы можем работать с визуальными контролами
                      2. В onStart Presenter уже создан и View готова, можно например уже запустить первичную загрузку данных, в onStop фрагмент уже не виден пользователю, поэтому можно освободить ресурсы
                      • 0
                        Start/Stop вызывается довольно часто, к примеру при переходе к следующей Activity у текущей будет вызван Stop, а потом при возвращении будет вызван Start, в итоге если в Presenter мы грузим данные при onStart будут выполнены лишние запросы.
                        • 0
                          Тут все от реализуемого use case зависит, в любом случае если у Аctivity вызвался onStop, то её состояние лучше сохранить и в onStart восстановить, а как мы будем его восстанавливать: делать запрос к серверу или локальной БД решить должен Interactor
                          • 0
                            ОК, а что делать с данными которые не закешированы.
                            Чтобы не потерять их при смене конфигурации, я обычно кладу их в Bundle.
                            Однако при использовании данного подхода, Activity/Fragment не лучшее место для сохранения состояния.
                            • –3
                              Закешировать)
                              • 0
                                Хорошее предложение, но бывают результаты, которые не нужно кешировать. Они нужны здесь и сейчас.
                                • –1
                                  Но записывая в Bundle мы по сути кэшируем)
                                  • 0
                                    да, я так и делаю. Но при использовании Clean архитектуры, View в данном случае фрагменты, не должны сами данные кешировать.
                                    • 0
                                      Плюс что делать с данными которые вернулись в момент изменения конфигурации, когда фрагмент уже был уничтожен?
                                      • 0
                                        Данными занимается data layer, там данные и записываются в кэш сразу после получения
                                        • 0
                                          Согласен, но не всегда нужно данные закешировать.
                                          К примеру пользователь делает поиск по введенному слову через API.
                                • 0
                                  Хотелось бы рассказать как мы решили этот вопрос в Moxy (т.к. мы решили что это главная проблема, которая стоит перед нами) — View аттачится в onStart(только если до этого был onCreate), а детачится в onDestroy. Так и утечек памяти нет, и лишний раз не применяются команды ко View из ViewState. В то же время, т.к. ViewState храниться в Presenter, а не во View, он не должен быть сериализуемым :)
                                  • 0
                                    Это все хорошо, но использовать библиотеку для построения Архитектуры…
                                    За это дядюшка Боб может и в угол поставить))
                                    • 0
                                      Библиотека за вас архитектуру не построит — только поможет автоматизировать рутинную работу(по типу сохранения стейте). Но, конечно – не хотите — не используйте
                                      • 0
                                        Слишком много кода придется переписать, если я вдруг решу выпилить из проекта Moxy, а это уже не есть хорошо.
                                        • 0
                                          Если брать именно Moxy, то вам ничего не придётся переписывать при выпиливании либы. Придётся только дописывать =) Я вас не уговариваю, а просто информирую ;)
                                          • 0
                                            Естественно дописать) либу же целую выпиливаем.
                                            В любом случае спасибо за проделанную работу.
                • 0
                  А что такое VIPER? по сути это все та же Архитектура Дядюшки Боба.
                  Зачем только для нее новое название придумали? Из-за Router?
                  • +1
                    По сути да, это Архитектура Дядюшки Боба с Router, но для мобилок он очень даже необходим. Просто в последнее время в iOS говорят об VIPER, а в Android про clean architecture пора было уже прояснить ситуацию и понять, что мир мобилок движется в одном направлении)
                  • –4
                    Что такое «реактивное» программирование? С React.js оно никак не связано?
                    И кто такой «Presenter»? Очень похож на ViewModel в MVVM.
                    • 0
                      Жаль нету ссылки на проект.
                      А Interactor получается является оберткой над observable? Если я правильно понял по базовому interactor'у, то он являпется связным между презентером и datalayer, а так же берет на себя работу по созданию observable. В таком случем Presenter, только хранит состояние фрагмента и прокидывает полученые данные с interactor'а в view?
                      • 0
                        Вот ссылка на проект: https://github.com/VictoriaSlmn/Android-VIPER, а в остальном вы все поняли верно, так же в данной реализации Interactor настраивает планировщики потоков для выполнения операций в Observable и Subscription. Presenter скорее хранит не состояние фрагмента, а зависит от него, то есть в нужный момент запускает Interactor и останавливает.
                      • 0
                        Каждый Presenter использует один или несколько Interactor для работы с данными.

                        А можете привести пример когда одному презентеру надо несколько интеракторов? Потому что мне казалось, что как раз нужен только один, который и инкапсулирует разнообразную логику получения данных, например, один интерактор с несколькими провайдерами данных…

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое
                        Интересные публикации