Фрагменты и Kotlin DSL

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

Зависимости

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

классный

dependencies {
    def nav_version = "2.8.4"

    api "androidx.navigation:navigation-fragment-ktx:$nav_version"
}

Котлин

dependencies {
    val nav_version = "2.8.4"

    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 использует сериализуемые типы вместо идентификаторов. Каждый маршрут представлен уникальным типом.

При работе с аргументами они встроены в тип маршрута . Это обеспечивает безопасность типов для аргументов навигации.

@Serializable data object Home
@Serializable data class Plant(val id: String)

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

val navController = findNavController(R.id.nav_host_fragment)
navController.graph = navController.createGraph(
    startDestination = Home
) {
    fragment<HomeFragment, Home> {
        label = resources.getString(R.string.home_title)
    }
    fragment<PlantDetailFragment, PlantDetail> {
        label = resources.getString(R.string.plant_detail_title)
    }
}

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

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

Во-вторых, маршрут. Это должен быть сериализуемый тип, наследуемый от Any . Он должен содержать все аргументы навигации, которые будут использоваться этим пунктом назначения, и их типы.

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

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

private fun navigateToPlant(plantId: String) {
   findNavController().navigate(route = PlantDetail(id = plantId))
}

В PlantDetailFragment вы можете получить аргументы навигации, получив текущий NavBackStackEntry и вызвав для него toRoute , чтобы получить экземпляр маршрута.

val plantDetailRoute = findNavController().getBackStackEntry<PlantDetail>().toRoute<PlantDetail>()
val plantId = plantDetailRoute.id

Если PlantDetailFragment использует ViewModel , получите экземпляр маршрута с помощью SavedStateHandle.toRoute .

val plantDetailRoute = savedStateHandle.toRoute<PlantDetail>()
val plantId = plantDetailRoute.id

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

Направления

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

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

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

fragment<MyFragment, MyRoute> {
   label = getString(R.string.fragment_title)
   // custom argument types, deepLinks
}

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

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

activity<MyRoute> {
   label = getString(R.string.activity_title)
   // custom argument types, deepLinks...

   activityClass = MyActivity::class
}

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

@Serializable data object HomeGraph
@Serializable data object Home

navigation<HomeGraph>(startDestination = 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
}

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

Аргументы назначения могут быть определены как часть класса маршрута. Их можно определить так же, как и для любого класса Kotlin. Обязательные аргументы определяются как типы, не допускающие значения NULL, а необязательные аргументы определяются со значениями по умолчанию.

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

Плагин сериализации Kotlin автоматически генерирует методы сериализации для базовых типов , когда к объекту добавляется аннотация @Serializable .

@Serializable
data class MyRoute(
  val id: String,
  val myList: List<Int>,
  val optionalArg: String? = null
)

fragment<MyFragment, MyRoute>

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

Для пользовательских типов аргументов вам потребуется предоставить собственный класс NavType . Это позволяет вам точно контролировать, как ваш тип анализируется из маршрута или глубокой ссылки.

Например, маршрут, используемый для определения экрана поиска, может содержать класс, представляющий параметры поиска:

@Serializable
data class SearchRoute(val parameters: SearchParameters)

@Serializable
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, SearchRoute> {
    label = getString(R.string.plant_search_title)
    typeMap = mapOf(typeOf<SearchParameters>() to SearchParametersType)
}

При переходе к месту назначения создайте экземпляр своего маршрута:

val params = SearchParameters("rose", listOf("available"))
navController.navigate(route = SearchRoute(params))

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

val searchRoute = navController().getBackStackEntry<SearchRoute>().toRoute<SearchRoute>()
val params = searchRoute.parameters

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

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

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

Глубокие ссылки добавляются к месту назначения путем вызова функции deepLink внутри лямбды места назначения. Он принимает маршрут как параметризованный тип и параметр basePath для базового пути URL-адреса, используемого для глубокой ссылки.

Вы также можете добавить действие и mimetype, используя завершающую лямбду deepLinkBuilder .

В следующем примере создается URI глубокой ссылки для Home пункта назначения.

@Serializable data object Home

fragment<HomeFragment, Home>{
  deepLink<Home>(basePath = "www.example.com/home"){
    // Optionally, specify the action and/or mime type that this destination
    // supports
    action = "android.intent.action.MY_ACTION"
    mimeType = "image/*"
  }
}

формат URI

Формат URI глубокой ссылки автоматически генерируется из полей маршрута с использованием следующих правил:

  • Обязательные параметры добавляются как параметры пути (пример: /{id} ).
  • Параметры со значением по умолчанию (необязательные параметры) добавляются как параметры запроса (пример: ?name={name} ).
  • Коллекции добавляются как параметры запроса (пример: ?items={value1}&items={value2} ).
  • Порядок параметров соответствует порядку полей в маршруте.

Например, следующий тип маршрута:

@Serializable data class PlantDetail(
  val id: String,
  val name: String,
  val colors: List<String>,
  val latinName: String? = null,
)

имеет сгенерированный формат URI:

basePath/{id}/{name}/?colors={color1}&colors={color2}&latinName={latinName}

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

Ограничения

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