Typowy schemat interfejsu
Ten przepis pokazuje, jak wdrożyć typowy wzorzec interfejsu nawigacji z dolnym paskiem nawigacyjnym i kilkoma stosami wstecznymi, w którym każda karta na pasku nawigacyjnym ma własną historię nawigacji.
Jak to działa
Ten przykład zawiera 3 miejsca docelowe najwyższego poziomu: Home, ChatList i Camera. Miejsce docelowe ChatList ma też trasę podrzędną ChatDetail.
TopLevelBackStack
Podstawą tego przepisu jest klasa TopLevelBackStack, która odpowiada za zarządzanie stanem nawigacji. Działa to w ten sposób:
- Dla każdego miejsca docelowego najwyższego poziomu (karty) utrzymuje oddzielny stos wsteczny.
- Śledzi aktualnie wybrane miejsce docelowe najwyższego poziomu.
- Zapewnia pojedynczy, spłaszczony stos wsteczny, który może być używany przez funkcję kompozycyjną
NavDisplay. Spłaszczona lista wsteczna to połączenie poszczególnych list wstecznych wszystkich kart.
Struktura interfejsu
Interfejs użytkownika jest tworzony za pomocą komponentu Scaffold, a NavigationBar jest jego bottomBar.
NavigationBarwyświetla element dla każdego miejsca docelowego najwyższego poziomu. Gdy użytkownik kliknie element, wywoływana jest funkcjatopLevelBackStack.addTopLevel, która przełącza się na odpowiednią kartę, zachowując historię nawigacji każdej karty.- Kompozycja
NavDisplayjest umieszczana w obszarze treściScaffold. Jest on odpowiedzialny za wyświetlanie bieżącego ekranu na podstawie spłaszczonej listy wstecznej dostarczonej przezTopLevelBackStack.
Takie podejście umożliwia stosowanie wspólnego wzorca nawigacji, w którym użytkownicy mogą przełączać się między różnymi sekcjami aplikacji, a każda sekcja zachowuje własną historię nawigacji.
Zachowywanie stanu
Zwróć uwagę na sposób zarządzania stanem nawigacji w tym przepisie. Gdy użytkownik opuści miejsce docelowe najwyższego poziomu (np. naciskając przycisk Wstecz, aż wróci do poprzedniej karty), cała historia nawigacji dla tego miejsca docelowego zostanie wyczyszczona. Stan nie jest zapisywany. Gdy użytkownik wróci później do tej karty, zacznie od jej ekranu początkowego.
Uwaga: w tym przykładzie trasa Home może znajdować się powyżej tras ChatList i Camera, co oznacza, że powrót z trasy Home nie musi powodować zamknięcia aplikacji. Aplikacja zostanie zamknięta, gdy użytkownik wróci z jednej pozostałej trasy najwyższego poziomu na liście wstecznej.
/* * Copyright 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.nav3recipes.commonui import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Face import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.graphics.vector.ImageVector import androidx.lifecycle.compose.dropUnlessResumed import androidx.navigation3.runtime.entryProvider import androidx.navigation3.ui.NavDisplay import com.example.nav3recipes.content.ContentBlue import com.example.nav3recipes.content.ContentGreen import com.example.nav3recipes.content.ContentPurple import com.example.nav3recipes.content.ContentRed import com.example.nav3recipes.ui.setEdgeToEdgeConfig private sealed interface TopLevelRoute { val icon: ImageVector } private data object Home : TopLevelRoute { override val icon = Icons.Default.Home } private data object ChatList : TopLevelRoute { override val icon = Icons.Default.Face } private data object ChatDetail private data object Camera : TopLevelRoute { override val icon = Icons.Default.PlayArrow } private val TOP_LEVEL_ROUTES : List<TopLevelRoute> = listOf(Home, ChatList, Camera) class CommonUiActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { setEdgeToEdgeConfig() super.onCreate(savedInstanceState) setContent { val topLevelBackStack = remember { TopLevelBackStack<Any>(Home) } Scaffold( bottomBar = { NavigationBar { TOP_LEVEL_ROUTES.forEach { topLevelRoute -> val isSelected = topLevelRoute == topLevelBackStack.topLevelKey NavigationBarItem( selected = isSelected, onClick = { topLevelBackStack.addTopLevel(topLevelRoute) }, icon = { Icon( imageVector = topLevelRoute.icon, contentDescription = null ) } ) } } } ) { _ -> NavDisplay( backStack = topLevelBackStack.backStack, onBack = { topLevelBackStack.removeLast() }, entryProvider = entryProvider { entry<Home>{ ContentRed("Home screen") } entry<ChatList>{ ContentGreen("Chat list screen"){ Button(onClick = dropUnlessResumed { topLevelBackStack.add(ChatDetail) }) { Text("Go to conversation") } } } entry<ChatDetail>{ ContentBlue("Chat detail screen") } entry<Camera>{ ContentPurple("Camera screen") } }, ) } } } } class TopLevelBackStack<T: Any>(startKey: T) { // Maintain a stack for each top level route private var topLevelStacks : LinkedHashMap<T, SnapshotStateList<T>> = linkedMapOf( startKey to mutableStateListOf(startKey) ) // Expose the current top level route for consumers var topLevelKey by mutableStateOf(startKey) private set // Expose the back stack so it can be rendered by the NavDisplay val backStack = mutableStateListOf(startKey) private fun updateBackStack() = backStack.apply { clear() addAll(topLevelStacks.flatMap { it.value }) } fun addTopLevel(key: T){ // If the top level doesn't exist, add it if (topLevelStacks[key] == null){ topLevelStacks.put(key, mutableStateListOf(key)) } else { // Otherwise just move it to the end of the stacks topLevelStacks.apply { remove(key)?.let { put(key, it) } } } topLevelKey = key updateBackStack() } fun add(key: T){ topLevelStacks[topLevelKey]?.add(key) updateBackStack() } fun removeLast(){ val removedKey = topLevelStacks[topLevelKey]?.removeLastOrNull() // If the removed key was a top level key, remove the associated top level stack topLevelStacks.remove(removedKey) topLevelKey = topLevelStacks.keys.last() updateBackStack() } }