Урок 12: Навигация – Base Compose Navigation
Оглавление:
Введение
В этом уроке мы разберемся, как заставить наше приложение переключаться между экранами. Навигация – неотъемлемая часть любого приложения и часто этот предмет вызывает много вопросов и обсуждений. Есть много способов ее реализации, есть много сторонних библиотек для ее реализации.
Наша цель – добавить в проект контролируемую и предсказуемую систему переходов между экранами, которая легко внедряется и поддерживается. Сейчас расскажу, как навигация эволюционировала, какие способы используются в продакшне сегодня и почему иногда все сводится всего к одной строке кода “currentScreen = nextScreen”.
Зачем нам навигация?
Странный вопрос, потому, что вроде бы имеет очевидный ответ. Но он поможет нам заложить фундамент понимания концепции переходов. В реальных приложениях даже самого базового уровня есть, например:
- Экран авторизации: чтобы войти (или зарегистрироваться).
- Главный экран: условная домашняя страница.
- Список данных: будь то уроки, статьи или товары.
- Экран деталей: детальная страница выбранного элемента.
- …и так далее.
Безусловно, мы не можем запихнуть всё это в один-единственный Composable экран. Тут мы внедряем понятие “навигация”. Навигация нам говорит: “У меня есть понятие экрана и понятие перехода. Хочешь показать список — пожалуйста, выводим его на экран. Хочешь открыть детали в этом списке? Вот тебе команда на смену экрана и дополнительный аргумент, например, идентификатор, какой урок показывать”.
Способы реализовывать навигацию
Теперь кратко пройдемся по способам реализации навигации в принципе, ну только по основным. Чтобы на всякий случай иметь представление или просто освежить память.
1. Переходы с Activity на Activity
Самый старый метод, знакомый тем, кто давно на Android: на каждый экран — отдельная Activity, а переход реализовывался с помощью метода startActivity(intent) с параметром intent. Из плюсов тут разве что ничего дополнительно подключать не нужно, всё работает “из коробки” . Но лишние Activity плодят кучу кода, нужно где-то хранить общие данные, а в Compose вообще нет смысла создавать десять Activity (почти все делают один контейнер). В индустрии уже давно используется подход SingleActivity.
2. Фрагменты и NavController (до Compose)
До Compose был (ну и всё ещё есть) Navigation Component — это когда в xml-файле вы описываете “граф” навигации, привязываете фрагменты, а NavController управляет стеком переходов. Мы изучали его в практическом спринте по разработке на XML.
Это неплохой подход, есть визуальный редактор, можно задать анимации и аргументы. Проблемы могут быть в том случае, если приложение полностью на Compose. Фрагменты часто уже не нужны. Создается дополнительная сложность при смешении фрагментов с Composable-экранами.
3. “Чистый” Compose-подход (без библиотек)
Можно вообще не подключать либ, а завести переменную currentScreen, которая хранит значение, какой из Composable показывать. При клике на кнопку меняем, например, currentScreen = «lessonDetails».
- Плюсы: абсолютный минимализм, прямая декларативность. Просто меняем переменную — UI перестраивается.
- Минусы: при большом числе экранов придется вручную возиться со стеком экранов, аргументами, анимациями и возвратом назад — это чистая боль. Для крупных проектов совершенно неприемлемо.
4. Navigation Compose (рекомендуемый в 90% продакшн-проектов)
Сейчас это официальный способ навигации в Compose. Да, есть еще довольно много библиотек-аналогов от сторонних разработчиков. Здесь же останавливаемся на тех технологиях, которые рекомендуются в документации от Google.
Способ предоставляет NavHostController, где объявляются “роуты” (условные “адреса” экранов – аналог фрагментных “destination”). Имеет удобные функции: передача аргументов, popBackStack, переходы, анимации. Позволяет переходить на новый экран одной командой.
Этот подход является стандартом и рекомендован в официальной документации, хорошо интегрирован с остальными Jetpack-библиотеками, поддерживает разные сценарии типа deeplinks и т. п. Четкий API, легко передавать аргументы, есть обработка кнопки “назад”, можно создавать вложенные графы и многое другое.
Иногда это overkill для супер-маленького приложения, так как требует дополнительной зависимости. Но в реальной работе чаще всего Navigation Compose — маст-хэв.
На практике сейчас выбор чаще всего делается в пользу Navigation Compose. Но в этом уроке я продемонстрирую реализацию “чистого” подхода. Изучение новой технологии всегда стоит начинать с чистых примеров, чтобы иметь понимание ее работы под капотом.
Минимальный пример навигации в Jetpack Compose
Итак, далее я покажу минималистичный подход к навигации на новом пустом проекте. Создам его прямо сейчас с базовыми настройками. Запущу проект, чтобы проверить, что все работает нормально.

Подготавливаем проект к реализации “чистой” навигации
Мне нужна максимальная чистота кода для наглядности, поэтому я уберу из onCreate вызовы Greeting и вместо него добавлю Box с нужными модификаторами и сделаю обертку для навигации (назовем её AppNavigation). Ниже объявлю эту функцию.
Пусть в onCreate вызывается только AppNavigation(), а внутри уже будем управлять экранами, кнопками и так далее.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyApplicationTheme {
Scaffold { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
AppNavigation()
}
}
}
}
}
}
Создаем “свою” навигацию (AppNavigation)
Чтобы переключаться между несколькими экранами мы заведем переменную “какой экран показывать” и будем менять её при клике на кнопки. То есть иными словами можно сказать какое текущее состояние экрана должно быть активным.
currentScreen— это “строка-состояние” (по-хорошему, можно сделать sealed class – это позволяет создавать строго ограниченный набор подтипов, но для примера оставим так). Для нее реализуем стейт, с которым мы уже познакомились ранее. Значение по умолчание будет ”first”, чтобы сразу показывался первый экран.- Через
whenпроверяем, какое значение хранит стейт в данный момент при рекомпозиции. - Если
"first", то рендеримFirstScreen. Иначе"second"–SecondScreen. - Сами экраны при вызове будут принимать единственный параметр – коллбэк onNextClick – он будет дергать изменение стейта с новым значением
currentScreen, чем вызывать рекомпозицию функцииAppNavigation()с отрисовкой нового экрана.
@Composable
fun AppNavigation() {
val currentScreen = remember { mutableStateOf("first") }
when (currentScreen.value) {
"first" -> FirstScreen(
onNextClick = {
currentScreen.value = "second"
},
)
"second" -> SecondScreen(
onBackClick = {
currentScreen.value = "first"
},
)
}
}
Реализуем сами экраны (FirstScreen и SecondScreen)
Теперь создадим функции для самих экранов. Напишу их в том же файле для компактности. Каждый экран – Composable-функция. Соответственно принимают параметры коллбэков, внутри будет просто кнопка, которую можно поместить в Box и немного дооформить. Каждый экран будет отличаться еще и цветом.
В FirstScreen будет кнопка “Перейти на SecondScreen”. При нажатии мы вызываем коллбэк onNextClick. Он, как мы уже описали выше, будет изменять значение стейта currentScreen. Точно так же во 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. Следовательно вэлью считывается и вызывается соответствующая функция экрана. По клику дергается коллбэк, изменяет состояние стейта в AppNavigation и вызывается рекомпозиция.

Всё, теперь у вас есть рабочая, полностью автономная реализация навигации на “голом” Compose-проекте. Можно добавлять новые экраны, меняя состояние currentScreen и передавая разные параметры. Но о параметрах все же будет позже, а пока подытожим.
Почему в продакшене такое не используют?
На практике ручной подход через “строки и when” применяется крайне редко в реальных проектах. Опишу несколько ключевых проблем (они помогут подсветить преимущества правильного подхода, который будем использовать в следующем уроке):
- Отсутствие управления BackStack.
При создании навигации на фрагментах через navGraph – при каждом новом переходе новые фрагменты складывались в стек. То есть, например, при такой реализации, если бы мы перешли с первого экрана на второй – они бы сложились в цепочку. Что это дает? При нажатии на кнопку “Назад” на устройстве – осуществится навигация на предыдущий экран в стэке.
Однако, если мы попытаемся нажать “Назад” на втором экране сейчас – произойдет выход из приложения. Стэка нет, возвращаться некуда. Таким образом обработку BackStack требуется реализовывать вручную. В каких случаях делать popBackStack, куда возвращаться, что будет, если экраны идут цепочкой (A → B → C)? Это быстро становится громоздко.
- Проблемы с аргументами.
Передача аргументов при ручной навигации — это еще одна серьезная сложность. Например, если нам нужно передать наSecondScreenкакой-то идентификатор, можно сделать это через строку. То есть буквально в строке currentScreen добавляем через слэш нужные нам данные. Но это только намеренно утрированный пример, в проде и даже в своих проектах так делать не следует.
Все аргументы передаются как часть строки – это быстро становится нечитабельным. При сложных аргументах (например, объект данных) придется самим превращать объект в строку и обратно, увеличивая риск багов. Ну и еще много других сложностей с аргументами.
- Нет удобных анимаций, deeplinks.
Для добавления анимации между экранами или диплинков из браузера — все придется делать вручную, и сильно увеличит время на разработку той или иной фичи.
- Наконец самое важное — состояние экрана не сохраняется. При “ручном” переключении через
when(currentScreen), мы фактически заново вызываем метод экрана, когда возвращаемся с другого. Это значит, что все локальные переменные, LiveData-потоки, позиция в списке и прочие данные будут созданы заново. То есть это не рекомпозиция, где учитывается состояние, а вызов функции “как в первый раз”. Конечно, можно руками обыграть это черезrememberSaveableили делать отдельные ViewModel, но это всё превращается в трудоемкий ручной костыль, тогда как в системах с полноценной навигацией это поставляется “из коробки”.
В итоге, хоть этот способ и очень быстрый и “понятный” для простых приложений (2-3 экрана), при сколь-нибудь серьезном масштабе лучше использовать более формализованное решение.
На этом пока все. На следующих уроках разберем, как добавить Navigation Compose в проект, передавать аргументы и возвращать результат обратно на предыдущие экраны. Ощутим все прелести “декларативной” навигации.
Бесплатные Telegram-боты для обучения
Практика с проверкой кода и помощью ИИ-ментора
AndroidSprint AI Mentor
Проверяет Pull Request'ы в GitHub, проводит тестовые собеседования с голосом и таймером, помогает разбираться с кодом 24/7
Попробовать ИИ-ментора →KotlinSprint Bot
22 урока Kotlin, 220 тестов, 120 практических задач с код-ревью
Начать обучение Kotlin →