Crea un grafico in modo programmatico utilizzando la Kotlin DSL

Il componente Navigazione fornisce un linguaggio specifico del dominio basato su Kotlin, o DSL, che si basa sui generatori sicuri per il tipo di Kotlin. Questa API ti consente di comporre in modo dichiarativo il grafico nel codice Kotlin, anziché all'interno di una risorsa XML. 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 e poi utilizzare questa configurazione per creare dinamicamente un grafico di navigazione nella funzione onCreate() dell'attività.

Dipendenze

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

Groovy

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 Sunflower. 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 visualizza un elenco di piante del giardino dell'utente. Quando l'utente seleziona una delle piante, l'app raggiunge la destinazione plant_detail.

La figura 1 mostra queste destinazioni insieme agli argomenti richiesti dalla destinazione plant_detail e da un'azione, to_plant_detail, utilizzata dall'app per passare da home a plant_detail.

L'app Girasole ha due destinazioni con un'azione che le collega.
Figura 1. L'app Girasole ha due destinazioni, home e plant_detail, e un'azione che le collega.

Hosting di un grafico di navigazione DSL Kotlin

Prima di poter creare il grafico di navigazione dell'app, è necessario un luogo in cui ospitarlo. Questo esempio utilizza frammenti, quindi ospita il grafico in una 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 in questo esempio l'attributo app:navGraph non è impostato. Il grafico non è definito come 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 utilizzi la navigazione DSL, una route può contenere argomenti come parte della route. Ciò significa che non esiste un concetto di azioni quando si utilizza la DSL.

Il passaggio successivo consiste nel definire alcune costanti da utilizzare durante la definizione del 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 durante la creazione del grafico di navigazione in fase di runtime, quindi il DSL di navigazione utilizza stringhe di route anziché ID. Ogni route è rappresentata da una stringa univoca ed è buona norma definirle come costanti per ridurre il rischio di bug correlati a errori di battitura.

Quando si gestiscono argomenti, questi vengono incorporati nella stringa di route. L'integrazione di questa logica nella route può, ancora una volta, ridurre il rischio di intrusione di bug correlati a 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"
}

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

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

Infine, puoi navigare da home a plant_detail utilizzando le chiamate NavController.navigation() 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 gli argomenti durante la navigazione sono disponibili nella sezione Fornisci gli argomenti di destinazione.

Il resto della guida descrive gli elementi comuni dei grafici di navigazione, le destinazioni e come utilizzarli durante la creazione del grafico.

Destinazioni

La DSL Kotlin offre supporto integrato per tre tipi di destinazione: Fragment, Activity e NavGraph destinazioni, ognuna delle quali ha una propria funzione di estensione in linea disponibile per creare e configurare la destinazione.

Destinazioni dei frammenti

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

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

Destinazione dell'attività

La funzione DSL activity() richiede una stringa di route univoca da assegnare a questa destinazione, ma non è parametrizzata per alcuna classe di attività di implementazione. Puoi invece impostare un activityClass facoltativo in un lambda finale. Questa flessibilità consente di definire una destinazione per un'attività che deve essere avviata utilizzando un intent implicito, per cui una classe di attività esplicita non avrebbe senso. Come per le destinazioni dei frammenti, puoi anche configurare 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 route 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 la DSL Kotlin, puoi aggiungere queste destinazioni alla rete 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 unario più per aggiungere una destinazione di nuova costruzione direttamente al grafico:

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

Fornitura di 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 del generatore di destinazioni. Questa funzione prende il nome dell'argomento come stringa e una funzione lambda utilizzata per costruire e configurare una NavArgument.

All'interno di lambda puoi specificare il tipo di dati dell'argomento, un valore predefinito, se applicabile, e se è possibile specificare un valore null.

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, è possibile dedurre il tipo. 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.

Fornitura di tipi personalizzati

Alcuni tipi, come ParcelableType e SerializableType, non supportano l'analisi dei valori delle stringhe utilizzate da route o link diretti. perché non fanno affidamento sulla riflessione in fase di runtime. Se fornisci una classe NavType personalizzata, puoi controllare esattamente il modo in cui il tipo viene analizzato da una route o da un link diretto. In questo modo puoi utilizzare la serie di serie di Kotlin o altre librerie per fornire una codifica e decodifica senza riflessioni del tuo tipo personalizzato.

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

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

Un NavType personalizzato potrebbe essere scritto come:

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

Questa opzione può quindi essere utilizzata nella tua DSL Kotlin 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())
    }
}

NavType include sia la scrittura che la lettura di ogni campo, il che significa che NavType deve essere utilizzato anche quando accedi alla destinazione per garantire la corrispondenza dei formati:

val params = SearchParameters("rose", listOf("available"))
val searchArgument = SearchParametersType.serializeAsValue(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 per un grafico di navigazione basata su XML. Tutte le stesse procedure definite in Creazione di un link diretto per una destinazione si applicano al processo di creazione di un link diretto esplicito utilizzando il DSL di Kotlin.

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

Puoi fornire un valore deeplink più specifico per ogni destinazione con collegamento 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 esiste un limite al numero di link diretti che puoi aggiungere. Ogni volta che chiami deepLink(), un nuovo link diretto viene aggiunto a un elenco gestito per quella destinazione.

Di seguito è riportato uno scenario di link diretto implicito più complesso che definisce anche i parametri basati su query e percorso:

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 il DSL Kotlin, in quanto cerca i 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 dei tipi per il tuo codice DSL Kotlin e Navigator Compose.