Русский
Русский
English
Статистика
Реклама

Navigation Component-дзюцу, vol. 4 Переоценка

Спустя два месяца после написания цикла статей Navigation Component-дзюцу я задумался: неужели всё действительно так плохо? Может быть я поддался волне критики гугловых разработок и просто пропустил тревожный звоночек, принявшись исправлять баг за багом, проблему за проблемой с помощью костылей и палок?

Оказалось, во многом так оно и есть: в этой статье-дополнении я хочу рассказать, в чём была проблема, как её исправить и как это поменяло моё мнение о Navigation Component.

Кейс с BottomNavigationView

Первая статья начиналась с примера использования BottomNavigationView в приложении с Navigation Component: я описывал тернистый путь от использования стандартного шаблона Android Studio с нижней навигацией до применения специальной extension-функции из репозитория Navigation Advanced Sample.

Напомни схему тестового приложения

Стандартный шаблон Android Studio с нижней навигацией, который использует Navigation Component, реализует нижнюю навигацию в полном соответствии с гайдлайнами Material Design то есть при переключении между вкладками стек экранов сбрасывается. Чтобы реализовать сохранение состояния вкладок можно воспользоваться специальной extension-функцией, которая под капотом создаёт для каждой вкладки нижней навигации отдельный NavHostFragment. К нему и будет привязан отдельный граф навигации со своим back stack-ом.

Оказалось, что при адаптации этой extension-функции для фрагментов я допустил серьёзную ошибку: использовал не тот FragmentManager. Так как мы строим навигацию внутри фрагмента, а не Activity, мне следовало использовать childFragmentManager, привязанный к фрагменту-контейнеру нижней навигации, а не supportFragmentManager, который был привязан к Activity.

Правильный вариант выглядит так:

Код настройки BottomNavigationView внутри фрагмента
/** * Main fragment -- container for bottom navigation */class MainFragment : Fragment(R.layout.fragment_main) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        if (savedInstanceState == null) {            setupBottomNavigationBar()        }    }    override fun onViewStateRestored(savedInstanceState: Bundle?) {        super.onViewStateRestored(savedInstanceState)        // Now that BottomNavigationBar has restored its instance state        // and its selectedItemId, we can proceed with setting up the        // BottomNavigationBar with Navigation        setupBottomNavigationBar()    }    /**     * Called on first creation and when restoring state.     */    private fun setupBottomNavigationBar() {        val navGraphIds = listOf(            R.navigation.search__nav_graph,            R.navigation.favorites__nav_graph,            R.navigation.responses__nav_graph,            R.navigation.profile__nav_graph        )        // Setup the bottom navigation view with a list of navigation graphs        fragment_main__bottom_navigation.setupWithNavController(            navGraphIds = navGraphIds,            fragmentManager = childFragmentManager, // Самая важная строка            containerId = R.id.fragment_main__nav_host_container,            intent = requireActivity().intent        )    }}

Эта ошибка повлекла за собой описанные мной проблемы: краши в неожиданных местах, костыли для обратной навигации, странную привязку NavController-а.

Как так не заметили, что используете parentFragmentManager?

У меня есть несколько версий:

  • часто меняя код для описания большого примера, я мог не заметить разницы между поведением приложения при использовании supportFragmentManager-а и childFragmentManager-а;

  • мог подвести эмулятор;

  • а может быть, имела место банальная невнимательность при переносе кода с Advanced navigation sample с Activity на фрагменты; в исходном коде примера с Activity по понятным причинам использовался supportFragmentManager.

Как видите, нам больше не нужны никакие Handler.post для фиксов крашей IllegalStateException: FragmentManager already execute transaction. Кроме того, исчезает необходимость привязывать NavController, полученный из extension-а setupWithNavController, ко View нашего фрагмента. Плюс ко всему, у нас нет никаких крашей при сворачивании и разворачивании приложения ура.

С учётом этого фикса кейс с BottomNavigationView делается по щелчку. Из коробки вам может не хватить только одного: иногда приложению требуется запоминать порядок выбора табов нижней навигации и при нажатии на кнопку Back возвращаться не на первый таб, а на предыдущий выбранный.

Навигация из вложенного графа во внешний граф

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

Напомни схему

Речь идёт об этой части схемы:

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

Правильно инициализировав BottomNavigationView, мы избавились от необходимости руками привязывать NavController ко View контейнера с нижней навигацией (в коде это MainFragment). Это было неочевидно, но привело к проблемам, связанным с обратной навигацией во флоу авторизации, когда мы были вынуждены искать там правильный NavController:

В коде StartAuthFragment было вот так
callback = object : OnBackPressedCallback(true) {    override fun handleOnBackPressed() {            Navigation.findNavController(                requireActivity(),                R.id.activity_root__fragment__nav_host            ).popBackStack()    }}.also {            requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, it)}

Теперь мы можем спокойно избавиться от этих переопределений OnBackPressedCallback-ов. Всё стало гораздо проще.

Навигация по условию

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

Покажи на картинке

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

Первый способ заключался в пробрасывании флажка об открытии флоу авторизации со Splash-экрана и дальнейшей обработке этого флага в OnBackPressedCallback-е. Второй способ сводится к модификации текущего графа навигации: мы можем в runtime-е поменять startDestination графа на нужный нам первый фрагмент.

Ещё один вариант реализации навигации по условию
splashViewModel.splashNavCommand.observe(viewLifecycleOwner, Observer { splashNavCommand ->    val navController = Navigation.findNavController(requireActivity(), R.id.activity_root__fragment__nav_host)    val mainGraph = navController.navInflater.inflate(R.navigation.app_nav_graph)    // Way to change the first screen at runtime.    mainGraph.startDestination = when (splashNavCommand) {        SplashNavCommand.NAVIGATE_TO_MAIN -> R.id.MainFragment        SplashNavCommand.NAVIGATE_TO_AUTH -> R.id.auth__nav_graph        null -> throw IllegalArgumentException("Illegal splash navigation command")    }    navController.graph = mainGraph})

Мы по-прежнему выбираем начальный экран в SplashViewModel, но теперь в observer-е перестраиваем граф навигации и устанавливаем его в рутовый NavController, который получаем из Activity.

При таком способе навигации экран Splash-а больше не находится в back stack-е, и нажатие на кнопку Back на первом экране авторизации сразу закроет приложение без необходимости добавлять OnBackPressedCallback, завязанный на аргумент.

Что ещё нужно сделать: поправить способ перехода с последнего экрана флоу авторизации на главный экран. Раньше мы закрывали флоу авторизации с помощью findNavController().popBackStack и пробрасывали результат о пройденной авторизации через SavedStateHandle, чтобы заново открывшийся Splash-экран перевёл нас на главный экран. Теперь можно поступить проще:

Навигация с последнего экрана авторизации
// Navigate back from auth flowval result = findNavController().popBackStack(R.id.auth__nav_graph, true)if (result.not()) {    // we can't open new destination with this action    // --> we opened Auth flow from splash    // --> need to open main graph    findNavController().navigate(R.id.MainFragment)}

Метод popBackStack возвращает true, если стек был извлечён хотя бы один раз и пользователь был перемещён в какой-то другой destination, а false в противном случае. Если граф авторизации был первым открытым destination-ом после Splash-экрана (а так и будет, поскольку мы изменили startDestination), этот метод вернёт нам false.

Убрав из back stack-а все экраны авторизации, мы вернулись в рутовый граф, где в качестве start destination-а выбран именно граф авторизации. При этом, если открыть граф авторизации, например, с главного экрана, вызов popBackStack уже вернёт true, и мы не выполним ещё один переход на главный экран.

Работа с диплинками

С исправленной инициализацией BottomNavigationView при запуске команды на открытие диплинка через ADB больше не происходит никаких крашей это прекрасно. Но никуда не делась особенность со сбросом стека: приложение по-прежнему целиком перезапускается, и нужно придумывать свои собственные способы обработки диплинков.

И как же это повлияло на мнение о Navigation Component

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

  • Нижняя навигация через BottomNavigationView Navigation Component из коробки соответствует гайдам Material design-а (не сохраняется стек при переходе между вкладками), но если вам требуется поведение а-ля iOS (когда стек вкладок должен сохраняться), можно использовать extension-функцию, которая даст нужное поведение.

  • Навигация во вложенные графы и обратно всё работает корректно, навигацию обратно можно реализовать через NavController.popBackStack(R.id.nestednavgraph), никаких костылей.

  • Навигация из вложенного контейнера во внешний (например, из контейнера с нижней навигацией в контейнер без неё) реализуется через поиск правильного NavController-а и не вызывает никаких проблем.

  • Навигацию на старте приложения по условию можно реализовывать разными способами либо через аргументы, либо через модификацию стартового графа в runtime-е. Модификация графа может сильно упростить этот кейс;

  • Навигация между модулями из трёх предложенных способов надёжно работают два: описание графа в app-модуле + навигация через интерфейсы и описание графа в отдельном модуле, который подсоединяется ко всем остальным.

  • Кейс с пробросом результата из вложенного графа, как правильно заметили в комментариях к одной из статей, проще сделать через какую-нибудь реактивную шину или Result API и не использовать никакой SavedStateHandle.

Что может оттолкнуть вас в Navigation Component:

  • Навигация через deep link-и потому что есть особенность со сбросом back stack-а, а это поведение подойдёт не всем приложениям;

  • Зависимость от тулинга и (опционально) кодогенерация пока редактор графа навигации не выделили в отдельный плагин Android Studio, чтобы получить какие-то обновления редактора, нужно ожидать обновления Android Studio + опционально, с помощью gradle-плагинов вы можете сгенерировать много кода, а это может замедлить сборку;

  • Зависимость от платформы Navigation Component крепко завязан на Fragments и их жизненный цикл. Если фрагмент временно находится в уничтоженном состоянии, то буферизацию команд навигации придется реализовывать самостоятельно.

Спасибо @shipa_oблагодаря которому я нашел эту ошибку.

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

Источник: habr.com
К списку статей
Опубликовано: 30.12.2020 10:17:29
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании headhunter

Разработка под android

Android

Navigation component

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru