Эта статья для тех, кто стоит перед выбором реализации собственного решения для обработки deep links в Android приложении или использования, того что предлагает Google в своем Navigation Component.

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

Далее я опишу недостатки, с которыми пришлось столкнуться, и почему я больше не буду использовать deep linking из Navigation Component.

Нет возможности указать host через manifest placeholder

В приложении использовались два типа сборки для staging и production, соответственно для каждого из них нужен разный host, как для API так и для deep linking. Оказалось, что сделать это универсальным способом невозможно, например так:

<fragment
    android:id="@+id/favorites_fragment"
    android:name="com.company.FavoritesFragment">

    <deepLink app:uri="${deepLinkHost}/customer/favorites" /> 

</fragment>

Подходящего решения как это обойти я не нашел, и пришлось указывать один и тот же deepLink дважды:

<fragment
    android:id="@+id/favorites_fragment"
    android:name="com.company.FavoritesFragment">

    <deepLink app:uri="company.com/customer/favorites" /> 
    <deepLink app:uri="staging-company.com/customer/favorites" /> 

</fragment>

Далеко не идеальный вариант, так как приходится дублировать deepLink, легко что-то забыть, и кроме того, на основе этого будут сгенерированы intent фильтры в AndroidManifest.xml для обоих хостов, чего не очень хотелось бы для production сборки, чтобы не увеличивать ее размер и не раскрывать информацию о staging хосте. Issue этой проблемы висит уже несколько лет в трекере Google в статусе Won't fix (Infeasible), в другом issue появилась информация, что они добавили поддержку manifest placeholders для deepLink в AGP 7.3.0-alpha08, и теперь можно сделать так:

  • в build.gradle указать scheme и host:

staging {
    manifestPlaceholders = [scheme: "https", host: "staging-company.com"]
}
prod {
    manifestPlaceholders = [scheme: "https", host: "company.com"]
}
  • и использовать их в теге диплинки:

<deepLink app:uri="${scheme}://${host}/customer/favorites" />

Но только навигация через такой диплинк перестает работать, видимо дает о себе знать alpha версия AGP, но есть надежда, что это все-таки исправят в стабильном релизе.

Необходимо указывать deep links в xml

На первый взгляд мне показалось это удобным, так как deepLink указывается непосредственно там к чему относится. Но на практике нет, если у вас несколько графов, то все диплинки разбросаны по разным xml файлам, и усложняют их и без того непростую структуру, а совместно с отсутствием возможности указать host диплинок через manifest placeholder, фрагменты, к которым привязаны несколько диплинок, стали выглядеть громоздко:

<fragment
    android:id="@+id/video_fragment"
    android:name="com.company.VideoFragment">

    <deepLink android:autoVerify="true" app:uri="company.com/videos/{videoId}" />
    <deepLink android:autoVerify="true" app:uri="company.com/.*/videos/{videoId}" />
    <deepLink android:autoVerify="true" app:uri="company.com/.*/collections/videos/{videoId}" />
    <deepLink android:autoVerify="true" app:uri="company.com/user/videos/{videoId}" />
    <deepLink android:autoVerify="true" app:uri="staging-company.com/videos/{videoId}" />
    <deepLink android:autoVerify="true" app:uri="staging-company.com/.*/videos/{videoId}" />
    <deepLink android:autoVerify="true" app:uri="staging-company.com/.*/collections/videos/{videoId}" />
    <deepLink android:autoVerify="true" app:uri="staging-company.com/user/videos/{videoId}" />
  ...
</fragment>

Нужно добавлять autoVerify каждому deep link отдельно

Выглядит это как в предыдущем примере, нужно указывать каждому deep link android:autoVerify="true" отдельно, если необходимо, чтобы это работало как Android App Links. А раньше можно было настроить один intent-filter для всех диплинок одного хоста и указать autoVerify для них один раз.

Нет приоритетов для deep links с одинаковой структурой

С этой проблемой я столкнулся, когда понадобилось указать deepLink с одинаковой структурой для фрагментов в Bottom Navigation, и в основном графе приложения.

Например, в Bottom Navigation графе был экран профиля пользователя, который мог быть открыт с разными параметрами:

<fragment
    android:id="@+id/profile_fragment"
    android:name="com.company.ProfileFragment">
    
    <deepLink app:uri="company.com/user/.*" />
		...
</fragment>

а в основном графе был экран подписок пользователя:

<fragment
    android:id="@+id/subscriptions_fragment"
    android:name="com.company.SubscriptionsFragment">
    
    <deepLink app:uri="company.com/user/subscriptions" />

</fragment>

В результате, при открытии ссылки https://company.com/user/subscriptions срабатывали оба deepLink. Хотелось бы иметь возможность как-то указывать приоритеты или стратегию разрешения конфликтов в таких случаях.

Рекомендуется использовать только default launchMode

Google настоятельно рекомендует использовать только android:launchMode="standard" , чтобы handleDeepLink() вызывался автоматически, во время установки графа. В противном случае нужно будет делать это вручную, переопределив метод onNewIntent() у активити:

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    navController.handleDeepLink(intent)
}

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

Очистка backstack перед открытием deep link

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

Но для диплинки, которая открывает экран в графе Bottom Navigation это выглядит как баг, потому что при переключение на другую табу, мы можем увидеть тот же самый экран, что перед этим открыла диплинка, так как по умолчанию backstack для этой табы не сохраняет состояние. Есть issue в их трекере в статусе Won't fix (Intended behavior) , то есть и такое поведение они считают намеренным.

Выводы

По моим ощущениям, гораздо проще реализовать свой собственный DeepLinkHandler и иметь больше свободы и гибкости, чем постоянно искать способы обойти недостатки Navigation Component. Реализация deep linking в Navigation Component до сих пор оставляет желать лучшего, и обладает рядом недостатков и особенностей, и скорее подходит только для простых сценариев или небольших приложений.

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

Так как Compose уже наступает на пятки и набирает популярность, то надеюсь они учтут прошлый опыт и доработают для него deep linking и navigation в целом.

Полезные ссылки

  • Документация по deep linking в Navigation Component.

  • Обсуждение на stackoverflow как можно обойти проблему с настройкой разных хостов.

  • Статья от коллег из Head Hunter, в которой описаны некоторые проблемы и пути их решения.

  • Старая (но до сих пор актуальная) статья от коллег из Яндекса, о том с какими проблемами столкнулись при внедрении Jetpack navigation, в том числе рассказывают о проблемах с deep links.

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