Рекомендации по архитектуре Android

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

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

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

Многоуровневая архитектура

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

Рекомендация Описание
Используйте четко определенный слой данных . Слой данных предоставляет доступ к данным приложения остальной части приложения и содержит подавляющее большинство бизнес-логики вашего приложения.
  • Создавайте репозитории , даже если они содержат только один источник данных.
  • В небольших приложениях вы можете размещать типы слоев данных в пакете data или модуле.
Используйте четко определенный слой пользовательского интерфейса . Слой пользовательского интерфейса отображает данные приложения на экране и служит основной точкой взаимодействия с пользователем. Jetpack Compose — это рекомендуемый современный инструментарий для создания пользовательского интерфейса вашего приложения.
  • В небольших приложениях можно размещать типы данных в пакете или модуле ui .
Для получения дополнительной информации о передовых методах работы с пользовательским интерфейсом см. раздел «Пользовательский интерфейс» .
Предоставьте доступ к данным приложения из уровня данных с помощью репозитория.

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

  • Базы данных, DataStore, SharedPreferences, API Firebase.
  • Поставщики услуг определения местоположения по GPS.
  • Поставщики данных Bluetooth.
  • Поставщики информации о состоянии сетевого подключения.
Используйте сопрограммы и потоки выполнения . Используйте сопрограммы и потоки для взаимодействия между уровнями.

Для получения дополнительной информации о лучших практиках использования сопрограмм см. раздел «Лучшие практики использования сопрограмм в Android» .

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

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

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

Рекомендация Описание
Следуйте однонаправленному потоку данных (UDF) . Следуйте принципам однонаправленного потока данных (UDF) , где ViewModels предоставляют доступ к состоянию пользовательского интерфейса, используя шаблон наблюдателя, и получают действия от пользовательского интерфейса посредством вызовов методов.
Используйте AAC ViewModels, если их преимущества применимы к вашему приложению. Используйте AAC ViewModels для обработки бизнес-логики и получения данных приложения, чтобы предоставить пользователю доступ к состоянию пользовательского интерфейса.

Для получения дополнительной информации о передовых методах работы с ViewModel см. раздел «Рекомендации по архитектуре».

Для получения дополнительной информации о преимуществах ViewModel см. статью «ViewModel как хранилище состояния бизнес-логики».

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

Подробнее о функции collectAsStateWithLifecycle можно прочитать здесь.

Не отправляйте события из ViewModel в пользовательский интерфейс. Обработайте событие немедленно в ViewModel и вызовите обновление состояния с результатом обработки события. Для получения дополнительной информации о событиях пользовательского интерфейса см. раздел « Обработка событий ViewModel» .
Используйте приложение, предназначенное для выполнения одной задачи. Используйте Navigation 3 для навигации между экранами и создания прямых ссылок на ваше приложение, если оно состоит из нескольких экранов.
Используйте Jetpack Compose . Используйте Jetpack Compose для создания новых приложений для телефонов, планшетов, складных устройств и Wear OS.

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

  @Composable
  fun MyScreen(
      viewModel: MyViewModel = viewModel()
  ) {
      val uiState by viewModel.uiState.collectAsStateWithLifecycle()
  }

ViewModel

ViewModels отвечают за предоставление состояния пользовательского интерфейса и доступ к слою данных. Вот несколько рекомендаций по использованию ViewModels:

Рекомендация Описание
Сохраняйте независимость ViewModel от жизненного цикла Android. В ViewModel не следует хранить ссылки на какие-либо типы, связанные с жизненным циклом. Не передавайте Activity , Context или Resources в качестве зависимостей. Если чему-то в ViewModel требуется Context , тщательно проверьте, находится ли это на нужном уровне.
Используйте сопрограммы и потоки выполнения .

Модель представления взаимодействует с уровнями данных или предметной области следующим образом:

  • Kotlin-процессы для приема данных приложения
  • suspend функции для выполнения действий с использованием viewModelScope
Используйте ViewModel на уровне экрана.

Не используйте ViewModels в многократно используемых элементах пользовательского интерфейса. ViewModels следует использовать в следующих случаях:

  • Компоненты экранного уровня,
  • Действия/фрагменты в Views,
  • Пункты назначения или графики при использовании Jetpack Navigation .
Используйте простые классы-хранилища состояния в многократно используемых компонентах пользовательского интерфейса. Для обработки сложных данных в многократно используемых компонентах пользовательского интерфейса используйте простые классы-хранилища состояния . В этом случае состояние можно будет перемещать и контролировать извне.
Не используйте AndroidViewModel . Используйте класс ViewModel , а не AndroidViewModel . Не используйте класс Application в ViewModel. Вместо этого перенесите зависимость в пользовательский интерфейс или слой данных.
Предоставьте доступ к состоянию пользовательского интерфейса. Сделайте так, чтобы ваши ViewModel предоставляли данные пользовательскому интерфейсу через одно свойство с именем uiState . Если пользовательский интерфейс отображает несколько несвязанных между собой элементов данных, ViewModel может предоставлять несколько свойств состояния пользовательского интерфейса .
  • Превратите uiState в StateFlow .
  • Создайте uiState используя оператор stateIn с политикой WhileSubscribed(5000) , если данные поступают в виде потока данных из других уровней иерархии. (См. этот пример кода .)
  • В более простых случаях, когда потоки данных не поступают из слоя данных, допустимо использовать MutableStateFlow представленный как неизменяемый StateFlow .
  • Вы можете выбрать класс данных ${Screen}UiState , который может содержать данные, ошибки и сигналы загрузки. Этот класс также может быть закрытым классом, если различные состояния являются взаимоисключающими.

В следующем фрагменте кода показано, как предоставить доступ к состоянию пользовательского интерфейса из ViewModel:

@HiltViewModel
class BookmarksViewModel @Inject constructor(
    newsRepository: NewsRepository
) : ViewModel() {

    val feedState: StateFlow<NewsFeedUiState> =
        newsRepository
            .getNewsResourcesStream()
            .mapToFeedState(savedNewsResourcesState)
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = NewsFeedUiState.Loading
            )

    // ...
}

Жизненный цикл

Следуйте передовым методам работы с жизненным циклом действий :

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

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

  • Используйте LifecycleStartEffect для выполнения синхронной работы при запуске и остановке вашей активности.
  • Используйте LifecycleResumeEffect для выполнения синхронной работы при возобновлении и приостановке вашей активности.
  • Используйте repeatOnLifecycle для выполнения асинхронной работы в ответ на события жизненного цикла.
  • Собирайте асинхронные данные из Flows с помощью collectAsStateWithLifecycle .

В следующем фрагменте кода описано, как выполнять операции, учитывая определенное состояние жизненного цикла:

  @Composable
  fun LocationChangedEffect(
    locationManager: LocationManager,
    onLocationChanged: (Location) -> Unit
  ) {
    val currentOnLocationChanged by rememberUpdatedState(onLocationChanged)

    LifecycleStartEffect(locationManager) {
        val listener = LocationListener { newLocation ->
            currentOnLocationChanged(newLocation)
        }

        try {
            locationManager.requestLocationUpdates(
                LocationManager.GPS_PROVIDER,
                1000L,
                1f,
                listener,
            )
        } catch (e: SecurityException) {
            // TODO: Handle missing permissions
        }

        onStopOrDispose {
            locationManager.removeUpdates(listener)
        }
    }
  }

Обработка зависимостей

При управлении зависимостями между компонентами следуйте передовым практикам:

Рекомендация Описание
Используйте внедрение зависимостей . Используйте лучшие практики внедрения зависимостей , в основном, по возможности, внедрение через конструктор .
При необходимости ограничьте область действия отдельным компонентом. Область видимости ограничивается контейнером зависимостей , если тип содержит изменяемые данные, которые необходимо совместно использовать, или если инициализация типа является ресурсоемкой и широко используется в приложении.
Используйте рукоять . В простых приложениях используйте Hilt или ручное внедрение зависимостей . Hilt следует использовать, если ваш проект достаточно сложен — например, если он включает в себя что-либо из перечисленного ниже:
  • Несколько экранов с использованием ViewModels
  • Использует WorkManager
  • Имеет ViewModel, область видимости которой ограничена стеком возврата в навигации.

Тестирование

Ниже приведены некоторые рекомендации по тестированию :

Рекомендация Описание
Знайте, что нужно проверять .

Если проект не сводится к простому приложению "Hello World", протестируйте его. Как минимум, включите следующее:

  • Модульные тесты для ViewModel, включая Flow-тесты.
  • Модульные тесты для сущностей уровня данных, то есть для репозиториев и источников данных.
  • Тесты навигации пользовательского интерфейса, полезные в качестве регрессионных тестов в CI.
Предпочитайте подделки насмешкам. Для получения дополнительной информации об использовании поддельных файлов см. раздел «Использование тестовых дубликатов в Android» .
Тестирование потоков состояний. При тестировании StateFlow выполните следующие действия:

Для получения дополнительной информации см. разделы «Что тестировать в Android» и «Тестируйте макет Compose» .

Модели

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

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

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

  • Удаленный источник данных может сопоставить модель, полученную по сети, с более простым классом, содержащим только те данные, которые необходимы приложению.
  • Репозитории могут сопоставлять модели DAO с более простыми классами данных, содержащими только ту информацию, которая необходима уровню пользовательского интерфейса.
  • В классы UiState класс ViewModel может включать модели уровня данных.

Правила именования

При именовании кода следует учитывать следующие рекомендации:

Рекомендация Описание
Методы именования.
Необязательный
Используйте глагольные фразы для именования методов — например, makePayment() .
Наименование свойств.
Необязательный
Используйте именные группы для обозначения свойств — например, inProgressTopicSelection .
Именование потоков данных.
Необязательный
Когда класс предоставляет доступ к потоку Flow или любому другому потоку, используется соглашение об именовании get{model}Stream . Например, getAuthorStream(): Flow<Author> . Если функция возвращает список моделей, используйте имя модели во множественном числе: getAuthorsStream(): Flow<List<Author>> .
Реализации интерфейсов именования.
Необязательный
Используйте осмысленные имена для реализаций интерфейсов. Используйте префикс Default , если не удается найти более подходящее имя. Например, для интерфейса NewsRepository может использоваться OfflineFirstNewsRepository или InMemoryNewsRepository . Если подходящее имя найти не удается, используйте DefaultNewsRepository . Для фиктивных реализаций используйте префикс Fake , например, FakeAuthorsRepository .

Дополнительные ресурсы

Для получения более подробной информации об архитектуре Android см. следующие дополнительные ресурсы:

Документация

Просмотры контента