Crea un grafico in modo programmatico utilizzando la Kotlin DSL

Il componente Navigazione fornisce un linguaggio specifico per il dominio basato su Kotlin (o DSL) che si basa sugli strumenti di creazione tipo-safe di Kotlin. Questa API ti consente di comporre il grafico in modo dichiarativo nel codice Kotlin, anziché all'interno di una risorsa XML. Questo può essere utile se vuoi creare la navigazione dell'app in modo dinamico. Ad esempio, l'app potrebbe scaricare e memorizzare nella cache una configurazione di navigazione da un servizio web esterno per poi utilizzare questa configurazione per creare dinamicamente un grafico di navigazione nella funzione onCreate() dell'attività.

Dipendenze

Per utilizzare la connessione DSL di Kotlin, aggiungi la seguente dipendenza al file build.gradle dell'app:

trendy

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")
}

Creazione di un grafico

Iniziamo con un esempio di base basato sull'app Girasole. Per questo esempio, abbiamo due destinazioni: home e plant_detail. La destinazione home è presente quando l'utente avvia l'app per la prima volta. Questa destinazione mostra un elenco di piante del giardino dell'utente. Quando l'utente seleziona una delle piante, l'app accede alla destinazione plant_detail.

La Figura 1 mostra queste destinazioni insieme agli argomenti richiesti dalla destinazione plant_detail e un'azione, to_plant_detail, che l'app utilizza per passare da home a plant_detail.

L'app Sunflower ha due destinazioni insieme a un'azione che le connette.
Figura 1. L'app Sunflower ha due destinazioni, home e plant_detail, insieme a un'azione che le collega.

Hosting di un grafico di navigazione DSL Kotlin

Per poter creare il grafico di navigazione dell'app, devi prima ospitarlo. Questo esempio utilizza i frammenti, quindi ospita il grafico in un elemento NavHostFragment all'interno di un FragmentContainerView:

<!-- 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>

Tieni presente che l'attributo app:navGraph non è impostato in questo esempio. Il grafico non è definito come una risorsa nella cartella res/navigation, quindi deve essere impostato come parte del processo onCreate() nell'attività.

In XML, un'azione collega un ID destinazione a uno o più argomenti. Tuttavia, quando si utilizza il DSL di navigazione, una route può contenere argomenti. Ciò significa che non esiste il concetto di azioni quando si utilizza la DSL.

Il passaggio successivo consiste nel definire alcune costanti da utilizzare nel definire il grafico.

Creare costanti per il grafico

I grafici di navigazione basati su XML vengono analizzati come parte del processo di compilazione di Android. Viene creata una costante numerica per ogni attributo id definito nel grafico. Questi ID statici generati in fase di build non sono disponibili quando crei il grafico di navigazione in fase di runtime, pertanto DSL di navigazione utilizza stringhe di route anziché ID. Ogni route è rappresentata da una stringa univoca ed è buona norma definirla come costanti per ridurre il rischio di errori di battitura.

Nel caso di argomenti, questi sono integrati nella stringa di route. L'integrazione di questa logica nel percorso può, ancora una volta, ridurre il rischio di errori di battitura.

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"
}

Una volta definite le costanti, puoi creare il grafico di navigazione.

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 questo esempio, la funzione lambda finale definisce due destinazioni dei frammenti utilizzando la funzione del builder DSL fragment(). Questa funzione richiede una stringa di route per la destinazione, ottenuta dalle costanti. La funzione accetta anche un comando lambda facoltativo per una configurazione aggiuntiva, ad esempio l'etichetta di destinazione, nonché funzioni incorporate del builder per argomenti e link diretti.

La classe Fragment che gestisce l'interfaccia utente di ogni destinazione viene trasmessa come tipo con parametri all'interno di parentesi angolari (<>). Questo ha lo stesso effetto dell'impostazione dell'attributo android:name sulle destinazioni dei frammenti definite utilizzando XML.

Infine, puoi navigare da home a plant_detail utilizzando le chiamate NavController.naviga() standard:

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

In PlantDetailFragment, puoi ottenere il valore dell'argomento come mostrato nell'esempio seguente:

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

I dettagli su come fornire argomenti durante la navigazione sono disponibili nella sezione Fornire argomenti di destinazione.

La parte rimanente di questa guida descrive gli elementi comuni dei grafici di navigazione, le destinazioni e il loro utilizzo nella creazione del grafico.

Destinazioni

Il DSL di Kotlin fornisce supporto integrato per tre tipi di destinazione: Fragment, Activity e NavGraph, ognuno dei quali dispone di una propria funzione di estensione in linea per creare e configurare la destinazione.

Destinazioni dei frammenti

La funzione DSL fragment() può essere parametrizzata alla classe di frammenti di implementazione e prende una stringa di route univoca da assegnare a questa destinazione, seguita da una funzione lambda in cui puoi fornire una configurazione aggiuntiva, come descritto nella sezione Navigazione con il grafico DSL di Kotlin.

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

Destinazione attività

La funzione DSL activity() richiede una stringa di route univoca da assegnare a questa destinazione, ma non è parametrizzata ad alcuna classe di attività di implementazione. Invece, devi impostare un activityClass facoltativo in una lambda finale. Questa flessibilità ti consente di definire la destinazione di un'attività che dovrebbe essere avviata utilizzando un intent implicito, in cui una classe di attività esplicita non avrebbe senso. Come per le destinazioni dei frammenti, puoi anche configurare un'etichetta, argomenti e link diretti.

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

   activityClass = ActivityDestination::class
}

La funzione DSL navigation() può essere utilizzata per creare un grafico di navigazione nidificato. Questa funzione accetta tre argomenti: una route da assegnare al grafico, la route della destinazione iniziale del grafico e una funzione lambda per configurare ulteriormente il grafico. Gli elementi validi includono altre destinazioni, argomenti, link diretti e un'etichetta descrittiva della destinazione. Questa etichetta può essere utile per associare il grafico di navigazione ai componenti dell'interfaccia utente utilizzando NavigatorUI

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

Supporto di destinazioni personalizzate

Se utilizzi un nuovo tipo di destinazione che non supporta direttamente il Kotlin DSL, puoi aggiungere queste destinazioni alla tua DSL Kotlin utilizzando addDestination():

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

In alternativa, puoi utilizzare l'operatore più unario per aggiungere una destinazione appena creata direttamente al grafico:

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

Specifica degli argomenti di destinazione

Qualsiasi destinazione può definire argomenti facoltativi o obbligatori. Le azioni possono essere definite utilizzando la funzione argument() su NavDestinationBuilder, che è la classe base per tutti i tipi di builder di destinazione. Questa funzione prende il nome dell'argomento come stringa e come funzione lambda utilizzata per costruire e configurare un NavArgument.

All'interno della funzione lambda puoi specificare il tipo di dati dell'argomento, un valore predefinito, se applicabile, e se è nullo o meno.

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
    }
}

Se viene specificato un valore defaultValue, il tipo può essere dedotto. Se vengono specificati sia defaultValue sia type, i tipi devono corrispondere. Consulta la documentazione di riferimento di NavType per un elenco completo dei tipi di argomenti disponibili.

Specificare tipi personalizzati

Alcuni tipi, come ParcelableType e SerializableType, non supportano l'analisi dei valori delle stringhe utilizzate da route o link diretti. Questo perché non si basano sulla riflessione in fase di runtime. Fornendo una classe NavType personalizzata, puoi controllare esattamente il modo in cui il tuo tipo viene analizzato da una route o da un link diretto. Ciò ti consente di utilizzare la serializzazione Kotlin o altre librerie per fornire codifica e decodifica senza riflessi del tuo tipo personalizzato.

Ad esempio, una classe di dati che rappresenta i parametri di ricerca trasmessi alla schermata di ricerca potrebbe implementare sia Serializable (per fornire il supporto per la codifica/decodifica) sia Parcelize (per supportare il salvataggio e il ripristino da un elemento Bundle):

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

Una NavType personalizzata potrebbe essere scritta come segue:

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"
}

e può essere utilizzata in Kotlin DSL come qualsiasi altro tipo:

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

Questo esempio utilizza la serializzazione Kotlin per analizzare il valore della stringa, il che significa che la serializzazione Kotlin deve essere utilizzata anche quando accedi alla destinazione per garantire la corrispondenza dei formati:

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

Il parametro può essere ottenuto dagli argomenti nella destinazione:

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

Link diretti

I link diretti possono essere aggiunti a qualsiasi destinazione, proprio come con un grafico di navigazione basato su XML. Tutte le stesse procedure definite nella sezione Creazione di un link diretto per una destinazione si applicano al processo di creazione di un link diretto esplicito utilizzando la DSL di Kotlin.

Tuttavia, quando crei un link diretto implicito, non è disponibile una risorsa di navigazione XML che possa essere analizzata per gli elementi <deepLink>. Pertanto, non puoi fare affidamento sull'inserimento di un elemento <nav-graph> nel file AndroidManifest.xml, ma devi aggiungere manualmente filtri intent alle tue attività. Il filtro per intent fornito deve corrispondere al pattern URL di base, all'azione e al tipo MIME dei link diretti dell'app.

Puoi fornire un deeplink più specifico per ogni destinazione con link diretto individuale utilizzando la funzione DSL deepLink(). Questa funzione accetta un NavDeepLink che contiene un String che rappresenta il pattern URI, un String che rappresenta le azioni di intent e un String che rappresenta il mimeType .

Ecco alcuni esempi:

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

Non c'è limite al numero di link diretti che puoi aggiungere. Ogni volta che chiami deepLink(), viene aggiunto un nuovo link diretto a un elenco gestito per quella destinazione.

Di seguito è mostrato uno scenario di link diretto implicito più complesso, che definisce anche percorsi e parametri basati su query:

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}"
   })
}

Puoi utilizzare l'interpolazione di stringhe per semplificare la definizione.

Limitazioni

Il plug-in Safe Args non è compatibile con Kotlin DSL, poiché il plug-in cerca file di risorse XML per generare le classi Directions e Arguments.

Scopri di più

Consulta la pagina Sicurezza del tipo di navigazione per scoprire come garantire la sicurezza del tipo per il codice Kotlin DSL e Navigazione Compose.