Programmatisch mit Kotlin-DSL ein Diagramm erstellen

Die Navigationskomponente bietet eine Kotlin-basierte domainspezifische Sprache (DSL), die auf den typsicheren Buildern von Kotlin basiert. Mit dieser API können Sie Diagramme deklarativ in Ihrem Kotlin-Code anstatt in einer XML-Ressource erstellen. Dies kann nützlich sein, wenn Sie die Navigation Ihrer App dynamisch erstellen möchten. Ihre App könnte beispielsweise eine Navigationskonfiguration von einem externen Webdienst herunterladen, im Cache speichern und diese Konfiguration dann verwenden, um dynamisch ein Navigationsdiagramm in der Funktion onCreate() Ihrer Aktivität zu erstellen.

Abhängigkeiten

Fügen Sie der Datei build.gradle Ihrer App die folgende Abhängigkeit hinzu, um Kotlin-DSL zu verwenden:

Groovig

dependencies {
    def nav_version = "2.7.7"

    api "androidx.navigation:navigation-fragment-ktx:$nav_version"
}

Kotlin

dependencies {
    val nav_version = "2.7.7"

    api("androidx.navigation:navigation-fragment-ktx:$nav_version")
}

Diagramm erstellen

Beginnen wir mit einem einfachen Beispiel, das auf der Sunflower-Anwendung basiert. In diesem Beispiel gibt es zwei Ziele: home und plant_detail. Das Ziel home ist vorhanden, wenn der Nutzer die App zum ersten Mal startet. An diesem Ziel wird eine Liste der Pflanzen aus dem Garten des Nutzers angezeigt. Wenn der Nutzer eine der Pflanzen auswählt, ruft die App das Ziel plant_detail auf.

Abbildung 1 zeigt diese Ziele zusammen mit den Argumenten, die vom Ziel plant_detail und der Aktion to_plant_detail erforderlich sind, mit der die Anwendung von home nach plant_detail navigiert.

Die Sunflower-Anwendung hat zwei Ziele sowie eine Aktion, die diese miteinander verbindet.
Abbildung 1: Die Sunflower-Anwendung hat zwei Ziele, home und plant_detail, sowie eine Aktion, die sie verbindet.

Kotlin-DSL-Navigationsdiagramm hosten

Bevor Sie das Navigationsdiagramm Ihrer Anwendung erstellen können, benötigen Sie einen Ort, an dem das Diagramm gehostet werden kann. In diesem Beispiel werden Fragmente verwendet und der Graph daher in einem NavHostFragment innerhalb eines FragmentContainerView gehostet:

<!-- activity_garden.xml -->
<FrameLayout
    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">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true" />

</FrameLayout>

Beachten Sie, dass das Attribut app:navGraph in diesem Beispiel nicht festgelegt ist. Das Diagramm ist im Ordner res/navigation nicht als Ressource definiert. Daher muss es als Teil des Prozesses onCreate() in der Aktivität festgelegt werden.

In XML verbindet eine Aktion eine Ziel-ID mit einem oder mehreren Argumenten. Bei Verwendung der Navigations-DSL kann eine Route jedoch Argumente als Teil der Route enthalten. Das bedeutet, dass es bei der Verwendung von DSL keine Aktionen gibt.

Der nächste Schritt besteht darin, einige Konstanten zu definieren, die Sie beim Definieren der Grafik verwenden werden.

Konstanten für die Grafik erstellen

XML-basierte Navigationsdiagramme werden als Teil des Android-Build-Prozesses geparst. Für jedes im Diagramm definierte id-Attribut wird eine numerische Konstante erstellt. Diese statischen, während der Build-Zeit generierten IDs sind nicht verfügbar, wenn das Navigationsdiagramm zur Laufzeit erstellt wird. Daher verwendet die Navigations-DSL Routenstrings anstelle von IDs. Jede Route wird durch einen eindeutigen String dargestellt. Es empfiehlt sich, diese als Konstanten zu definieren, um das Risiko von Fehlern im Zusammenhang mit Tippfehlern zu verringern.

Bei der Verarbeitung von Argumenten werden diese in den Routingstring integriert. Durch das Einbinden dieser Logik in die Route können Sie wiederum das Risiko verringern, dass sich Tippfehler einschleichen.

object nav_routes {
    const val home = "home"
    const val plant_detail = "plant_detail"
}

object nav_arguments {
    const val plant_id = "plant_id"
    const val plant_name = "plant_name"
}

Nachdem Sie die Konstanten definiert haben, können Sie das Navigationsdiagramm erstellen.

val navController = findNavController(R.id.nav_host_fragment)
navController.graph = navController.createGraph(
    startDestination = nav_routes.home
) {
    fragment<HomeFragment>(nav_routes.home) {
        label = resources.getString(R.string.home_title)
    }

    fragment<PlantDetailFragment>("${nav_routes.plant_detail}/{${nav_arguments.plant_id}}") {
        label = resources.getString(R.string.plant_detail_title)
        argument(nav_arguments.plant_id) {
            type = NavType.StringType
        }
    }
}

In diesem Beispiel definiert die nachgestellte Lambda zwei Fragmentziele mithilfe der DSL-Builder-Funktion fragment(). Diese Funktion benötigt einen Routenstring für das Ziel, der aus den Konstanten abgerufen wird. Die Funktion akzeptiert auch ein optionales Lambda für die zusätzliche Konfiguration, z. B. das Ziellabel, sowie eingebettete Builder-Funktionen für Argumente und Deeplinks.

Die Klasse Fragment, die die UI jedes Ziels verwaltet, wird als parametrierter Typ in spitzen Klammern (<>) übergeben. Dies hat denselben Effekt wie das Festlegen des Attributs android:name bei Fragmentzielen, die über XML definiert werden.

Schließlich können Sie mithilfe der standardmäßigen NavController.browse()-Aufrufe von home zu plant_detail wechseln:

private fun navigateToPlant(plantId: String) {
   findNavController().navigate("${nav_routes.plant_detail}/$plantId")
}

In PlantDetailFragment können Sie den Wert des Arguments abrufen, wie im folgenden Beispiel gezeigt:

val plantId: String? = arguments?.getString(nav_arguments.plant_id)

Ausführliche Informationen zum Bereitstellen von Argumenten beim Navigieren finden Sie im Abschnitt Zielargumente bereitstellen.

Im weiteren Verlauf dieses Leitfadens werden gängige Elemente und Ziele von Navigationsdiagrammen beschrieben und es wird erläutert, wie Sie diese beim Erstellen des Diagramms verwenden können.

Ziele

Kotlin-DSL bietet integrierte Unterstützung für drei Zieltypen: Fragment-, Activity- und NavGraph-Ziele, von denen jedes eine eigene Inline-Erweiterungsfunktion zum Erstellen und Konfigurieren des Ziels hat.

Fragmentziele

Die DSL-Funktion fragment() kann für die implementierende Fragmentklasse parametrisiert werden und verwendet einen eindeutigen Routenstring, der diesem Ziel zugewiesen wird, gefolgt von einer Lambda-Funktion, in der Sie zusätzliche Konfigurationen bereitstellen können, wie im Abschnitt Mit der Kotlin-DSL-Grafik navigieren beschrieben.

fragment<FragmentDestination>(nav_routes.route_name) {
   label = getString(R.string.fragment_title)
   // arguments, deepLinks
}

Aktivitätsziel

Die DSL-Funktion activity() verwendet einen eindeutigen Routenstring, um diesem Ziel zuzuweisen, ist aber nicht mit einer implementierenden Aktivitätsklasse parametriert. Stattdessen legst du einen optionalen activityClass in einer nachgestellten Lambda-Funktion fest. Mit dieser Flexibilität können Sie ein Aktivitätsziel für eine Aktivität definieren, die mit einem impliziten Intent gestartet werden soll, wenn eine explizite Aktivitätsklasse nicht sinnvoll wäre. Wie bei Fragmentzielen können Sie auch ein Label, Argumente und Deeplinks konfigurieren.

activity(nav_routes.route_name) {
   label = getString(R.string.activity_title)
   // arguments, deepLinks...

   activityClass = ActivityDestination::class
}

Mit der DSL-Funktion navigation() können Sie eine verschachtelte Navigationsgrafik erstellen. Diese Funktion verwendet drei Argumente: eine Route, die der Grafik zugewiesen werden soll, die Route des Startziels der Grafik und ein Lambda zur weiteren Konfiguration des Diagramms. Zu den gültigen Elementen gehören andere Ziele, Argumente, Deeplinks und ein beschreibendes Label für das Ziel. Dieses Label kann nützlich sein, um das Navigationsdiagramm über NavigationUI an UI-Komponenten zu binden.

navigation("route_to_this_graph", nav_routes.home) {
   // label, other destinations, deep links
}

Unterstützung benutzerdefinierter Ziele

Wenn Sie einen neuen Zieltyp verwenden, der Kotlin-DSL nicht direkt unterstützt, können Sie dem Kotlin-DSL die folgenden Ziele mit addDestination() hinzufügen:

// The NavigatorProvider is retrieved from the NavController
val customDestination = navigatorProvider[CustomNavigator::class].createDestination().apply {
    route = Graph.CustomDestination.route
}
addDestination(customDestination)

Alternativ können Sie auch den unären Plus-Operator verwenden, um ein neu erstelltes Ziel direkt dem Diagramm hinzuzufügen:

// The NavigatorProvider is retrieved from the NavController
+navigatorProvider[CustomNavigator::class].createDestination().apply {
    route = Graph.CustomDestination.route
}

Zielargumente bereitstellen

Jedes Ziel kann Argumente definieren, die optional oder erforderlich sind. Aktionen können mit der Funktion argument() in NavDestinationBuilder definiert werden. Das ist die Basisklasse für alle Ziel-Builder-Typen. Diese Funktion verwendet den Namen des Arguments als String und eine Lambda-Funktion, mit der ein NavArgument erstellt und konfiguriert wird.

Innerhalb der Lambda-Funktion können Sie den Datentyp des Arguments, einen Standardwert (falls anwendbar) und angeben, ob Nullwerte zulässig sind.

fragment<PlantDetailFragment>("${nav_routes.plant_detail}/{${nav_arguments.plant_id}}") {
    label = getString(R.string.plant_details_title)
    argument(nav_arguments.plant_id) {
        type = NavType.StringType
        defaultValue = getString(R.string.default_plant_id)
        nullable = true  // default false
    }
}

Wenn ein defaultValue angegeben ist, kann der Typ abgeleitet werden. Wenn sowohl ein defaultValue als auch ein type angegeben ist, müssen die Typen übereinstimmen. Eine vollständige Liste der verfügbaren Argumenttypen finden Sie in der Referenzdokumentation zu NavType.

Benutzerdefinierte Typen bereitstellen

Bestimmte Typen wie ParcelableType und SerializableType unterstützen nicht das Parsen von Werten aus den Strings, die von Routen oder Deeplinks verwendet werden. Das liegt daran, dass sie nicht von einer Reflexion zur Laufzeit abhängig sind. Durch Angabe einer benutzerdefinierten NavType-Klasse können Sie genau steuern, wie Ihr Typ aus einer Route oder einem Deeplink geparst wird. So können Sie die Kotlin-Serialisierung oder andere Bibliotheken verwenden, um eine reflektierende Codierung und Decodierung für Ihren benutzerdefinierten Typ bereitzustellen.

Beispielsweise könnte eine Datenklasse, die Suchparameter darstellt, die an den Suchbildschirm übergeben werden, sowohl Serializable (zur Unterstützung der Codierung/Decodierung) als auch Parcelize (zur Unterstützung des Speicherns und Wiederherstellens aus einem Bundle) implementieren:

@Serializable
@Parcelize
data class SearchParameters(
  val searchQuery: String,
  val filters: List<String>
)

Eine benutzerdefinierte NavType könnte so geschrieben werden:

val SearchParametersType = object : NavType<SearchParameters>(
  isNullableAllowed = false
) {
  override fun put(bundle: Bundle, key: String, value: SearchParameters) {
    bundle.putParcelable(key, value)
  }
  override fun get(bundle: Bundle, key: String): SearchParameters {
    return bundle.getParcelable(key) as SearchParameters
  }

  override fun parseValue(value: String): SearchParameters {
    return Json.decodeFromString<SearchParameters>(value)
  }

  // Only required when using Navigation 2.4.0-alpha07 and lower
  override val name = "SearchParameters"
}

Dieser kann dann in Ihrer Kotlin-DSL wie jeder andere Typ verwendet werden:

fragment<SearchFragment>(nav_routes.plant_search) {
    label = getString(R.string.plant_search_title)
    argument(nav_arguments.search_parameters) {
        type = SearchParametersType
        defaultValue = SearchParameters("cactus", emptyList())
    }
}

In diesem Beispiel wird die Kotlin-Serialisierung verwendet, um den Wert aus dem String zu parsen. Das bedeutet, dass die Kotlin-Serialisierung auch beim Aufrufen des Ziels verwendet werden muss, damit die Formate übereinstimmen:

val params = SearchParameters("rose", listOf("available"))
val searchArgument = Uri.encode(Json.encodeToString(params))
navController.navigate("${nav_routes.plant_search}/$searchArgument")

Der Parameter kann aus den Argumenten im Ziel abgerufen werden:

val params: SearchParameters? = arguments?.getParcelable(nav_arguments.search_parameters)

Deeplinks

Deeplinks können jedem Ziel hinzugefügt werden, genau wie bei einer XML-gestützten Navigationsgrafik. Die unter Deeplink für ein Ziel erstellen beschriebenen Verfahren gelten auch für das Erstellen eines expliziten Deeplinks mit Kotlin-DSL.

Beim Erstellen eines impliziten Deeplinks haben Sie jedoch keine XML-Navigationsressource, die auf <deepLink>-Elemente analysiert werden kann. Daher können Sie sich nicht darauf verlassen, ein <nav-graph>-Element in Ihre AndroidManifest.xml-Datei einzufügen. Stattdessen müssen Sie Ihrer Aktivität manuell Intent-Filter hinzufügen. Der von Ihnen angegebene Intent-Filter sollte dem Basis-URL-Muster, der Aktion und dem MIME-Typ der Deeplinks Ihrer App entsprechen.

Mit der DSL-Funktion deepLink() können Sie für jedes einzelne Ziel mit Deeplink eine spezifischere deeplink angeben. Diese Funktion akzeptiert einen NavDeepLink, der einen String für das URI-Muster, einen String für die Intent-Aktionen und einen String für den mimeType enthält .

Beispiele:

deepLink {
    uriPattern = "http://www.example.com/plants/"
    action = "android.intent.action.MY_ACTION"
    mimeType = "image/*"
}

Sie können beliebig viele Deeplinks hinzufügen. Jedes Mal, wenn Sie deepLink() aufrufen, wird einer Liste, die für dieses Ziel gepflegt wird, ein neuer Deeplink angehängt.

Im Folgenden wird ein komplexeres Szenario mit impliziten Deeplinks dargestellt, in dem auch pfad- und abfragebasierte Parameter definiert werden:

val baseUri = "http://www.example.com/plants"

fragment<PlantDetailFragment>(nav_routes.plant_detail) {
   label = getString(R.string.plant_details_title)
   deepLink(navDeepLink {
    uriPattern = "${baseUri}/{id}"
   })
   deepLink(navDeepLink {
    uriPattern = "${baseUri}/{id}?name={plant_name}"
   })
}

Mit der Stringinterpolation können Sie die Definition vereinfachen.

Einschränkungen

Das Safe Args-Plug-in ist nicht mit Kotlin-DSL kompatibel, da das Plug-in nach XML-Ressourcendateien sucht, um die Klassen Directions und Arguments zu generieren.

Weitere Informationen

Auf der Seite Sicherheit für Navigationstypen erfahren Sie, wie Sie für Kotlin-DSL- und Navigation Compose-Code Sicherheit bieten.