Zbuduj wykres programowo za pomocą DSL Kotlin

Komponent Nawigacja udostępnia język odpowiedni dla domeny (DSL) oparty na platformie Kotlin, wykorzystujący kreatory bezpieczne typu Kotlin. Ten interfejs API pozwala deklaratywnie skomponować graf w kodzie Kotlin, a nie w zasobie XML. Może to być przydatne, jeśli chcesz stworzyć dynamiczną nawigację w aplikacji. Aplikacja może na przykład pobrać i zapisać w pamięci podręcznej konfigurację nawigacji z zewnętrznej usługi internetowej, a potem użyć jej do dynamicznego tworzenia grafu nawigacyjnego w funkcji onCreate() Twojej aktywności.

Zależności

Aby używać DSL Kotlin, dodaj do pliku build.gradle aplikacji tę zależność:

Odlotowy

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

Tworzenie wykresu

Zacznijmy od podstawowego przykładu opartego na aplikacji Słonecznik. W tym przykładzie mamy 2 miejsca docelowe: home i plant_detail. Miejsce docelowe home pojawia się, gdy użytkownik po raz pierwszy uruchamia aplikację. Wyświetla ono listę roślin z ogrodu użytkownika. Gdy użytkownik wybierze jedną z roślin, aplikacja przejdzie do miejsca docelowego plant_detail.

Rysunek 1 przedstawia te miejsca docelowe wraz z argumentami wymaganymi przez miejsce docelowe plant_detail i działanie to_plant_detail, których aplikacja używa do przechodzenia z home do plant_detail.

Aplikacja Sunflower ma 2 miejsca docelowe wraz z działaniem, które je łączy.
Rysunek 1. Aplikacja Sunflower ma 2 miejsca docelowe: home i plant_detail, oraz działanie, które je łączy.

Hosting grafu nawigacyjnego DSL Kotlin

Zanim utworzysz wykres nawigacji dla aplikacji, potrzebujesz miejsca, w którym będzie można go przechowywać. W tym przykładzie użyto fragmentów, więc wykres znajduje się w elemencie NavHostFragment wewnątrz 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>

Zwróć uwagę, że w tym przykładzie atrybut app:navGraph nie jest ustawiony. Wykres nie jest zdefiniowany jako zasób w folderze res/navigation, więc należy go ustawić w ramach procesu onCreate() w aktywności.

Działanie w pliku XML wiąże identyfikator miejsca docelowego z co najmniej jednym argumentem. Jednak w przypadku korzystania z nawigacji DSL trasa może zawierać argumenty jako część trasy. Oznacza to, że przy korzystaniu z DSL nie ma koncepcji działań.

Następnym krokiem jest określenie pewnych stałych, których użyjesz do definiowania wykresu.

Utwórz stałe do wykresu

Wykresy nawigacyjne oparte na formacie XML są analizowane w ramach procesu kompilacji Androida. Dla każdego atrybutu id zdefiniowanego na wykresie jest tworzona stała numeryczna. Te statyczne identyfikatory wygenerowane podczas kompilacji są niedostępne podczas tworzenia grafu nawigacyjnego w czasie działania, więc nawigacja DSL korzysta z ciągów trasy zamiast identyfikatorów. Każda trasa jest reprezentowana przez unikalny ciąg znaków i warto zdefiniować je jako stałe, by zmniejszyć ryzyko pomyłek związanych z literówką.

W przypadku argumentów są one wbudowane w ciąg znaków trasy. Wbudowanie takiej logiki w trasę także może zmniejszyć ryzyko pojawienia się błędów związanych z typami kodu.

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

Po zdefiniowaniu stałych możesz utworzyć wykres nawigacyjny.

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

W tym przykładzie na końcu lambda określamy 2 miejsca docelowe fragmentów za pomocą funkcji konstruktora DSL fragment(). Ta funkcja wymaga ciągu trasy do miejsca docelowego uzyskanego ze stałych. Funkcja ta akceptuje też opcjonalną funkcję lambda na potrzeby dodatkowej konfiguracji, takiej jak etykieta miejsca docelowego, a także funkcje wbudowanego kreatora dla argumentów i precyzyjnych linków.

Klasa Fragment zarządzająca interfejsem każdego miejsca docelowego jest przekazywana jako typ z parametrami w nawiasach kątowych (<>). Efekt jest taki sam jak w przypadku ustawienia atrybutu android:name w miejscach docelowych fragmentów zdefiniowanych za pomocą kodu XML.

Możesz też przejść z home do plant_detail, używając standardowych wywołań NavController.navigation():

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

W funkcji PlantDetailFragment możesz uzyskać wartość argumentu zgodnie z poniższym przykładem:

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

Szczegółowe informacje o tym, jak podać argumenty podczas nawigacji, znajdziesz w sekcji o podawaniu argumentów miejsca docelowego.

W pozostałej części tego przewodnika znajdziesz opis typowych elementów grafu nawigacyjnego i miejsc docelowych oraz sposobów korzystania z nich podczas tworzenia wykresów.

Miejsca docelowe

Kotlin DSL zapewnia wbudowaną obsługę 3 typów miejsc docelowych: Fragment, Activity i NavGraph, z których każde ma własną funkcję wbudowanego rozszerzenia do tworzenia i konfigurowania miejsca docelowego.

Miejsca docelowe fragmentów kodu

Do funkcji DSL fragment() można przyporządkować parametry klasy implementującej fragment kodu, po czym pobiera ona unikalny ciąg trasy do tego miejsca docelowego, po którym następuje funkcja lambda, która pozwala podać dodatkową konfigurację zgodnie z opisem w sekcji Nawigacja za pomocą wykresu DSL Kotlin.

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

Miejsce docelowe aktywności

Funkcja DSL activity() przyjmuje unikalny ciąg trasy, aby przypisać ją do tego miejsca docelowego, ale nie jest określona jako parametr do żadnej implementowanej klasy aktywności. Zamiast tego ustawiasz opcjonalny activityClass w parametrze lambda. Ta elastyczność umożliwia zdefiniowanie miejsca docelowego działania, które powinno zostać uruchomione za pomocą intencji niejawnej, gdzie klasa aktywności nie ma sensu. Podobnie jak w przypadku miejsc docelowych fragmentów, możesz też skonfigurować etykietę, argumenty i precyzyjne linki.

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

   activityClass = ActivityDestination::class
}

Funkcji DSL navigation() można używać do tworzenia zagnieżdżonego wykresu nawigacyjnego. Ta funkcja przyjmuje 3 argumenty: trasę do przypisania do grafu, trasę początkowego miejsca docelowego grafu i lambdę w celu dalszego skonfigurowania grafu. Prawidłowe elementy to inne miejsca docelowe, argumenty, precyzyjne linki i opisowa etykieta miejsca docelowego. Ta etykieta może być przydatna do powiązania grafu nawigacyjnego z komponentami UI za pomocą NavigationUI.

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

Obsługa niestandardowych miejsc docelowych

Jeśli używasz nowego typu miejsca docelowego, który nie obsługuje bezpośrednio DSL Kotlin, możesz dodać te miejsca docelowe do tej usługi za pomocą addDestination():

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

Możesz też użyć jednoargumentowego operatora plus, by dodać nowo utworzone miejsce docelowe bezpośrednio na wykresie:

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

Podawanie argumentów miejsca docelowego

Każde miejsce docelowe może definiować argumenty opcjonalne lub wymagane. Działania można definiować za pomocą funkcji argument() w elemencie NavDestinationBuilder, który jest klasą podstawową dla wszystkich typów kreatora miejsc docelowych. Ta funkcja przyjmuje nazwę argumentu jako ciąg znaków i element lambda wykorzystywaną do utworzenia i konfigurowania parametru NavArgument.

W obiekcie lambda możesz określić typ danych argumentu, w razie potrzeby wartość domyślną oraz określić, czy dopuszczalna jest wartość 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
    }
}

Jeśli podano defaultValue, można wywnioskować typ. Jeśli podano zarówno defaultValue, jak i type, typy muszą być zgodne. Pełną listę dostępnych typów argumentów znajdziesz w dokumentacji NavType.

Podawanie typów niestandardowych

Niektóre typy, np. ParcelableType i SerializableType, nie obsługują analizy wartości z ciągów znaków używanych przez trasy lub precyzyjne linki. Nie wymagają one odbicia w czasie działania. Podając niestandardową klasę NavType, możesz dokładnie kontrolować sposób analizowania typu z trasy lub precyzyjnego linku. Dzięki temu możesz używać serializacji Kotlin lub innych bibliotek, aby zapewnić bezodbiciowe kodowanie i dekodowanie niestandardowych typów.

Na przykład klasa danych, która reprezentuje parametry wyszukiwania przekazywane na ekran wyszukiwania, może implementować zarówno Serializable (aby zapewnić obsługę kodowania i dekodowania), jak i Parcelize (aby obsługiwać zapisywanie i przywracanie z poziomu Bundle):

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

Niestandardowy obiekt NavType może być zapisany w ten sposób:

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

Możesz go następnie użyć w DSL Kotlin tak jak dowolnego innego:

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

Pole NavType obejmuje zarówno zapis, jak i odczyt każdego pola. Oznacza to, że pole NavType musi być też używane podczas przechodzenia do miejsca docelowego, aby zapewnić zgodność formatów:

val params = SearchParameters("rose", listOf("available"))
val searchArgument = SearchParametersType.serializeAsValue(params)
navController.navigate("${nav_routes.plant_search}/$searchArgument")

Parametr ten można uzyskać z argumentów w miejscu docelowym:

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

Precyzyjne linki

Precyzyjne linki można dodawać do dowolnego miejsca docelowego, tak jak w przypadku grafu nawigacyjnego opartego na formacie XML. Wszystkie procedury opisane w sekcji Tworzenie precyzyjnego linku do miejsca docelowego mają zastosowanie do procesu tworzenia precyzyjnego linku precyzyjnego za pomocą DSL Kotlin.

Jednak podczas tworzenia niejawnego precyzyjnego linku nie masz zasobu nawigacji XML, który można przeanalizować pod kątem elementów <deepLink>. Dlatego nie możesz polegać na umieszczeniu elementu <nav-graph> w pliku AndroidManifest.xml. Zamiast tego musisz ręcznie dodać do aktywności filtry intencji. Podany filtr intencji powinien pasować do wzorca podstawowego adresu URL, działania i typu MIME precyzyjnych linków aplikacji.

Za pomocą funkcji DSL deepLink() możesz podać bardziej szczegółowe deeplink dla każdego miejsca docelowego z precyzyjnymi linkami. Ta funkcja akceptuje obiekt NavDeepLink, który zawiera wzorzec String reprezentujący wzorzec identyfikatora URI, String reprezentujący działania intencji, a obiekt String reprezentujący typ mime .

Na przykład:

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

Nie ma ograniczeń co do liczby precyzyjnych linków, które możesz dodać. Za każdym razem, gdy wywołujesz metodę deepLink(), do listy prowadzonej w tym miejscu docelowym jest dodawany nowy precyzyjny link.

Poniżej przedstawiono bardziej złożony scenariusz niejawnego precyzyjnego linku, który dodatkowo definiuje ścieżkę i parametry oparte na zapytaniach:

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

Aby uprościć definicję, możesz użyć interpolacji ciągów znaków.

Ograniczenia

Wtyczka Safe Args jest niezgodna z DSLL Kotlin, ponieważ szuka plików zasobów XML do generowania klas Directions i Arguments.

Więcej informacji

Zapoznaj się ze stroną Bezpieczeństwo typu nawigacji, aby dowiedzieć się, jak zapewnić bezpieczeństwo typów w kodzie Kotlin DSL i nawigacji.