Вводная часть (со ссылками на все статьи)

В водной статье я уже писал о том, что планируемым клиентом для проекта должен стать клиент Android: доступный большой аудитории, лёгкий, функциональный, красивый, быстрый (не приложение, а мечта!). Если с основаниями выбора платформы всё понятно, то с тем как реализовывать на базе неё все перечисленные требования – ясно было далеко не всё.

Ранее разработкой под Android не занимался поэтому достаточно ценными источниками информации для меня являлись:


После изучения указанных источников вопросов с архитектурой Android и взаимодействия их компонентов не осталось. Однако остался один наиважнейший вопрос: какова будет структура самого приложения? Пара примеров и прототипов показала, при росте функционала всё быстро начинало превращаться в «лапшу»:

  • Логика работы с объектами Android (Activity, Preferences, TextView ….) перемешивалась с бизнес-логикой;
  • Объекты хранения фигурировали в коде построения интерфейса;
  • Модульное тестирование превращалось в ад из-за необходимости работы с родными объектами Android и их подмены экземплярами Robolectric;
  • Проверка асинхронного кода была возможна только на устройстве или эмуляторе (по принципу: «запустил-проверил-повторил»).

Стало понятно, что нужно сделать шаг назад и осмотреться: Android существует не первый год, есть люди, которые разрабатывают код продолжительное время под эту платформу, есть большие развивающиеся проекты – соответственно есть откуда подчерпнуть информацию о хорошо зарекомендовавших себя практиках.

Основными критериями в поиске хорошей архитектуры для Android-приложения были:

  • лёгкая тестируемость разрабатываемого кода и его компонентов — легко тестируемый код просто развивать и изменять без страха создать баг или «свалить» приложение;
  • слабая связанность компонентов, при которой части приложения/компоненты могут разрабатываться разными разработчиками без необходимости сверхинтенсивного взаимодействия (хотя бы какое-то время).

Поиски привели меня к интересному ролику на YouTube: «Пишем тестируемый код» (запись выступления Евгения Мацюк(а) с конференции по мобильной разработке Mobius) (там было МНОГО ВСЕГО!), в котором описывалось то, что было мне нужно. Для реализации потребовалось изучить некоторые дополнительные ресурсы и инструменты:


Разработка прототипа с указанными практиками совместно с изучением RxJava заняла немало времени, однако через какое-то время был готов первый прототип. Отличительной особенностью его являлось ужасное количество создаваемых интерфейсов и классов при добавлении новых экранов: 3 интерфейса и 3 класса (Activity/Fragment и его интерфейс, Presenter и его интерфейс, Interactor и его интерфейс) – классический пример overengineering’а. Формально к текущему моменту ничего не поменялось, но я полагаю это оборотная сторона получаемых преимуществ. Зато на выходе получаем легко тестируемое приложение со слабо связанной структурой.

Реализация


Приведу для освежения в памяти компоненты Clean Architecture из статьи на Habr’е «Заблуждения Clean Architecture».
image
Каждый компонент Android и элемент выбранной архитектуры представлены в следующей таблице:
Класс Уровень Реализуемые интерфейсы Назначение
Реализация Activity/Fragment (XXXX_Activity / XXXX_Fragment) UI I_XXXX_View Фактическая реализация действия с элементами Android: изменение свойств, получение обратных вызовов, старт сервисов, работа с Android API
XXXX_PresenterImpl UI I_XXXX_Presenter Координация действий уровня представления, логика представления – вызовы методов интерфейсов I_XXXX_View, I_XXXX_Interactor
XXXX_InteractorImpl Business/Use Cases I_XXXX_Interactor Реализация основной логики приложения, вызовы методов интерфейсов I_XXXX_Repository
XXXX_RepositoryImpl Data/Repository I_XXXX_Repository Реализация непосредственного взаимодействия с источниками данных, внешними API, сетью и БД Android, ContentProvider’ами и т.д.


Организация взаимодействия


Взаимодействие компонентов и передача данных организована с учётом того, что пользователь любого Android- приложения больше получает данных чем, вводит их. Соответственно:

  • передача сигналов в более глубокие слои идёт через обычные синхронные вызовы (нажали кнопку/прокрутили/ввели данные -> вызвали метод);
  • получение данных из нижних слоёв организовано через асинхронные Rx-потоки (получили вызов -> выслали данные с результатами);
  • минимизировано синхронное получение данных (большая часть в коде инициализации и в других вспомогательных и редких экранах).

Организация пакетов


В оригинальной статье Fernando Cejas предлагалось 2 варианта организации «по уровням» и «по функционалу», я для себя выработал комбинированный подход:

  • Вначале по уровням (ui, data, business)
  • В «ui» по основным экранам «news_watcher», «news_tape» и т.д.
  • В «data» и «business» — по основным сущностям «news_header», «news_article» и т.д.

Интересной особенностью стало, то что количество Interactor’ов стало равно «кол-во основных экранов» + «кол-во сущностей»: нередки ситуации, когда требуется организовать хитрое получение данных (например, с комбинированием из разных источников) и копировать данный код в каждый Interactor, где он требуется совершенно не хотелось. При этом с учётом того, что Interactor используются в единственном экземпляре – они могут хранить некое состояние, важное для выполнения метода, я реализовал это следующим образом: Interactor’ы экранов, обращаются к Interactor’ам сущностей за соответствующими методами (что приводит к появлению делегирующих методов в Interactor’ах экранов).

Инициализация


  • Activity/Fragment:
    1. создаётся Android (non-singletone, w/ scope)
    2. инициализируется в методах View#onCreate() (с завершением в Fragment#onViewCreated() для Fragment)
    3. Presenter внутри присваивается ч/з Dagger2
    4. инициализация Presenter внутри осуществляется в указанных методах (View#onCreate(), с завершением в Fragment#onViewCreated() для Fragment)
  • Presenter:
    1. создаётся через Dagger2 (non-singletone, w/ scope)
    2. View присваивается самим View, ч/з Presenter#bindView()
    3. инициализируется в методе Presenter#initializePresenter(), вызываемой View (потому что инициализацию нужно делать в подходящий момент, после инициализации View)
    4. Interactor внутри присваивается/инициализируется ч/з Dagger2
    5. создание связи Interactor->Presenter выполняется в методе Presenter#initializePresenter() (ч/з другие методы Interactor'а для Rx-инициализации)
  • Interactor:
    1. создаётся через Dagger2 (singletone, w/o scope)
    2. инициализируется через Dagger2 (Interactor#initializeInteractor)
    3. Repository внутри присваивается/инициализируется ч/з Dagger2
  • Repository:
    1. создаётся через Dagger2 (singletone, w/o scope)
    2. инициализируется через Dagger2 (Repository#initializeRepository)

Подходы к тестированию


С точки зрения тестирования – ничего революционного:

  • UI уровень – JUnit + Mockito + Robolectric
  • Business уровень – JUnit + Mockito
  • Data уровень — JUnit + Mockito

Спасибо за внимание!

Комментарии (2)


  1. Mihail57
    29.11.2017 17:44

    Не смотрели в сторону Moxy (https://github.com/Arello-Mobile/Moxy) для реализации MVP?


    1. fedor_malyshkin Автор
      29.11.2017 21:10

      Откровенно говоря — просто не обратил внимания, когда первый раз услышал. Думаю, что Moxy не заменяет архитектуру приложения, лишь унифицирует и упорядочивает часть UI+Business, не вводя других требований к архитектуре, например в части зависимостей уровней.

      Из того, что прочитал про Moxy сразу понятно, что разработку UI она сильно облегчает жизнь, но (IMHO) скрывает часть Android за своим собственным жизненным циклом: на моём уровне изучения Android я бы этого не хотел :)