Создаём навигацию в Android Studio на Kotlin: урок

Когда приложение разрастается, один экран перестаёт справляться — нужны переходы, передача данных и понятная структура. В этом уроке из цикла Android Studio Kotlin уроки мы разберём навигацию в Jetpack Compose с нуля. Вы научитесь связывать несколько экранов, передавать параметры и управлять стеком переходов — и всё это простым, читаемым кодом, доступным даже тем, кто только вчера открыл Android Studio.


Зачем нужна навигация и как она устроена в Compose

Навигация — это механизм, который позволяет пользователю перемещаться между разделами приложения. В Compose за это отвечает библиотека navigation-compose, входящая в Jetpack. Она работает на основе графа навигации: вы описываете маршруты, а библиотека сама управляет анимациями, стеком и восстановлением состояния. Для тех, кто только начинает изучать Android Studio Kotlin уроки, навигация станет первым шагом к построению многоэкранных приложений.


Шаг 1: Добавляем зависимость

Откройте файл build.gradle.kts на уровне модуля и добавьте библиотеку навигации. Убедитесь, что версия актуальна — на момент написания урока это 2.8.0.

dependencies {
    implementation("androidx.navigation:navigation-compose:2.8.0")
}

После синхронизации Gradle библиотека готова к использованию. Никаких дополнительных настроек не требуется.


Шаг 2: Определяем маршруты

Маршрут — это строковый идентификатор экрана. Хорошей практикой считается выносить их в константы, чтобы избежать опечаток. Создайте объект Routes:

object Routes {
    const val HOME = "home"
    const val DETAILS = "details"
}

Пока у нас два экрана: главный и экран с деталями. Позже вы сможете добавлять сюда любое количество маршрутов.


Шаг 3: Создаём экраны

Для наглядности сделаем два компонуемых метода. Первый — HomeScreen — будет содержать приветствие и кнопку перехода. Второй — DetailsScreen — просто покажет текст.

@Composable
fun HomeScreen(onNavigate: () -> Unit) {
    Column(
        modifier = Modifier.fillMaxSize().padding(24.dp),
        verticalArrangement = Arrangement.Center
    ) {
        Text("Главный экран", style = MaterialTheme.typography.headlineMedium)
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = onNavigate) {
            Text("Перейти к деталям")
        }
    }
}

@Composable
fun DetailsScreen() {
    Column(
        modifier = Modifier.fillMaxSize().padding(24.dp),
        verticalArrangement = Arrangement.Center
    ) {
        Text("Экран с деталями", style = MaterialTheme.typography.headlineMedium)
    }
}

Обратите внимание на параметр onNavigate — через него мы будем запускать переход из главного экрана. Это стандартный подход в Compose: экраны не знают о навигации напрямую, а лишь вызывают переданные колбэки.


Шаг 4: Строим NavHost

Сердце навигации — компонент NavHost. Он принимает контроллер navController и стартовый маршрут, а внутри описываются все экраны с помощью composable(). Обернём это в Scaffold для красивого отступа под верхнюю панель.

val navController = rememberNavController()
Scaffold { paddingValues ->
    NavHost(
        navController = navController,
        startDestination = Routes.HOME,
        modifier = Modifier.padding(paddingValues)
    ) {
        composable(Routes.HOME) {
            HomeScreen(onNavigate = {
                navController.navigate(Routes.DETAILS)
            })
        }
        composable(Routes.DETAILS) {
            DetailsScreen()
        }
    }
}

Теперь при нажатии кнопки на главном экране вызывается navController.navigate(), и Compose отображает DetailsScreen. Кнопка «Назад» на устройстве автоматически возвращает пользователя на HomeScreen — стек работает из коробки.


Шаг 5: Передаём параметры между экранами

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

object Routes {
    const val HOME = "home"
    const val DETAILS = "details/{itemId}"
    fun details(itemId: Int) = "details/$itemId"
}

Функция details() генерирует правильный путь с подставленным значением. Теперь обновим навигацию и добавим аргумент:

composable(
    route = Routes.DETAILS,
    arguments = listOf(navArgument("itemId") { type = NavType.IntType })
) { backStackEntry ->
    val itemId = backStackEntry.arguments?.getInt("itemId") ?: 0
    DetailsScreen(itemId = itemId)
}

Вызов перехода с параметром выглядит так: navController.navigate(Routes.details(42)). В DetailsScreen мы получаем переданный идентификатор и можем отобразить соответствующие данные.


Шаг 6: Возвращаемся назад программно

Кроме аппаратной кнопки «Назад», иногда нужно вернуться на предыдущий экран из кода. Для этого у navController есть метод popBackStack(). Добавим кнопку «Назад» на экран деталей:

@Composable
fun DetailsScreen(itemId: Int, onBack: () -> Unit) {
    Column(
        modifier = Modifier.fillMaxSize().padding(24.dp),
        verticalArrangement = Arrangement.Center
    ) {
        Text("Детали элемента $itemId", style = MaterialTheme.typography.headlineMedium)
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = onBack) {
            Text("Назад")
        }
    }
}

А в composable() передадим колбэк: DetailsScreen(itemId = itemId, onBack = { navController.popBackStack() }). Теперь пользователь может вернуться и по кнопке, и по системной стрелке назад.


Шаг 7: Добавляем анимации переходов

Простые переходы можно украсить анимациями. Библиотека навигации поддерживает enterTransition и exitTransition. Например, сделаем плавное появление и сдвиг:

composable(
    route = Routes.DETAILS,
    arguments = listOf(navArgument("itemId") { type = NavType.IntType }),
    enterTransition = { fadeIn(animationSpec = tween(300)) + slideInHorizontally(initialOffsetX = { it }) },
    exitTransition = { fadeOut(animationSpec = tween(300)) }
) { ... }

Теперь экран деталей плавно выезжает справа и затухает при возврате. Такие мелочи делают интерфейс живым и профессиональным.


Полный код MainActivity

Ниже — итоговый вариант, который объединяет все шаги. Скопируйте его в проект и запустите.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                AppNavigation()
            }
        }
    }
}

object Routes {
    const val HOME = "home"
    const val DETAILS = "details/{itemId}"
    fun details(itemId: Int) = "details/$itemId"
}

@Composable
fun AppNavigation() {
    val navController = rememberNavController()
    Scaffold { paddingValues ->
        NavHost(
            navController = navController,
            startDestination = Routes.HOME,
            modifier = Modifier.padding(paddingValues)
        ) {
            composable(Routes.HOME) {
                HomeScreen(onNavigate = { navController.navigate(Routes.details(42)) })
            }
            composable(
                route = Routes.DETAILS,
                arguments = listOf(navArgument("itemId") { type = NavType.IntType }),
                enterTransition = { fadeIn(tween(300)) + slideInHorizontally { it } },
                exitTransition = { fadeOut(tween(300)) }
            ) { backStackEntry ->
                val itemId = backStackEntry.arguments?.getInt("itemId") ?: 0
                DetailsScreen(itemId = itemId, onBack = { navController.popBackStack() })
            }
        }
    }
}

@Composable
fun HomeScreen(onNavigate: () -> Unit) {
    Column(
        modifier = Modifier.fillMaxSize().padding(24.dp),
        verticalArrangement = Arrangement.Center
    ) {
        Text("Главный экран", style = MaterialTheme.typography.headlineMedium)
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = onNavigate) {
            Text("Перейти к деталям")
        }
    }
}

@Composable
fun DetailsScreen(itemId: Int, onBack: () -> Unit) {
    Column(
        modifier = Modifier.fillMaxSize().padding(24.dp),
        verticalArrangement = Arrangement.Center
    ) {
        Text("Детали элемента $itemId", style = MaterialTheme.typography.headlineMedium)
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = onBack) {
            Text("Назад")
        }
    }
}

Чему вы научились

В этом уроке из цикла Android Studio Kotlin уроки вы освоили навигацию между экранами на Jetpack Compose: создали NavHost, определили маршруты, настроили переходы с параметрами и добавили анимации. Эти знания — база для построения любого многоэкранного приложения. Теперь вы можете спроектировать структуру экранов и управлять стеком навигации осознанно, а не наугад.