1. Введение
В этой лаборатории вы узнаете, как создавать адаптивные приложения для телефонов, планшетов и складных устройств, а также как они повышают доступность с помощью Jetpack Compose. Вы также изучите лучшие практики использования компонентов и тем Material 3.
Прежде чем мы углубимся, важно понять, что мы подразумеваем под адаптивностью.
Адаптивность
Пользовательский интерфейс вашего приложения должен адаптироваться к различным размерам, ориентациям и форм-факторам окон. Адаптивный макет меняется в зависимости от доступного ему места на экране. Эти изменения варьируются от простой настройки макета для заполнения пространства, выбора соответствующих стилей навигации до полного изменения макета для использования дополнительного пространства.
Чтобы узнать больше, ознакомьтесь с разделом Адаптивный дизайн .
В этой лабораторной работе вы узнаете, как использовать и подумать об адаптивности при использовании Jetpack Compose. Вы создаете приложение под названием Reply, которое показывает, как реализовать адаптивность для всех типов экранов и как адаптивность и доступность работают вместе, чтобы предоставить пользователям оптимальное взаимодействие.
Что вы узнаете
- Как разработать приложение для всех размеров окон с помощью Jetpack Compose.
- Как настроить таргетинг вашего приложения на различные складные элементы.
- Как использовать различные типы навигации для лучшей доступности и доступности.
- Как использовать компоненты Material 3, чтобы обеспечить наилучшее взаимодействие с окном любого размера.
Что вам понадобится
- Последняя стабильная версия Android Studio .
- Виртуальное устройство Android 13 с изменяемым размером .
- Знание Котлина.
- Базовое понимание Compose (например, аннотации
@Composable
). - Базовое знакомство с макетами Compose (например,
Row
иColumn
). - Базовое знакомство с модификаторами (например,
Modifier.padding()
).
Для этой лаборатории кода вы будете использовать эмулятор Resizable , который позволяет переключаться между различными типами устройств и размерами окон.
Если вы не знакомы с Compose, рассмотрите возможность пройти лабораторную работу по основам Jetpack Compose, прежде чем приступить к ее выполнению.
Что ты построишь
- Интерактивное приложение-клиент электронной почты под названием «Ответ», использующее лучшие практики для адаптируемого дизайна, различной навигации по материалам и оптимального использования экранного пространства.
2. Настройте
Чтобы получить код для этой лаборатории, клонируйте репозиторий GitHub из командной строки:
git clone https://github.com/android/codelab-android-compose.git cd codelab-android-compose/AdaptiveUiCodelab
Альтернативно вы можете скачать репозиторий в виде ZIP-файла:
Мы рекомендуем вам начать с кода в основной ветке и следовать кодовой лаборатории шаг за шагом в удобном для вас темпе.
Откройте проект в Android Studio.
- В окне «Добро пожаловать в Android Studio» выберите
Откройте существующий проект.
- Выберите папку
<Download Location>/AdaptiveUiCodelab
(убедитесь, что вы выбрали каталогAdaptiveUiCodelab
содержащийbuild.gradle
). - Когда Android Studio импортирует проект, проверьте, можете ли вы запустить
main
ветку.
Изучите стартовый код
Основной код ветки содержит пакет ui
. В этом пакете вы будете работать со следующими файлами:
-
MainActivity.kt
— действие точки входа, где вы запускаете свое приложение. -
ReplyApp.kt
— содержит компоненты пользовательского интерфейса главного экрана. -
ReplyHomeViewModel.kt
— предоставляет данные и состояние пользовательского интерфейса для содержимого приложения. -
ReplyListContent.kt
— содержит составные элементы для предоставления списков и экранов с подробными сведениями.
Если вы запустите это приложение на эмуляторе с изменяемым размером и попробуете разные типы устройств, например, телефон или планшет, пользовательский интерфейс просто расширится до заданного пространства вместо того, чтобы использовать пространство экрана или обеспечить эргономику доступности.
Вы обновите его, чтобы максимально эффективно использовать пространство экрана, повысить удобство использования и улучшить общее впечатление от пользователя.
3. Сделайте приложения адаптируемыми
В этом разделе рассказывается, что означает обеспечение адаптируемости приложений и какие компоненты Материал 3 предоставляет для упрощения этой задачи. Он также охватывает типы экранов и состояния, на которые вы ориентируетесь, включая телефоны, планшеты, большие планшеты и складные устройства.
Вы начнете с изучения основ размеров окон, положения их сгиба и различных типов параметров навигации. Затем вы можете использовать эти API в своем приложении, чтобы сделать его более адаптивным.
Размеры окон
Устройства Android бывают самых разных форм и размеров: от телефонов и складных устройств до планшетов и устройств ChromeOS. Чтобы поддерживать как можно больше размеров окон, ваш пользовательский интерфейс должен быть отзывчивым и адаптивным. Чтобы помочь вам найти правильный порог для изменения пользовательского интерфейса вашего приложения, мы определили значения точек останова, которые помогают классифицировать устройства по предопределенным классам размеров (компактным, средним и расширенным), называемым классами размеров окон . Это набор продуманных точек останова в области просмотра, которые помогут вам проектировать, разрабатывать и тестировать адаптивные макеты приложений.
Категории были выбраны специально, чтобы сбалансировать простоту макета и возможность оптимизировать ваше приложение для уникальных случаев. Класс размера окна всегда определяется пространством экрана, доступным приложению, которое может не занимать весь физический экран для многозадачности или других сегментаций.
И ширина, и высота классифицируются отдельно, поэтому в любой момент времени ваше приложение имеет два класса размеров окон — один для ширины и один для высоты. Доступная ширина обычно важнее доступной высоты из-за повсеместного распространения вертикальной прокрутки, поэтому в этом случае вы также будете использовать классы размеров ширины.
Свернуть состояния
Складные устройства создают еще больше ситуаций, к которым может адаптироваться ваше приложение, из-за их разных размеров и наличия петель. Петли могут закрывать часть дисплея, делая эту область непригодной для отображения контента; они также могут быть раздельными, то есть, когда устройство развернуто, имеется два отдельных физических дисплея.
Кроме того, пользователь может смотреть на внутренний дисплей, когда петля частично открыта, что приводит к различным физическим позам в зависимости от ориентации сгиба: поза стола (горизонтальный сгиб, показанный справа на изображении выше) и поза книги ( вертикальная складка).
Узнайте больше о положениях сгиба и петлях .
Все это следует учитывать при реализации адаптивных макетов, поддерживающих сгибаемые элементы.
Получите адаптивную информацию
adaptive
библиотека Material3 обеспечивает удобный доступ к информации об окне, в котором работает ваше приложение.
- Добавьте записи об этом артефакте и его версии в файл каталога версий:
градиент/libs.versions.toml
[versions]
material3Adaptive = "1.0.0-beta01"
[libraries]
androidx-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3Adaptive" }
- В файл сборки модуля приложения добавьте новую зависимость библиотеки, а затем выполните синхронизацию Gradle:
приложение/build.gradle.kts
dependencies {
implementation(libs.androidx.material3.adaptive)
}
Теперь в любой компонуемой области вы можете использовать currentWindowAdaptiveInfo()
для получения объекта WindowAdaptiveInfo
, содержащего такую информацию, как текущий класс размера окна и то, находится ли устройство в складном положении, например в положении столешницы.
Вы можете попробовать это сейчас в MainActivity
.
- В
onCreate()
внутри блокаReplyTheme
получите адаптивную информацию окна и отобразите классы размеров в компонуемомText
. Вы можете добавить это после элементаReplyApp()
:
MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ReplyTheme {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
ReplyApp(
replyHomeUIState = uiState,
onEmailClick = viewModel::setSelectedEmail
)
val adaptiveInfo = currentWindowAdaptiveInfo()
val sizeClassText =
"${adaptiveInfo.windowSizeClass.windowWidthSizeClass}\n" +
"${adaptiveInfo.windowSizeClass.windowHeightSizeClass}"
Text(
text = sizeClassText,
color = Color.Magenta,
modifier = Modifier.padding(20.dp)
)
}
}
}
Запустив приложение сейчас, вы увидите классы размеров окон, напечатанные поверх содержимого приложения. Не стесняйтесь изучать, что еще представлено в адаптивной информации окна. После этого вы можете удалить этот Text
, поскольку он охватывает содержимое приложения и не понадобится для следующих шагов.
4. Динамическая навигация
Теперь вы адаптируете навигацию приложения при изменении состояния и размера устройства, чтобы упростить использование приложения.
Когда пользователи держат телефон, их пальцы обычно находятся внизу экрана. Когда пользователи держат раскрытое складное устройство или планшет, их пальцы обычно расположены по бокам. Ваши пользователи должны иметь возможность перемещаться по приложению или инициировать взаимодействие с ним, не требуя чрезмерного положения рук или изменения положения рук.
Когда вы разрабатываете свое приложение и решаете, где разместить интерактивные элементы пользовательского интерфейса в макете, учитывайте эргономические последствия различных областей экрана.
- К каким местам удобно добираться, удерживая устройство?
- До каких областей можно дотянуться, только вытянув пальцы, что может быть неудобно?
- Какие места труднодоступны или находятся далеко от того места, где пользователь держит устройство?
Навигация — это первое, с чем взаимодействуют пользователи, и она содержит важные действия, связанные с критически важными действиями пользователя, поэтому ее следует размещать в наиболее легко доступных областях. Адаптивная библиотека Material предоставляет несколько компонентов, которые помогают реализовать навигацию в зависимости от класса размера окна устройства.
Нижняя навигация
Нижняя навигация идеально подходит для компактных размеров, поскольку мы естественным образом держим устройство так, чтобы большой палец мог легко добраться до всех точек касания нижней навигации. Используйте его всякий раз, когда у вас есть компактное устройство или складное устройство в компактном сложенном состоянии.
Навигационный рельс
Для окна средней ширины направляющая идеально подходит для обеспечения доступности, поскольку наш большой палец естественным образом падает вдоль боковой части устройства. Вы также можете объединить направляющую навигации с панелью навигации, чтобы отобразить дополнительную информацию.
Навигационный ящик
Панель навигации обеспечивает простой способ просмотра подробной информации на вкладках навигации и легко доступна при использовании планшетов или более крупных устройств . Доступны два типа панелей навигации: модальная панель навигации и постоянная панель навигации.
Модальный навигационный ящик
Вы можете использовать модальную панель навигации для компактных и средних телефонов и планшетов, поскольку ее можно развернуть или скрыть в виде наложения на контент. Иногда это можно комбинировать с навигационным рельсом.
Постоянный навигационный ящик
Вы можете использовать постоянный ящик навигации для фиксированной навигации на больших планшетах, Chromebook и настольных компьютерах.
Реализуйте динамическую навигацию
Теперь вы будете переключаться между различными типами навигации по мере изменения состояния и размера устройства.
В настоящее время приложение всегда отображает NavigationBar
под содержимым экрана независимо от состояния устройства. Вместо этого вы можете использовать компонент Material NavigationSuiteScaffold
для автоматического переключения между различными компонентами навигации на основе такой информации, как текущий класс размера окна.
- Добавьте зависимость Gradle, чтобы получить этот компонент, обновив каталог версий и сценарий сборки приложения, а затем выполните синхронизацию Gradle:
градиент/libs.versions.toml
[versions]
material3AdaptiveNavSuite = "1.3.0-beta01"
[libraries]
androidx-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3AdaptiveNavSuite" }
приложение/build.gradle.kts
dependencies {
implementation(libs.androidx.material3.adaptive.navigation.suite)
}
- Найдите составную функцию
ReplyNavigationWrapper()
вReplyApp.kt
и заменитеColumn
и его содержимое наNavigationSuiteScaffold
:
ОтветитьApp.kt
@Composable
private fun ReplyNavigationWrapperUI(
content: @Composable () -> Unit = {}
) {
var selectedDestination: ReplyDestination by remember {
mutableStateOf(ReplyDestination.Inbox)
}
NavigationSuiteScaffold(
navigationSuiteItems = {
ReplyDestination.entries.forEach {
item(
selected = it == selectedDestination,
onClick = { /*TODO update selection*/ },
icon = {
Icon(
imageVector = it.icon,
contentDescription = stringResource(it.labelRes)
)
},
label = {
Text(text = stringResource(it.labelRes))
},
)
}
}
) {
content()
}
}
Аргумент navigationSuiteItems
— это блок, который позволяет добавлять элементы с помощью функции item()
, аналогично добавлению элементов в LazyColumn
. Внутри завершающей лямбды этот код вызывает content()
переданную в качестве аргумента ReplyNavigationWrapperUI()
.
Запустите приложение на эмуляторе и попробуйте изменить размеры телефона, складного устройства и планшета, и вы увидите, что панель навигации изменится на направляющую и обратно.
В очень широких окнах, например на планшете в альбомной ориентации, вы можете захотеть отобразить постоянный ящик навигации. NavigationSuiteScaffold
поддерживает отображение постоянного ящика, хотя он не отображается ни в одном из текущих значений WindowWidthSizeClass
. Однако вы можете сделать это, внеся небольшое изменение.
- Добавьте следующий код непосредственно перед вызовом
NavigationSuiteScaffold
:
ОтветитьApp.kt
@Composable
private fun ReplyNavigationWrapperUI(
content: @Composable () -> Unit = {}
) {
var selectedDestination: ReplyDestination by remember {
mutableStateOf(ReplyDestination.Inbox)
}
val windowSize = with(LocalDensity.current) {
currentWindowSize().toSize().toDpSize()
}
val layoutType = if (windowSize.width >= 1200.dp) {
NavigationSuiteType.NavigationDrawer
} else {
NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(
currentWindowAdaptiveInfo()
)
}
NavigationSuiteScaffold(
layoutType = layoutType,
...
) {
content()
}
}
Этот код сначала получает размер окна и преобразует его в единицы DP с помощью currentWindowSize()
и LocalDensity.current
, а затем сравнивает ширину окна, чтобы определить тип макета пользовательского интерфейса навигации. Если ширина окна не менее 1200.dp
, используется NavigationSuiteType.NavigationDrawer
. В противном случае происходит возврат к расчету по умолчанию.
Когда вы снова запустите приложение на эмуляторе с изменяемым размером и опробуете разные типы, обратите внимание, что всякий раз, когда изменяется конфигурация экрана или вы разворачиваете складное устройство, навигация меняется на тип, соответствующий этому размеру.
Поздравляем, вы узнали о различных типах навигации, поддерживающих разные размеры и состояния окон!
В следующем разделе вы узнаете, как использовать оставшуюся область экрана вместо того, чтобы растягивать один и тот же элемент списка от края до края.
5. Использование пространства экрана
Независимо от того, запускаете ли вы приложение на маленьком планшете, развернутом устройстве или большом планшете, экран растягивается, чтобы заполнить оставшееся пространство. Вы хотите быть уверены, что сможете использовать это пространство экрана для отображения дополнительной информации, как в этом приложении, показывая электронную почту и обсуждения пользователям на одной странице.
Материал 3 определяет три канонических макета , каждый из которых имеет конфигурации для классов компактных, средних и расширенных размеров окон. Канонический макет List Detail идеально подходит для этого варианта использования и доступен в Compose как ListDetailPaneScaffold
.
- Получите этот компонент, добавив следующие зависимости и выполнив синхронизацию Gradle:
градиент/libs.versions.toml
[libraries]
androidx-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "material3Adaptive" }
androidx-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "material3Adaptive" }
приложение/build.gradle.kts
dependencies {
implementation(libs.androidx.material3.adaptive.layout)
implementation(libs.androidx.material3.adaptive.navigation)
}
- Найдите составную функцию
ReplyAppContent()
вReplyApp.kt
, которая в настоящее время отображает панель списка только при вызовеReplyListPane()
. Замените эту реализацию наListDetailPaneScaffold
, вставив следующий код. Поскольку это экспериментальный API, вы также добавите аннотацию@OptIn
в функциюReplyAppContent()
:
ОтветитьApp.kt
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ReplyAppContent(
replyHomeUIState: ReplyHomeUIState,
onEmailClick: (Email) -> Unit,
) {
val navigator = rememberListDetailPaneScaffoldNavigator<Long>()
ListDetailPaneScaffold(
directive = navigator.scaffoldDirective,
value = navigator.scaffoldValue,
listPane = {
ReplyListPane(replyHomeUIState, onEmailClick)
},
detailPane = {
ReplyDetailPane(replyHomeUIState.emails.first())
}
)
}
Этот код сначала создает навигатор, используя rememberListDetailPaneNavigator ()
rememberListDetailPaneNavigator ()
. Навигатор обеспечивает некоторый контроль над тем, какая панель отображается и какой контент должен быть представлен на этой панели, что будет продемонстрировано позже.
ListDetailPaneScaffold
будет отображать две панели при расширении класса ширины окна. В противном случае будет отображаться одна или другая панель в зависимости от значений, предусмотренных для двух параметров: директивы scaffold и значения scaffold. Чтобы получить поведение по умолчанию, этот код использует директиву scaffold и значение scaffold, предоставленное навигатором.
Остальные обязательные параметры представляют собой составные лямбды для панелей. ReplyListPane()
и ReplyDetailPane()
(находятся в ReplyListContent.kt
) используются для заполнения ролей панелей списка и подробностей соответственно. ReplyDetailPane()
ожидает аргумент электронной почты, поэтому на данный момент этот код использует первое электронное письмо из списка электронных писем в ReplyHomeUIState
.
Запустите приложение и переключите вид эмулятора на складной или планшетный (вам также может потребоваться изменить ориентацию), чтобы увидеть макет двух панелей. Это уже выглядит намного лучше, чем раньше!
Теперь давайте рассмотрим некоторые желаемые варианты поведения этого экрана. Когда пользователь нажимает на электронное письмо на панели списка, оно должно отображаться на панели сведений вместе со всеми ответами. В настоящее время приложение не отслеживает, какой адрес электронной почты был выбран, и нажатие на элемент ничего не дает. Лучшее место для хранения этой информации — остальная часть состояния пользовательского интерфейса в ReplyHomeUIState
.
- Откройте
ReplyHomeViewModel.kt
и найдите класс данныхReplyHomeUIState
. Добавьте свойство для выбранного электронного письма со значением по умолчаниюnull
:
ОтветHomeViewModel.kt
data class ReplyHomeUIState(
val emails : List<Email> = emptyList(),
val selectedEmail: Email? = null,
val loading: Boolean = false,
val error: String? = null
)
- В том же файле
ReplyHomeViewModel
есть функцияsetSelectedEmail()
, которая вызывается, когда пользователь касается элемента списка. Измените эту функцию, чтобы скопировать состояние пользовательского интерфейса и записать выбранное электронное письмо:
ОтветHomeViewModel.kt
fun setSelectedEmail(email: Email) {
_uiState.update {
it.copy(selectedEmail = email)
}
}
Следует учитывать, что происходит до того, как пользователь коснется любого элемента и выбранный адрес электронной почты станет null
. Что должно отображаться в области подробностей? Существует несколько способов справиться с этим случаем, например, показать первый элемент в списке по умолчанию.
- В том же файле измените функцию
observeEmails()
. Когда список электронных писем загружен, если в предыдущем состоянии пользовательского интерфейса не было выбранного электронного письма, установите его в первый элемент:
ОтветHomeViewModel.kt
private fun observeEmails() {
viewModelScope.launch {
emailsRepository.getAllEmails()
.catch { ex ->
_uiState.value = ReplyHomeUIState(error = ex.message)
}
.collect { emails ->
val currentSelection = _uiState.value.selectedEmail
_uiState.value = ReplyHomeUIState(
emails = emails,
selectedEmail = currentSelection ?: emails.first()
)
}
}
}
- Вернитесь в
ReplyApp.kt
и используйте выбранный адрес электронной почты, если он доступен, для заполнения содержимого панели сведений:
ОтветитьApp.kt
ListDetailPaneScaffold(
// ...
detailPane = {
if (replyHomeUIState.selectedEmail != null) {
ReplyDetailPane(replyHomeUIState.selectedEmail)
}
}
)
Запустите приложение еще раз, переключите эмулятор на размер планшета и увидите, что нажатие на элемент списка обновляет содержимое панели сведений.
Это отлично работает, когда видны обе панели, но когда в окне есть место для отображения только одной панели, создается впечатление, что при касании элемента ничего не происходит. Попробуйте переключить вид эмулятора на телефон или складное устройство в книжной ориентации и обратите внимание, что даже после касания элемента видна только панель списка. Это связано с тем, что даже несмотря на то, что выбранное электронное письмо обновляется, ListDetailPaneScaffold
сохраняет фокус на панели списка в этих конфигурациях.
- Чтобы это исправить, вставьте следующий код в качестве лямбды, передаваемой в
ReplyListPane
:
ОтветитьApp.kt
ListDetailPaneScaffold(
// ...
listPane = {
ReplyListPane(
replyHomeUIState = replyHomeUIState,
onEmailClick = { email ->
onEmailClick(email)
navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
}
)
},
// ...
)
Эта лямбда использует созданный ранее навигатор для добавления дополнительного поведения при нажатии на элемент. Он вызовет исходную лямбду, переданную в эту функцию, а затем также вызовет navigator.navigateTo()
, определяющий, какая панель должна отображаться. С каждой панелью в шаблоне связана связанная с ней роль, а для панели сведений — ListDetailPaneScaffoldRole.Detail
. В меньших окнах это создаст впечатление, что приложение продвинулось вперед.
Приложению также необходимо обрабатывать то, что происходит, когда пользователь нажимает кнопку «Назад» на панели сведений, и это поведение будет различаться в зависимости от того, видна ли одна панель или две панели.
- Поддержите обратную навигацию, добавив следующий код.
ОтветитьApp.kt
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ReplyAppContent(
replyHomeUIState: ReplyHomeUIState,
onEmailClick: (Email) -> Unit,
) {
val navigator = rememberListDetailPaneScaffoldNavigator<Long>()
BackHandler(navigator.canNavigateBack()) {
navigator.navigateBack()
}
ListDetailPaneScaffold(
directive = navigator.scaffoldDirective,
value = navigator.scaffoldValue,
listPane = {
AnimatedPane {
ReplyListPane(
replyHomeUIState = replyHomeUIState,
onEmailClick = { email ->
onEmailClick(email)
navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
}
)
}
},
detailPane = {
AnimatedPane {
if (replyHomeUIState.selectedEmail != null) {
ReplyDetailPane(replyHomeUIState.selectedEmail)
}
}
}
)
}
Навигатор знает полное состояние ListDetailPaneScaffold
, возможна ли обратная навигация и что делать во всех этих сценариях. Этот код создает BackHandler
, который включается каждый раз, когда навигатор может вернуться назад, а внутри лямбда-выражения вызывает navigateBack()
. Кроме того, чтобы сделать переход между панелями более плавным, каждая панель заключена в составной объект AnimatedPane()
.
Запустите приложение еще раз на эмуляторе изменяемого размера для всех типов устройств и обратите внимание, что всякий раз, когда изменяется конфигурация экрана или вы раскладываете складное устройство, навигация и содержимое экрана динамически изменяются в ответ на изменения состояния устройства. Также попробуйте нажать на электронные письма на панели списка и посмотреть, как макет ведет себя на разных экранах, показывая обе панели рядом или плавно анимируя их.
Поздравляем, вы успешно адаптировали свое приложение для всех состояний и размеров устройств. Продолжайте и поэкспериментируйте с запуском приложения на складных планшетах, планшетах или других мобильных устройствах.
6. Поздравления
Поздравляем! Вы успешно завершили эту лабораторную работу и узнали, как сделать приложения адаптивными с помощью Jetpack Compose.
Вы узнали, как проверить размер и состояние сложенного устройства, а также соответствующим образом обновить пользовательский интерфейс, навигацию и другие функции вашего приложения. Вы также узнали, как адаптивность повышает доступность и удобство использования.
Что дальше?
Ознакомьтесь с другими лабораториями по написанию кода .
Примеры приложений
- Примеры компоновки представляют собой коллекцию множества приложений, в которых используются лучшие практики, описанные в лабораториях кода.