В разрезе: новостной агрегатор на Android с бэкендом. Разработка под Android: выработка архитектуры

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

    В водной статье я уже писал о том, что планируемым клиентом для проекта должен стать клиент 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

    Спасибо за внимание!
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 2
    • 0
      Не смотрели в сторону Moxy (https://github.com/Arello-Mobile/Moxy) для реализации MVP?
      • 0
        Откровенно говоря — просто не обратил внимания, когда первый раз услышал. Думаю, что Moxy не заменяет архитектуру приложения, лишь унифицирует и упорядочивает часть UI+Business, не вводя других требований к архитектуре, например в части зависимостей уровней.

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

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