Архитектура приложения — это основа высококачественного Android-приложения. Четко определенная архитектура позволяет создать масштабируемое, поддерживаемое приложение, способное адаптироваться к постоянно расширяющейся экосистеме Android-устройств, включая телефоны, планшеты, складные устройства, устройства ChromeOS, автомобильные дисплеи и XR.
композиция приложения
Типичное Android-приложение состоит из множества компонентов , таких как сервисы , поставщики контента и широковещательные приемники . Эти компоненты объявляются в манифесте приложения .
Пользовательский интерфейс приложения также является компонентом. Исторически сложилось так, что пользовательские интерфейсы создавались с использованием нескольких активностей (Activity) . Однако современные приложения используют архитектуру с одной активностью. Одна Activity служит контейнером для экранов или целевых объектов Jetpack Compose.
Множественные форм-факторы
Приложения могут работать на различных форм-факторах, включая не только телефоны, но и планшеты, складные устройства , устройства ChromeOS и многое другое. Не следует предполагать, что ваше приложение всегда остается зафиксированным в портретной или альбомной ориентации. Изменения конфигурации, такие как поворот устройства или складывание и раскладывание складного устройства, заставляют ваше приложение перестраивать свой пользовательский интерфейс, что влияет на его состояние.
Ограничения ресурсов
Мобильные устройства — даже устройства с большими экранами — ограничены в ресурсах, поэтому в любой момент операционная система может остановить процесс вашего приложения, чтобы перенаправить свои ресурсы на другие процессы.
Переменные условия запуска
В условиях ограниченных ресурсов компоненты вашего приложения могут запускаться по отдельности и в произвольном порядке; более того, операционная система или пользователь могут уничтожить их в любой момент. Поэтому не храните данные или состояние приложения в его компонентах. Сделайте компоненты приложения самодостаточными и независимыми друг от друга.
Общие архитектурные принципы
Если вы не можете использовать компоненты приложения для хранения данных и состояния приложения, как же вам следует проектировать свое приложение?
По мере роста размеров Android-приложений важно определить архитектуру, которая позволит приложению масштабироваться. Хорошо продуманная архитектура приложения определяет границы между его частями и обязанности каждой из них.
Разделение интересов
При проектировании архитектуры приложения руководствуйтесь несколькими конкретными принципами.
Важнейшим принципом является разделение ответственности : разделение приложения на методы, классы, файлы, пакеты, модули и слои, каждый из которых имеет четко определенные обязанности и границы.
Распространенная ошибка — писать весь код в Activity .
Основная роль Activity — размещение пользовательского интерфейса вашего приложения. Операционная система Android контролирует их жизненный цикл, часто уничтожая и создавая их заново в ответ на действия пользователя, такие как поворот экрана, или системные события, например, нехватка памяти.
Эта эфемерная природа делает их непригодными для хранения данных или состояния приложения. Если вы храните данные в Activity , эти данные теряются при повторном создании компонента. Чтобы обеспечить сохранение данных и стабильный пользовательский опыт, не доверяйте состояние этим компонентам пользовательского интерфейса.
Адаптивные макеты
Создавайте приложения, которые корректно обрабатывают изменения конфигурации, такие как изменение ориентации устройства или изменение размера окна приложения. Внедряйте адаптивные канонические макеты для обеспечения оптимального взаимодействия с пользователем на различных форм-факторах.
Управление пользовательским интерфейсом на основе моделей данных.
Ещё один важный принцип — управлять пользовательским интерфейсом с помощью моделей данных, предпочтительно персистентных моделей. Модели данных представляют данные приложения. Они независимы от элементов пользовательского интерфейса и других компонентов приложения. Это означает, что они не привязаны к жизненному циклу компонентов пользовательского интерфейса и приложения, но всё равно будут уничтожены, когда операционная система удалит процесс приложения из памяти.
Постоянные модели идеально подходят по следующим причинам:
Пользователи не теряют данные, если операционная система Android уничтожает ваше приложение для освобождения ресурсов.
Ваше приложение продолжает работать даже в случаях, когда сетевое соединение нестабильно или отсутствует.
Основывайте архитектуру своего приложения на классах модели данных, чтобы сделать его надежным и тестируемым.
Единый источник истины
Когда в вашем приложении определяется новый тип данных, назначьте ему единый источник истины (SSOT). SSOT является владельцем этих данных, и только SSOT может их изменять или модифицировать. Для этого SSOT предоставляет доступ к данным, используя неизменяемый тип; для изменения данных SSOT предоставляет функции или принимает события, которые могут вызывать другие типы.
Этот вариант имеет множество преимуществ:
- Централизует все изменения определенного типа данных в одном месте.
- Защищает данные, предотвращая их несанкционированное изменение другими лицами.
- Это делает изменения в данных более отслеживаемыми, благодаря чему ошибки легче обнаружить.
В приложениях, ориентированных на работу в автономном режиме, источником достоверной информации о данных приложения обычно является база данных. В некоторых других случаях источником достоверной информации может быть ViewModel .
Однонаправленный поток данных
Принцип единственного источника истины часто используется в паттерне однонаправленного потока данных (UDF). В UDF состояние передается только в одном направлении, как правило, от родительского компонента к дочернему. События, изменяющие данные, передаются в противоположном направлении.
В Android состояние или данные обычно передаются от типов с более высокой областью видимости к типам с более низкой областью видимости. События обычно запускаются типами с более низкой областью видимости, пока не достигнут SSOT для соответствующего типа данных. Например, данные приложения обычно передаются из источников данных в пользовательский интерфейс. Пользовательские события, такие как нажатия кнопок, передаются из пользовательского интерфейса в SSOT, где данные приложения изменяются и предоставляются в неизменяемом типе.
Этот шаблон лучше обеспечивает согласованность данных, менее подвержен ошибкам, упрощает отладку и предоставляет все преимущества шаблона SSOT.
Для получения дополнительной информации о пользовательских функциях (UDF) см. раздел «Однонаправленный поток данных в Jetpack Compose» .
Рекомендуемая архитектура приложения
С учетом общих архитектурных принципов, проектируйте каждое приложение как минимум с двумя уровнями:
- Пользовательский интерфейс: отображает данные приложения на экране.
- Слой данных: содержит бизнес-логику вашего приложения и предоставляет доступ к данным приложения.
Для упрощения и повторного использования взаимодействия между пользовательским интерфейсом и слоями данных можно добавить дополнительный слой, называемый слоем предметной области .

Современная архитектура приложений
Современная архитектура Android-приложений использует следующие методы (среди прочих):
- Адаптивная и многоуровневая архитектура
- Однонаправленный поток данных (UDF) на всех уровнях приложения.
- Слой пользовательского интерфейса с элементами управления состоянием для управления сложностью интерфейса.
- Сопрограммы и потоки
- лучшие практики внедрения зависимостей
Для получения более подробной информации см. Рекомендации по архитектуре Android .
слой пользовательского интерфейса
Роль уровня пользовательского интерфейса (или уровня представления ) заключается в отображении данных приложения на экране. Всякий раз, когда данные изменяются, будь то в результате взаимодействия с пользователем (например, нажатия кнопки) или внешнего ввода (например, ответа от сети), пользовательский интерфейс обновляется, отражая эти изменения.
Слой пользовательского интерфейса включает в себя два типа конструкций:
- Элементы пользовательского интерфейса, отображающие данные на экране. Эти элементы создаются с помощью функций Jetpack Compose для поддержки адаптивной компоновки.
- Состояния (например,
ViewModel) — это элементы, которые хранят данные, предоставляют к ним доступ пользовательскому интерфейсу и обрабатывают логику. Время существования таких элементов должно совпадать со временем существования элемента пользовательского интерфейса, для которого они предоставляют состояние. Например, ViewModel для экрана должен храниться в памяти до тех пор, пока этот экран не будет удален из стека навигации приложения. Дополнительную информацию см. в разделе «Время жизни состояний» .

Для адаптивных пользовательских интерфейсов, такие объекты, как ViewModel предоставляют состояние интерфейса, которое адаптируется к различным классам размеров окна . Вы можете использовать currentWindowAdaptiveInfo() для получения этого состояния интерфейса. Затем такие компоненты, как NavigationSuiteScaffold могут использовать эту информацию для автоматического переключения между различными шаблонами навигации (например, NavigationBar , NavigationRail или NavigationDrawer ) в зависимости от доступного экранного пространства.
Для получения более подробной информации см. разделы «Уровень пользовательского интерфейса» и «Создание архитектуры пользовательского интерфейса» .
Для получения дополнительной информации об адаптивных приложениях и навигации см. разделы «Создание адаптивных приложений» и «Создание адаптивной навигации» .
Слой данных
Слой данных приложения содержит бизнес-логику . Бизнес-логика — это то, что придает ценность вашему приложению; она включает в себя правила, определяющие, как ваше приложение создает, хранит и изменяет данные.
Слой данных состоит из репозиториев, каждый из которых может содержать от нуля до множества источников данных. Создайте класс репозитория для каждого типа данных, которые вы обрабатываете в своем приложении. Например, вы можете создать класс MoviesRepository для данных, связанных с фильмами, или класс PaymentsRepository для данных, связанных с платежами.

Классы репозитория отвечают за следующее:
- Предоставление доступа к данным остальной части приложения.
- Централизация изменений данных
- Разрешение конфликтов между несколькими источниками данных.
- Абстрагирование источников данных от остальной части приложения.
- Содержит бизнес-логику
Каждый класс источника данных отвечает за работу только с одним источником данных, которым может быть файл, сетевой источник или локальная база данных. Классы источников данных выступают связующим звеном между приложением и системой для выполнения операций с данными.
Для получения более подробной информации посетите страницу, посвященную слою данных .
Уровень предметной области
Слой предметной области — это необязательный слой, расположенный между слоями пользовательского интерфейса и данных.
Слой предметной области отвечает за инкапсуляцию сложной или более простой бизнес-логики, которая используется несколькими моделями представления. Слой предметной области является необязательным, поскольку не все приложения предъявляют такие требования. Используйте его только при необходимости — например, для обработки сложной логики или для обеспечения возможности повторного использования.

Классы на уровне предметной области обычно называются вариантами использования или интеракторами . Каждый вариант использования отвечает за одну конкретную функциональность. Например, в вашем приложении может быть класс GetTimeZoneUseCase если несколько моделей представления используют часовые пояса для отображения соответствующего сообщения на экране.
Для получения более подробной информации посетите страницу, посвященную доменному слою .
Управление зависимостями между компонентами
Классы в вашем приложении зависят от других классов для корректной работы. Для определения зависимостей конкретного класса можно использовать один из следующих шаблонов проектирования:
- Внедрение зависимостей (DI) : Внедрение зависимостей позволяет классам определять свои зависимости без их создания. Во время выполнения другой класс отвечает за предоставление этих зависимостей.
- Паттерн "локатор сервисов ": предоставляет реестр, где классы могут получать свои зависимости вместо их создания.
Эти шаблоны позволяют масштабировать код, поскольку предоставляют четкие схемы управления зависимостями без дублирования кода или усложнения. Шаблоны также позволяют быстро переключаться между тестовой и производственной реализациями.
Общие передовые методы
Программирование — это творческая область, и разработка приложений для Android не является исключением. Существует множество способов решения проблем: можно передавать данные между несколькими активностями или фрагментами, получать удаленные данные и сохранять их локально для автономного режима, или обрабатывать множество других распространенных сценариев, с которыми сталкиваются нетривиальные приложения.
Хотя следующие рекомендации не являются обязательными, в большинстве случаев их следование делает ваш код более надежным, тестируемым и удобным для сопровождения.
Не храните данные в компонентах приложения.
Избегайте назначения точек входа вашего приложения — таких как действия, сервисы и широковещательные приемники — источниками данных. Настройте точки входа так, чтобы они координировали работу с другими компонентами для получения только того подмножества данных, которое имеет отношение к данной точке входа. Каждый компонент приложения существует недолго, в зависимости от взаимодействия пользователя с устройством и возможностей системы.
Уменьшите зависимость от классов Android.
Сделайте так, чтобы компоненты вашего приложения использовали только API SDK фреймворка Android, такие как Context или Toast . Абстрагирование других классов в вашем приложении от компонентов улучшает тестируемость и уменьшает связанность внутри приложения.
Четко определите границы ответственности между модулями вашего приложения.
Не следует распределять код, загружающий данные из сети, по нескольким классам или пакетам в кодовой базе. Аналогично, не следует определять несколько несвязанных задач, таких как кэширование данных и привязка данных, в одном классе. Следуйте рекомендуемой архитектуре приложения .
Сведите к минимуму доступ к содержимому каждого модуля.
Не создавайте упрощенные решения, раскрывающие внутренние детали реализации. В краткосрочной перспективе это может сэкономить вам немного времени, но в дальнейшем, по мере развития вашего кода, вы, скорее всего, многократно увеличите свой технический долг.
Сосредоточьтесь на уникальной сути вашего приложения, чтобы оно выделялось среди других.
Не изобретайте велосипед, переписывая один и тот же шаблонный код снова и снова. Вместо этого сосредоточьте свое время и энергию на том, что делает ваше приложение уникальным. Пусть библиотеки Jetpack и другие рекомендуемые библиотеки возьмут на себя повторяющийся шаблонный код.
Используйте канонические макеты и шаблоны проектирования приложений.
Библиотеки Jetpack Compose предоставляют мощные API для создания адаптивных пользовательских интерфейсов. Используйте канонические макеты в своем приложении, чтобы оптимизировать взаимодействие с пользователем на различных форм-факторах и размерах экрана. Ознакомьтесь с галереей шаблонов проектирования приложений, чтобы выбрать макеты, которые лучше всего подходят для ваших задач.
Сохранение состояния пользовательского интерфейса при изменении конфигурации.
При проектировании адаптивных макетов необходимо сохранять состояние пользовательского интерфейса при изменении конфигурации, например, при изменении размера экрана, сворачивании и ориентации. Ваша архитектура должна гарантировать сохранение текущего состояния пользователя, обеспечивая бесперебойную работу.
Разрабатывайте многократно используемые и компонуемые компоненты пользовательского интерфейса.
Создавайте многократно используемые и компонуемые компоненты пользовательского интерфейса для поддержки адаптивного дизайна. Это позволяет комбинировать и переставлять компоненты для соответствия различным размерам экрана и положениям без существенной рефакторизации.
Подумайте, как сделать каждую часть вашего приложения тестируемой изолированно.
Четко определенный API для получения данных из сети упрощает тестирование модуля, который сохраняет эти данные в локальной базе данных. Если же вы смешаете логику этих двух функций в одном месте или распределите сетевой код по всей кодовой базе, тестирование станет намного сложнее, если не невозможным.
Типы отвечают за свою политику параллельного доступа.
Если тип выполняет длительную блокирующую работу, он должен отвечать за перенос этих вычислений в нужный поток. Тип знает, какие вычисления он выполняет и в каком потоке их следует запускать. Типы должны быть безопасны для основного потока, то есть их можно безопасно вызывать из основного потока, не блокируя его.
Старайтесь сохранять как можно больше актуальных и достоверных данных.
Таким образом, пользователи смогут пользоваться функционалом вашего приложения, даже когда их устройство находится в автономном режиме. Помните, что не всем вашим пользователям необходимо постоянное высокоскоростное подключение к интернету, и даже если оно им необходимо, в людных местах может наблюдаться плохой сигнал.
Преимущества архитектуры
Наличие хорошо продуманной архитектуры в вашем приложении приносит много пользы проектной и инженерной командам:
- Улучшает удобство сопровождения, качество и надежность приложения в целом.
- Это позволяет масштабировать приложение. Больше людей и команд могут вносить свой вклад в одну и ту же кодовую базу с минимальным количеством конфликтов.
- Помогает в адаптации новых членов команды. Поскольку архитектура обеспечивает согласованность проекта, новые участники команды могут быстро освоиться и работать эффективнее за меньшее время.
- Проще тестировать. Хорошая архитектура поощряет использование более простых типов, которые, как правило, проще тестировать.
- Позволяет методично исследовать ошибки с помощью четко определенных процессов.
Хотя хорошая архитектура требует первоначальных временных затрат, она также оказывает прямое влияние на пользователей. Они получают выгоду от более стабильного приложения и большего количества функций благодаря более продуктивной работе команды разработчиков.
Образцы
Следующие примеры демонстрируют хорошую архитектуру приложения: