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

ссылка на оригинал статьи https://habrahabr.ru/post/277003/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *