Постройте график программно с помощью Kotlin DSL,Постройте график программно с помощью Kotlin DSL

Компонент навигации предоставляет предметно-ориентированный язык на основе Kotlin, или DSL, который опирается на типобезопасные компоновщики Kotlin. Этот API позволяет вам декларативно составлять график в коде Kotlin, а не внутри ресурса XML. Это может быть полезно, если вы хотите динамически создавать навигацию вашего приложения. Например, ваше приложение может загрузить и кэшировать конфигурацию навигации из внешней веб-службы, а затем использовать эту конфигурацию для динамического построения графа навигации в функции onCreate() вашего действия.

Зависимости

Чтобы использовать Kotlin DSL, добавьте следующую зависимость в файл build.gradle вашего приложения:

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

Построение графика

Начнем с базового примера на основе приложения Sunflower . В этом примере у нас есть два пункта назначения: home и plant_detail . home пункт назначения присутствует, когда пользователь впервые запускает приложение. В этом пункте назначения отображается список растений из сада пользователя. Когда пользователь выбирает один из заводов, приложение переходит к месту назначения plant_detail .

На рис. 1 показаны эти пункты назначения вместе с аргументами, необходимыми для пункта назначения plant_detail , и действием to_plant_detail , которое приложение использует для перехода от home к plant_detail .

В приложении «Подсолнух» есть два пункта назначения и действие, которое их соединяет.
Рисунок 1. Приложение Sunflower имеет два пункта назначения: home и plant_detail , а также действие, которое их соединяет.

Хостинг Kotlin DSL Nav Graph

Прежде чем вы сможете построить граф навигации вашего приложения, вам нужно место для его размещения. В этом примере используются фрагменты, поэтому граф размещается в NavHostFragment внутри 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>

Обратите внимание, что атрибут app:navGraph в этом примере не установлен. График не определен как ресурс в папке res/navigation , поэтому его необходимо установить как часть процесса onCreate() в действии.

В XML действие связывает идентификатор назначения с одним или несколькими аргументами. Однако при использовании Navigation DSL маршрут может содержать аргументы как часть маршрута. Это значит, что нет понятия действий при использовании DSL.

Следующим шагом будет определение некоторых констант, которые вы будете использовать при определении вашего графика.

Создайте константы для вашего графика

Навигационные графики на основе XML анализируются как часть процесса сборки Android. Числовая константа создается для каждого атрибута id , определенного на графике. Эти статические идентификаторы, созданные во время сборки, недоступны при построении графа навигации во время выполнения, поэтому навигационный DSL использует строки маршрута вместо идентификаторов. Каждый маршрут представлен уникальной строкой, и рекомендуется определять их как константы, чтобы снизить риск ошибок, связанных с опечатками.

При работе с аргументами они встроены в строку маршрута . Встраивание этой логики в маршрут может еще раз снизить риск появления ошибок, связанных с опечатками.

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

Определив константы, вы можете построить граф навигации.

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

В этом примере завершающая лямбда определяет два места назначения фрагмента с помощью функции построения DSL fragment() . Этой функции требуется строка маршрута для пункта назначения, полученная из констант. Функция также принимает необязательную лямбду для дополнительной настройки, например метку назначения, а также встроенные функции компоновщика для аргументов и глубоких ссылок.

Класс Fragment , который управляет пользовательским интерфейсом каждого пункта назначения, передается как параметризованный тип внутри угловых скобок ( <> ). Это имеет тот же эффект, что и установка атрибута android:name для целевых фрагментов, определенных с помощью XML.

Наконец, вы можете перейти из home в plant_detail используя стандартные вызовы NavController.navigate() :

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

В PlantDetailFragment вы можете получить значение аргумента, как показано в следующем примере:

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

Подробную информацию о том, как предоставлять аргументы при навигации, можно найти в разделе предоставления аргументов пункта назначения .

Остальная часть этого руководства описывает общие элементы навигационного графа, пункты назначения и способы их использования при построении графика.

Направления

Kotlin DSL обеспечивает встроенную поддержку трех типов назначений: Fragment , Activity и NavGraph , каждый из которых имеет собственную встроенную функцию расширения, доступную для создания и настройки места назначения.

Назначения фрагментов

Функция fragment() DSL может быть параметризована реализующему классу фрагмента и принимает уникальную строку маршрута для назначения этому пункту назначения, за которой следует лямбда-выражение, в котором вы можете предоставить дополнительную конфигурацию, как описано в разделе «Навигация с графом Kotlin DSL» .

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

Место действия

Функция activity() DSL принимает уникальную строку маршрута для назначения этому пункту назначения, но не параметризуется для какого-либо реализующего класса активности. Вместо этого вы устанавливаете необязательный activityClass в завершающей лямбде. Эта гибкость позволяет вам определить место назначения для действия, которое должно быть запущено с использованием неявного намерения , где явный класс действия не имеет смысла. Как и в случае с местами назначения фрагментов, вы также можете настроить метку, аргументы и глубокие ссылки.

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

   activityClass = ActivityDestination::class
}

Функцию DSL navigation() можно использовать для построения вложенного графа навигации . Эта функция принимает три аргумента: маршрут, который нужно назначить графу, маршрут начального пункта назначения графа и лямбда-выражение для дальнейшей настройки графа. Допустимые элементы включают другие пункты назначения, аргументы, глубокие ссылки и описательную метку для пункта назначения . Эта метка может быть полезна для привязки графа навигации к компонентам пользовательского интерфейса с помощью NavigationUI.

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

Поддержка пользовательских направлений

Если вы используете новый тип назначения , который напрямую не поддерживает Kotlin DSL, вы можете добавить эти места назначения в свой Kotlin DSL с помощью addDestination() :

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

В качестве альтернативы вы также можете использовать унарный оператор плюс, чтобы добавить вновь созданный пункт назначения непосредственно в граф:

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

Предоставление аргументов назначения

Любой пункт назначения может определять аргументы, которые являются необязательными или обязательными. Действия можно определить с помощью функции argument() в NavDestinationBuilder , которая является базовым классом для всех типов построителей пунктов назначения. Эта функция принимает имя аргумента в виде строки и лямбда-выражение, которое используется для создания и настройки NavArgument .

Внутри лямбда-выражения вы можете указать тип данных аргумента, значение по умолчанию, если оно применимо, а также указать, может ли оно иметь значение 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
    }
}

Если задано defaultValue , можно определить тип. Если указаны и defaultValue , и type , типы должны совпадать. Полный список доступных типов аргументов см. в справочной документации NavType .

Предоставление пользовательских типов

Некоторые типы, такие как ParcelableType и SerializableType , не поддерживают синтаксический анализ значений из строк, используемых маршрутами или глубокими ссылками. Это связано с тем, что они не полагаются на отражение во время выполнения. Предоставляя собственный класс NavType , вы можете точно контролировать, как ваш тип анализируется на маршруте или глубокой ссылке. Это позволяет вам использовать сериализацию Kotlin или другие библиотеки для обеспечения безотражательного кодирования и декодирования вашего пользовательского типа.

Например, класс данных, который представляет параметры поиска, передаваемые на ваш экран поиска, может реализовать как Serializable (для обеспечения поддержки кодирования/декодирования), так и Parcelize (для поддержки сохранения и восстановления из Bundle ):

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

Пользовательский NavType можно записать так:

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

Затем это можно использовать в вашем Kotlin DSL, как и любой другой тип:

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 инкапсулирует как запись, так и чтение каждого поля, а это означает, что NavType также необходимо использовать при переходе к месту назначения, чтобы обеспечить соответствие форматов:

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

Параметр можно получить из аргументов в месте назначения:

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

Глубокие ссылки

Глубокие ссылки можно добавлять в любой пункт назначения, как и в случае с навигационным графом на основе XML. Все те же процедуры, которые определены в разделе Создание глубокой ссылки для места назначения, применяются к процессу создания явной глубокой ссылки с использованием Kotlin DSL.

Однако при создании неявной глубокой ссылки у вас нет ресурса навигации XML, который можно было бы проанализировать на наличие элементов <deepLink> . Таким образом, вы не можете полагаться на размещение элемента <nav-graph> в файле AndroidManifest.xml и вместо этого должны добавлять фильтры намерений в свою активность вручную. Предоставляемый вами фильтр намерений должен соответствовать базовому шаблону URL-адреса, действию и mime-типу глубоких ссылок вашего приложения.

Вы можете указать более конкретную deeplink для каждого отдельного места назначения с глубокой ссылкой, используя функцию deepLink() DSL. Эта функция принимает NavDeepLink , который содержит String , представляющую шаблон URI, String представляющую действия намерения, и String представляющую mimeType .

Например:

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

Количество добавляемых глубоких ссылок не ограничено. Каждый раз, когда вы вызываете deepLink() новая глубокая ссылка добавляется в список, который поддерживается для этого места назначения.

Ниже показан более сложный сценарий неявной глубокой ссылки, в котором также определяются параметры пути и запроса:

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

Вы можете использовать строковую интерполяцию, чтобы упростить определение.

Ограничения

Плагин Safe Args несовместим с Kotlin DSL, поскольку плагин ищет файлы ресурсов XML для создания классов Directions и Arguments .

Узнать больше

Посетите страницу безопасности типов навигации , чтобы узнать, как обеспечить безопасность типов для кода Kotlin DSL и Navigation Compose .