NavController — istota, metody i zarządzanie nawigacją w Jetpack Compose

Autor: IT Sectr Opublikowano: 2026-06-29 Czas czytania: 7 min

NavController to centralny komponent biblioteki Navigation Compose, zarządzający stosem nawigacji i stanem back stack w aplikacjach Android. Za pomocą NavController wykonuje się przejścia między ekranami, powrót do poprzednich stron i przekazywanie danych między trasami. Według danych Android Developers (2025), NavController jest obowiązkowym elementem każdej aplikacji Compose z więcej niż jednym ekranem. Kontroler tworzony jest przez rememberNavController(), przekazywany do NavHost i dostępny do wywołania navigate() z dowolnego miejsca kompozycji. Wbudowana obsługa SavedStateHandle automatycznie zapisuje stan ViewModel przy rekonfiguracji.

Najważniejsze

  • NavController — centralny kontroler nawigacji Compose, zarządzający back stack i przejściami między ekranami
  • navigate() — główna metoda przejścia na trasę z obsługą NavOptions do zarządzania stosem
  • popBackStack() — powrót do poprzedniego ekranu z opcjonalnym czyszczeniem do określonej trasy
  • SavedStateHandle — integracja z ViewModel do zapisywania stanu ekranu przy nawigacji
  • currentBackStackEntryAsState() — obserwacja bieżącej trasy do synchronizacji UI

Czym jest NavController w Jetpack Compose?

NavController — to klasa z biblioteki Navigation Compose, implementująca kontroler nawigacji dla aplikacji Compose. NavController zarządza stosem NavBackStackEntry, gdzie każdy wpis zawiera trasę, argumenty i stan ekranu. Kontroler obsługuje podstawowe operacje nawigacji: przejście, powrót, zastąpienie i czyszczenie.

W przeciwieństwie do systemu View, gdzie nawigacja odbywała się przez FragmentManager lub Intent, NavController działa wyłącznie w kontekście Compose. Back stack przechowywany jest w postaci grafu NavDestination, a nie stosu Fragment. Eliminuje to narzut na tworzenie i niszczenie Fragment, a także upraszcza testowanie — NavController można zamockować przez TestNavHostController.

NavController jest ściśle powiązany z NavHost — kontenerem renderującym bieżący ekran z grafu. Bez NavHost NavController nie może wyświetlać funkcji composable, ale zachowuje możliwość zarządzania stosem. W typowej architekturze NavController tworzony jest na poziomie Activity lub głównego composable i przekazywany w dół drzewa kompozycji przez parametry.

Według Google, NavController przeszedł kilka major-wydań. Wersja 2.8.0 dodała Type-Safe Navigation, wersja 2.9.0 — obsługę predictive back gesture (Android 14+). Kontroler jest kompatybilny z Material3 Scaffold i BottomNavigation. Dla projektów multimodularnych NavController przekazywany jest przez DI (Hilt/Koin) lub parametry konstruktora.

NavController tworzony jest przez funkcję composable rememberNavController(). Funkcja zwraca instancję NavHostController (dziedziczącą po NavController), powiązaną z cyklem życia bieżącego composable. Po wyjściu z kompozycji kontroler jest czyszczony. Do zapisania kontrolera przy rekonfiguracji użyj rememberSaveable lub ViewModel.

kotlin
@Composable
fun MyApp() {
    val navController = rememberNavController()
    NavHost(
        navController = navController,
        startDestination = "main"
    ) {
        composable("main") { MainScreen(navController) }
        composable("details") { DetailsScreen(navController) }
    }
}

Konfiguracja NavController obejmuje: NavHostController (główny), TestNavHostController (testowanie) i ScopedNavController (podrzędny dla zagnieżdżonych grafów). Dla BottomNavigation NavController powinien być jeden na całą aplikację — tworzenie nowego kontrolera w każdej zakładce spowoduje utratę stosu. Do przekazywania kontrolera do zagnieżdżonych ekranów używaj parametru funkcji, a nie CompositionLocalProvider, aby zachować czytelność.

Do testowania nawigacji używaj TestNavHostController z compose-test-rule. Kontroler pozwala ustawić początkową trasę i sprawdzić, czy navigate() wywołał oczekiwane przejście. Testowanie NavController nie wymaga emulatora — działa z matcherami Semantics Compose Test.

Metoda navigate(route: String) — podstawowy sposób nawigacji w NavController. Przyjmuje ciąg trasy, opcjonalne NavOptions i Navigator.Extras. NavOptions zarządzają zachowaniem przejścia: launchSingleTop (nie duplikuj trasy w stosie), popUpTo (wyczyść stos do trasy), restoreState (przywróć poprzedni stan).

NavOptions są ustawiane przez składnię builder: NavOptionsBuilder. Główne parametry: popUpTo (route + inclusive/saveState), launchSingleTop (Boolean, true — nie twórz duplikatu), restoreState (przywróć stan przy powrocie). Bez popUpTo każdy navigate() dodaje wpis do stosu, co prowadzi do gromadzenia back stack i nieprawidłowego działania przycisku Back.

kotlin
navController.navigate("profile/42") {
    popUpTo("main") { saveState = true }
    launchSingleTop = true
    restoreState = true
}

Navigator.Extras pozwala przekazać dodatkowe dane niebędące częścią trasy: shared element do animacji, flagi Intent, Pac-Man bundle. Extras są używane rzadko — głównie do integracji z Accompanist Animation lub niestandardowymi Navigator. W większości scenariuszy wystarczy ciąg trasy i NavOptions.

popBackStack: zarządzanie powrotem i czyszczeniem stosu

popBackStack() — metoda powrotu do poprzedniego ekranu. Bez argumentów usuwa górny wpis stosu i zwraca true, jeśli usunięcie się powiodło. Jeśli stos jest pusty — metoda zwraca false, a Activity jest zamykane (analogicznie do super.onBackPressed()).

Przeciążona wersja popBackStack(route: String, inclusive: Boolean) usuwa wszystkie wpisy do określonej trasy. Jeśli inclusive = true — usuwana jest również sama trasa. Metoda zwraca Boolean — true, jeśli udało się znaleźć i usunąć wpisy. Wersja z inclusive jest przydatna w scenariuszach „wyjście do ekranu głównego“ po autoryzacji lub złożeniu zamówienia.

MetodaOpisPrzykład
popBackStack()Powrót o jeden ekran wstecznavController.popBackStack()
popBackStack(route, false)Czyszczenie do route (route pozostaje)popBackStack("home", false)
popBackStack(route, true)Czyszczenie do i z routepopBackStack("home", true)
navigate(route) { popUpTo(route) { inclusive = true } }Przejście z pełnym czyszczeniemnavigate("login") { popUpTo(0) { inclusive = true } }

Do obsługi systemowego przycisku Back (hardware back button) użyj BackHandler z Compose. BackHandler przyjmuje enabled i onBack — callback wywoływany przy naciśnięciu. Dla Android 14+ używany jest PredictiveBackGesture, integrowany przez NavController od wersji 2.9.0. Predictive back dodaje animację podglądu powrotu.

SavedStateHandle: zapisywanie stanu ekranu

SavedStateHandle — to mechanizm zapisywania stanu ViewModel przy nawigacji i rekonfiguracji. NavController automatycznie udostępnia SavedStateHandle dla każdego NavBackStackEntry. Przez SavedStateHandle ViewModel przechowuje stan ekranu i przywraca go przy powrocie (restoreState = true).

W Navigation Compose SavedStateHandle jest używany razem z ViewModel: ViewModel jest inicjalizowany przez SavedStateHandle, który jest przekazywany z backStackEntry. Przy przejściu na inny ekran i powrocie (z restoreState) ViewModel otrzymuje zapisany stan, a nie jest tworzony od nowa. Jest to krytyczne dla ekranów z wprowadzaniem danych, filtrami lub przewijaniem.

kotlin
class ProfileViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    val userId: String = savedStateHandle.get<String>("userId") ?: ""
    var searchQuery by savedStateHandle.getStateFlow("search", "")
        .collectAsState()
}

SavedStateHandle obsługuje typy prymitywne, String, Bundle i Parcelable. Dla złożonych obiektów zapisuj tylko ID, a pełne dane ładuj z repozytorium. Limit SavedStateHandle — około 1 MB, przekroczenie powoduje TransactionTooLargeException. Dla dużych wolumenów używaj Room lub DataStore zamiast zapisywania w handle.

Ważne: SavedStateHandle zapisuje stan tylko przy użyciu restoreState = true w NavOptions. Jeśli restoreState nie jest określony, przy powrocie ViewModel jest tworzony od nowa z domyślnymi wartościami. Dla przełączania BottomNavigation z restoreState NavController zapisuje stan każdej zakładki i przywraca go przy ponownym wyborze.

Obserwacja bieżącej trasy przez currentBackStackEntryAsState

currentBackStackEntryAsState() — funkcja zwracająca State<NavBackStackEntry?>, która aktualizuje się przy każdej zmianie bieżącej trasy. Jest to główny mechanizm synchronizacji UI z nawigacją: BottomNavigation podświetla aktywny element, Toolbar aktualizuje tytuł, Drawer zamyka się przy przejściu.

Funkcja działa przez snapshotFlow i collectAsState: przy zmianie back stack Compose przerenderowuje subskrybowane elementy. Ważne: currentBackStackEntryAsState() aktualizuje się dopiero po zakończeniu animacji przejścia. Do natychmiastowej aktualizacji użyj currentDestination, który zmienia się synchronicznie z navigate(), ale nie obsługuje stanu.

kotlin
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route

Text(
    text = when (currentRoute) {
        "home" -> "Home"
        "profile" -> "Profile"
        else -> ""
    }
)

Do dostępu do argumentów bieżącej trasy użyj navBackStackEntry?.arguments. Jest to wygodne w BottomNavigation: selectedItem jest obliczany na podstawie currentRoute. Do debugowania nawigacji użyj NavController.addOnDestinationChangedListener(), który loguje każde przejście. W produkcji unikaj subskrypcji wewnątrz dużej liczby composable — utwórz jeden źródło w ViewModel i przekazuj State do UI.

Często zadawane pytania

Czy można utworzyć kilka NavController w jednym Activity?

Technicznie tak, ale nie jest to zalecane. Jeden NavController zapewnia spójny back stack i upraszcza debugowanie. Wiele kontrolerów jest uzasadnione tylko dla zagnieżdżonych grafów z oddzielną nawigacją (np. modal bottom sheet z własnym stosem).

Jak przekazać NavController przez ViewModel?

Przekazuj NavController do ViewModel przez konstruktor lub DI. Lepiej jednak przekazywać tylko funkcje callback (onNavigate, onBack), a nie sam NavController — upraszcza to testowanie. Dla zdarzeń używaj Channel<NavEvent> w ViewModel i zbieraj w UI.

Dlaczego navigate nie działa po operacji asynchronicznej?

Problem leży w cyklu życia: jeśli NavController nie jest jeszcze zainicjalizowany (NavHost nie zbudowany), navigate() jest ignorowane. Użyj LaunchedEffect do wywołania nawigacji po załadowaniu danych, a nie wewnątrz coroutine z dowolnym lifecycle.

Jak wyczyścić cały back stack i przejść na nowy ekran?

Wywołaj navController.navigate("target") { popUpTo(0) { inclusive = true } }. Parametr popUpTo(0) czyści stos całkowicie, inclusive = true usuwa również wpis początkowy. Flaga launchSingleTop = true zapobiega duplikowaniu nowej trasy.

Czym różni się NavHostController od NavController?

NavHostController — dziedziczy po NavController z dodatkowymi metodami dla NavHost (np. setOnBackStackChangedListener). NavController — klasa bazowa, którą można używać poza NavHost do programowego zarządzania stosem. W większości przypadków używany jest NavHostController.

Podsumowanie

  • NavController — centralny komponent Navigation Compose, zarządzający stosem tras i przejściami między ekranami
  • navigate() wykonuje przejście z ustawieniami popUpTo, launchSingleTop i restoreState przez NavOptions
  • popBackStack() zarządza powrotem: pojedynczy krok lub masowe czyszczenie do określonej trasy z inclusive
  • SavedStateHandle integruje się z ViewModel do automatycznego zapisywania stanu ekranu przy nawigacji
  • currentBackStackEntryAsState() zapewnia reaktywną obserwację bieżącej trasy do synchronizacji UI
  • BackHandler obsługuje systemowy przycisk Back, a PredictiveBackGesture jest obsługiwany od NavController 2.9.0
  • Do testowania używaj TestNavHostController z compose-test-rule i matcherami Semantics

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również