Глубокие ссылки все чаще используются в мобильных приложениях.
Это ссылки, которые позволяют не просто зайти в приложение извне, но попасть на конкретный экран.
Android-разработчик из Яндекс.
Еда Владислав Кожушко рассказал, почему мы внедрили навигацию из Jetpack для реализации диплинков, с какими проблемами столкнулись, как их решили и что получилось в итоге.
- Всем привет! Меня зовут Влад. Android-разработкой интересуюсь с 2013 года, в Яндекс.
Еде работаю с лета прошлого года.
Я расскажу вам о нашем пути внедрения библиотеки «Навигационные компоненты» в боевое приложение.
Все началось с того, что у нас появилось техническое задание на рефакторинг навигации.
Потом к нам пришли продакт-менеджеры и сказали, что мы сделаем глубокие ссылки, их будет много, они будут вести на разные экраны.
И тогда мы подумали — навигация, представленная на Google I/O 2018, очень хорошо подходит для реализации задач диплинкинга.
Мы решаем посмотреть, что произойдет. У наших коллег с iOS в Xcode есть удобный графический редактор, в котором они могут с помощью мыши создавать всю компоновку экранов, а также задавать переходы между экранами.
Теперь и у нас есть такая возможность, мы можем с помощью мыши задавать переходы, глубокие ссылки на экраны и привязывать их к фрагментам.
Кроме того, мы можем задать аргументы для экрана.
То есть аргументы необходимо либо ввести в редакторе пользовательского интерфейса, либо записать в XML. Помимо перехода между экранами появился тег действия, в котором мы указываем его id и экран, на который нам нужно перейти.
Также мы указали глубокую ссылку, с помощью которой хотим открыть экран.
В данном случае есть параметр itemId. Если мы передаем параметр такого типа, и он ведет к фрагменту, то значение этого параметра будет передано в аргументы фрагмента, и с помощью ключа itemId мы сможем его получить и использовать.
Библиотека также поддерживает восходящие каналы.
Если мы установим аплинк, например, navdemo.ru/start/{itemId}, то нам больше не нужно будет беспокоиться о регистрации http/https схем для открытия таких глубоких ссылок.
Библиотека все сделает за нас.
Теперь поговорим об аргументах.
Мы добавили два аргумента, целочисленный и логический, а также установили для них значения по умолчанию.
После этого при сборке фрагмента у нас появится класс NextFragmentArgs. В нем есть Builder, с помощью которого можно собирать и задавать нужные нам аргументы.
Также в самом классе NextFragmentArgs есть геттеры для получения наших аргументов.
Вы можете собирать NextFragmentArgs из бандла и конвертировать бандл, что очень удобно.
Об основных возможностях библиотеки.
У него есть удобный редактор пользовательского интерфейса, в котором мы можем осуществлять всю навигацию.
Мы получаем читаемую XML-разметку, которая раздувается так же, как и Views. И у нас нет такой боли, как у iOS-разработчиков, когда они что-то поправили в графическом редакторе, и многое изменилось.
Диплинки могут работать как с фрагментами, так и с Activity, и для этого не нужно прописывать в манифесте большое количество IntentFilters. Мы также поддерживаем восходящие каналы связи, а в Android 6 вы можете включить автопроверку для проверки.
Кроме того, при сборке проектов происходит генерация кода с аргументами нужного экрана.
Навигация поддерживает вложенные графики и вложенную навигацию, что позволяет логически разложить всю навигацию на отдельные подкомпоненты.
Теперь поговорим о нашем пути, который мы прошли при реализации библиотеки.
Все началось с альфа-версии 3. Все реализовали, заменили всю навигацию на компоненты навигации, все отлично, все работает, глубокие ссылки открываются, но появились проблемы.
Первая проблема — IllegalArgumentException. Оно появлялось в двух случаях: несогласованность графа, потому что представление графа и фрагментов в стеке становилось рассинхронизированным, из-за чего и возникало это исключение.
Вторая проблема — двойные клики.
Когда мы сделали первый клик, у нас появилась навигация.
Происходит переход к следующему экрану, и состояние графика меняется.
Когда мы делаем второй щелчок, граф уже находится в новом состоянии и пытается сделать старый переход, которого больше не существует, поэтому мы получаем это исключение.
В этой версии не открывались глубокие ссылки, схема которых содержит точку, например, project.company. В последующих версиях библиотеки это было решено, и в стабильной версии все работает хорошо.
Общие элементы также не поддерживаются.
Вы наверняка видели, как работает Google Play: есть список приложений, вы нажимаете на приложение, у вас открывается экран и происходит красивая анимация перемещения иконки.
У нас в приложении это тоже есть в списке ресторанов, но нам нужна была поддержка общих элементов.
Кроме того, SafeArgs нам не подошли, поэтому мы жили без них.
Исправить глубокую ссылку было легко.
Пришлось заменить схему, которая была в библиотеке, на свою, поддерживающую точку.
С помощью рефлексии стучимся в класс, меняем значение регулярного выражения, и всё работает.
Чтобы исправить двойной щелчок, мы использовали следующий метод. У нас есть функции расширения для назначения кликов на навигацию.
После нажатия кнопки или другого элемента мы обновляем ClickListener и выполняем навигацию, чтобы избежать двойных щелчков.
Или если у вас в проекте есть RxJava, я рекомендую использовать библиотеку RxBindingsgs от Джейка Уортона, и с ее помощью вы сможете обрабатывать события из View в реактивном стиле, используя доступные нам операторы.
Давайте поговорим об общих элементах.
Так как они появились чуть позже, мы решили доработать навигатор и добавить в него навигацию.
Мы программисты, почему бы и нет?
Модификация заключалась в следующем: мы наследуем наш навигатор от навигатора, который есть в библиотеке.
Здесь представлен не весь код, но это основная часть, которую мы выполнили.
Хочу отметить, что перед выполнением навигации проверяется состояние FragmentManager. Если оно сохранилось, то мы теряем свои команды.
На мой взгляд, это недостаток.
Кроме того, когда мы запускаем транзакцию фрагмента, мы создаем транзакцию и указываем все наши представления, которые необходимо обработать.
Но возникает вопрос, что это за класс TransitionDestination? Это наш собственный класс, в котором можно устанавливать представления.
Мы наследуем его от Destination и расширяем функционал.
Мы устанавливаем виды, и наш пункт назначения готов ими поделиться.
Следующая часть — нам нужно сделать навигацию.
При нажатии на кнопку осуществляем поиск по id пункта назначения и вытаскиваем вершину графа, к которой нам нужно перейти.
После этого конвертируем его в наш TransitionDestination, в котором у нас есть Views. Затем мы настраиваем все наши представления для анимации переходов и навигации.
Все работает, все отлично.
Но тут появился альфа06. Это не значит, что мы прыгали между версиями.
Мы старались обновлять библиотеки по мере необходимости, но это, пожалуй, самые основные изменения, с которыми мы столкнулись.
у альфа06 проблемы.
Так как это была альфа-версия библиотеки, то постоянно происходили изменения, связанные с переименованием методов, обратных вызовов, интерфейсов, не говоря уже о добавлении и удалении параметров в методах.
Поскольку мы написали свой навигатор, нам пришлось синхронизировать код навигатора библиотеки с нашим, чтобы также загружать исправления ошибок и новые возможности.
Также в самой библиотеке по мере перехода от ранних альфа-версий к стабильным версиям изменилось поведение и были удалены некоторые функции.
Раньше был флаг launchDocument, но он ни разу не использовался, потом его удалили.
Например, было изменение, в котором разработчики сказали, что метод NavigationUp(), работающий с DrawerLayout, устарел, используйте другой, в котором просто поменялись параметры.
Наш следующий большой переход был на альфа11. Здесь мы исправили основные проблемы с навигацией при работе с графиком.
Наконец мы убрали готовый контроллер и использовали все, что было из коробки.
Безопасные аргументы по-прежнему не работали для нас, и мы были разочарованы.
Потом вышла версия beta01, и в этой версии фактически ничего не изменилось в поведении навигации, но появилась следующая проблема: если в приложении открыто определенное количество экранов, то мы очищаем стек перед открытием нашей диплинка.
Нас такое поведение не устраивало.
Безопасные аргументы все еще не работали.
Мы написали вопрос в Google, на что нам ответили, что всё нормально, так и было задумано изначально, а в самом коде это произошло потому, что перед переходом на диплинкинг был возврат к корневому фрагменту по id графа , лежащий в корне.
А еще в методе setPopUpTo() передается флаг true, который говорит о том, что когда мы вернемся к этому экрану, нам тоже нужно сбросить его из стека.
Мы решили вернуть наш готовый навигатор и исправить то, что считаем неправильным.
Это первоначальная проблема, которая вызывала очистку стека.
Мы решили это следующим образом.
Проверяем, равен ли startDestination нулю, начальному экрану, дальше будем его использовать, в качестве идентификатора берем графический идентификатор.
Если наш startDestination ID не равен нулю, то мы возьмем этот ID из графа, благодаря чему мы не сможем очистить стек и открыть глубокую ссылку на тот контент, который у нас есть.
Или, как вариант, можно просто убрать всплывающее окно true из параметров навигации.
По идее тоже всё должно работать.
И наконец выходит стабильная версия.
Мы радовались и думали, что все хорошо, но в стабильной версии поведение по большому счету не изменилось.
Они только что завершили это дело.
Наконец-то у нас заработали безопасные аргументы, поэтому мы начали активно добавлять аргументы на наши экраны и использовать их повсюду в коде.
Мы также обнаружили, что навигация не работает с DialogFragments. Поскольку у нас были DialogFragments, мы хотели перенести их все в граф, в XML, и описать переходы между ними.
Но у нас это не получилось.
Двойное открытие.
Также у нас возникла проблема, которая преследовала нас с самой первой версии — двойное открытие Activity при холодном старте приложения.
Это происходит следующим образом.
Существует отличный метод handle deeplink, который мы можем вызвать из нашего кода, например, когда получаем onNewIntent() в действии, чтобы перехватить deeplink. Здесь в Интент приходит флаг ACTIVITY_NEW_TASK при запуске приложения по диплинку, поэтому происходит следующее: запускается новый Activity, и если есть текущий Activity, он убивается.
Итак, если мы запускаем приложение, то сначала запускается белый экран, потом он пропадает, появляется другой экран, и они выглядят очень красиво.
В результате, внедрив данную библиотеку, мы получили следующие преимущества.
У нас есть документация по нашей навигации, а также графическое представление переходов между экранами, и если человек приходит к нам за проектом, он быстро это понимает, посмотрев на график и открыв его представление в Студии.
У нас есть SingleActivity. Все экраны сделаны на фрагментах, все глубокие ссылки ведут на фрагменты, и я считаю, что это удобно.
Оказалось, что это простой способ связать диплинк с фрагментами, мы просто добавляем тег deeplink к фрагменту, и библиотека все делает за нас.
Мы также разделили нашу навигацию на вложенные подграфы и создали вложенную навигацию.
Это разные вещи.
Просто вложенный граф — это по сути включение внутрь графа, а вложенная навигация — это когда для обхода экранов используется отдельный навигатор.
Также мы динамически меняем граф в коде, можем добавлять вершины, можем удалять вершины, можем менять стартовый экран — всё работает. Мы почти забыли, как работать с FragmentManager, так как вся логика работы с ним инкапсулирована внутри библиотеки, и всю магию библиотека делает за нас.
Библиотека также работает с DrawerLayout, если у вас указан корневой фрагмент, то библиотека сама нарисует на нем гамбургер, а при переходе на следующие экраны будет рисовать стрелку и делать это анимировано при возврате с предпоследнего фрагмента.
Мы также переместили все аргументы, большую часть из них, в SafeArgs, и все создается при сборке проекта.
Кроме того, мы разобрались со всеми мучившими нас проблемами и модифицировали библиотеку под свои нужды.
SafeArgs может генерировать код на Kotlin; для этого используется отдельный плагин.
Кроме того, у библиотеки есть недостатки.
Во-первых, в стабильную версию была добавлена очистка стека.
Не знаю, зачем это сделано, может кому-то это удобно, но в случае с нашим приложением нам хотелось бы открывать глубокие ссылки поверх того контента, который у нас есть.
Сами фрагменты создаются навигатором фрагментов и создаются посредством отражения.
Не думаю, что это плюс в реализации.
Условие для глубоких ссылок не поддерживается.
В вашем приложении могут быть секретные экраны, доступные только авторизованным пользователям.
А чтобы открывать диплинки по условию, нам нужно для этого написать костыли, так как невозможно указать обработчик диплинка, в котором бы мы получали управление и указывали, что нам делать, либо открыть экран по диплинку, или откройте другой экран.
Также теряются команды для навигации, все потому, что состояние фрагмента навигатора проверяется, сохранен он или нет, а если сохранен, то просто ничего не делаем.
Еще пришлось дорабатывать библиотеку напильником, это не плюс.
И еще один существенный недостаток — у нас нет возможности открыть цепочку экранов перед диплинком.
В случае с нашим приложением мы хотели бы сделать глубокую ссылку на корзину, перед которой бы открывались все предыдущие экраны: рестораны, конкретный ресторан и только после этого корзина.
Также для работы с навигацией необходим экземпляр View. Чтобы получить навигатор, нужно обратиться к Представлению — библиотека навигации сама свяжется с родителями этого Представления — и попытаться найти в нем навигатор.
Если она его находит, то переходит на нужный нам экран.
Но возникает вопрос: стоит ли использовать библиотеку в бою? Я скажу да, если:
Если нам нужно получить быстрые результаты.
Библиотека реализуется очень быстро, вставить ее в проект можно минут за 20, все переходы можно нажимать мышкой, все удобно.
Он также быстро пишется на XML, быстро собирается и быстро работает. Все отлично.
Если вам нужно иметь в приложении много экранов, которые работают с глубокими ссылками, и нет условий, при которых они должны открываться.
Никакого единого вида деятельности.
Здесь я имею в виду не только одиночную активность.
Мы можем ориентироваться как по фрагментам с одним Activity, так и по смеси фрагментов и Activity; просто график также может описывать Активность.
Для этого мы используем провайдер внутри навигатора, который имеет две реализации навигации.
Один предназначен для действия, которое создает намерения для перехода к необходимым действиям, вторая реализация — это навигатор фрагментов, который работает внутри с менеджером фрагментов.
Если только у вас нет сложной логики для навигации между экранами.
Каждое приложение по-своему уникально, и если есть сложная логика открытия экранов, эта библиотека не для вас.
Готовы ли вы зайти в исходный код и изменить его, как это делаем мы.
На самом деле это очень интересно.
Я скажу нет, если в приложении сложная логика навигации, если вам нужно открыть цепочку экранов перед диплинком, если вам нужны сложные условия открытия экранов по диплинку.
В принципе, на примере авторизации это можно сделать; просто открывается нужный вам экран, а поверх него экран авторизации.
Если пользователь не авторизован, то происходит сброс в стек на предыдущий экран, перед которым был открыт секретный экран.
Это такое костыльное решение.
Потеря команд имеет для вас решающее значение.
Тот же Цицерон умеет сохранять команды в буфер, а когда навигатор становится доступным, выполняет их.
Если у вас нет желания доделывать код, это не значит, что вы как разработчик не хотите что-то делать.
Допустим, у вас есть сроки, менеджеры по продукту говорят вам, что нужно урезать фичи, бизнес хочет выкатить фичи.
Вам нужно готовое решение, которое работает «из коробки».
Компоненты навигации не об этом случае.
Еще одна важная вещь заключается в том, что DialogFragments не поддерживаются.
Можно было бы добавить навигатор, который бы с ними работал, но их почему-то не добавили.
Проблема в том, что сами они открываются как обычные фрагменты.
Флажок isShowing не установлен и, соответственно, жизненный цикл DialogFragments для создания диалога не выполняется.
Фактически DialogFragment открывается как обычный фрагмент, весь экран без создания окна.
Это все, что у меня есть.
Покопайтесь в источниках, это действительно интересно и увлекательно.
Здесь полезные материалы для тех, кому интересна работа с навигационной библиотекой.
Спасибо за внимание.
Теги: #Разработка Android #Разработка мобильных приложений #интерфейсы #Google API #активность #Яндекс.
еда #jetpack #навигационный компонент #глубинные ссылки #общие элементы
-
Грибы
19 Oct, 24 -
Отличный Сервис Для Скачивания Файлов
19 Oct, 24 -
Пользователи Живого Журнала Сопоставлены
19 Oct, 24