Основы работы с приложениями

Как создавать типобезопасные ссылки на контент с помощью Jetpack Navigation 3

Колонки

  • Клара Фок (Clara Fok)

    Разработчик ПО

  • Иэн Лейк (Ian Lake)

    Менеджер по разработке

О приложении

О выступлении

Управление ссылками на контент в модульных приложениях Android может привести к неработающим маршрутам и ошибкам во время выполнения.На этом занятии мы рассмотрим новую типобезопасную архитектуру ссылок на контент Jetpack Navigation3 для Compose, которая поможет вам создать надежную и масштабируемую систему маршрутизации. Мы рассмотрим, как реализовать готовые API сопоставления ссылок на контент, написать собственные сериализаторы для сложных типобезопасных аргументов и создать динамические иерархические обратные стеки, которые делают навигацию пользователя предсказуемой.

Сводка

  • API ссылок на контент Navigation 3 преобразуют входящие запросы ссылок на контент в ключи навигации, чтобы определить начальный обратный стек. Совместимый с KMP класс DeepLinkRequest инкапсулирует входные данные, включая конструктор Android Intent.
  • UriDeepLinkMatcher использует сериализацию Kotlin для сопоставления шаблонов URI и десериализации аргументов в типобезопасные свойства NavKey. Примитивные типы работают автоматически, и совпадения не находятся, если значения не могут быть проанализированы.
  • Разработчики могут расширить DeepLinkSerializer или переопределить matchArguments, чтобы обрабатывать сложные строки ссылок на контент. Создание подкласса UriDeepLinkMatcher позволяет предварительно обрабатывать необработанные плейсхолдеры URI перед запуском сериализации Kotlin.
  • Вызов withBackStack для сопоставителей создает синтетические обратные стеки для наилучшего совпадения. Это обеспечивает правильное поведение системы при возврате назад без вычисления ненужных обратных стеков для каждого другого допустимого совпадения.

Ресурсы

Gemini Notebook

Темы

  • jetpack-navigation-3
  • ссылки на контент
  • kotlin-serialization
  • kotlin-multiplatform
  • android-architecture

Расшифровка

Введение и навигация: 3 рецепта

  • Здравствуйте! Меня зовут Иэн Лейк, я менеджер по разработке в команде Jetpack Architecture.

  • А я Клара Фок, технический руководитель Nav 3.

  • Сегодня мы поговорим о функции, которая мне очень нравится, – ссылках на контент.

  • На самом деле, они мне так нравятся, что я реализовал их и для Nav2, и для Nav3 в рамках выпуска Nav3 1.2.

  • Думаю, вы сделали это трижды, потому что мы также реализовали это в рецептах Nav 3.

  • Да, это так.

  • Мы многократно дорабатывали API, чтобы он идеально вписывался в подход Nav3, ориентированный на Compose.

  • И во многом это началось с нашего акцента на рецептах.

  • Да, одной из наших главных целей при создании Navigation 3 было сделать так, чтобы она не мешала вам.

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

  • Поэтому, когда мы получаем вопрос: "Хорошо, как мне это сделать?"

  • или "Как вообще подступиться к этой проблеме?".

  • Вместо того чтобы сразу создавать пользовательский API, мы создаем рецепт – руководство, в котором объясняется, как вы можете добавить эту функцию в свое приложение без необходимости вносить какие-либо изменения в Navigation 3.

  • Именно такой подход – показывать, а не рассказывать – привел нас к созданию репозитория рецептов Nav3 на GitHub.

  • Сейчас в ней более 35 рецептов для распространенных сценариев, таких как несколько обратных стеков и нижних экранов. В рецептах есть весь код, который нужен для реализации.

  • Это значит, что даже для версии 1.0 Nav3, выпущенной в ноябре 2025 года, у нас уже был рецепт реализации ссылок на контент, поскольку мы знали, что это важно для многих из вас.

  • Но когда мы понимаем, что рецепт требует наличия кучи вспомогательных методов, которые вам нужно скопировать и вставить в свое приложение, мы рассматриваем процесс, называемый выпуском, когда мы берем этих помощников и говорим: "Хорошо, как бы это выглядело на самом деле, если бы они были частью самой навигации три?"

  • Это гарантирует, что у разработчиков, которые уже скопировали и вставили этот код, все будет в порядке.

  • Вы можете сохранить этот код.

  • Если у вас есть собственные решения, которые работают, это отлично.

  • У вас нет принудительных переносов.

  • Но если вы столкнулись с новой проблемой или требованием, вы получаете простое, проверенное решение, которое можно использовать непосредственно в навигации.

  • Но самое приятное для меня лично – это то, что после добавления помощника мы возвращаемся и обновляем рецепты, получая дофаминовый заряд от удаления кода.

  • Таким образом, у любого, кто ищет рецепты для рекомендуемого подхода к этой проблеме, есть самое современное решение.

  • Конечно, задним умом все крепки, и этот процесс выпуска дает нам возможность по-настоящему взглянуть на те помощники, которые мы создали в рецептах, и сказать: "Действительно ли это лучшие API для этого?"

  • И в этом случае у нас есть возможность улучшить эти вещи, когда мы переместим их в библиотеку.

  • И это определенно относится к ссылкам на контент.

Разработка API для ссылок на контент и сопоставление шаблонов URI

  • Спасибо, Ян.

  • Поэтому концептуально и рецепты, и наши конечные API очень похожи, поскольку в Nav 3 вы владеете обратным стеком.

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

  • Но что происходит, когда пользователь нажимает на веб-URI?

  • Теперь, независимо от того, используете ли вы Nav3, фреймворк Android требует, чтобы вы объявляли фильтры интентов для каждой ссылки на контент, которую поддерживает ваше приложение.

  • Теперь самое интересное: что происходит, когда ссылка на контент запускает Activity.

  • В данном случае входными данными является намерение, с которым запускается ваше приложение, и запущенное действие.

  • Но ссылка на контент может принимать разные формы.

  • Например, вы можете создать его вручную для тестирования или воспользоваться поддержкой Kotlin Multiplatform.

  • Поэтому мы инкапсулировали входные данные ссылки на контент в класс DeepLinkRequest, совместимый с KMP.

  • А в Android у нас есть конструктор DeepLinkRequest, который принимает Android Intent.

  • Теперь наша цель – выяснить, как по запросу определить, что NavKey нужно поместить в обратный стек.

  • Для этого мы используем класс DeepLinkMatcher.

  • Он принимает запрос и возвращает объект MatchResult, который содержит NavKey, связанный с совпадением.

  • Для веб-URI у нас есть UriDeepLinkMatcher, который инкапсулирует всю логику, необходимую для преобразования данного запроса в MatchResult.

    val matcher = UriDeepLinkMatcher(
      ...,
      ...
    )
  • Первый параметр, который принимает UriDeepLinkMatcher, – это шаблон URI.

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri(...),
      ...
    )
  • Допустим, вы создаете приложение ADS и хотите добавить ссылку на контент на главный экран.

  • В этом случае шаблон представляет собой точный URI, и мы можем опустить схему HTTP, чтобы автоматически сопоставлять как HTTP, так и HTTPS.

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/home"),
      ...
    )
  • Но если мы хотим создать ссылку на контент для определенного рекламного сеанса ADS, то она будет содержать информацию, которую нам нужно извлечь, а именно идентификатор сеанса.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      …
    )
  • В этом случае в шаблоне используются фигурные скобки, которые служат плейсхолдерами для информации, которую нужно извлечь из ссылки на контент.

  • И оказывается, что уже есть удобная библиотека для преобразования любого вида входных данных в экземпляры классов, которая называется Kotlin Serialization.

  • Поэтому наш UriDeepLinkMatcher использует декодер Kotlin для десериализации входного URI и вывода экземпляра NavKey.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
  • Теперь декодеру нужно знать, с каким классом работать, поэтому вам также нужно передать сериализатор для вашего NavKey в сопоставитель.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
  • Чтобы декодер мог правильно сопоставить плейсхолдеры со свойствами ключа, их названия должны совпадать.

  • В этом примере название свойства – ID, что соответствует параметру ID сеанса.

  • Теперь, когда у нас есть UriDeepLinkMatcher, нам нужно создать DeepLinkRequest с входящей ссылкой на контент.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
    
    val request = DeepLinkRequest(
      uri = DeepLinkUri("http://ads.android.com/session/abcd")
    )
    
    val matchResult: UriMatchResult<Session>? = matcher.match(request)
    
    val backStack = listOf(matchResult?.key ?: Home)
  • Затем выполняется сопоставление путем вызова функции match, передающей наш запрос.

  • Это даст вам допустимое значение NULL MatchResult, содержащее NavKey, которое в нашем примере будет ключом сеанса, или, если результат равен NULL, мы можем просто по умолчанию использовать главный экран.

  • Теперь для каждой ссылки на контент нужно указывать отдельный сопоставитель.

  • Например, здесь у нас есть два шаблона URI для ключа сеанса: один для устаревшего шаблона новостной рассылки и один для обычного веб-URI.

  • У нас также есть ссылка на контент, ведущая на главный экран ADS, поэтому мы добавляем третье сопоставление.

  • И все сопоставители будут добавлены в один и тот же список, который мы перебираем.

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

  • Для этого мы можем использовать mapNotNull, чтобы отфильтровать несовпадения и получить список только действительных совпадений.

    val matchers = listOf(...)
    
    // gets all non-null MatchResult
    val matchResults = matchers.mapNotNull { matcher ->
      matcher.match(request)
    }
  • В большинстве случаев в списке будет только одно ненулевое совпадение, но иногда их может быть несколько.

  • Например, мы хотим добавить специальный экран для ключевых докладов, используя тот же шаблон URI, но вместо плейсхолдера ID у него будет статический путь.

  • В этом случае один и тот же запрос может соответствовать обоим сопоставителям: сеансовому ключу, где идентификатор равен keynote, и keynote NavKey.

  • Поскольку MatchResult реализует Comparable, мы можем найти наилучшее совпадение, сравнив все допустимые совпадения в списке с помощью функции max или maxOrNull, чтобы обработать случай, когда не было ссылок на контент, соответствующих запросу.

    val matchers = listOf(...)
    
    // gets all non-null MatchResult
    val matchResults = matchers.mapNotNull { matcher ->
      matcher.match(request)
    }
    
    // compare matches
    val backStack = listOf(
      matchResults.maxOrNull()?.key ?: Home
    )
  • Статический список сопоставителей может иметь смысл для блиц-конференции, но так же, как ваши экраны могут быть распределены по нескольким модулям, мы ожидаем, что эти же модули будут вносить вклад в глобальный список сопоставителей через вашу систему DI.

    // Feature A Module
    @ContributesTo(ActivityScope::class)
    @BindingContainer
    object FeatureAModule {
        @IntoSet
        @Provides
        fun provideDeepLinkMatcher() : UriDeepLinkMatcher<NavKey> =
          UriDeepLinkMatcher(
            uriPattern = DeepLinkUri("ads.android.com/featureA"),
            serializer<FeatureBKey>(),
          )
    }
    
    // in MainActivity
    @Inject lateinit var matchers: Set<UriDeepLinkMatcher<NavKey>>
  • Используя этот подход, мы обновили наши рецепты Hilt, Metro и Koin в репозитории рецептов, включив в них ссылки на контент.

Типобезопасная десериализация и пользовательские сериализаторы

  • Клара упомянула, что наше UriDeepLinkMatcher использует сериализацию Kotlin для извлечения полей из запроса ссылки на контент и преобразования его в наш типобезопасный класс, например класс сеанса, который мы показали.

  • Как написано здесь, это довольно простое преобразование.

  • URI представляет собой строку.

  • Поле представляет собой строку.

  • Ого, это было просто.

  • Но мы понимали, что нужно выйти за рамки строковых свойств.

  • Здесь Kotlin Serialization уже дает нам прочную основу для работы.

  • Все примитивные типы, такие как логические значения, целые числа, числа с плавающей запятой и даже перечисления, работают по умолчанию. Если значение ссылки на контент не может быть проанализировано как определенный тип, сопоставление автоматически завершается ошибкой и возвращается значение null.

  • Например, если бы параметр ID сеанса был целым числом, а не строкой, то передача строки, которую нельзя преобразовать в целое число, привела бы к ошибке.

    // pattern = "http://ads.android.com/session/{id}"
    @Serializable data class Session(val id: Int) : NavKey
  • Аналогично, если вы хотите использовать необязательные аргументы, например для данных аналитики, прикрепленных к URL, они могут быть свойствами ключа со значениями по умолчанию.

    // pattern = "http://ads.android.com/session/{id}#referrer={referrer}
    @Serializable data class Session(
      val id: Int,
      val referrer: String? = null,
    ) : NavKey
  • Обратите внимание, что мы постарались отделить структуру URL сайта, за которую может отвечать другая команда, от типобезопасных классов или классов, которые представляют совпадение ссылки на контент в приложении.

  • Потому что я знаю, что некоторые из вас работают с абсолютно ужасными ссылками на контент.

    // "id={id}&base64={placeholderImage}"
    @Serializable data class Session(
      val id: String,
      val placeholderImage: Bitmap? = null
    ) : NavKey
    
    // "jsonBlob={user}"
    @Serializable data class Profile(
      val user: User,
    ) : NavKey
  • Например, к ссылке на контент может быть прикреплено изображение в кодировке Base64.

  • Например, гигантский JSON-объект, который они решили добавлять в каждый URL приложения.

  • Теперь у вас есть возможность извлечь нужную информацию, даже если вы не можете изменить URL.

  • Итак, первый метод заключается в расширении нашего класса DeepLinkSerializer, чтобы точно контролировать, как этот аргумент десериализуется или преобразуется из строки в ваше типобезопасное свойство.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      ...
    }
  • DeepLinkSerializer – это вспомогательная оболочка для Kotlin Serialization, которая упрощает реализацию класса сериализатора, поскольку в этом случае входные данные десериализатора всегда представляют собой строку из URL.

    public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
  • Чтобы реализовать DeepLinkSerializer, нужно написать три метода.

  • Сначала выберите название сериала.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      override val serialName: String
        get() = "Bitmap"
    
      ...
    }
  • Пространство имен не требуется, но название должно быть достаточно уникальным, чтобы Kotlin Serialization понимал, когда использовать ваш сериализатор.

  • Затем вам нужно реализовать десериализацию, которая передает эту строку из вашего веб-URL и отвечает за создание вашего типобезопасного объекта.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      override val serialName: String
        get() = "Bitmap"
    
      override fun deserialize(value: String): Bitmap {
        val imageBytes = Base64.decode(value, Base64.DEFAULT)
       // This better be a small image…
        return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
      }
    
       ...
    
    }
  • Наконец, вы реализуете сериализацию, которая является противоположным подходом и используется, если один и тот же класс ключа используется в rememberNavBackStack случаях.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
    
      ...
    
      override fun serialize(value: Bitmap): String {
        val byteArrayOutputStream = ByteArrayOutputStream()
        // Compress the bitmap into the output stream (e.g., JPEG or PNG)
        value.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
        val byteArray = byteArrayOutputStream.toByteArray()
        // Encode the byte array to a Base64 string
        return Base64.encodeToString(byteArray, Base64.DEFAULT)
      }
    }
  • Теперь вы примените свой собственный сериализатор к свойству, у которого есть этот класс, который является немного особенным, а веб-UriDeepLinkMatcher позаботится обо всем остальном.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      ...
    }
    
    @Serializable data class Session(
      val id: String,
      @Serializable(BitmapSerializer::class)
      val placeholderImage: Bitmap? = null
    ) : NavKey

Предварительная обработка аргументов URL с помощью специальных сопоставителей

  • Но что, если вам нужна только часть данных из строки base64 или JSON, прикрепленного к URL?

  • В этом случае может иметь смысл предварительно обработать часть URL-адреса вашего веб-сайта, прежде чем он вообще попадет в сериализацию Kotlin.

  • Чтобы обработать такие случаи, вы можете расширить UriDeepLinkMatcher и переопределить его метод matchArguments.

  • Теперь, возможно, для нашего JSON-объекта все, что нам действительно нужно, это только идентификатор сеанса, как будто нас на самом деле не волнует все остальное.

  • Поэтому мы создаем подкласс и переопределяем matchArguments.

    class SessionUriDeepLinkMatcher(
      uriPattern: DeepLinkUri
    ) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {
    
      override fun matchArguments(
        pathArgs: Map<String, List<String>>,
        queryArgs: Map<String, List<String>>,
        fragmentArgs: Map<String, List<String>>
      ): UriMatchResult<Session>? {
        ...
    }
  • Входные данные – это необработанные строки, все заполнители, добавленные из пути URI.

  • Это позволяет нам предварительно обрабатывать и редактировать эти значения перед вызовом super.matchArguments.

    class SessionUriDeepLinkMatcher(
      uriPattern: DeepLinkUri
    ) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {
    
      override fun matchArguments(
        pathArgs: Map<String, List<String>>,
        queryArgs: Map<String, List<String>>,
        fragmentArgs: Map<String, List<String>>
      ): UriMatchResult<Session>? {
        val finalQueryArgs = buildMap {
          putAll(queryArgs)
          val jsonBlob = remove("session")?.firstOrNull()
          if (jsonBlob != null) { put("session", extractSessionIdFromJson(jsonBlob)) }
        }
        return super.matchArguments(pathArgs, finalQueryArgs, fragmentArgs)
    }
  • Теперь мы можем использовать упрощенные значения и нам не нужно возиться с сериализацией Kotlin.

Создание синтетических обратных стеков для ссылок на контент

  • Спасибо, Ян.

  • Надеемся, что вам не придется часто прибегать к этим методам, но мы уделили много внимания тому, как вы можете управлять поведением сопоставления.

  • Окей.

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

  • Поэтому, чтобы обеспечить правильное поведение при возврате, вам понадобится синтетический обратный стек.

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

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

  • Этот главный экран становится частью синтетического обратного стека.

  • При разработке API для ссылок на контент мы хотели сбалансировать два конкурирующих требования.

  • С одной стороны, мы знаем, что для разных ссылок на контент или сопоставителей может потребоваться разная логика создания синтетического обратного стека.

  • Например, добавление HomeScreen перед каждой ссылкой на контент может работать до тех пор, пока вы не захотите создать ссылку на контент для подэкрана внутри экрана настроек.

  • Теперь вам нужен дополнительный уровень в синтетическом обратном стеке для этой конкретной ссылки на контент.

  • Естественно, это связывает построитель обратного стека с каждым сопоставителем ссылок на контент.

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

  • Поэтому мы не хотели вычислять синтетический обратный стек для каждого результата матча.

  • Мы хотели найти наилучшее совпадение.

  • Итак, если мы хотим использовать собственную логику для каждого сопоставителя, но не хотим создавать синтетический обратный стек для каждого совпадения, что нам делать?

  • После нескольких неудачных попыток мы нашли подход, который позволяет соблюсти оба требования.

  • Независимо от типа сопоставителя, вы можете дополнить его, вызвав withBackStack, чтобы создать обратный стек с заданным MatchResult.

    val homeMatcher = UriDeepLinkMatcher(...).withBackStack { matchResult ->
      listOf(Home, matchResult.key)
    }
    
    val sessionMatcher = UriDeepLinkMatcher(...).withBackStack { matchResult ->
      listOf(Home, SessionList, matchResult.key)
    }
    
    val backStackMatchers: List<BackStackMatcher<NavKey, NavKey>> = listOf(
      homeMatcher, sessionMatcher
    )
    
    val matchResults = backStackMatchers.mapNotNull { matcher.match(request) }
    val bestMatch: BackStackMatchResult<NavKey, NavKey>? = matchResults.maxOrNull()
    
    val backStack = bestMatch?.backStack ?: listOf(Home)
  • Поэтому, если у нас есть несколько сопоставителей, у каждого из них будет свой собственный вызов withBackStack для создания соответствующего обратного стека для этого конкретного NavKey.

  • Чтобы получить наиболее чистый код, используйте withBackStack для каждого вызываемого сопоставителя. Это позволит получить список сопоставителей обратного стека.

  • А когда вы найдете наилучшее совпадение, вы получите результат в виде BackStackMatchResult.

  • Это подкласс MatchResult, который добавляет новое свойство backStack – весь обратный стек, созданный лямбда-функцией withBackStack.

  • Такой многоуровневый подход позволил нам отделить логику внутри сопоставителей от любой пользовательской логики, которую вы хотите использовать внутри лямбда-функции withBackStack.

Блиц-опрос, итоги и ресурсы

  • Это блиц-конференция, поэтому у нас почти не осталось времени. Но мы можем кратко рассмотреть ещё несколько функций, которые есть в Navigation 3 и которые вы найдете в наших рецептах для Navigation 3.

  • Окей.

  • Вы можете добавить фильтры к любому сопоставителю, чтобы сократить логику сопоставления и быстро исключить сопоставитель на основе действия, MIME-типа или собственных фильтров.

  • И это происходит до того, как будет выполнен синтаксический анализ URI или сериализация Kotlin.

  • Мы также предлагаем StaticKeyDeepLinkMatcher, который можно использовать, если вам не нужно выполнять синтаксический анализ для получения NavKey, но вы хотите применить фильтры.

    @Serializable object ImagesKey: NavKey
    
    // custom filter
    val mimeTypeFilter = DeepLinkMatcher.mimeTypeFilter("image/jpg")
    
    val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
  • Это идеально подходит для обработки ссылок на контент, связанных только с действиями или MIME-типами, например для приложения для редактирования фотографий, которое может открывать изображения JPEG.

  • Кстати, о действиях и MIME-типах. К DeepLinkRequest можно прикрепить любой пользовательский RequestExtras, который затем можно использовать в логике пользовательских фильтров и подклассов DeepLinkRequest.

  • Это позволяет использовать фреймворки для A/B-тестирования или внешние сигналы.

  • А ещё у нас есть типобезопасные API для дополнительных данных.

    val extras = requestExtras {
        put(SectionExtrasKey, "A")
    }
  • И вся эта логика находится в общем коде KMP Nav3, поэтому все ваши сопоставители и логика обратного стека могут использоваться на разных платформах.

  • Если вы хотите узнать больше о том, как Navigation 3 поддерживает ссылки на контент, ознакомьтесь с нашими рецептами Nav3.

  • Да, и мы также интегрировали наши API ссылок на контент в наши рецепты DI, а также в наше приложение NowInAndroid.

  • Спасибо за внимание к нашей блиц-конференции и удачной навигации!

  • Спасибо!

Код

Фрагменты кода

Фрагмент кода на Kotlin в видео "Deep link API design and URI pattern matching" (Проектирование API для ссылок на контент и сопоставление шаблонов URI)
val matcher = UriDeepLinkMatcher(
  ...,
  ...
)
Фрагмент кода на Kotlin в видео "Deep link API design and URI pattern matching" (04:53)
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri(...),
  ...
)
Фрагмент кода Kotlin на 05:04 в видео "Разработка API для ссылок на контент и сопоставление шаблонов URI"
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/home"),
  ...
)
Фрагмент кода на Kotlin в видео "Разработка API для ссылок на контент и сопоставление шаблонов URI"
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
  …
)
Фрагмент кода на Kotlin в видео "Deep link API design and URI pattern matching" (05:39)
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
  serializer = serializer<Session>()
)
Фрагмент кода на Kotlin в видео "Deep link API design and URI pattern matching"
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
  serializer = serializer<Session>()
)
Фрагмент кода на Kotlin в видео "Deep link API design and URI pattern matching" (Проектирование API для ссылок на контент и сопоставление шаблонов URI)
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
  serializer = serializer<Session>()
)

val request = DeepLinkRequest(
  uri = DeepLinkUri("http://ads.android.com/session/abcd")
)

val matchResult: UriMatchResult<Session>? = matcher.match(request)

val backStack = listOf(matchResult?.key ?: Home)
Фрагмент кода на Kotlin в видео "Разработка API для ссылок на контент и сопоставление шаблонов URI"
val matchers = listOf(...)

// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
  matcher.match(request)
}
Фрагмент кода на Kotlin в видео "Deep link API design and URI pattern matching" (07:48)
val matchers = listOf(...)

// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
  matcher.match(request)
}

// compare matches
val backStack = listOf(
  matchResults.maxOrNull()?.key ?: Home
)
Фрагмент кода Kotlin на 08:04 в видео "Разработка API для ссылок на контент и сопоставление шаблонов URI"
// Feature A Module
@ContributesTo(ActivityScope::class)
@BindingContainer
object FeatureAModule {
    @IntoSet
    @Provides
    fun provideDeepLinkMatcher() : UriDeepLinkMatcher<NavKey> =
      UriDeepLinkMatcher(
        uriPattern = DeepLinkUri("ads.android.com/featureA"),
        serializer<FeatureBKey>(),
      )
}

// in MainActivity
@Inject lateinit var matchers: Set<UriDeepLinkMatcher<NavKey>>
Фрагмент кода на Kotlin в разделе "Типобезопасная десериализация и пользовательские сериализаторы"
// pattern = "http://ads.android.com/session/{id}"
@Serializable data class Session(val id: Int) : NavKey
Фрагмент кода на Kotlin в видео "Типобезопасная десериализация и пользовательские сериализаторы" (09:27)
// pattern = "http://ads.android.com/session/{id}#referrer={referrer}
@Serializable data class Session(
  val id: Int,
  val referrer: String? = null,
) : NavKey
Фрагмент кода на Kotlin в разделе "Типобезопасная десериализация и пользовательские сериализаторы"
// "id={id}&base64={placeholderImage}"
@Serializable data class Session(
  val id: String,
  val placeholderImage: Bitmap? = null
) : NavKey

// "jsonBlob={user}"
@Serializable data class Profile(
  val user: User,
) : NavKey
Фрагмент кода Kotlin на 10:19 в разделе "Типобезопасная десериализация и пользовательские сериализаторы"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}
Фрагмент кода на Kotlin в разделе "Типобезопасная десериализация и пользовательские сериализаторы"
public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
Фрагмент кода Kotlin на 10:51 в видео "Типобезопасная десериализация и пользовательские сериализаторы"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  override val serialName: String
    get() = "Bitmap"

  ...
}
Фрагмент кода Kotlin на 11:01 в видео "Типобезопасная десериализация и пользовательские сериализаторы"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  override val serialName: String
    get() = "Bitmap"

  override fun deserialize(value: String): Bitmap {
    val imageBytes = Base64.decode(value, Base64.DEFAULT)
   // This better be a small image…
    return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
  }

   ...

}
Фрагмент кода на Kotlin в разделе "Типобезопасная десериализация и пользовательские сериализаторы" (11:13)
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {

  ...

  override fun serialize(value: Bitmap): String {
    val byteArrayOutputStream = ByteArrayOutputStream()
    // Compress the bitmap into the output stream (e.g., JPEG or PNG)
    value.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
    val byteArray = byteArrayOutputStream.toByteArray()
    // Encode the byte array to a Base64 string
    return Base64.encodeToString(byteArray, Base64.DEFAULT)
  }
}
Фрагмент кода Kotlin на 11:24 в разделе "Типобезопасная десериализация и пользовательские сериализаторы"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}

@Serializable data class Session(
  val id: String,
  @Serializable(BitmapSerializer::class)
  val placeholderImage: Bitmap? = null
) : NavKey
Фрагмент кода на Kotlin в разделе "Предварительная обработка аргументов URL с помощью специальных сопоставителей" (12:09)
class SessionUriDeepLinkMatcher(
  uriPattern: DeepLinkUri
) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {

  override fun matchArguments(
    pathArgs: Map<String, List<String>>,
    queryArgs: Map<String, List<String>>,
    fragmentArgs: Map<String, List<String>>
  ): UriMatchResult<Session>? {
    ...
}
Фрагмент кода на Kotlin в разделе "Предварительная обработка аргументов URL с помощью специальных сопоставителей"
class SessionUriDeepLinkMatcher(
  uriPattern: DeepLinkUri
) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {

  override fun matchArguments(
    pathArgs: Map<String, List<String>>,
    queryArgs: Map<String, List<String>>,
    fragmentArgs: Map<String, List<String>>
  ): UriMatchResult<Session>? {
    val finalQueryArgs = buildMap {
      putAll(queryArgs)
      val jsonBlob = remove("session")?.firstOrNull()
      if (jsonBlob != null) { put("session", extractSessionIdFromJson(jsonBlob)) }
    }
    return super.matchArguments(pathArgs, finalQueryArgs, fragmentArgs)
}
Фрагмент кода на Kotlin в видео "Создание синтетических обратных стеков для ссылок на контент"
val homeMatcher = UriDeepLinkMatcher(...).withBackStack { matchResult ->
  listOf(Home, matchResult.key)
}

val sessionMatcher = UriDeepLinkMatcher(...).withBackStack { matchResult ->
  listOf(Home, SessionList, matchResult.key)
}

val backStackMatchers: List<BackStackMatcher<NavKey, NavKey>> = listOf(
  homeMatcher, sessionMatcher
)

val matchResults = backStackMatchers.mapNotNull { matcher.match(request) }
val bestMatch: BackStackMatchResult<NavKey, NavKey>? = matchResults.maxOrNull()

val backStack = bestMatch?.backStack ?: listOf(Home)
Фрагмент кода на Kotlin в разделе "Блиц-опрос, итоги и ресурсы"
@Serializable object ImagesKey: NavKey

// custom filter
val mimeTypeFilter = DeepLinkMatcher.mimeTypeFilter("image/jpg")

val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
Фрагмент кода на Kotlin в видео "Блиц-опрос, итоги и ресурсы"
val extras = requestExtras {
    put(SectionExtrasKey, "A")
}