4.5 Navigation
Sie haben Intents als Boten zwischen den Grundbausteinen von Apps kennengelernt. Mit startActivity() und startActivityForResult() werden beispielsweise nach einem Buttonklick Ihre und ggf. fremde Activities angezeigt. Man nennt den Wechsel von Activity zu Activity Navigation. Aber auch innerhalb einer Activity kann Navigation stattfinden, beispielsweise beim Anzeigen und Entfernen von Fragmenten. Bislang habe ich jede Navigation ausprogrammiert. Das funktioniert in einfachen Beispielen wunderbar. Sobald eine App aber etwas komplexer wird, muss man mehr Aufwand treiben.
4.5.1 Jetpack Navigation
Jetpack Navigation besteht aus Bibliotheken, einem Gradle-Plugin und Werkzeugen innerhalb von Android Studio. Zentrales Element ist der Navigation Graph. Dieser neue Ressourcetyp (eine XML-Datei) bündelt alle für die Navigation erforderlichen Informationen. Er wird ab Android Studio 3.3 im Navigation Editor bearbeitet. Sie können Projekten einen Navigation Graph hinzufügen, indem Sie im Werkzeugfenster Project den Knoten res mit der rechten Maustaste anklicken und New • Android Resource File auswählen. Der Dialog New Resource File ist in Abbildung 4.16 zu sehen. Sofern sie nicht bereits vorhanden sind, werden beim Anlegen die erforderlichen Bibliotheken in der modulspezifischen build.gradle-Datei eingetragen. Dies sind:
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.0'
Wenn Sie bei der Navigation sichere Argumente verwenden möchten (wir kommen gleich darauf zurück), tragen Sie zusätzlich die Zeile
ein, am besten nach
apply plugin: 'kotlin-android-extensions'
Und noch eine Sache sollten Sie prüfen und ggf. ergänzen. In der build.gradle-Datei des Projekts muss im Block dependencies { die Zeile
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.0"
stehen.
Der Navigation Graph enthält Ziele und Aktionen. Ziele sind die Orte, zu denen man navigieren kann. Das können Activities oder Fragmente sein. Aktionen repräsentieren die Pfade, auf denen sich der Benutzer in der App bewegt. Ziele werden also über Aktionen miteinander verbunden. Damit das funktioniert, sind einige weitere Puzzleteile erforderlich. Der Navigation Controller (NavController) steuert die Navigation innerhalb eines Navigation Host. Das Interface androidx.navigation.NavHost wird von der Klasse androidx.navigation.fragment.NavHostFragment implementiert. Es definiert die Methode getNavController(). Apps instanziieren den Controller also üblicherweise nicht selbst, sondern erhalten ihn von einem Host oder über Hilfsmethoden der Klasse Navigation.
[»] Hinweis
Normalerweise werden Navigation Graphen aus XML-Dateien entfaltet. Bei Bedarf können sie aber auch programmatisch erzeugt werden. Das ist dann praktisch, wenn sich die möglichen Navigationspfade aus Aufrufen von Webservices oder anderen entfernten Quellen ergeben.
Auch wenn Activities Navigationsziele sein können, wurde Jetpack Navigation doch für die Verwendung mit Fragmenten konzipiert. Mein Beispiel NavigationDemo1 (Abbildung 4.17) zeigt, wie Sie von einem Hauptfragment aus ein untergeordnetes Fragment als Ziel ansteuern.
Listing 4.28 zeigt die von der Haupt-Activity NavigationDemo1Activity geladene Layoutdatei activity_main.xml. Sie ist sehr einfach gehalten. Das einzige Kind des Wurzelelements ConstraintLayout ist eine FragmentContainerView. Diese Klasse leitet von FrameLayout ab. Sie wurde speziell für das Einbetten von Fragmenten entwickelt. Mit dem Attribut android:name geben Sie das Fragment an, hier ein NavHostFragment. Wenn Sie möchten, können Sie mit android:tag ein Tag definieren, um das Fragment mit findFragmentByTag() finden zu können.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".NavigationDemo1Activity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
Mit dem Attribut app:navGraph geben Sie den Navigation Graph, den das NavHostFragment verwenden soll, an. In meinem Beispiel ist das die Datei nav_graph.xml. Navigation Graphen werden unter res/navigation abgelegt. app:defaultNavHost steuert, ob der Navigation Host auf das Antippen der Zurück-Schaltfläche reagieren soll.
Der Navigation Graph
Navigation Graphen werden in Android Studio mit dem Navigation Editor bearbeitet. Er ist in Abbildung 4.18 zu sehen. Mein Beispiel enthält zwei Ziele, die Fragmente ChildFragment und MainFragment. Das Haus-Symbol zeigt an, dass MainFragment der Einstiegspunkt ist, also als Erstes angezeigt wird. Wenn Sie den Editor auf Textdarstellung umschalten, sehen Sie im Tag <navigation> das Attribut app:startDestination.
Für jedes Ziel kann eine Reihe von Eigenschaften festgelegt werden. Die id wird verwendet, um zu diesem Ziel zu navigieren. Entsprechender Code könnte folgendermaßen aussehen:
findNavController().navigate(R.id.childFragment)
In meinem Beispiel finden Sie aber eine etwas andere Form, weil ich das Gradle-Plugin androidx.navigation.safeargs.kotlin verwende. Dazu kommen wir gleich. label legt den Titel fest, der in der Action Bar angezeigt wird. name gibt die Klasse des Fragments an. Die Pfeile, die in den Modi Split und Design des Navigation Editors zu sehen sind, heißen Aktionen. Eine Aktion definiert einen Navigationspfad, den Weg von einem Ziel zu einem anderen. NavigationDemo1 hat nur eine Aktion, nämlich von mainFragment zu childFragment. Um den Weg zurück (Antippen der Zurück-Schaltfläche) kümmert sich der Navigation Host. Auch Aktionen haben einige Eigenschaften, wichtig sind insbesondere id und destination. Darüber hinaus können Sie verschiedene Animationen festlegen und Standardwerte für Argumente angeben.
Sichere Argumente
Jedes Ziel kann ein oder mehrere Argumente erwarten. Ein Argument hat einen Namen und einen Typ. Außerdem legen Sie fest, ob das Argument vorhanden sein muss. Listing 4.29 zeigt die Definition des Arguments color in XML. Es nimmt ganze Zahlen auf. Meine Klasse ChildFragment interpretiert diese als Farbe und setzt den Hintergrund entsprechend.
<fragment
...
<argument
android:name="color"
app:argType="integer"
app:nullable="false" />
</fragment>
Das Gradle-Plugin androidx.navigation.safeargs.kotlin generiert aus den Zielen, Aktionen und Argumenten des Navigation Graphs Klassen, die Sie bei der Navigation unterstützen. In Listing 4.30 sehen Sie zweimal den Aufruf MainFragmentDirections.mainToChildFragment(). Je nachdem, welcher Button angeklickt wurde, wird der Funktion ein anderer Farbwert übergeben. Das Ergebnis ist ein Objekt, das das Interface androidx.navigation.NavDirections implementiert. Es wird an findNavController().navigate() übergeben.
package com.thomaskuenneth.androidbuch.navigationdemo1
import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.widget.Button
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
class MainFragment : Fragment(R.layout.fragment_main) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<Button>(R.id.b1)?.setOnClickListener {
val action =
MainFragmentDirections.mainToChildFragment(Color.GREEN)
findNavController().navigate(action)
}
view.findViewById<Button>(R.id.b2)?.setOnClickListener {
val action =
MainFragmentDirections.mainToChildFragment(Color.RED)
findNavController().navigate(action)
}
}
}
Die Verwendung von sicheren Argumenten in Zielen zeigt Listing 4.31. ChildFragmentArgs ist ebenfalls eine generierte Klasse. Sie gestattet den komfortablen typsicheren Zugriff auf die übergebenen Argumente. args ist aufgrund des by eine delegierte Eigenschaft und wird erst beim Zugriff initialisiert.
package com.thomaskuenneth.androidbuch.navigationdemo1
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.navArgs
class ChildFragment : Fragment(R.layout.fragment_child) {
val args: ChildFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.setBackgroundColor(args.color)
}
}
Sie haben es fast geschafft. Allerdings möchte ich Ihnen noch kurz die Klasse NavigationDemo1Activity (Listing 4.32) zeigen. Damit die Action Bar den bekannten Pfeil am linken Rand anzeigt wenn nicht das Hauptfragment zu sehen ist, ist nämlich ein bisschen Konfigurationsarbeit nötig. Sie müssen der Methode setupActionBarWithNavController() drei Objekte übergeben:
-
eine AppCompatActivity (this)
-
eine AppBarConfiguration
AppBarConfiguration.Builder erhält die IDs aller Ziele, die Einstiege in die Navigation bilden. Anders formuliert: Bei diesen Zielen ist kein Pfeil zu sehen, bei allen anderen schon. In meinem Beispiel trifft dies nur auf R.id.mainFragment zu.
package com.thomaskuenneth.androidbuch.navigationdemo1
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI
class NavigationDemo1Activity : AppCompatActivity() {
private lateinit var navController: NavController
private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment)
as NavHostFragment
navController = navHostFragment.navController
appBarConfiguration = AppBarConfiguration.Builder(
setOf(R.id.mainFragment)
).build()
NavigationUI.setupActionBarWithNavController(this, navController,
appBarConfiguration)
}
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(navController, appBarConfiguration)
}
}
Außerdem müssen Sie die Methode onSupportNavigateUp() wie gezeigt implementieren. NavigationUI.navigateUp() delegiert die Behandlung des Navigationspfeils an den Navigation Controller.
Wie Sie gesehen haben, lassen sich mit Jetpack Navigation sehr bequem Sprünge innerhalb Ihrer App realisieren. Im folgenden Abschnitt stelle ich Ihnen die Klasse BottomNavigationView vor. Sie setzt ein sehr wichtiges Interaktionsmuster um, das Umschalten zwischen gleichberechtigten Seiten durch eine Leiste am unteren Bildschirmrand.
4.5.2 Die Klasse »BottomNavigationView«
com.google.android.material.bottomnavigation.BottomNavigationView gehört zu Googles Material Components. Diese Bibliothek erweitert die Standard-Widgets von Android um einige zusätzliche UI-Elemente, die ebenfalls die Designsprache Material Design umsetzen. Sie muss mit der Zeile
in der modulspezifischen build.gradle-Datei referenziert werden. Damit alle Komponenten richtig funktionieren, ist es wichtig, eigene Activitites von AppCompatActivity abzuleiten und ein Theme der Material Components zu verwenden. Hierfür reicht es üblicherweise, in der Datei styles.xml im Verzeichnis res/values die Zeile
<style name="AppTheme"
parent="Theme.AppCompat.Light.DarkActionBar">
folgendermaßen zu ändern:
<style name="AppTheme"
parent="Theme.MaterialComponents.DayNight.DarkActionBar">
Durch die Verwendung eines DayNight-Themes machen Sie Ihre App auch gleich für den Dark Mode fit.
In meinem Beispiel NavigationDemo2 (Abbildung 4.19) zeige ich Ihnen, wie Sie mit der Klasse BottomNavigationView zwischen mehreren primären Zielen umschalten. Lassen Sie uns als Erstes einen Blick auf die Layoutdatei in Listing 4.33 werfen. Ein ConstraintLayout enthält zwei Kinder, eine TextView sowie die BottomNavigationView. Aus Gründen der Einfachheit repräsentiert die TextView die Seiten der App. Ich erkläre Ihnen gleich, was damit gemeint ist.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:gravity="center"
android:textColor="@color/colorPrimary"
android:textSize="32pt"
app:layout_constraintBottom_toTopOf="@+id/bottom_navigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/bottom_navigation_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
Das Attribut app:menu der BottomNavigationView referenziert eine XML-Datei, die unter res/menu abgelegt wird. In Abschnitt 5.2.4, »Menüs und Action Bar«, stelle ich Ihnen das Konzept von Menüs ausführlicher vor. Fürs Erste ist wichtig, dass in dieser Datei (Listing 4.34) die umschaltbaren Seiten der App definiert werden, und zwar in <item />-Tags. android:id ordnet jeder Seite eine ID zu. android:icon legt das anzuzeigende Symbol fest, und android:title enthält den Namen der Seite. android:enabled legt fest, ob eine Seite angeklickt werden kann.
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/page_home"
android:enabled="true"
android:icon="@drawable/ic_baseline_home_24"
android:title="@string/home"/>
<item
android:id="@+id/page_info"
android:enabled="true"
android:icon="@drawable/ic_baseline_info_24"
android:title="@string/info"/>
</menu>
Die Klasse NavigationDemo2Activity (Listing 4.35) lädt die Layoutdatei activity_main.xml mit setContentView(R.layout.activity_main). findViewById<BottomNavigationView>(R.id.bottom_navigation)
liefert die Referenz auf ein BottomNavigationView-Objekt. Um informiert zu werden, wenn der Benutzer eine andere Seite anwählt, registrieren Sie mit setOnNavigationItemSelectedListener() einen OnNavigationItemSelectedListener. Dessen Methode onNavigationItemSelected() erhält eine MenuItem-Instanz. Sie muss true liefern, wenn das übergebene Menüelement als ausgewähltes Element angezeigt werden soll, sonst false.
package com.thomaskuenneth.androidbuch.navigationdemo2
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.bottomnavigation.BottomNavigationView
class NavigationDemo2Activity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textview = findViewById<TextView>(R.id.textview)
val bottomNavigationView =
findViewById<BottomNavigationView>(R.id.bottom_navigation)
bottomNavigationView.setOnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.page_home -> {
textview.text = getString(R.string.home)
true
}
R.id.page_info -> {
textview.text = getString(R.string.info)
true
}
else -> false
}
}
bottomNavigationView.selectedItemId = R.id.page_home
}
}
Mein Beispiel simuliert einen Seitenwechsel, indem mit textview.text = getString ( ... ) ein anderer Text angezeigt wird. Was Sie in Ihrer App tun müssen, hängt davon ab, wie Sie die darzustellenden Seiten implementieren. Sind es Fragmente, können Sie wie in den Beispielen dieses Kapitels gezeigt den FragmentManager nutzen, um sie ein- und auszublenden. Eine andere Möglichkeit beschreibe ich in Abschnitt 5.1.1, »Views«. Am praktischsten ist aber wahrscheinlich, Jetpack Navigation zu verwenden. Dann müssen Sie nur einen Navigation Graph erstellen, der die Seiten als Ziele enthält (aber keine Aktionen), und Ihrem Layout ein NavHostFragment hinzufügen.
Zum Schluss noch zwei Tipps. Mit labelVisibilityMode können Sie steuern, wann Navigationselemente Texte anzeigen. Fügen Sie in der Layoutdatei dem Tag <BottomNavigationView> beispielsweise den Ausdruck app:labelVisibilityMode="unlabeled" hinzu, sind nur noch die Symbole zu sehen. Ist Ihnen ein farbiger Hintergrund lieber, können Sie ihn mit
style="@style/Widget.MaterialComponents.BottomNavigationView.Colored"
aktivieren (Abbildung 4.20).