Ce este Jetpack Navigation 3? Jetpack Navigation 3 este o nouă bibliotecă de navigare Google care este fundamental diferită de versiunile anterioare. — o listă normală mutabilă în care fiecare element reprezintă un ecran în aplicația dvs. NavBackStack Adăugați și eliminați elemente din această listă, iar UI-ul se actualizează automat. O clasă obișnuită de Kotlin. NavKey Acest lucru vă oferă control deplin asupra navigației, dar necesită scrierea unei cantități destul de mari de cod boilerplate pentru operațiunile tipice. De ce este incomod să lucrați direct cu NavBackStack Să vedem cum arată codul atunci când lucrăm direct cu : NavBackStack @Composable fun MyApp() { val backStack = rememberNavBackStack(Screen.Home) // Add a screen backStack.add(Screen.Details("123")) // Go back backStack.removeLastOrNull() // Replace current screen backStack.set(backStack.lastIndex, Screen.Success) } Problemele încep atunci când trebuie să declanșați navigația dintr-un ViewModel. la ViewModel (care, în opinia mea, încalcă principiile arhitecturale, deoarece cred că ViewModel nu ar trebui să știe despre lucruri specifice Compose), sau să creeze apeluri intermediare pentru fiecare acțiune de navigare. NavBackStack În plus, atunci când lucrați direct cu stivă, este ușor să uitați să gestionați cazurile de margine. Cum Nav3 Router simplifică munca Nav3 Router este un înveliș subțire față de Navigation 3 care oferă o API familiară pentru navigație. În loc să vă gândiți la indici și operațiuni de listă, spuneți pur și simplu "mergeți la ecranul X" sau "mergeți înapoi". Punct important: Nav3 Router nu își creează propriul stack. pe care Navigation 3 le oferă, doar făcându-l mai convenabil de a lucra cu. , biblioteca traduce acest lucru în operația corespunzătoare cu stivă originală. NavBackStack router.push(Screen.Details) Principalele avantaje : Poate fi utilizat de la ViewModel Comenzile de navigare sunt tamponate dacă UI-ul este temporar indisponibil (de exemplu, în timpul rotirii ecranului) Toate operațiunile stack au loc atomic Foc limpede Flexibilitate în modificarea și adăugarea comportamentului personalizat Instalarea Nav3 Router este disponibil pe Maven Central. Adăugați dependența la : build.gradle.kts // For shared module in KMP project kotlin { sourceSets { commonMain.dependencies { implementation("io.github.arttttt.nav3router:nav3router:1.0.0") } } } // For Android-only project dependencies { implementation("io.github.arttttt.nav3router:nav3router:1.0.0") } Codul sursă al bibliotecii este disponibil pe GitHub: pe github.com/arttttt/Nav3Router Cum este structurat routerul Nav3 Biblioteca se compune din trei părți principale, fiecare rezolvând propria sarcină: Router - Interfață pentru dezvoltatori Router oferă metode precum , , Când apelați aceste metode, routerul creează comenzile corespunzătoare și le trimite în lanț. routerul însuși nu știe nimic despre modul în care va fi executată navigația - acest lucru permite utilizarea acestuia de oriunde. push() pop() replace() CommandQueue – buffer între comenzi și executarea lor CommandQueue rezolvă problema timingului. Imaginați-vă: utilizatorul a apăsat un buton în timpul rotirii ecranului. UI-ul este recreat, iar navigatorul este temporar indisponibil. CommandQueue salvează comanda și o execută de îndată ce navigatorul este gata din nou. Fără aceasta, comanda ar fi pur și simplu pierdută. // Simplified queue logic class CommandQueue<T : Any> { private var navigator: Navigator<T>? = null private val pending = mutableListOf<Command<T>>() fun executeCommand(command: Command<T>) { if (navigator != null) { navigator.apply(command) // Navigator exists - execute immediately } else { pending.add(command) // No - save for later } } } Navigator - Cel care lucrează cu stack-ul Navigatorul primeşte comenzi şi le aplică Detaliu important: creează mai întâi o copie a stack-ului curent, aplică toate comenzile la acesta și numai apoi înlocuiește atomic stack-ul original cu copia modificată. NavBackStack // Simplified Navigator logic fun applyCommands(commands: Array<Command>) { val stackCopy = backStack.toMutableList() // Work with a copy for (command in commands) { when (command) { is Push -> stackCopy.add(command.screen) is Pop -> stackCopy.removeLastOrNull() // ... other commands } } backStack.swap(stackCopy) // Atomically apply changes } Începeți cu Nav3 Router Cel mai simplu mod – nici măcar nu creați routerul manual. Nav3Host o va face pentru dvs.: @Composable fun App() { val backStack = rememberNavBackStack(Screen.Home) // Nav3Host will create Router automatically Nav3Host(backStack = backStack) { backStack, onBack, router -> NavDisplay( backStack = backStack, onBack = onBack, entryProvider = entryProvider { entry<Screen.Home> { HomeScreen( onOpenDetails = { router.push(Screen.Details) // Use router } ) } entry<Screen.Details> { DetailsScreen( onBack = { router.pop() } ) } } ) } } Pentru aplicații mai complexe, are sens să creați un Router prin DI și să-l treceți la ViewModel: Definiți ecranele @Serializable sealed interface Screen : NavKey { @Serializable data object Home : Screen @Serializable data class Product(val id: String) : Screen @Serializable data object Cart : Screen } Router pentru Nav3Host. @Composable fun App() { val backStack = rememberNavBackStack(Screen.Home) val router: Router<Screen> = getSomehowUsingDI() // Pass Router to Nav3Host Nav3Host( backStack = backStack, router = router, ) { backStack, onBack, _ -> NavDisplay( backStack = backStack, onBack = onBack, entryProvider = entryProvider { entry<Screen.Home> { HomeScreen() } entry<Screen.Details> { DetailsScreen() } } ) } } ViewModel primește Router prin constructor. class ProductViewModel( private val router: Router<Screen>, private val cartRepository: CartRepository ) : ViewModel() { fun addToCart(productId: String) { viewModelScope.launch { cartRepository.add(productId) router.push(Screen.Cart) // Navigation from ViewModel } } } În UI, utilizați doar ViewModel. @Composable fun ProductScreen(viewModel: ProductViewModel = koinViewModel()) { Button(onClick = { viewModel.addToCart(productId) }) { Text("Add to Cart") } } Exemple de scenarii tipice Navigare înapoi-înapoi // Navigate to a new screen router.push(Screen.Details(productId)) // Go back router.pop() // Navigate with replacement of current screen (can't go back) router.replaceCurrent(Screen.Success) Lucrul cu lanțurile de ecran // Open multiple screens at once router.push( Screen.Category("electronics"), Screen.Product("laptop-123"), Screen.Reviews("laptop-123") ) // Return to a specific screen // Will remove all screens above Product from the stack router.popTo(Screen.Product("laptop-123")) Scenariul Checkout @Composable fun CheckoutScreen(router: Router<Screen>) { Button( onClick = { // After checkout we need to: // 1. Show confirmation screen // 2. Prevent going back to cart router.replaceStack( Screen.Home, Screen.OrderSuccess(orderId) ) // Now only Home and OrderSuccess are in the stack } ) { Text("Place Order") } } Exit Nested Navigație // User is deep in settings: // Home -> Settings -> Account -> Privacy -> DataManagement // "Done" button should return to home Button( onClick = { // Will leave only root (Home) router.clearStack() } ) { Text("Done") } // Or if you need to close the app from anywhere Button( onClick = { // Will leave only current screen and trigger system back router.dropStack() } ) { Text("Exit") } Bonus: SceneStrategy și Dialoguri Până acum, am vorbit doar despre navigarea simplă între ecrane.Dar dacă aveți nevoie să afișați un dialog sau o foaie de bord?Aici vine în ajutor conceptul SceneStrategy din Navigation 3. Ce este SceneStrategy? SceneStrategy este un mecanism care determină exact modul în care vor fi afișate ecranele din stack. , care arată pur și simplu ultimul ecran din stivă.Dar puteți crea propriile strategii pentru scenarii mai complexe. SinglePaneSceneStrategy Gândiți-vă la SceneStrategy ca la un regizor care se uită la pachetul dvs. de ecrane și decide: "Bine, aceste trei ecrane le afișăm în mod normal, dar ultimul - ca o fereastră modală deasupra celor anterioare". Crearea unei strategii pentru ModalBottomSheet Să creăm o strategie care să afișeze anumite ecrane ca foaie de fundal.În primul rând, să definim cum vom marca astfel de ecrane: @Serializable sealed interface Screen : NavKey { @Serializable data object Home : Screen @Serializable data class Product(val id: String) : Screen // This screen will be shown as bottom sheet @Serializable data object Filters : Screen } Acum, să creăm strategia în sine. va verifica metadatele din ultimul ecran și, dacă găsește un marker special, îl arată ca o foaie de fund: class BottomSheetSceneStrategy<T : Any> : SceneStrategy<T> { companion object { // Metadata key by which we identify bottom sheet private const val BOTTOM_SHEET_KEY = "bottomsheet" // Helper function to create metadata fun bottomSheet(): Map<String, Any> { return mapOf(BOTTOM_SHEET_KEY to true) } } @Composable override fun calculateScene( entries: List<NavEntry<T>>, onBack: (Int) -> Unit ): Scene<T>? { val lastEntry = entries.lastOrNull() ?: return null // Check if the last screen has bottom sheet marker val isBottomSheet = lastEntry.metadata[BOTTOM_SHEET_KEY] as? Boolean if (isBottomSheet == true) { // Return special Scene for bottom sheet return BottomSheetScene( entry = lastEntry, previousEntries = entries.dropLast(1), onBack = onBack ) } // This is not a bottom sheet, let another strategy handle it return null } } Combinarea mai multor strategii Într-o aplicație reală, este posibil să aveți nevoie de foile de fundal, dialoguri și ecrane regulate.Pentru aceasta, puteți crea o strategie de delegare care va alege strategia potrivită pentru fiecare ecran: class DelegatedScreenStrategy<T : Any>( private val strategyMap: Map<String, SceneStrategy<T>>, private val fallbackStrategy: SceneStrategy<T> ) : SceneStrategy<T> { @Composable override fun calculateScene( entries: List<NavEntry<T>>, onBack: (Int) -> Unit ): Scene<T>? { val lastEntry = entries.lastOrNull() ?: return null // Check all keys in metadata for (key in lastEntry.metadata.keys) { val strategy = strategyMap[key] if (strategy != null) { // Found suitable strategy return strategy.calculateScene(entries, onBack) } } // Use default strategy return fallbackStrategy.calculateScene(entries, onBack) } } Utilizarea în aplicaţie Acum, să punem totul împreună.Iată cum arată utilizarea foii de fund într-o aplicație reală: @Composable fun ShoppingApp() { val backStack = rememberNavBackStack(Screen.Home) val router = rememberRouter<Screen>() Nav3Host( backStack = backStack, router = router ) { backStack, onBack, router -> NavDisplay( backStack = backStack, onBack = onBack, // Use our combined strategy sceneStrategy = DelegatedScreenStrategy( strategyMap = mapOf( "bottomsheet" to BottomSheetSceneStrategy(), "dialog" to DialogSceneStrategy() // Navigation 3 already has this strategy ), fallbackStrategy = SinglePaneSceneStrategy() // Regular screens ), entryProvider = entryProvider { entry<Screen.Home> { HomeScreen( onOpenFilters = { // Open filters as bottom sheet router.push(Screen.Filters) } ) } entry<Screen.Product> { screen -> ProductScreen(productId = screen.id) } // Specify that Filters should be bottom sheet entry<Screen.Filters>( metadata = BottomSheetSceneStrategy.bottomSheet() ) { FiltersContent( onApply = { filters -> // Apply filters and close bottom sheet applyFilters(filters) router.pop() } ) } } ) } } Ce se întâmplă aici? când sună Dar datorită metadatelor și a strategiei noastre, UI înțelege că acest ecran trebuie să fie afișat ca o foaie de fund pe partea de sus a ecranului anterior, mai degrabă decât să îl înlocuiască complet. router.push(Screen.Filters) Când sună din punctul de vedere al routerului, aceasta este o navigare înapoi regulată, dar vizual pare a închide o fereastră modală. router.pop() Avantajele acestei abordări Utilizarea SceneStrategy oferă mai multe avantaje importante. În primul rând, logica dvs. de navigare rămâne simplă – utilizați în continuare şi În al doilea rând, starea de navigare rămâne consecventă – foaia de jos este doar un alt ecran din grămadă care este salvat în mod corespunzător în timpul rotirii ecranului sau procesului de ucidere. push pop Această abordare este deosebit de utilă atunci când același ecran poate fi afișat în mod diferit în funcție de context.De exemplu, un ecran de conectare poate fi un ecran regulat la prima lansare a aplicației și un dialog modal atunci când încercați să efectuați o acțiune care necesită autorizare. De ce ar trebui să folosiți routerul Nav3 Nav3 Router nu încearcă să înlocuiască Navigation 3 sau să adauge noi caracteristici. Sarcina sa este de a face lucrul cu navigația convenabil și previzibil. Veți obține o API simplă care poate fi utilizată din orice strat al aplicației, gestionarea automată a problemelor de timp și capacitatea de a testa cu ușurință logica de navigație. În același timp, sub capac, navigația obișnuită 3 încă funcționează cu toate capacitățile sale: salvarea stării, suportul pentru animație și manipularea corespunzătoare a butonului de sistem "Înapoi". Dacă utilizați deja Navigation 3 sau intenționați să migrați la acesta, Nav3 Router vă va ajuta să faceți această experiență mai plăcută fără a adăuga complexitate inutilă proiectului. stânga Repozitorul GitHub: github.com/arttttt/Nav3Router Exemple de utilizare: vezi folderul de eșantionare din repository