Урок 13: Navigation Compose – правильная навигация
Оглавление:
- Введение
- Добавление зависимости navigation-compose в Gradle
- Навигация через Navigation Compose
- Создание sealed class Destination
- Создание AppNavHost
- Что вообще такое NavHost?
- Почему AppNavHost принимает параметр navController?
- Создание объекта NavHost
- Подключение AppNavHost в MainActivity
- Почему это организовано таким образом?
Введение
Сегодня у нас очень важная тема — навигация в Jetpack Compose с использованием Navigation Compose. Это подход, который используется в продакшене и позволяет создавать современные приложения, где экраны плавно переключаются, передают данные и корректно возвращаются назад.
Мы начнем с самого начала: я покажу, как настроить Navigation Compose, чтобы буквально за несколько минут реализовать переключение между экранами. Вы узнаете, как передавать аргументы, управлять стеком экранов и строить удобную архитектуру для ваших проектов. Разницу между ручной навигацией, которую реализовали в прошлом уроке, и этим инструментом вы почувствуете сразу.
К концу урока у вас будет готовый работающий пример, который легко адаптировать под реальные задачи.
Великолепно. На прошлом уроке мы разобрали “чистую” навигацию через переменную currentScreen, наглядно показали, как можно переключаться между экранами практически одной строкой кода. Однако, там мы выяснили, что ручное управление состоянием экрана быстро становится неудобным для реальных приложений. И вот теперь у нас появится более промышленное решение — официальная библиотека Navigation Compose.
Добавление зависимости navigation-compose в Gradle
Первым делом нужно подключить библиотеку в файле build.gradle.kts. Для этого обратимся к официальной документации. Можно имплементировать библиотеку прям тут, в разделе dependencies, но это устаревший способ. Сами версии для имплементации хранятся в специальном каталоге с расширением .toml.
Перехожу туда и создаю новую строку для имплементации навигации в разделе с библиотеками. Раскладываю адрес зависимости на группу, название и версию. Переменную с версией создам отдельно.
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "composeNavigation" }
Остается только сослаться на эту строку в конфиг файле build.gradle.kts:
implementation(libs.androidx.navigation.compose)
В конце не забываем синхронизировать проект. Все должно подгрузиться без ошибок. Все, библиотека подключена и с ней можно работать.
Если у вас уже есть опыт работы с классическим NavController и Navigation Graph (до появления Compose), вы знаете, что это мощная система для задания маршрутов, передачи аргументов и управления стеком экранов. В Compose команда Google переписала часть функционала, чтобы органично вписать его в декларативный подход.
Навигация через Navigation Compose
Основная идея осталась прежней: есть NavController, который хранит состояние навигационного стека и умеет переключаться между “маршрутами” (или “destinations”). Но теперь вместо XML графа мы объявляем наши экраны (routes) прямо в коде Kotlin, внутри NavHost. И да, эти роуты точно также будут храниться в виде обычных строк.
У меня подготовлено два пустых экрана с кнопками, я уже полностью вычистил старую реализацию. Сначала мы реализуем навигацию на этих тестовых экранах, а потом вернемся в гипотетическое приложение со списком уроков и осуществим переход из списка на страницу конкретного урока.
Итак, мы могли бы сделать все в одном файле, но это не будет похоже на продакшен-реди подход. Поэтому давайте сразу учиться настраивать навигацию правильно. Нам необходимо будет создать несколько дополнительных файлов. И пусть они хранятся в отдельном пакете navigation.
Создание sealed class Destination
Для начала создадим класс с роутами (его можно назвать Destination или Screen) – там мы объявляем sealed класс с будущими “routes”. Всего два “роута” в виде singleton-представлений data object. Максимально простые названия для открытия первого и второго экранов. Сами переходы я вынесу в константы для надежности и красоты.
Почему именно sealed? В этом типе класса, помимо прочих его преимуществ, все наследники должны быть объявлены в одном файле. Мы всегда видим полный список всех возможных маршрутов. Добавить «недопустимый» маршрут (или другой подкласс) невозможно, если он не объявлен в этом же файле. Рекомендую изучить возможности и применение sealed классов более подробно.
sealed class Destination(val route: String) {
data object First : Destination(ROUTE_FIRST)
data object Second : Destination(ROUTE_SECOND)
companion object {
private const val ROUTE_FIRST = "route_first"
private const val ROUTE_SECOND = "route_second"
}
}
Готово. Передачу аргументов внедрим чуть позже, идем к следующему шагу.
Создание AppNavHost
Далее создаю файл AppNavHost.kt. Там будет функция AppNavHost, которая принимает navController.
@Composable
fun AppNavHost(
navController: NavHostController,
) {
}
Что вообще такое NavHost?
AppNavHost — это точка настройки навигации для приложения. Она:
- Связывает
NavController(системный объект, управляющий переходами между экранами) сNavHost(специальным контейнером, который отрисовывает экраны). - Объявляет маршруты и логику переходов между экранами.
- Указывает начальный экран (
startDestination), который отображается при старте.
Проще говоря, AppNavHost в целом отвечает за управление экранами и переключение между ними.
Почему AppNavHost принимает параметр navController?
NavHostController — это ключевой компонент навигации. Он:
- Управляет стеком экранов: отслеживает текущий экран, обрабатывает возврат назад (например, когда мы нажимаем кнопку «Назад»).
- Также он используется для переходов между экранами: метод
navigate(route)переключает экран, аpopBackStack()возвращает на предыдущий. Сейчас вы увидите их в действии.
Создание объекта NavHost
Далее я создам объект NavHost. Это специальный контейнер, который отрисовывает экран, соответствующий текущему маршруту (route) из navController.
Мы можем посмотреть его параметры и увидеть первый обязательный параметр NavHostController, поэтому мы должны передавать его извне. Второй обязательный параметр здесь – это startDestination. Присваиваем наш стартовый роут. Таким образом обозначаем условно “домашний” экран или любой другой, который хотите, чтобы он стал стартовой точкой.
@Composable
fun AppNavHost(
navController: NavHostController,
) {
NavHost(
navController = navController,
startDestination = Destination.First.route,
) {
}
}
И наконец добавляются экраны (или маршруты) с помощью специальных функций, которые называются composable:
- Каждый метод
composableпредставляет один экран, связанный с определённым маршрутом (route). Этот роут принимается функцией в качестве обязательного параметра.
@Composable
fun AppNavHost(
navController: NavHostController,
) {
NavHost(
navController = navController,
startDestination = Destination.First.route,
) {
composable(route = Destination.First.route) {
}
}
}
- Если посмотреть на другие необязательные параметры – убедимся, что здесь же реализуется функционал по передаче аргументов, диплинков, а также можно настраивать анимации при переходе.

- Ну и последний параметр функции – это content. Собственно здесь можно вызывать необходимые нам composable экраны. Сделаем это прямо сейчас.
Логика будет такая. В этом методе вызывается функция создания экрана FirstScreen, которая принимает параметр коллбэка onClickButton. Это значит, что когда на экране происходит клик по кнопке “Перейти на второй экран” вызывается эта лямбда с методом navigate. В него поставляется наш прописанный роут на второй экран. Вот и все. Все необходимую логику и работу с бэкстэком библиотека сделает самостоятельно под капотом.
Для второго экрана можем прописать такой же роут обратно, а можем вызвать метод popBackStack(), который как раз отвечает за обработку кнопки назад. Таким образом это будет имитация нажатия кнопки “Назад” и мы вернемся снова на первый экран. И мы закончили настройку AppNavHost.
@Composable
fun AppNavHost(
navController: NavHostController,
) {
NavHost(
navController = navController,
startDestination = Destination.First.route,
) {
composable(route = Destination.First.route) {
FirstScreen(onClickButton = {
navController.navigate(Destination.Second.route)
})
}
composable(route = Destination.Second.route) {
SecondScreen(onClickButton = {
navController.popBackStack()
})
}
}
}
Подключение AppNavHost в MainActivity
Отлично, мы уже заложили надежный и расширяемый фундамент. Осталось дописать всего пару строк кода, чтобы навигация заработала. Возвращаемся в MainActivity.
Первое, что нужно сделать, это в том месте, где мы начинаем располагать контентные функции экрана – вызвать нашу главную функцию управления навигацией AppNavHost. Причем сделать это лучше в созданном ранее Box, чтобы ко всем отрисовываеымым в будущем экранам применялся стандартный отступ innerPadding.
Метод сейчас принимает единственный параметр типа NavHostController. Это ключевой объект для хранения состояния навигации. И так как это стейт, то его нужно создать при помощи функции remember, а именно rememberNavController(). Созданную переменную и передаем в навхост в качестве параметра.
setContent {
MyApplicationTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Box(
modifier = Modifier.padding(innerPadding)
) {
val navController = rememberNavController()
AppNavHost(navController = navController)
}
}
}
}
И это все, что надо, чтобы заработала навигация, теперь мы имеем компактное и масштабируемое решение. AppNavigation нам больше вообще не нужна. Давайте проверим, как это выглядит. Запускаем.
Обращаю ваше внимание на появление стандартных анимаций. Переходы стали более плавными и красивыми. На втором экране мы можем нажать кнопку назад и осуществится возврат на первый, а не выход из приложения, как это было на предыдущем уроке. Выглядит великолепно.
Почему это организовано таким образом?
Подытожим. Какие преимущества нам дал такой подход?
- Навигация вынесена в отдельную функцию, чтобы код был проще поддерживать и масштабировать. Например, если потребуется добавить новый экран, это делается только внутри
AppNavHost. Вся навигация полностью инкапсулирована. - Благодаря
NavHost, легко увидеть структуру навигации приложения. В коде видно, какие маршруты есть и как они связаны. Нам достаточно открыть один файл, чтобы разобраться в роутах. Нет необходимости изучать сразу несколько файлов и следить откуда куда вызываются функции навигации.
Бесплатные Telegram-боты для обучения
Практика с проверкой кода и помощью ИИ-ментора
AndroidSprint AI Mentor
Проверяет Pull Request'ы в GitHub, проводит тестовые собеседования с голосом и таймером, помогает разбираться с кодом 24/7
Попробовать ИИ-ментора →KotlinSprint Bot
22 урока Kotlin, 220 тестов, 120 практических задач с код-ревью
Начать обучение Kotlin →