Zbuduj wykres programowo za pomocą DSL Kotlin

Komponent Nawigacja udostępnia specyficzny dla domeny język Kotlin (DSL), który korzysta z konstruktorów udostępnianych przez Kotlina w bezpieczny sposób. Ten interfejs API umożliwia deklaratywne tworzenie grafu w kodzie Kotlin, a nie wewnątrz zasobu XML. Jest to przydatne, jeśli chcesz dynamicznie tworzyć nawigację w aplikacji. Aplikacja może na przykład pobrać konfigurację nawigacji z zewnętrznej usługi internetowej i zapisać ją w pamięci podręcznej, a potem wykorzystać tę konfigurację do dynamicznego tworzenia wykresu nawigacyjnego w funkcji onCreate() Twojej aktywności.

Zależności

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

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, w którym wykorzystamy aplikację Słonecznik. Mamy 2 miejsca docelowe: home i plant_detail. Miejsce docelowe home jest widoczne, gdy użytkownik po raz pierwszy uruchomi aplikację. To miejsce docelowe zawiera 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 nawigacji 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.

Hostowanie grafu Nav DSL Kotlin

Aby utworzyć wykres nawigacyjny swojej aplikacji, musisz mieć miejsce na jego umieszczenie. W tym przykładzie użyto fragmentów, dlatego wykres znajduje się w elemencie NavHostFragment wewnątrz elementu 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 nie ustawiono atrybutu app:navGraph. Wykres nie jest zdefiniowany jako zasób w folderze res/navigation, więc trzeba go ustawić w procesie onCreate() w działaniu.

W języku XML działanie wiąże identyfikator miejsca docelowego z co najmniej 1 argumentem. Jednak w przypadku korzystania z DSL w Nawigacji trasa może zawierać jej argumenty. Oznacza to, że w przypadku DSL nie trzeba wykonywać żadnych działań.

Następnym krokiem jest zdefiniowanie kilku stałych, których będziesz używać do definiowania wykresu.

Utwórz stałe dla wykresu

Wykresy nawigacyjne w formacie XML są analizowane w ramach procesu tworzenia Androida. Dla każdego atrybutu id zdefiniowanego na wykresie tworzona jest stała liczbowa. Identyfikatory statyczne generowane podczas kompilacji nie są dostępne podczas tworzenia wykresu nawigacyjnego w czasie działania, więc DSL nawigacji używa ciągów trasy zamiast identyfikatorów. Każda trasa jest reprezentowana przez unikalny ciąg znaków. Warto zdefiniować je jako stałe, aby zmniejszyć ryzyko błędów związanych z literówką.

W przypadku obsługi argumentów są one wbudowane w ciąg trasy. Wbudowanie tej logiki w trasę może ponownie zmniejszyć ryzyko pojawienia się błędów związanych z typo.

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 końcowa funkcja lambda definiuje 2 miejsca docelowe fragmentów za pomocą funkcji konstruktora DSL fragment(). Ta funkcja wymaga ciągu trasy dla miejsca docelowego, który jest uzyskiwany ze stałych. Ta funkcja akceptuje też opcjonalną funkcję lambda na potrzeby dodatkowej konfiguracji, np. etykiety docelowej, a także osadzone funkcje kreatora dotyczące argumentów i precyzyjnych linków.

Klasa Fragment, która zarządza interfejsem użytkownika każdego miejsca docelowego, jest przekazywana jako typ z parametrami w nawiasach kątowych (<>). Działa to tak samo jak ustawienie atrybutu android:name w miejscach docelowych fragmentów zdefiniowanych za pomocą kodu XML.

Możesz też przejść z home do plant_detail za pomocą 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 w następujący sposób:

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

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

W pozostałej części tego przewodnika znajdziesz opis typowych elementów wykresu nawigacji i miejsc docelowych oraz sposobów korzystania z nich przy tworzeniu wykresu.

Miejsca docelowe

DSL Kotlin zapewnia wbudowaną obsługę 3 typów miejsc docelowych: Fragment, Activity i NavGraph. Każdy z nich ma własną funkcję rozszerzenia dostępną do tworzenia i konfigurowania miejsc docelowych.

Miejsca docelowe fragmentów

Funkcję DSL fragment() można parametryzować w klasie implementującej fragment. Na jej podstawie znajduje się unikalny ciąg trasy do przypisania do miejsca docelowego. Następnie parametr lambda 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() wykorzystuje unikalny ciąg trasy, który jest przypisany do tego miejsca docelowego, ale nie jest określana jako parametry na żadnej implementującej klasie aktywności. Zamiast tego ustawiasz opcjonalny element activityClass w końcowej funkcji lambda. Ta elastyczność pozwala zdefiniować miejsce docelowe działania, które powinno być uruchamiane za pomocą intencji niejawnej, ponieważ klasa bezpośredniego działania nie miałaby 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żesz użyć do utworzenia zagnieżdżonego wykresu nawigacyjnego. Ta funkcja przyjmuje 3 argumenty: trasę do przypisania do wykresu, trasę początkowego punktu docelowego wykresu i lambda do dalszej konfiguracji wykresu. Prawidłowe elementy to m.in. inne miejsca docelowe, argumenty, precyzyjne linki i opisowa etykieta miejsca docelowego. Ta etykieta może być przydatna do powiązania wykresu nawigacyjnego z komponentami interfejsu za pomocą Nawigacja

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 DSL Kotlin 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 plusa, aby dodać nowo utworzone miejsce docelowe bezpośrednio do wykresu:

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

Podaję argumenty docelowe

Dowolne miejsce docelowe może definiować argumenty opcjonalne lub wymagane. Działania można definiować za pomocą funkcji argument() w NavDestinationBuilder, która jest klasą bazową wszystkich typów konstruktora miejsc docelowych. Ta funkcja przyjmuje nazwę argumentu jako ciąg znaków i lambda, która służy do stworzenia i konfiguracji NavArgument.

Funkcja lambda pozwala określić typ danych argumentu, w razie potrzeby wartość domyślną oraz określić, czy ma ona 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 podana jest zarówno wartość defaultValue, jak i type, typy muszą być takie same. Pełną listę dostępnych typów argumentów znajdziesz w dokumentacji NavType.

Dostarczanie typów niestandardowych

Niektóre typy, np. ParcelableType i SerializableType, nie obsługują analizy wartości ciągów używanych przez trasy lub precyzyjne linki. Dzieje się tak, ponieważ nie polegają na odczuciu w czasie działania. Udostępniając niestandardową klasę NavType, możesz dokładnie kontrolować sposób analizowania typu na podstawie trasy lub precyzyjnego linku. Dzięki temu możesz użyć serializacji Kotlin lub innych bibliotek, aby zapewnić bezrefleksyjne kodowanie i dekodowanie niestandardowego typu.

Na przykład klasa danych reprezentująca parametry wyszukiwania przekazywane do ekranu wyszukiwania może implementować zarówno Serializable (aby zapewnić obsługę kodowania/dekodowania), jak i Parcelize (do obsługi zapisywania w Bundle i przywracania z niego):

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

Niestandardowy element NavType można zapisać w następujący 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 parseValue(value: String): SearchParameters {
    return Json.decodeFromString<SearchParameters>(value)
  }

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

Można go używać w strumieniu DSL Kotlin jak każdego innego typu:

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

W tym przykładzie użyto serializacji Kotlin do analizy wartości ciągu znaków, co oznacza, że przy przejściu do miejsca docelowego należy również użyć serializacji Kotlin, aby mieć pewność, że formaty są zgodne:

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

Parametr 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 dowolnych miejsc docelowych, tak jak w przypadku wykresów nawigacyjnych opartych na języku XML. Wszystkie procedury opisane w sekcji Tworzenie precyzyjnego linku do miejsca docelowego mają zastosowanie do procesu tworzenia precyzyjnego precyzyjnego linku za pomocą DSL Kotlin.

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

Za pomocą funkcji DSL deepLink() możesz podać bardziej szczegółową wartość deeplink dla każdego miejsca docelowego z precyzyjnymi linkami. Ta funkcja akceptuje obiekt NavDeepLink zawierający String reprezentujący wzorzec identyfikatora URI, String reprezentujący działania intencji i String reprezentujący mimeType .

Na przykład:

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

Możesz dodać nieograniczoną liczbę precyzyjnych linków. Za każdym razem, gdy wywołujesz deepLink(), do listy, która jest używana w przypadku tego miejsca docelowego, jest dodawany nowy precyzyjny link.

Poniżej przedstawiamy bardziej skomplikowany scenariusz dotyczący precyzyjnych linków, który określa też ś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 DSL Kotlin, ponieważ szuka ona plików zasobów XML, aby wygenerować klasy Directions i Arguments.

Więcej informacji

Aby dowiedzieć się, jak zapewnić bezpieczeństwo typów w kodzie DSL Kotlin i nawigacji w komponencie, zajrzyj na stronę Bezpieczeństwo nawigacji.