Навигация между экранами

Navigation Compose управляет переходами между экранами в single-activity приложении: NavController хранит стек экранов, NavHost описывает граф, а с версии 2.8 маршруты можно задавать типобезопасно.
Суть: вместо множества Activity современное приложение использует один граф навигации, где экраны — это composable, а переходы и аргументы описываются декларативно и типобезопасно.

Мы уже знаем, что современная архитектура — single-activity. Но приложению нужно несколько экранов: список, детали, профиль. За переходы между ними отвечает Navigation Compose. Два ключевых объекта: NavController управляет стеком экранов и выполняет переходы, а NavHost описывает граф — какие экраны существуют и какой стартовый.

import androidx.navigation.compose.*
import androidx.navigation.NavController

@Composable
fun AppNavigation() {
    val navController = rememberNavController()
    NavHost(navController = navController, startDestination = "list") {
        composable("list") {
            ListScreen(onItemClick = { id ->
                navController.navigate("details/$id")
            })
        }
        composable("details/{id}") { backStackEntry ->
            val id = backStackEntry.arguments?.getString("id")
            DetailsScreen(id = id)
        }
    }
}

Типобезопасная навигация

Строковые маршруты вроде "details/$id" легко сломать опечаткой. Начиная с Navigation Compose 2.8, появилась типобезопасная навигация: маршруты задаются сериализуемыми классами, а аргументы — их полями. Компилятор проверяет правильность переходов, и ошибка всплывает на этапе сборки, а не в рантайме.

import kotlinx.serialization.Serializable

@Serializable object ListRoute
@Serializable data class DetailsRoute(val id: Int)

// переход теперь типобезопасный:
// navController.navigate(DetailsRoute(id = 42))
// аргумент достаётся как объект, без ручного парсинга строки

Как работает под капотом

NavController хранит back stack — стек записей о посещённых экранах. Переход navigate кладёт запись наверх, а кнопка «назад» снимает её. NavHost при каждом переходе показывает composable, соответствующий верхней записи стека. В типобезопасном варианте маршрут — это сериализуемый объект: библиотека кодирует его аргументы и восстанавливает на целевом экране, поэтому ручной разбор строк не нужен. Чтобы не накапливать экраны в стеке (например, после логина), используют popUpTo, очищая часть стека при переходе.

  Back stack навигации

  navigate(Details(42))
  +------------------+
  | DetailsRoute(42) |  <-- верх стека (виден)
  +------------------+
  | ListRoute        |
  +------------------+

  кнопка "назад" --> снимает верхнюю запись

Частые ошибки

Создавать NavController вручную без remember. Используйте rememberNavController, иначе контроллер пересоздастся при рекомпозиции.

Передавать тяжёлые объекты как аргументы. В навигацию передают идентификаторы, а сами данные грузят на экране из репозитория.

Забывать про popUpTo. Без очистки стека после логина пользователь кнопкой «назад» вернётся на экран входа.

Best practices

  • Используйте rememberNavController и один NavHost на приложение.
  • Предпочитайте типобезопасную навигацию (2.8+) строковым маршрутам.
  • Передавайте между экранами идентификаторы, а не целые объекты.
  • Управляйте стеком через popUpTo там, где не нужен возврат.

Поведение back stack удобно смоделировать на Python как обычный стек. Запустите врезку.

# Аналог навигационного back stack
stack = ['ListRoute']

def navigate(route):
    stack.append(route)
    print('navigate ->', stack)

def back():
    if len(stack) > 1:
        stack.pop()
    print('back     ->', stack)

navigate('DetailsRoute(42)')
navigate('ProfileRoute')
back()
back()

Попробуй сам ▶ — добавьте функцию, очищающую стек до начального экрана (аналог popUpTo после логина). В Android это управляет тем, куда приведёт кнопка «назад».

Закрепим главное

Закрепите модель «один граф вместо множества Activity». В современном приложении экраны — это composable-узлы одного графа навигации, а переходы между ними — операции над стеком в NavController. Это упрощает и состояние, и жизненный цикл: вся навигация живёт внутри одной Activity, под единым деревом Compose. Мысленно представляйте навигацию как стек карточек, который вы наращиваете при переходах и снимаете кнопкой «назад».

Второй ориентир — передавайте ссылки, а не данные. Через маршрут отправляйте идентификатор сущности, а её содержимое экран загрузит сам из репозитория. Так вы не дублируете данные, не упираетесь в ограничения на размер аргументов и сохраняете единый источник истины. Типобезопасные маршруты 2.8 делают этот контракт ещё строже: аргументы описаны полями класса, и компилятор не даст перейти на экран, забыв передать нужный идентификатор.

Итог: Navigation Compose реализует переходы в single-activity приложении через NavController и NavHost, а типобезопасные маршруты делают навигацию надёжной. На этом курс замыкается: у вас есть полный набор — от синтаксиса Kotlin до архитектуры экрана с данными, сетью и навигацией.

Проверьте себя
1. Какую роль играет NavController в Navigation Compose?
AРисует пользовательский интерфейс
BУправляет стеком экранов (back stack) и выполняет переходы между ними
CХранит данные в базе
DЗаменяет ViewModel
2. В чём преимущество типобезопасной навигации (Navigation Compose 2.8+)?
AОна работает без NavController
BМаршруты задаются сериализуемыми классами, аргументы — их полями, и компилятор ловит ошибки на этапе сборки
CОна не требует composable-функций
DОна быстрее рисует экраны