Navigation ist der Prozess der Interaktion mit der Benutzeroberfläche einer App, um auf die Inhaltsziele der App zuzugreifen. Die Navigationsprinzipien von Android enthalten Richtlinien, mit denen Sie eine einheitliche, intuitive App-Navigation erstellen können.
Responsive UIs bieten responsive Inhaltsziele und enthalten oft verschiedene Arten von Navigationselementen als Reaktion auf Änderungen der Anzeigegröße, z. B. eine untere Navigationsleiste auf kleinen Displays, eine Navigationsleiste auf mittelgroßen Displays oder eine durchgehende Navigationsleiste auf großen Displays. Responsive UIs sollten jedoch weiterhin den Prinzipien der Navigation entsprechen.
Die Jetpack-Navigationskomponente implementiert die Prinzipien der Navigation und kann verwendet werden, um die Entwicklung von Apps mit responsiven Benutzeroberflächen zu erleichtern.
![](https://developer.android.com/static/images/guide/topics/large-screens/navigation/navigation_wireframes.png?authuser=6&hl=de)
Responsive Navigation auf der Benutzeroberfläche
Die Größe des Displayfensters einer App wirkt sich auf die Ergonomie und Nutzerfreundlichkeit aus. Mit Fenstergrößenklassen können Sie geeignete Navigationselemente wie Navigationsleisten, Schienen oder Leisten bestimmen und dort platzieren, wo sie für den Nutzer am besten zugänglich sind. In den Layoutrichtlinien von Material Design nehmen Navigationselemente einen festen Bereich am oberen Rand des Displays ein und können an den unteren Rand verschoben werden, wenn die App kompakt ist. Ihre Auswahl der Navigationselemente hängt vor allem von der Größe des App-Fensters und der Anzahl der Elemente ab, die das Element enthalten muss.
Fenstergrößenklasse | Einige Elemente | Viele Artikel |
---|---|---|
Kompakte Breite | Navigationsleiste unten | Navigationsleiste (vorne oder unten) |
mittlere Breite | Navigationsstreifen | Navigationsleiste (vorne) |
Breite nach Expansion | Navigationsstreifen | Persistente Navigationsleiste (an der Spitze) |
In ansichtsbasierten Layouts können Layoutressourcendateien durch Haltepunkte der Fenstergröße qualifiziert werden, um verschiedene Navigationselemente für unterschiedliche Anzeigeabmessungen zu verwenden. Jetpack Compose kann Haltepunkte verwenden, die von der Fenstergrößen-API bereitgestellt werden, um programmatisch das Navigationselement zu ermitteln, das am besten für das App-Fenster geeignet ist.
Aufrufe
<!-- res/layout/main_activity.xml --> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <com.google.android.material.bottomnavigation.BottomNavigationView android:layout_width="0dp" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" ... /> <!-- Content view(s) --> </androidx.constraintlayout.widget.ConstraintLayout> <!-- res/layout-w600dp/main_activity.xml --> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <com.google.android.material.navigationrail.NavigationRailView android:layout_width="wrap_content" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" ... /> <!-- Content view(s) --> </androidx.constraintlayout.widget.ConstraintLayout> <!-- res/layout-w1240dp/main_activity.xml --> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <com.google.android.material.navigation.NavigationView android:layout_width="wrap_content" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" ... /> <!-- Content view(s) --> </androidx.constraintlayout.widget.ConstraintLayout>
Textvorschläge
// This method should be run inside a Composable function. val widthSizeClass = calculateWindowSizeClass(this).widthSizeClass // You can get the height of the current window by invoking heightSizeClass instead. @Composable fun MyApp(widthSizeClass: WindowWidthSizeClass) { // Select a navigation element based on window size. when (widthSizeClass) { WindowWidthSizeClass.Compact -> { CompactScreen() } WindowWidthSizeClass.Medium -> { MediumScreen() } WindowWidthSizeClass.Expanded -> { ExpandedScreen() } } } @Composable fun CompactScreen() { Scaffold(bottomBar = { NavigationBar { icons.forEach { item -> NavigationBarItem( selected = isSelected, onClick = { ... }, icon = { ... }) } } } ) { // Other content } } @Composable fun MediumScreen() { Row(modifier = Modifier.fillMaxSize()) { NavigationRail { icons.forEach { item -> NavigationRailItem( selected = isSelected, onClick = { ... }, icon = { ... }) } } // Other content } } @Composable fun ExpandedScreen() { PermanentNavigationDrawer( drawerContent = { icons.forEach { item -> NavigationDrawerItem( icon = { ... }, label = { ... }, selected = isSelected, onClick = { ... } ) } }, content = { // Other content } ) }
Ziele für responsive Inhalte
In einer responsiven UI muss sich das Layout jedes Inhaltsziels an Änderungen der Fenstergröße anpassen. Ihre App kann den Layoutabstand anpassen, Elemente neu positionieren, Inhalte hinzufügen oder entfernen oder UI-Elemente, einschließlich Navigationselemente, ändern. Weitere Informationen finden Sie unter UI zu responsiven Layouts migrieren und Unterstützung verschiedener Bildschirmgrößen.
Wenn die Größenänderungsereignisse von jedem einzelnen Ziel ordnungsgemäß verarbeitet werden, werden die Änderungen auf der Benutzeroberfläche isoliert. Der restliche App-Status, einschließlich der Navigation, ist davon nicht betroffen.
Die Navigation sollte nicht als Nebeneffekt von Änderungen der Fenstergröße auftreten. Erstellen Sie keine Inhaltsziele nur für unterschiedliche Fenstergrößen. Erstellen Sie beispielsweise keine unterschiedlichen Zielanwendungen für die verschiedenen Displays eines faltbaren Geräts.
Wenn Sie als Nebeneffekt von Änderungen der Fenstergröße navigieren, treten die folgenden Probleme auf:
- Das alte Ziel (für die vorherige Fenstergröße) ist möglicherweise kurzzeitig sichtbar, bevor zum neuen Ziel navigiert wird
- Zur Aufrechterhaltung der Reversierbarkeit (z. B. beim Auf- und Zuklappen eines Geräts) ist eine Navigation für jede Fenstergröße erforderlich.
- Es kann schwierig sein, den App-Status zwischen Zielen beizubehalten, da die Navigation den Status nach dem Knacken des Backstacks zerstören kann.
Außerdem ist Ihre App möglicherweise nicht einmal im Vordergrund, während die Fenstergröße geändert wird. Für das Layout Ihrer App wird möglicherweise mehr Platz benötigt als für die App im Vordergrund. Wenn der Nutzer zu Ihrer App zurückkehrt, haben sich möglicherweise die Ausrichtung und die Fenstergröße geändert.
Wenn für Ihre App eindeutige Inhaltsziele basierend auf der Fenstergröße erforderlich sind, sollten Sie die relevanten Ziele in einem einzigen Ziel mit alternativen Layouts zusammenfassen.
Inhaltsziele mit alternativen Layouts
Als Teil eines responsiven Designs kann ein einzelnes Navigationsziel je nach Größe des App-Fensters alternative Layouts haben. Jedes Layout nimmt das gesamte Fenster ein, aber es werden verschiedene Layouts für unterschiedliche Fenstergrößen angezeigt.
Ein kanonisches Beispiel ist die Listenansicht. Bei kleinen Fenstergrößen zeigt Ihre App ein Inhaltslayout für die Liste und eins für die Details an. Wenn Sie zum Ziel der Listenansicht navigieren, wird anfangs nur das Listenlayout angezeigt. Wenn ein Listenelement ausgewählt wird, zeigt Ihre App das Layout mit den Details an und ersetzt die Liste. Wenn die Rückwärtssteuerung ausgewählt ist, wird das Listenlayout angezeigt, wodurch die Details ersetzt werden. Bei maximierten Fenstergrößen werden die Listen- und Detaillayouts jedoch nebeneinander angezeigt.
Aufrufe
Mit SlidingPaneLayout
können Sie ein einzelnes Navigationsziel erstellen, bei dem auf großen Bildschirmen zwei Inhaltsbereiche nebeneinander angezeigt werden, auf Geräten mit kleinen Bildschirmen, z. B. Smartphones, aber jeweils nur ein Bereich.
<!-- Single destination for list and detail. -->
<navigation ...>
<!-- Fragment that implements SlidingPaneLayout. -->
<fragment
android:id="@+id/article_two_pane"
android:name="com.example.app.ListDetailTwoPaneFragment" />
<!-- Other destinations... -->
</navigation>
Weitere Informationen zur Implementierung eines Layouts mit Listendetails mithilfe von SlidingPaneLayout
finden Sie unter Layout mit zwei Bereichen erstellen.
Textvorschläge
In Compose kann eine Listen-Detailansicht implementiert werden, indem alternative zusammensetzbare Funktionen in einer einzigen route kombiniert werden, die Fenstergrößenklassen verwendet, um die entsprechende zusammensetzbare Funktion für jede Größenklasse auszugeben.
Eine Route ist der Navigationspfad zu einem Inhaltsziel, bei dem es sich in der Regel um eine einzelne zusammensetzbare Funktion handelt. Es kann sich aber auch um alternative zusammensetzbare Funktionen handeln. Die Geschäftslogik bestimmt, welche der alternativen zusammensetzbaren Funktionen angezeigt wird. Die zusammensetzbare Funktion füllt das App-Fenster aus, unabhängig davon, welche Alternative angezeigt wird.
Die Listenansicht besteht aus drei zusammensetzbaren Funktionen, zum Beispiel:
/* Displays a list of items. */
@Composable
fun ListOfItems(
onItemSelected: (String) -> Unit,
) { /*...*/ }
/* Displays the detail for an item. */
@Composable
fun ItemDetail(
selectedItemId: String? = null,
) { /*...*/ }
/* Displays a list and the detail for an item side by side. */
@Composable
fun ListAndDetail(
selectedItemId: String? = null,
onItemSelected: (String) -> Unit,
) {
Row {
ListOfItems(onItemSelected = onItemSelected)
ItemDetail(selectedItemId = selectedItemId)
}
}
Eine einzelne Navigationsroute bietet Zugriff auf die Listenansicht:
@Composable
fun ListDetailRoute(
// Indicates that the display size is represented by the expanded window size class.
isExpandedWindowSize: Boolean = false,
// Identifies the item selected from the list. If null, a item has not been selected.
selectedItemId: String?,
) {
if (isExpandedWindowSize) {
ListAndDetail(
selectedItemId = selectedItemId,
/*...*/
)
} else {
// If the display size cannot accommodate both the list and the item detail,
// show one of them based on the user's focus.
if (selectedItemId != null) {
ItemDetail(
selectedItemId = selectedItemId,
/*...*/
)
} else {
ListOfItems(/*...*/)
}
}
}
ListDetailRoute
(das Navigationsziel) bestimmt, welche der drei zusammensetzbaren Funktionen ausgegeben werden sollen: ListAndDetail
für die Größe des maximierten Fensters; ListOfItems
oder ItemDetail
für „Schmal“, je nachdem, ob ein Listenelement ausgewählt wurde.
Die Route ist in einem NavHost
enthalten. Beispiel:
NavHost(navController = navController, startDestination = "listDetailRoute") {
composable("listDetailRoute") {
ListDetailRoute(isExpandedWindowSize = isExpandedWindowSize,
selectedItemId = selectedItemId)
}
/*...*/
}
Sie können das Argument isExpandedWindowSize
angeben, indem Sie WindowMetrics Ihrer Anwendung prüfen.
Das Argument selectedItemId
kann von einem ViewModel
bereitgestellt werden, der den Status über alle Fenstergrößen hinweg beibehält. Wenn der Nutzer ein Element aus der Liste auswählt, wird die Statusvariable selectedItemId
aktualisiert:
class ListDetailViewModel : ViewModel() {
data class ListDetailUiState(
val selectedItemId: String? = null,
)
private val viewModelState = MutableStateFlow(ListDetailUiState())
fun onItemSelected(itemId: String) {
viewModelState.update {
it.copy(selectedItemId = itemId)
}
}
}
val listDetailViewModel = ListDetailViewModel()
@Composable
fun ListDetailRoute(
isExpandedWindowSize: Boolean = false,
selectedItemId: String?,
onItemSelected: (String) -> Unit = { listDetailViewModel.onItemSelected(it) },
) {
if (isExpandedWindowSize) {
ListAndDetail(
selectedItemId = selectedItemId,
onItemSelected = onItemSelected,
/*...*/
)
} else {
if (selectedItemId != null) {
ItemDetail(
selectedItemId = selectedItemId,
/*...*/
)
} else {
ListOfItems(
onItemSelected = onItemSelected,
/*...*/
)
}
}
}
Die Route enthält auch ein benutzerdefiniertes BackHandler
, wenn die zusammensetzbaren Elementdetails das gesamte App-Fenster belegen:
class ListDetailViewModel : ViewModel() {
data class ListDetailUiState(
val selectedItemId: String? = null,
)
private val viewModelState = MutableStateFlow(ListDetailUiState())
fun onItemSelected(itemId: String) {
viewModelState.update {
it.copy(selectedItemId = itemId)
}
}
fun onItemBackPress() {
viewModelState.update {
it.copy(selectedItemId = null)
}
}
}
val listDetailViewModel = ListDetailViewModel()
@Composable
fun ListDetailRoute(
isExpandedWindowSize: Boolean = false,
selectedItemId: String?,
onItemSelected: (String) -> Unit = { listDetailViewModel.onItemSelected(it) },
onItemBackPress: () -> Unit = { listDetailViewModel.onItemBackPress() },
) {
if (isExpandedWindowSize) {
ListAndDetail(
selectedItemId = selectedItemId,
onItemSelected = onItemSelected,
/*...*/
)
} else {
if (selectedItemId != null) {
ItemDetail(
selectedItemId = selectedItemId,
/*...*/
)
BackHandler {
onItemBackPress()
}
} else {
ListOfItems(
onItemSelected = onItemSelected,
/*...*/
)
}
}
}
Die Kombination des App-Status aus einer ViewModel
mit Informationen zur Fenstergröße macht die Auswahl der geeigneten zusammensetzbaren Funktion durch einfache Logik möglich. Durch die Aufrechterhaltung eines unidirektionalen Datenflusses kann Ihre App den verfügbaren Platz optimal nutzen und gleichzeitig den Anwendungsstatus beibehalten.
Eine vollständige Implementierung der Listen-Detailansicht in Compose finden Sie im JetNews-Beispiel auf GitHub.
Ein Navigationsdiagramm
Um für eine einheitliche Nutzererfahrung auf allen Geräten und Fenstergrößen zu sorgen, sollten Sie ein einzelnes Navigationsdiagramm verwenden, bei dem das Layout jedes Inhaltsziels responsiv ist.
Wenn Sie für jede Fenstergrößenklasse ein anderes Navigationsdiagramm verwenden, müssen Sie bei jedem Wechsel der App von einer Größenklasse in eine andere das aktuelle Ziel des Nutzers in den anderen Grafiken ermitteln, einen Back-Stack erstellen und Statusinformationen abgleichen, die sich von den Diagrammen unterscheiden.
Verschachtelter Navigationshost
Ihre App kann ein Inhaltsziel enthalten, das eigene Inhaltsziele hat. In einer Listen-Detailansicht könnte der Element-Detailbereich beispielsweise UI-Elemente enthalten, die zu Inhalten navigieren, die das Element-Detail ersetzen.
Um diese Art der Unternavigation zu implementieren, kann der Detailbereich ein verschachtelter Navigationshost mit einem eigenen Navigationsdiagramm sein, das die Ziele angibt, auf die über den Detailbereich zugegriffen wird:
Aufrufe
<!-- layout/two_pane_fragment.xml --> <androidx.slidingpanelayout.widget.SlidingPaneLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/sliding_pane_layout" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/list_pane" android:layout_width="280dp" android:layout_height="match_parent" android:layout_gravity="start"/> <!-- Detail pane is a nested navigation host. Its graph is not connected to the main graph that contains the two_pane_fragment destination. --> <androidx.fragment.app.FragmentContainerView android:id="@+id/detail_pane" android:layout_width="300dp" android:layout_weight="1" android:layout_height="match_parent" android:name="androidx.navigation.fragment.NavHostFragment" app:navGraph="@navigation/detail_pane_nav_graph" /> </androidx.slidingpanelayout.widget.SlidingPaneLayout>
Textvorschläge
@Composable fun ItemDetail(selectedItemId: String? = null) { val navController = rememberNavController() NavHost(navController, "itemSubdetail1") { composable("itemSubdetail1") { ItemSubdetail1(...) } composable("itemSubdetail2") { ItemSubdetail2(...) } composable("itemSubdetail3") { ItemSubdetail3(...) } } }
Dies unterscheidet sich von einer verschachtelten Navigationsgrafik, da die Navigationsgrafik der verschachtelten NavHost
nicht mit der Hauptnavigationsgrafik verbunden ist. Das heißt, Sie können nicht direkt von Zielen in der einen Grafik zu Zielen in der anderen wechseln.
Weitere Informationen finden Sie unter Verschachtelte Navigationsgrafiken und Mit der Funktion „Schreiben“ navigieren.
Beibehaltener Status
Damit Ziele für responsive Inhalte bereitgestellt werden können, muss Ihre App ihren Status beibehalten, wenn das Gerät gedreht oder zugeklappt wird oder die Größe des App-Fensters angepasst wird. Standardmäßig werden durch solche Konfigurationsänderungen die Aktivitäten, Fragmente, Ansichtshierarchien und zusammensetzbaren Funktionen der Anwendung neu erstellt. Es wird empfohlen, den UI-Status mit einem ViewModel
- oder rememberSaveable
-Element zu speichern, das auch bei Konfigurationsänderungen erhalten bleibt. Weitere Informationen finden Sie unter UI-Status speichern und Status und Jetpack Compose.
Größenänderungen sollten umkehrbar sein, z. B. wenn der Nutzer das Gerät und dann wieder zurückdreht.
Responsive Layouts können verschiedene Inhalte in unterschiedlichen Fenstergrößen anzeigen. Daher müssen responsive Layouts häufig einen zusätzlichen, inhaltsbezogenen Status speichern, auch wenn der Zustand nicht für die aktuelle Fenstergröße anwendbar ist. Beispielsweise könnte ein Layout Platz bieten, um ein zusätzliches Scroll-Widget nur bei größeren Fensterbreiten anzuzeigen. Wenn ein Größenänderungsereignis dazu führt, dass die Fensterbreite zu klein wird, wird das Widget ausgeblendet. Wenn die App wieder die vorherigen Abmessungen verwendet, wird das Scroll-Widget wieder sichtbar und die ursprüngliche Scrollposition sollte wiederhergestellt werden.
ViewModel-Bereiche
Im Entwicklerleitfaden Zur Navigation-Komponente migrieren wird eine Architektur mit einer einzigen Aktivität empfohlen, in der Ziele als Fragmente und die zugehörigen Datenmodelle mithilfe von ViewModel
implementiert werden.
Ein ViewModel
ist immer einem Lebenszyklus zugeordnet. Sobald dieser Lebenszyklus dauerhaft endet, wird der ViewModel
gelöscht und kann verworfen werden. Der Lebenszyklus, auf den sich das ViewModel
bezieht, und somit, wie weit das ViewModel
weitergegeben werden kann, hängt davon ab, welcher Property-Delegate zum Abrufen des ViewModel
verwendet wird.
Im einfachsten Fall ist jedes Navigationsziel ein einzelnes Fragment mit einem vollständig isolierten UI-Status. Daher kann jedes Fragment den Property-Delegaten viewModels()
verwenden, um eine ViewModel
zu erhalten, die auf dieses Fragment beschränkt ist.
Um den UI-Status zwischen Fragmenten zu teilen, gehen Sie den ViewModel
auf die Aktivität zu, indem Sie activityViewModels()
in den Fragmenten aufrufen (die entsprechende Aktivität ist nur viewModels()
). Dadurch können die Aktivität und alle an sie angehängten Fragmente die ViewModel
-Instanz teilen. In einer Architektur mit nur einer Aktivität hält dieser ViewModel
-Bereich jedoch effektiv so lange wie die Anwendung, sodass ViewModel
im Arbeitsspeicher bleibt, auch wenn er nicht von Fragmenten verwendet wird.
Angenommen, Ihr Navigationsdiagramm enthält eine Folge von Fragmentzielen, die einen Bezahlvorgang darstellen, und der aktuelle Status des gesamten Bezahlvorgangs befindet sich in einer ViewModel
, die von den Fragmenten gemeinsam genutzt wird. Die Einbeziehung des ViewModel
auf die Aktivität ist nicht nur zu breit gefasst, sondern führt zu einem weiteren Problem: Wenn der Nutzer den Bezahlvorgang für eine Bestellung durchläuft und ihn dann für eine zweite Bestellung noch einmal durchläuft, wird für beide Bestellungen dieselbe Instanz der ViewModel
für den Bezahlvorgang verwendet. Vor dem Bezahlvorgang bei der zweiten Bestellung müssen Sie die Daten aus der ersten Bestellung manuell löschen. Fehler können für die Nutzenden kostspielig sein.
Legen Sie stattdessen ViewModel
auf ein Navigationsdiagramm in der aktuellen NavController
fest. Erstellen Sie eine verschachtelte Navigationsgrafik für die Ziele, die Teil des Bezahlvorgangs sind. Verwenden Sie dann in jedem dieser Fragmentziele den Property-Delegaten navGraphViewModels()
und übergeben Sie die ID des Navigationsdiagramms, um das gemeinsame ViewModel
-Objekt abzurufen. Dadurch wird sichergestellt, dass die entsprechende Instanz von ViewModel
verworfen wird, sobald der Nutzer den Bezahlvorgang beendet und das verschachtelte Navigationsdiagramm nicht berücksichtigt wird. Sie wird dann nicht für den nächsten Bezahlvorgang verwendet.
Aufgabenstellung | Property-Bevollmächtigter | Darf ViewModel teilen mit |
---|---|---|
Fragment | Fragment.viewModels() |
Nur aktuelles Fragment |
Aktivitäten | Activity.viewModels()
|
Aktivität und alle damit verbundenen Fragmente |
Navigationsdiagramm | Fragment.navGraphViewModels() |
Alle Fragmente in derselben Navigationsgrafik |
Wenn du einen verschachtelten Navigationshost verwendest (siehe oben), können die Ziele in diesem Host ViewModel
s nicht mit Zielen außerhalb des Hosts teilen, wenn navGraphViewModels()
verwendet wird, da die Grafiken nicht verbunden sind. In diesem Fall können Sie stattdessen den Umfang der Aktivität verwenden.
Hochgezogener Zustand
In „Compose“ können Sie den Status bei Änderungen der Fenstergröße mit State Hoisting beibehalten. Durch Heben des Zustands der zusammensetzbaren Elemente an eine höhere Position im Kompositionsbaum kann der Zustand beibehalten werden, auch wenn die zusammensetzbaren Elemente nicht mehr sichtbar sind.
Im Abschnitt Schreiben des Artikels Inhaltsziele mit alternativen Layouts haben wir den Status der zusammensetzbaren Funktionen der Listen-Detailansicht auf ListDetailRoute
hochgestuft, damit der Status unabhängig davon erhalten bleibt, welche zusammensetzbare Funktion angezeigt wird:
@Composable
fun ListDetailRoute(
// Indicates that the display size is represented by the expanded window size class.
isExpandedWindowSize: Boolean = false,
// Identifies the item selected from the list. If null, a item has not been selected.
selectedItemId: String?,
) { /*...*/ }
Zusätzliche Ressourcen
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Jetpack Navigation zu Navigation Compose migrieren
- Navigation mit der Funktion „Schreiben“
- Adaptive App mit dynamischer Navigation entwickeln