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 — 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.
@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.
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() — 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.
| Metoda | Opis | Przykład |
|---|---|---|
| popBackStack() | Powrót o jeden ekran wstecz | navController.popBackStack() |
| popBackStack(route, false) | Czyszczenie do route (route pozostaje) | popBackStack("home", false) |
| popBackStack(route, true) | Czyszczenie do i z route | popBackStack("home", true) |
| navigate(route) { popUpTo(route) { inclusive = true } } | Przejście z pełnym czyszczeniem | navigate("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 — 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.
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.
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.
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
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).
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.
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.
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.
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
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.
Przeczytaj również