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

Архитектура приложения — основа качественного приложения для Android. Чётко определённая архитектура позволяет создать масштабируемое и удобное в поддержке приложение, способное адаптироваться к постоянно растущей экосистеме устройств Android, включая телефоны, планшеты, складные устройства, устройства на ChromeOS, автомобильные дисплеи и XR.

Состав приложения

Типичное приложение Android состоит из множества компонентов , таких как сервисы , поставщики контента и приёмники вещания . Эти компоненты объявляются в манифесте приложения .

Пользовательский интерфейс приложения также является компонентом. Исторически UI создавались с использованием нескольких активностей . Однако современные приложения используют архитектуру с одной активностью. Одна Activity служит контейнером для экранов, реализованных в виде фрагментов или пунктов назначения Jetpack Compose.

Несколько форм-факторов

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

Ограничения ресурсов

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

Переменные условия запуска

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

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

Если вы не можете использовать компоненты приложения для хранения данных и состояния приложения, как вам следует проектировать свое приложение?

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

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

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

Самый важный принцип — разделение ответственности . Распространенная ошибка — писать весь код в Activity или Fragment .

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

Эта эфемерность делает их непригодными для хранения данных или состояния приложения. Если данные хранятся в Activity или Fragment , они теряются при повторном создании компонента. Чтобы обеспечить сохранность данных и стабильный пользовательский интерфейс, не доверяйте управление состоянием этим компонентам пользовательского интерфейса.

Адаптивные макеты

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

Пользовательский интерфейс на основе моделей данных

Другой важный принцип заключается в том, что пользовательский интерфейс должен разрабатываться на основе моделей данных, предпочтительно постоянных. Модели данных представляют данные приложения. Они независимы от элементов пользовательского интерфейса и других компонентов приложения. Это означает, что они не привязаны к жизненному циклу пользовательского интерфейса и компонентов приложения, но всё равно будут уничтожены, когда ОС удалит процесс приложения из памяти.

Постоянные модели идеальны по следующим причинам:

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

  • Ваше приложение продолжает работать в случаях, когда сетевое соединение нестабильно или отсутствует.

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

Единый источник истины

При определении нового типа данных в вашем приложении необходимо назначить ему единый источник истины (SSOT). SSOT является владельцем этих данных, и только он может изменять или мутировать их. Для этого SSOT предоставляет данные, используя неизменяемый тип; для изменения данных SSOT предоставляет функции или получает события, которые могут вызывать другие типы.

Эта модель имеет множество преимуществ:

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

В офлайн-приложении источником данных обычно служит база данных. В некоторых других случаях источником данных может быть ViewModel .

Однонаправленный поток данных

Принцип единственного источника истины часто используется в шаблоне однонаправленного потока данных (UDF). В UDF состояние передается только в одном направлении, обычно от родительского компонента к дочернему. События, которые изменяют поток данных, действуют в противоположном направлении.

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

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

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

  • Уровень пользовательского интерфейса: отображает данные приложения на экране.
  • Уровень данных: содержит бизнес-логику вашего приложения и предоставляет данные приложения.

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

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

Современная архитектура приложений

Современная архитектура приложений для Android использует следующие методы (среди прочих):

  • Адаптивная и многоуровневая архитектура
  • Однонаправленный поток данных (UDF) во всех слоях приложения
  • Уровень пользовательского интерфейса с держателями состояний для управления сложностью пользовательского интерфейса
  • Корутины и потоки
  • Лучшие практики внедрения зависимостей

Более подробную информацию см. в разделе Рекомендации по архитектуре Android .

слой пользовательского интерфейса

Роль слоя пользовательского интерфейса (или слоя представления ) заключается в отображении данных приложения на экране. При каждом изменении данных, будь то в результате взаимодействия с пользователем (например, нажатия кнопки) или внешнего ввода (например, ответа сети), пользовательский интерфейс должен обновляться, отражая эти изменения.

Уровень пользовательского интерфейса состоит из двух типов конструкций:

  • Элементы пользовательского интерфейса, отображающие данные на экране. Эти элементы создаются с помощью функций Jetpack Compose для поддержки адаптивных макетов.
  • Держатели состояний (например, ViewModel ), которые хранят данные, предоставляют их пользовательскому интерфейсу и обрабатывают логику
В типичной архитектуре элементы пользовательского интерфейса уровня пользовательского интерфейса зависят от состояний, которые, в свою очередь, зависят от классов либо уровня данных, либо необязательного уровня предметной области.
Рисунок 2. Роль слоя пользовательского интерфейса в архитектуре приложения.

В адаптивных пользовательских интерфейсах держатели состояний, такие как объекты ViewModel предоставляют состояние пользовательского интерфейса, которое адаптируется к различным классам размеров окна . Для получения этого состояния пользовательского интерфейса можно использовать currentWindowAdaptiveInfo() . Такие компоненты, как NavigationSuiteScaffold могут затем использовать эту информацию для автоматического переключения между различными шаблонами навигации (например, NavigationBar , NavigationRail или NavigationDrawer ) в зависимости от доступного пространства на экране.

Более подробную информацию см. на странице слоя пользовательского интерфейса .

Уровень данных

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

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

В типичной архитектуре репозитории уровня данных предоставляют данные остальной части приложения и зависят от источников данных.
Рисунок 3. Роль уровня данных в архитектуре приложения.

Классы репозитория отвечают за следующее:

  • Предоставление данных остальной части приложения
  • Централизация изменений данных
  • Разрешение конфликтов между несколькими источниками данных
  • Абстрагирование источников данных от остальной части приложения
  • Содержащий бизнес-логику

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

Более подробную информацию см. на странице уровня данных .

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

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

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

При включении дополнительный уровень домена обеспечивает зависимости от уровня пользовательского интерфейса и зависит от уровня данных.
Рисунок 4. Роль доменного уровня в архитектуре приложения.

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

Более подробную информацию см. на странице доменного уровня .

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

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

  • Внедрение зависимостей (DI) : Внедрение зависимостей позволяет классам определять свои зависимости без их создания. Во время выполнения за предоставление этих зависимостей отвечает другой класс.
  • Локатор служб : Шаблон локатора служб предоставляет реестр, в котором классы могут получать свои зависимости вместо того, чтобы создавать их.

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

Общие рекомендации

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

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

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

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

Уменьшите зависимости от классов Android.

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

Определите четкие границы ответственности между модулями вашего приложения.

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

Раскрывайте как можно меньше информации из каждого модуля.

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

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

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

Используйте канонические макеты и шаблоны дизайна приложений.

Библиотеки Jetpack Compose предоставляют надежные API для создания адаптивных пользовательских интерфейсов. Используйте канонические макеты в своем приложении для оптимизации пользовательского опыта на устройствах разных форм-факторов и размеров экранов. Ознакомьтесь с галереей шаблонов дизайна приложений, чтобы выбрать макеты, которые лучше всего подходят для ваших вариантов использования.

Сохранение состояния пользовательского интерфейса при изменении конфигурации.

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

Проектируйте повторно используемые и компонуемые компоненты пользовательского интерфейса.

Создавайте компоненты пользовательского интерфейса, пригодные для повторного использования и компоновки, для поддержки адаптивного дизайна. Это позволяет комбинировать и перестраивать компоненты для соответствия различным размерам и положениям экрана без существенной переделки.

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

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

Типы отвечают за свою политику параллелизма.

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

Сохраняйте как можно больше актуальных и свежих данных.

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

Преимущества архитектуры

Реализация хорошей архитектуры в вашем приложении дает множество преимуществ проектным и инженерным группам:

  • Улучшает удобство обслуживания, качество и надежность всего приложения.
  • Позволяет масштабировать приложение. Больше людей и больше команд могут работать над одной и той же кодовой базой, минимизируя конфликты кода.
  • Помогает в адаптации. Поскольку архитектура обеспечивает единообразие в вашем проекте, новые члены команды могут быстро освоиться и работать эффективнее за меньшее время.
  • Легче тестировать. Хорошая архитектура поощряет использование более простых типов, которые, как правило, легче тестировать.
  • Ошибки можно исследовать методично, используя четко определенные процессы.

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

Образцы

Следующие примеры демонстрируют хорошую архитектуру приложения: