Разработка
Осваиваем переходы между общими элементами в Compose
Переходы между общими элементами находятся на стыке UX и архитектуры. Когда они сделаны хорошо, пользователи это замечают. Когда они сделаны плохо, разработчики это замечают.
Переходы между общими элементами (Shared Element Transitions) — это одна из тех вещей, которые всем нравятся, когда их видят, но мало кто действительно получает удовольствие от их реализации. Когда они работают, приложение внезапно кажется премиальным. Когда же нет, вы смотрите на экран, недоумевая, почему простая анимация превратилась в полноценную сессию отладки.
В современных Android-приложениях навигация — это больше, чем просто переход с экрана А на экран Б. Речь идёт о непрерывности. Переходы между общими элементами помогают пользователям сохранять контекст: то, на что вы нажали, — это то же самое, что вы сейчас видите, только в более подробном виде. При правильной реализации они устраняют трение и делают пользовательский интерфейс спокойным и продуманным.
В этой статье я расскажу, как реализовать без сторонних библиотек переходы между общими элементами с помощью Jetpack Navigation и Compose, и, что более важно, как заставить их работать в реальном приложении.
Почему переходы между общими элементами важны
Речь идёт не о замысловатых анимациях. Речь идёт о снижении когнитивной нагрузки.
Когда пользователь нажимает на элемент в списке, и этот же элемент визуально трансформируется в экран с подробной информацией, интерфейс становится понятен сам собой. Нет никакого ментального скачка, никакого момента «подождите, где я сейчас?». Особенно в лентах новостей, каталогах и приложениях с большим объёмом контента такая непрерывность имеет огромное значение.
Это также помогает улучшить воспринимаемую производительность. Даже если ничего на самом деле не ускоряется, плавные переходы создают ощущение более быстрой работы приложения, а это часто то, что важно для пользователей.
Настройка проекта
Для работы вам потребуется доступ к экспериментальным API общих переходов в Compose. В проекте используется стандартная настройка, ничего экзотического.
Основные зависимости из libs.versions.toml и build.gradle.kts:
- Compose Animation, для
SharedTransitionScopeиModifier.sharedElement() - Navigation Compose, для обработки изменений целей и областей анимации
- Kotlinx Serialization, для типобезопасных маршрутов навигации
dependencies {
implementation(libs.androidx.navigation.compose)
implementation(libs.kotlinx.serialization.json)
implementation(platform(libs.androidx.compose.bom))
}
Если вы уже используете Compose BOM, вам обычно не нужно беспокоиться о версиях анимации явно.
Простой пример использования: список элементов, ведущий к подробному экрану
Для демонстрации я использую очень классический сценарий: список элементов, который ведет к экрану с подробностями. Каждый элемент имеет изображение и заголовок, и оба участвуют в переходе.
Пример намеренно упрощен. Этот шаблон уже охватывает большинство проблем, с которыми вы столкнетесь в реальных проектах: стабильные ключи, рекомпозиция, аргументы навигации и навигация назад.
Подробное руководство по реализации
В общих чертах, переходы между общими элементами в Compose зависят от трех вещей: общей области видимости родительского элемента, области видимости анимации и стабильных ключей. Упустите хотя бы один из них, и все начнет ломаться очень странным образом.
1. Определение SharedTransitionScope
Всем переходам между общими элементами нужен общий родительский элемент, который может координировать измерения и движение. В данном случае этим родительским элементом является SharedTransitionScope, который оборачивает весь NavHost.
SharedTransitionScope {
NavHost(
modifier = it,
navController = controller,
startDestination = Routes.List
) {
// destinations
}
}
Размещение его здесь упрощает настройку и позволяет избежать распространения проблем, связанных с анимацией.
2. Передача AnimatedVisibilityScope
Здесь часто возникают ошибки в реализации.
Modifier.sharedElement() требует AnimatedVisibilityScope, чтобы знать, когда происходит переход. Навигация предоставляет этот скоуп внутри каждого композабл элемента, но вы должны передать его экрану.
Если вы забудете этот шаг, код скомпилируется, ничего не сломается, но переход просто… не произойдёт.
3. Применение Modifier.sharedElement
Как на экране списка, так и на экране с подробной информацией, общие элементы должны использовать один и тот же ключ. Никаких обходных путей здесь нет.
Image(
modifier = Modifier
.sharedElement(
sharedContentState = rememberSharedContentState("item_${item.drawableRes}"),
animatedVisibilityScope = animatedVisibilityScope
)
.size(50.dp),
painter = painterResource(item.drawableRes),
contentDescription = item.text
)
А на экране назначения:
Image(
modifier = Modifier
.sharedElement(
sharedContentState = rememberSharedContentState("item_${item.drawableRes}"),
animatedVisibilityScope = animatedVisibilityScope
)
.size(200.dp),
painter = painterResource(item.drawableRes),
contentDescription = item.text
)
Если ключи не совпадают идеально, Compose просто будет рассматривать их как два несвязанных элемента и пропустит анимацию перехода.
Обработка навигации
Сама навигация довольно проста. Я использую типобезопасные маршруты и передаю данные элемента напрямую при переходе на экран с подробной информацией.
onClick = {
controller.navigate(
Routes.Details(
drawableRes = it.drawableRes,
text = it.text
)
)
}
Переход работает, потому что оба целевые области находятся внутри одного и того же SharedTransitionScope и получают совместимые экземпляры AnimatedVisibilityScope. За кулисами Compose использует Lookahead фазу для вычисления целевого макета до начала анимации, что позволяет элементам плавно изменять размер и перемещаться.
Обработка состояния и рекомпозиция
Если переходы между общими элементами кажутся нестабильными, обычно причина в состоянии.
Рекомпозиция может создавать новые элементы в неожиданные моменты. Если ключ общего элемента изменяется, даже на короткое время, система переходов теряет элемент из виду.
Используйте стабильные идентификаторы из вашей модели данных, избегайте индексов списков и будьте осторожны с производными состояниями. По моему опыту, если переход работает один раз, а затем внезапно перестает работать, это почти всегда является причиной.
Распространенные ошибки и подводные камни
Несколько вещей, которые часто доставляют проблемы:
- Использование индексов списков в качестве ключей общих элементов
- Запуск навигации до того, как пользовательский интерфейс полностью стабилизируется
- Забывание восстановить те же ключи при возврате
- Обрезка контента родительскими макетами во время перехода
Всегда тестируйте как навигацию вперед, так и назад. Многие проблемы проявляются только при возврате назад.
Accompanist против Jetpack Navigation
Долгое время Accompanist был единственным реальным вариантом для переходов между общими элементами в Compose. Он по-прежнему мощный, но для большинства современных приложений Jetpack Navigation — более простой и перспективный выбор.
Меньше зависимостей, лучшая интеграция и меньше затрат на долгосрочное обслуживание.
Когда не следует использовать переходы между общими элементами
Как бы хороши ни были переходы между общими элементами, они не бесплатны.
Они усложняют интерфейс, могут снижать производительность на устройствах низкого класса и вызывать проблемы с доступностью при чрезмерном использовании. Если анимация явно не улучшает понимание или плавность работы, обычно лучше от неё отказаться.
Заключительные мысли
Переходы между общими элементами находятся на стыке UX и архитектуры. Когда они сделаны хорошо, пользователи это замечают. Когда они сделаны плохо, разработчики это замечают.
Jetpack Navigation и Compose наконец-то предоставляют нам инструменты для их чистой реализации. Начните с простого, используйте стабильные ключи и рассматривайте анимацию как основную часть вашего пользовательского интерфейса, а не просто как украшение.
-
Вовлечение пользователей2 недели назад
Большинство приложений терпят неудачу не из-за плохой «идеи»
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2026.3
-
Новости2 недели назад
Видео и подкасты о мобильной разработке 2026.4
-
Видео и подкасты для разработчиков2 недели назад
Изоляционно-плагинная архитектура в Dart-приложениях, переносимость на Flutter
