Прямые ссылки на публичные уроки для быстрого старта и стабильной индексации lesson-страниц.
В этом уроке разберемся, как заставить приложение переключаться между экранами. Навигация — неотъемлемая часть любого приложения и часто вызывает много вопросов. Расскажу, как навигация эволюционировала, какие способы используются в продакшне сегодня и почему иногда всё сводится к одной строке currentScreen = nextScreen.
В реальных приложениях даже базового уровня есть:
Мы не можем разместить всё это в одном Composable. Навигация вводит понятия экрана и перехода: хочешь показать список — выводим его на экран; хочешь открыть детали — команда на смену экрана с дополнительным аргументом (например, идентификатором урока).
1. Переходы с Activity на Activity
Самый старый метод: на каждый экран — отдельная Activity, переход через startActivity(intent). Из плюсов — ничего дополнительно подключать не нужно. Но лишние Activity плодят кучу кода, нужно где-то хранить общие данные, а в Compose нет смысла создавать десять Activity. В индустрии давно используется подход SingleActivity.
2. Фрагменты и NavController (до Compose)
Navigation Component — в xml-файле описывается "граф" навигации, фрагменты привязываются, а NavController управляет стеком переходов. Есть визуальный редактор, анимации и аргументы. Проблема возникает, если приложение полностью на Compose: фрагменты уже не нужны, а их смешение с Composable-экранами создаёт дополнительную сложность.
3. “Чистый” Compose-подход (без библиотек)
Переменная currentScreen хранит значение, какой Composable показывать. При клике меняем currentScreen = "lessonDetails".
4. Navigation Compose (рекомендуемый в 90% продакшн-проектов)
Официальный способ навигации в Compose. Предоставляет NavHostController, где объявляются "роуты" — адреса экранов. Есть передача аргументов, popBackStack, переходы, анимации, deeplinks, вложенные графы. Хорошо интегрирован с остальными Jetpack-библиотеками.
Иногда избыточен для маленького приложения, так как требует дополнительной зависимости. Но в реальных проектах Navigation Compose — маст-хэв.
В этом уроке демонстрирую "чистый" подход — изучение новой технологии всегда стоит начинать с чистых примеров, чтобы понимать, как она работает под капотом.
Убираем лишнее из onCreate и добавляем Box с нужными модификаторами и обёртку для навигации — AppNavigation(). В onCreate вызывается только она, а внутри управляем экранами и кнопками.
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { MyApplicationTheme { Scaffold { innerPadding -> Box( modifier = Modifier .fillMaxSize() .padding(innerPadding) ) { AppNavigation() } } } } } }
Заводим переменную currentScreen — стейт, хранящий имя активного экрана. Через when проверяем текущее значение при рекомпозиции и рендерим соответствующий экран. Каждый экран принимает коллбэк, который меняет стейт и вызывает рекомпозицию AppNavigation() с отрисовкой нового экрана.
"first" — показывается первый экран.when ветвится на "first" → FirstScreen и "second" → SecondScreen.@Composable fun AppNavigation() { val currentScreen = remember { mutableStateOf("first") } when (currentScreen.value) { "first" -> FirstScreen( onNextClick = { currentScreen.value = "second" }, ) "second" -> SecondScreen( onBackClick = { currentScreen.value = "first" }, ) } }
Каждый экран — Composable-функция, принимающая коллбэк. Внутри — кнопка в Box. Экраны различаются цветом фона.
FirstScreen содержит кнопку "Перейти на SecondScreen", которая вызывает onNextClick. SecondScreen — кнопку "Вернуться на FirstScreen" с коллбэком onBackClick.
@Composable fun FirstScreen( onNextClick: () -> Unit, ) { Box( modifier = Modifier .fillMaxSize() .background(Color.LightGray), contentAlignment = Alignment.Center ) { Button( onClick = onNextClick ) { Text(text = "Перейти на SecondScreen") } } } @Composable fun SecondScreen( onBackClick: () -> Unit, ) { Box( modifier = Modifier .fillMaxSize() .background(Color.Gray), contentAlignment = Alignment.Center ) { Button( onClick = onBackClick ) { Text(text = "Вернуться на FirstScreen") } } }
При старте вызывается AppNavigation, инициализируется стейт со значением "first", отрисовывается FirstScreen. По клику дёргается коллбэк, меняет стейт и запускает рекомпозицию с новым экраном.
Ручной подход через "строки и when" применяется крайне редко. Ключевые проблемы:
1. Отсутствие управления BackStack. При нажатии "Назад" на втором экране произойдёт выход из приложения — стека нет, возвращаться некуда. Обработку BackStack придётся реализовывать вручную: в каких случаях делать popBackStack, куда возвращаться при цепочке A → B → C. Это быстро становится громоздко.
2. Проблемы с аргументами. Передача данных через строку currentScreen = "second/id=42") быстро становится нечитабельной. При сложных аргументах придётся самостоятельно сериализовывать объекты в строку и обратно, увеличивая риск багов.
3. Нет удобных анимаций и deeplinks. Всё придётся делать вручную, что значительно увеличивает время разработки.
4. Состояние экрана не сохраняется. При "ручном" переключении через when(currentScreen) функция экрана вызывается заново — все локальные переменные, позиция в списке и прочие данные пересоздаются. Обойти это через rememberSaveable или отдельные ViewModel можно, но это трудоёмкий ручной костыль, тогда как в системах с полноценной навигацией это поставляется "из коробки".
Этот способ подходит для простых приложений на 2-3 экрана. При сколь-нибудь серьёзном масштабе — лучше использовать формализованное решение.
На следующих уроках разберём, как добавить Navigation Compose в проект, передавать аргументы и возвращать результат на предыдущие экраны.