Сюда дошли лишь самые стойкие… Для тех кто впервые:

HTTP/1.1 303 See other 

Location_1: https://habr.com/ru/post/686990/

Location_2: https://habr.com/ru/post/691220/

Location_3: https://habr.com/ru/post/693368/

Location_4: https://habr.com/ru/post/698094/ 

Мы уже поняли, что deep link это механизмы для навигации в Android ОС. Его можно использовать как внутри одного приложения, так и между несколькими. Всей этой навигацией заправляет Android ОС через intent resolution. Нам (разработчикам) лишь остается закинуть удочку (настроить intent-filter) и ждать поклевки (наших пользователей :)). На исход этой рыбалки может повлиять Android 8.0, предустановленные вместе с оболочкой приложения, разработчики приложений (из которых открывается ссылка), Android 12.+ (если не настроил App Link) да и мы сами. Как еще можно навредить себе?

Содержание

  1. Нюансы. Ссылки без port

  2. Нюансы. Mime type

  3. Нюансы. Ссылки c параметризованным path

  4. Нюансы. Обработка сложных path на примере Jetpack Navigation

  5. Диспетчеризация URL

  6. MyUrlDispatcher

  7. Обратная совместимость Deep Link

  8. Резюме

Нюансы. Ссылки без port

Ранее мы писали, что в data теге можно прописать атрибут port. Давайте пропишем его манифесте

Пример data тега с явно указанным портом 80
Пример data тега с явно указанным портом 80

Теперь попробуем покликать на ссылки:

Для явно указанного порта 80 deep link отрабатывает только по ссылке с 80 портом. Остальные редиректит в браузер.
Для явно указанного порта 80 deep link отрабатывает только по ссылке с 80 портом. Остальные редиректит в браузер.

Теперь удалим port из data тега в intent-filter

Пример data тега без явно указаного порта
Пример data тега без явно указаного порта

Как думаете, что получится? В прошлой статье мы разбирали пример, когда приложение начало перехватывать переходы по всем ссылкам (с одинаковыми scheme, host). Это произошло из-за того, что мы не указали path. Посмотрим, не подвела ли нас логика:

При отсутствии явно указанного port через фильтр проходят всевозможные значения
При отсутствии явно указанного port через фильтр проходят всевозможные значения

Не подвела! 

Отсутствие явно указанного port означает, что может быть использовано любое значение.

Нюансы. Mime type

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

Пример data тега с атрибутом mimeType
Пример data тега с атрибутом mimeType

Компилируем, устанавливаем, открываем приложение…

При нажатии на изображение мы попадаем в браузер
При нажатии на изображение мы попадаем в браузер

Видно, что после нажатия на ссылку, система перенаправила нас в браузер, а не в наше приложение. Возможно, это связано с тем, что для intent-filter ссылка это строка. А по строке ней нельзя точно определить mime type.

Для тех, кому все-таки хочется открыть ссылку из примера выше

Можно схитрить и указать путь к изображению в path:

Содержимое data тега без указания mimeType
Содержимое data тега без указания mimeType
Настроенный data тег позволил перехватить переход по ссылке.
Настроенный data тег позволил перехватить переход по ссылке.

Нюансы. Ссылки c параметризованным path

Мы хотели обработать вполне стандартный кейс: получить идентификатор из ссылки на детальную информацию о сущности (например, из http://example.com/user/1 достать 1). 

Для этого можно настроить intent-filter одним из представленных способов. В этом случае pathPattern и pathPrefix имеют равносильный эффект.

<!-- Способ первый: pathPattern с произвольным содержимым после /about/ -->
<data
   android:host="example.com"
   android:pathPattern="/user/.*"
   android:scheme="http"/>
<!-- Способ второй: pathPrefix на фиксированную часть URL, а дальше может быть что угодно -->
<data
   android:host="example.com"
   android:pathPrefix="/about/"
   android:scheme="http"/>

Что мы получили?

Пример работы настроенного pathPattern/pathPrefix для обработки id в URL.
Пример работы настроенного pathPattern/pathPrefix для обработки id в URL.
  • Ссылки с валидными id (http://example.com/user/1, http://example.com/user/2…) начали обрабатываться 

  • Ссылки с невалидным id (http://example.com/user/1ab2) начали перехватываться приложением

  • Ссылки без id (http://example.com/user/) также начали попадать в приложение

Почему так случилось? Атрибут pathPrefix выполняет мэтчинг (как следует из названия) лишь префикса URL. Такой ограниченный функционал не позволяет правильно обработать idшник (динамическую части path). Из-за этого появляются описанные выше проблемы.

C android:pathPattern дела обстоят получше. В нем можно использовать . и *. Работают они точно также, как и в регулярных выражениях. К сожалению, этими двумя символами разработчики Android и ограничились.

Почему pathPattern поддерживает так мало специальных символов?

Возможно, это сделано для того, чтобы ускорить intent resolution. Представьте, что у вас 50 приложений и в каждом настроено по 5 intent-filter и вот вы переходите по ссылке… И Android начинает проходиться по 250 intent-filter. А теперь представьте, что в каждом написан pathPattern с такой здоровенной регуляркой… Поиск может затянуться…

Но даже с . и * можно обыграть некоторые сценарии:

  • URL с фиксированным количеством символов. Например, pathPattern=”/user/..” обраработает http://example.com/user/11 (положительно), http://example.com/user/1 (отрицательно), http://example.com/user/11а (отрицательно), http://example.com/user/1a (ложноположительно)

  • URL изменяющейся частью в середине path. Например, pathPattern=”/user/.*/books” обработает http://example.com/user/11/books (положительно), http://example.com/user/blabla/books (ложноположительно), http://example.com/user/books (отрицательно)

Нюансы. Обработка сложных path на примере Jetpack Navigation

Неужели ситуация настолько безнадежна? Неужели нет библиотеки, которая сможет все это красиво обработать? Есть! Jetpack Navigation Component может обработать типизированные аргументы в path и query параметры!

Пример обработки типизированной части в URL.
Пример обработки типизированной части в URL.

Это глава не называлась бы “нюансы” если бы все вот так просто закончилось :) Давайте откроем смердженный манифест: 

Merged Manifest из примера выше.
Merged Manifest из примера выше.

Кто-нибудь видит типизированные аргумент? :) Мы нет. К сожалению, чуда не случилось. В intent-filter были проставлены старые-добрые data теги со стандартными атрибутами.

Тогда как Navigation Component их обрабатывает? Дело в том, что обработка URL (или его диспетчеризация) проводится в 2 этапа.

Диспетчеризация URL

На первом (внешнем) уровне операционная система выполняет фильтрацию согласно настройкам из intent-filter приложений. Именно здесь происходит intent-resolution. И именно для этого мы настраивали intent-filter в своих приложениях. 

Диспетчеризация URL на уровне Android ОС (внешняя).
Диспетчеризация URL на уровне Android ОС (внешняя).

Далее URL передается в выбранное на внешнем уровне приложение-обработчик. И именно в приложении начинается внутренняя диспетчеризация. Тут мы решаем что делать: открыть экран, залогировать intent и прочее.

Диспетчеризация URL на уровне приложения (внутренняя).
Диспетчеризация URL на уровне приложения (внутренняя).

URL dispatcher на уровне Android ОС не может быть изменен разработчиком приложения (если вы не разработчик Android :)). Поэтому здесь приходится довольствоваться тем, что есть. Что касается внутреннего URL dispatcher, то здесь есть пространство для маневров: 

  • Использовать Jetpack Navigation Component. Весь тот крутой функционал с типизацией и прочим работает именно на уровне приложения, а не ОС

  • Использовать DeepLinkDispatch от airbnb

  • Использовать другие библиотеки…

  • Написать собственный диспетчер

О чем нужно помнить при внутренней диспетчеризации? 

  • Он должен правильно обрабатывать нужны URLы

  • Должен иметь обработчик для внештатных ситуаций: для отображения экрана с пояснением, например

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

MyUrlDispatcher

Мы решили попробовать самостоятельно обработать deep link. Начали с внешней диспетчеризации:

Пример настроенных data тегов для того, чтобы наше приложение было видно внешнему URL dispatcher.
Пример настроенных data тегов для того, чтобы наше приложение было видно внешнему URL dispatcher.

Мы настроили intent-filter, указали все схемы (1), домены (2). Установили нужны path с учетом слешей (3). Внешняя диспетчеризация настроена!

Теперь ко внутренней. Для начала надо перехватить исходный intent. Перехватили интенты в методах (4) onCreate, onNewIntent:

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

Сначала в catchAndWrapIntent мы выполняем классификацию (5) intent по информации из него (category, action, data…). Именно здесь у нас появляется возможность использовать всю мощь регулярных выражений, которой нам так не хватало в pathPattern! Каждому классу intent соответствует специальная обертка (6)

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

Осталось выполнить обработку. Каждому классу-оболочке соответствует действие. В большинстве случаев это навигация на какой-нибудь экран (7). Обратите внимание на (8). Intent оболочки были специально сделаны для того, чтобы упростить извлечение полезных данных из intent.

По intent оболочке определяем следующие действия. При необходимости достаем из исходного intent полезные данные.
По intent оболочке определяем следующие действия. При необходимости достаем из исходного intent полезные данные.

Вот такая диспетчеризация получилась!

Обратная совместимость Deep Link

Еще одно важное замечание. Если пользователи в одной из версий приложения получили возможность перехода по deep link, то эту возможность необходимо поддерживать в следующих обновлениях. 

Очень важно следить за изменением intent-filter, а именно, scheme, host, path, port, mimeType. Простой пример:

Изменение path c /about на /docs в новой версии приложения.
Изменение path c /about на /docs в новой версии приложения.
/about больше не может быть открыта в приложении. А теперь представьте, если это была ссылка на какие-нибудь платежи…
/about больше не может быть открыта в приложении. А теперь представьте, если это была ссылка на какие-нибудь платежи…

Изменения значений scheme, host, path, port, mimeType могут привести к потере обратной совместимости с уже используемыми deep link.

Как можно избежать проблем с обратной совместимостью deep link?

  • Дважды подумать при изменении data. Оно вам действительно надо? 

  • При появлении новых ссылок, можно разрулить их обработку не на уровне ОС, а на уровне приложения.

  • Создать специальный UI для deep link, которые больше не поддерживаются, где объяснить пользователю ситуацию и порекомендовать пути решения.

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

Резюме

Давайте подведем итоги всего цикла статей.

Метафора связи между deep link, web link, app link.
Метафора связи между deep link, web link, app link.

Deep link — это база. Все остальные виды ссылок наследуют ее недостатки и преимущества. Web Link пытается отобрать трафик у web. App Link ориентирован на UX.

Основная задача Deep Link — это навигация. 

Для того, чтобы эффективно выполнить эту задачу необходимо помнить, что пользователю нет никакого дела до того, что написано в URL. Он видит ключевые слова и ему этого достаточно для перехода по ссылке. Поэтому учитывайте все схемы, все домены, все порты. Старайтесь максимально покрывать pathы, обязательно учитывая комбинации со слешами

Чем больше действий нужно сделать, тем меньше людей это сделает.

Большое количество кликов сужает воронку и снижает конверсию. Чтобы сократить количество кликов при переходе по Deep Link, рекомендуем обратить внимание на App Link.

На Android 12.0 web трафик (http, https) нельзя перехватить приложением с настроенными Deep Link. App Link способен обойти это ограничение.

Тут все понятно. Следим за развитием технологий и не забываем адаптироваться.

Зависимость и влияние четырех факторов на обработку deep link.
Зависимость и влияние четырех факторов на обработку deep link.

На обработку deep link не влияет:

  • Аппаратная часть. В устройстве нет специального чипа, который выполняет обработку deep link. Deep link реализован на уровне ПО.

  • Версия Android OS. С учетом примечания к Android 8.0. Напоминаем, там нужно было выбрать сначала браузер, а потом уже наше приложение.

  • Оболочка ОС. Если мы считаем, что оболочка — это, в том числе, и приложения, которые в ней предустановлены, то оболочка влияет на работу Deep Link. Но все-таки по итогу она влияет ЧЕРЕЗ ПРИЛОЖЕНИЯ.

На обработку deep link влияет:

  • Приложение, из которого открывается ссылка! Поэтому

Было бы хорошо знать, где и на чем будут распространятся ваши ссылки (приложение, оболочка, версия ОС). Эти знания помогут приоритизировать тестирование.

Способы обработки scheme
Способы обработки scheme

Существуют разные способы обработки схем и URL:

  • Linkify — специализированный API для решения этой задачи;

  • HTML удобен, когда в приложении есть размеченные с его помощью данные;

  • Span может быть использован при наличии прочих элементов этой разметки.

Сравнение URI форматов исходной спецификации и Android
Сравнение URI форматов исходной спецификации и Android

URI формат deep link в Android имеет свои особенности, некоторых атрибутов в нем нет в явном виде (по крайнер мере они не доступны для настройки в intent-filter). scheme, host обязательные. port и path опциональные (при их отсутствии может быть использовано любое значение).

Не существуют удобного и гибкого инструмента для фильтрации сложных path на уровне ОС. 

Поэтому пытаемся выжимать максимум из pathPrefix и pathPattern. И помним, что дальше придется обрабатывать неудобные URL.

Диспетчеризация deep link URL
Диспетчеризация deep link URL

Эту неудобную обработку придется выполнит в URL dispatcher на уровне вашего приложения. Можете использовать существующие решения или создать свое. Главное помнить, что диспетчер должен уметь обрабатывать правильные URL, вести себя предсказуемо с кривыми URL и по возможности покрыть как можно больше видов URL.

Иногда (очень редко:)) web удобнее mobile. Не нужно пренебрегать пользователем в  пользу повышения трафика.

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

Валера Петров

Android-разработчик. @valeryvpetrov

Ангелина Евсикова

Android-разработчица Технократии. @Angelina_dev


Также подписывайтесь на наш телеграм-канал «Голос Технократии». Каждое утро мы публикуем новостной дайджест из мира ИТ, а по вечерам делимся интересными и полезными мастридами.

Комментарии (1)


  1. beeline09
    25.11.2022 11:32

    Спасибо за цикл статей! Очень информативно, последовательно и вовремя вышел.