[Перевод] Гайд по архитектуре приложений для Android. Часть 1: обзор

В конце декабря 2021-го Android обновил рекомендации по архитектуре мобильных приложений. Публикуем перевод гайда в пяти частях:

Обзор архитектуры (вы находитесь здесь)

Слой UI

События UI

Доменный слой

Слой данных

В этом гайде собраны лучшие практики и рекомендации по архитектуре для создания надежных и качественных приложений.

Взаимодействие пользователя с мобильным приложением

Как правило, приложение под Android состоит из множества компонентов, включая Activity, Fragment, Service, Content Provider и Broadcast Receiver. Большинство компонентов приложения разработчик описывает в манифесте приложения. Далее с помощью этого файла Android OS решает, как интегрировать приложение в пользовательский интерфейс устройства.

Учитывая, что среднестатистическое приложение для Android может содержать множество компонентов, а пользователи зачастую взаимодействуют с несколькими приложениями в течение короткого промежутка времени, приложениям необходимо адаптироваться к разнообразным процессам и задачам пользователя.

Мобильные устройства ограничены в ресурсах: операционная система в любое время может убить какой-нибудь процесс приложения, чтобы освободить место для новых. Не исключено, что компоненты приложения будут запускаться по отдельности и в неправильном порядке, а операционная система или пользователь в любой момент смогут их прибить.

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

Общепринятые архитектурные принципы

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

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

  • разделения ответственностей,

  • построения UI на основе модели данных.

Разделение ответственностей

Принцип разделения ответственностей — самый важный.

Писать весь код в Activity или Fragment — распространённая ошибка. Эти классы находятся в слое UI: в них должна содержаться только логика, которая обрабатывает взаимодействия UI и операционной системы. Если максимально облегчить эти классы, можно избежать проблем с жизненным циклом компонентов и упростить тестирование классов.

Вы не владеете реализациями Activity и Fragment — они, скорее, просто связующие звенья, которые представляют собой контракт между ОС Android и приложением. ОС в любой момент может их уничтожить при определенных взаимодействиях пользователя с приложением или состояниях системы — например, при недостатке оперативной памяти. Если хотите создать приемлемый пользовательский опыт и облегчить обслуживание приложения, лучше свести к минимуму зависимости от них.

Построение UI на основе моделей данных

UI нужно строить на основе моделей данных. Желательно — поддерживающих постоянное хранение данных. 

Модели данных дают представление о данных, используемых приложением. Они:

  • Не зависят от элементов UI и других компонентов.

  • Не ограничены жизненным циклом компонентов приложения и UI.

  • Будут уничтожены, когда ОС решит удалить из памяти процесс приложения.

Модели, поддерживающие постоянное хранение данных, идеально подходят по следующим причинам:

  • Пользователи не потеряют данные, если Android закроет приложение, чтобы освободить ресурсы.

  • Приложение продолжит работать, если подключение к Интернету нестабильно или недоступно.

Построив архитектуру приложения на основе классов моделей данных, вы сделаете его тестируемым и надежным.

Рекомендации по созданию архитектуры приложения

Важно: эти рекомендации и методы помогут сделать приложения масштабируемыми, более качественными и надёжными, а также облегчат тестирование. Тем не менее, их стоит рассматривать как примерный план, который следует подстраивать под собственные нужды.

Согласно общепринятым архитектурным принципам у каждого приложения должно быть как минимум два слоя:

  • Слой UI, который отображает данные приложения на экране.

  • Слой данных, который содержит бизнес-логику приложения и открывает доступ к данным приложения.

К ним можно добавить ещё один слой — доменный. Он помогает упростить и переиспользовать взаимодействия между слоями UI и данных.

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

Слой UI

Роль слоя UI (или слоя представления) — отображать на экране данные приложения. Если данные меняются — из-за взаимодействия с пользователем (например, нажатия кнопки) или внешнего воздействия (например, отклика сети) — UI должен обновиться и отразить изменения.

Слой UI состоит из двух частей:

  • Элементов UI, которые отображают данные на экране. Создаются с помощью функций Jetpack Compose или View.

  • Экземпляров state holder (например, классов ViewModel), которые хранят данные, открывают к ним доступ для UI и работают с логикой.

Роль слоя UI в архитектуре приложенийРоль слоя UI в архитектуре приложений

Подробнее об этом слое можно почитать в гайде «Слой UI».

Слой данных

Слой данных в приложении содержит бизнес-логику — правила, по которым приложение создаёт, хранит и изменяет данные. Именно благодаря бизнес-логике приложение имеет ценность.

Слой данных состоит из классов Repository. В каждом может содержаться множество классов DataSource или не быть ни одного. Отдельный класс Repository следует создать для каждого уникального типа данных в приложении. Например, у вас может быть класс MoviesRepository для данных, связанных с фильмами, или класс PaymentsRepository для данных, связанных с платежами.

Роль слоя данных в архитектуре приложенияРоль слоя данных в архитектуре приложения

Классы Repository:

  • Открывают доступ к данным для остальных элементов приложения.

  • Централизуют изменения в данных.

  • Разрешают конфликты между несколькими классами DataSource.

  • Абстрагируют классы DataSource от остальных элементов приложения.

  • Содержат бизнес-логику.

Каждый DataSource-класс должен отвечать за работу только с одним источником данных. Им может оказаться файл, сетевой источник или локальная база данных. DataSource-классы — связующее звено между приложением и системой для работы с данными.

Подробнее об этом слое можно почитать в гайде «Слой данных».

Доменный слой

Доменный слой располагается между слоями UI и данных.

Доменный слой отвечает за инкапсуляцию сложной бизнес-логики или простой бизнес-логики, которую переиспользуют несколько ViewModel. 

Этот слой необязателен: не всем приложениям он нужен. Внедрять его следует только при необходимости. Например, при работе со сложной логикой или чтобы переиспользовать логику.

Роль доменного слоя в архитектуре приложенияРоль доменного слоя в архитектуре приложения

Классы этого слоя, как правило, называют UseCase или Interactor. Каждый UseCase должен отвечать только за одну функциональность. Например, в приложении может быть один класс GetTimeZoneUseCase и несколько ViewModel, которые в зависимости от часового пояса отображают соответствующее сообщение на экране.

Подробнее об этом слое можно почитать в гайде «Доменный слой».

Управление зависимостями между компонентами

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

  • Dependency injection (DI). Благодаря DI, классы могут описывать свои зависимости, не создавая их. Во время выполнения программы за предоставление этих зависимостей отвечает другой класс.

  • Service Locator. Шаблон Service Locator предоставляет реестр, из которого классы могут получать зависимости, не создавая их самостоятельно.

Эти шаблоны позволяют масштабировать код: они дают простой способ управления зависимостями, не дублируют и не усложняют код. Кроме того, они позволяют быстро переключаться между тестовой и релизной версией приложения.

В приложениях для Android рекомендуется применять шаблоны DI и использовать библиотеку Hilt. Hilt автоматически создаёт объекты, проходя по дереву зависимостей, проверяет и гарантирует наличие зависимостей на этапе компиляции и создаёт контейнеры зависимостей для классов Android-фреймворка.

Основные рекомендации

Программирование — творческая область, и разработка приложений под Android не исключение. Задачи можно решать множеством способов: передавать данные между всевозможными Activity и Fragment, загружать удалённые данные и сохранять их в локальном хранилище, если они понадобятся в офлайн-режиме, или выбирать любые другие сценарии.

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

Не храните данные в компонентах приложения

Старайтесь не назначать точки входа в приложение (например, Activity, Service и Broadcast Receiver) источниками данных. Эти элементы просто взаимодействуют с другими компонентами, чтобы получить подмножество данных, соответствующее той или иной точке входа. Жизненный цикл у любого компонента приложения может оказаться довольно коротким — в зависимости от взаимодействий приложения с пользователем и общего состояния системы.

Сократите число зависимостей от классов Android

Компоненты приложения должны быть единственными классами, зависящими от таких API SDK Android фреймворка, как Context или Toast. Абстрагируя от них другие классы приложения, вы облегчаете тестирование приложения и ослабляете в нём связность.

Задавайте чёткие границы ответственности между различными модулями приложения

Например, не распространяйте по нескольким классам или пакетам приложения код, который подгружает данные из сети. Не стоит задавать несколько несвязанных друг с другом ответственностей (например, кэширование данных и их привязку) одному и тому же классу. Следуйте рекомендациям по архитектуре приложения.

Предоставляйте доступ только к необходимым компонентам модуля

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

Сконцентрируйтесь на уникальной сущности своего приложения, чтобы оно отличалось от остальных

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

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

К примеру, у вас есть чётко описанный API, который получает данные из сети. Куда проще будет тестировать модуль, который поддерживает хранение этих данных в локальной базе данных. Если же вы смешаете логику двух этих модулей вместе или рассредоточите сетевой код по всей кодовой базе, эффективно его протестировать станет куда сложнее — если вообще получится.

Поддерживайте хранение как можно большего объема необходимых свежих данных

Таким образом пользователи смогут без проблем пользоваться функциональностью приложения, даже если их устройство работает без доступа к интернету. Не забывайте, что не у всех пользователей есть доступ к постоянному высокоскоростному интернет-соединению. Например, в людных местах устройства могут хуже ловить сигнал.

Примеры

Ниже представлены примеры приложений от Google с качественной архитектурой. Переходите по ссылкам и посмотрите, как наши рекомендации выглядят на практике:

Читайте далее

Слой UI

События UI

Доменный слой

Слой данных

© Habrahabr.ru