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 Ihre Grafik deklarativ in Ihrem Kotlin-Code anstatt in einer XML-Ressource erstellen. Das kann nützlich sein, wenn Sie die App-Navigation dynamisch gestalten möchten. Ihre App könnte beispielsweise eine Navigationskonfiguration von einem externen Webdienst herunterladen und im Cache speichern und dann mit dieser Konfiguration dynamisch ein Navigationsdiagramm in der onCreate()-Funktion Ihrer Aktivität erstellen.

Abhängigkeiten

Wenn Sie Kotlin DSL verwenden möchten, fügen Sie der Datei build.gradle Ihrer App die folgende Abhängigkeit hinzu:

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-App basiert. Für dieses Beispiel haben wir zwei Ziele: home und plant_detail. Das Ziel home ist vorhanden, wenn der Nutzer die App zum ersten Mal startet. Hier wird eine Liste der Pflanzen aus dem Garten des Nutzers angezeigt. Wenn die Nutzenden eine der Pflanzen auswählen, navigiert die App zum Ziel plant_detail.

Abbildung 1 zeigt diese Ziele zusammen mit den für das Ziel plant_detail erforderlichen Argumenten und der Aktion to_plant_detail, mit der die Anwendung von home nach plant_detail navigiert.

Die Sunflower-App hat zwei Ziele sowie eine Verbindung, die sie verbindet.
Abbildung 1. Die Sunflower-App hat zwei Ziele, home und plant_detail, sowie eine Aktion, die sie verbindet.

Kotlin-DSL-Navigationsdiagramm hosten

Bevor Sie den Navigationsdiagramm Ihrer Anwendung erstellen können, müssen Sie ihn hosten. In diesem Beispiel werden Fragmente verwendet. Daher wird die Grafik in einem NavHostFragment innerhalb einer 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. Da die Grafik nicht als Ressource im Ordner res/navigation definiert ist, muss sie im Rahmen des onCreate()-Prozesses in der Aktivität festgelegt werden.

In XML verknüpft 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 bei Verwendung von DSL kein Konzept für Aktionen vorliegt.

Im nächsten Schritt definieren Sie einige Konstanten, die Sie beim Definieren Ihrer Grafik verwenden.

Konstanten für die Grafik erstellen

XML-basierte Navigationsdiagramme werden im Rahmen des Android-Build-Prozesses geparst. Für jedes im Diagramm definierte Attribut id wird eine numerische Konstante erstellt. Diese zur Build-Zeit generierten statischen IDs sind beim Erstellen des Navigationsdiagramms zur Laufzeit nicht verfügbar. 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 aufgrund von Tippfehlern zu verringern.

Bei der Verarbeitung von Argumenten sind diese in den Routenstring integriert. Die Einbindung dieser Logik in die Route kann wieder das Risiko verringern, dass typisierte Fehler 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-Funktion zwei Fragmentziele mithilfe der DSSL-Builder-Funktion fragment(). Für diese Funktion ist ein Routenstring für das Ziel erforderlich, der aus den Konstanten abgerufen wird. Die Funktion akzeptiert auch eine optionale Lambda-Funktion 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 parametrisierter Typ in spitzen Klammern (<>) übergeben. Dies hat denselben Effekt wie das Festlegen des Attributs android:name für Fragmentziele, die mit XML definiert sind.

Abschließend können Sie mithilfe der standardmäßigen NavController.Navigate()-Aufrufe von home nach plant_detail navigieren:

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)

Wie Sie beim Navigieren Argumente angeben, erfahren Sie im Abschnitt Zielargumente bereitstellen.

Im weiteren Verlauf dieses Leitfadens werden allgemeine Navigationsgrafikelemente und Ziele beschrieben. Außerdem erfahren Sie, wie Sie diese beim Erstellen der Grafik verwenden.

Ziele

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

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, mit der Sie zusätzliche Konfiguration 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
}

Ziel der Aktivität

Die DSSL-Funktion activity() verwendet einen eindeutigen Routenstring, der diesem Ziel zugewiesen wird, ist jedoch keiner implementierenden Aktivitätsklasse parametrisiert. Stattdessen legst du ein optionales activityClass in einer nachgestellten Lambda-Funktion fest. Dank dieser Flexibilität können Sie ein Aktivitätsziel für eine Aktivität definieren, die mit einem impliziten Intent gestartet werden soll, wobei 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() kann eine verschachtelte Navigationsgrafik erstellt werden. Diese Funktion verwendet drei Argumente: eine Route, die der Grafik zugewiesen werden soll, die Route des Startziels der Grafik und eine Lambda-Funktion zur weiteren Konfiguration der Grafik. Gültige Elemente sind andere Ziele, Argumente, Deeplinks und ein beschreibendes Label für das Ziel. Dieses Label kann nützlich sein, um die Navigationsgrafik mithilfe von 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 diese Ziele mit addDestination() zu Ihrer Kotlin-DSL 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 der Grafik 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() für NavDestinationBuilder definiert werden. Dies ist die Basisklasse für alle Ziel-Builder-Typen. Diese Funktion verwendet den Namen des Arguments als String und als Lambda, das zum Erstellen und Konfigurieren einer NavArgument verwendet wird.

Innerhalb der Lambda-Funktion können Sie den Datentyp des Arguments angeben, gegebenenfalls einen Standardwert und festlegen, 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 eine defaultValue angegeben ist, kann der Typ abgeleitet werden. Wenn sowohl ein defaultValue als auch ein type angegeben sind, 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 das Parsen von Werten aus Strings, die von Routen oder Deeplinks verwendet werden. Das liegt daran, dass sie nicht auf Reflexion zur Laufzeit basieren. Mit einer benutzerdefinierten NavType-Klasse können Sie genau steuern, wie Ihr Typ aus einer Route oder einem Deeplink geparst wird. Auf diese Weise können Sie Kotlin Serialization oder andere Bibliotheken verwenden, um reflexionsfreie Codierung und Decodierung Ihres benutzerdefinierten Typs 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 der Wiederherstellung in einem Bundle) implementieren:

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

Eine benutzerdefinierte NavType könnte folgendermaßen 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 serializeAsValue(value: SearchParameters): String {
    // Serialized values must always be Uri encoded
    return Uri.encode(Json.encodeToString(value))
  }

  override fun parseValue(value: String): SearchParameters {
    // Navigation takes care of decoding the string
    // before passing it to parseValue()
    return Json.decodeFromString<SearchParameters>(value)
  }
}

Dieser kann dann wie jeder andere Typ in Kotlin-DSL 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())
    }
}

NavType kapselt sowohl das Schreiben als auch das Lesen jedes Felds. Das bedeutet, dass NavType auch verwendet werden muss, wenn Sie zum Ziel navigieren, damit die Formate übereinstimmen:

val params = SearchParameters("rose", listOf("available"))
val searchArgument = SearchParametersType.serializeAsValue(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-gesteuerten Navigationsgrafik. Die unter Deeplink für ein Ziel erstellen definierten Verfahren gelten auch für das Erstellen eines expliziten Deeplinks mithilfe der 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 der Datei AndroidManifest.xml zu platzieren, sondern Ihrer Aktivität stattdessen manuell Intent-Filter hinzufügen. Der von Ihnen angegebene Intent-Filter sollte mit dem Basis-URL-Muster, der Aktion und dem MIME-Typ der Deeplinks Ihrer App übereinstimmen.

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

Beispiel:

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 ein neuer Deeplink an eine Liste angehängt, die für dieses Ziel verwaltet wird.

Ein komplexeres implizites Deeplink-Szenario, das auch pfad- und abfragebasierte Parameter definiert, ist unten zu sehen:

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 Stringinterpolation können Sie die Definition vereinfachen.

Einschränkungen

Das Plug-in Safe Args 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 Navigation type Safety (Navigationstyp-Sicherheit) erfahren Sie, wie Sie Typsicherheit für Kotlin-DSL und Navigation Compose-Code bereitstellen.