- Navigation Component-дзюцу, vol. 1 — BottomNavigationView
- Disclaimer
- Кейсы с BottomNavigationView
- Первый опыт
- Пора запускать приложение
- У нас есть workaround
- Адаптация workaround-а для фрагментов
- Выводы по работе с BottomNavigationView
- Навигация для Android с использованием Navigation Architecture Component: пошаговое руководство
- Часть 1. Подготовительные работы
- 1.1. Создание проекта
- 1.2. Зависимости (Dependencies)
- 1.3. Страницы: создание фрагментов
- 1.4. Адреса: файл ресурсов типа «Navigation»
- 1.5. Фрейм: виджет NavHostFragment
- Часть 2. Элементы навигации
- 2.1. Навигация с помощью кнопок
- 2.2. Боковое меню (drawer)
- 2.2.1. Ресурсный файл меню
- 2.2.2. Шаблон DrawerLayout и виджет NavigationView в макете активности
- 2.2.3. Подключение бокового меню в классе активности
- 2.3. Кнопка и название фрагмента на панели инструментов
- 2.4. Нижнее меню (Bottom Navigation)
- 2.5. Всплывающее меню (Overflow Menu)
- Краткие выводы и ссылка на github
Navigation Component-дзюцу, vol. 1 — BottomNavigationView
Два года назад на Google I/O Android-разработчикам представили новое решение для навигации в приложениях — библиотеку Jetpack Navigation Component . Про маленькие приложения уже было сказано достаточно, а вот о том, с какими проблемами можно столкнуться при переводе большого приложения на Navigation Component , информации практически нет.
В этой и следующих двух статьях я расскажу о кейсах, с которыми может встретиться разработчик, желающий опробовать Navigation Component в большом Android-приложении.
UPDATE 30.12.2020:
Выводы, сделанные в этой статье, некорректны, так как при настройке BottomNavigationView была допущена серьёзная ошибка, которая повлекла за собой все описанные в статье проблемы. Подробности — в последней статье цикла.
Это текстовая версия моего выступления в рамках серии митапов по Android 11 в Android Academy. Само выступление было на английском, статью пишу на русском. Кому удобнее смотреть – велкам.
В первой статье я расскажу о кейсах, связанных с BottomNavigationView , во второй – о кейсах с вложенными графами навигации, в третьей – про навигацию в многомодульных приложениях, дип линки, встраиваемые фрагменты и диалоги. Все три статьи — лонгриды, которые, однако, способны сэкономить много времени и вам, и вашей команде.
Disclaimer
Я сделал пример, по структуре навигации повторяющий основные моменты навигации соискательского приложения hh.ru, и выхватил ряд проблем, о которых и собираюсь рассказать. Я основательно поресёрчил практическую сторону вопроса, но, разумеется, рассмотрел далеко не все возможные кейсы.
Схема моего тестового приложения выглядит так:
В цикле статей мы разберём каждый переход, который описан на этой схеме, а также несколько кейсов, которые не поместились на картинку.
Кейсы с BottomNavigationView
Когда я только-только услышал про Navigation Component , мне стало интересно: как будет работать BottomNavigationView и как Google подружит несколько отдельных back stack-ов в разных вкладках. Два года назад с этим кейсом были некоторые проблемы, и я решил проверить, как там обстоят дела сегодня.
Если кратко — проблемы не пропали, но появился способ их обойти. И, поскольку нижняя навигация сейчас есть практически в каждом большом приложении, нужно разбираться.
Первый опыт
Я установил Android Studio 4.1 Beta (последнюю более-менее стабильную версию на тот момент) и попробовал шаблон приложения с нижней навигацией. Начало было многообещающим.
- Мне сгенерировали Activity в качестве контейнера для хоста навигации и нижней навигации
Я убрал «шумовые» атрибуты, чтобы было проще читать.
Стандартный ConstraintLayout , в который добавили BottomNavigationView и тэг для инициализации NavHostFragment -а ( Android Studio , кстати, подсвечивает, что вместо фрагмента лучше использовать FragmentContainerView ).
- Для каждой вкладки BottomNavigationView был создан отдельный фрагмент
Все фрагменты были добавлены в качестве отдельных destination-ов в общий граф навигации.
- А ещё в проект был добавлен файл-ресурс для описания меню BottomNavigationView
При этом я заметил, что идентификаторы элементов меню должны совпадать с идентификаторами destination-ов в графе навигации. Не самая очевидная связь между табами BottomNavigationView и фрагментами, но работаем с тем, что есть.
Пора запускать приложение
После создания приложения из шаблона я запустил его и сразу столкнулся с двумя проблемами.
Первая проблема: при переключении между вкладками их состояние не сохранялось.
Для проверки я добавил во вкладку Dashboard простенькую ViewModel со счётчиком. На гифке видно, как я переключаюсь со вкладки Home на вкладку Dashboard , увеличиваю счётчик до четырёх. После этого я переключился обратно на вкладку Home и вновь вернулся на Dashboard . Счётчик сбросился.
Баг с описанием этой проблемы уже два года висит в Issue Tracker-е. Чтобы решить её, Google-у потребовалось серьёзно переработать внутренности фреймворка Fragment -ов, чтобы поддержать возможность работать с несколькими back stack-ами одному FragmentManager -у. Недавно на Medium вышла статья Ian Lake, в которой он рассказывает, что Google серьёзно продвинулись в этом вопросе, так что, возможно, фикс проблемы с BottomNavigationView не за горами.
Вторая проблема – следствие первой. По принятым принципам навигации, когда вы нажимаете на уже выбранную вкладку BottomNavigationView, вы должны вернуться на первый фрагмент в стеке текущей вкладки. Но когда это происходит, состояние этого первого фрагмента сбрасывается, так как сам фрагмент пересоздаётся.
Для демонстрации этой проблемы я добавил на вкладку Dashboard кнопку, которая ведёт на следующий экран. На гифке видно, как я переключаюсь на вкладку Dashboard , увеличиваю счётчик до трёх, а затем перехожу на экран Graphic . Если я нажимаю на кнопку Back – то всё работает как надо, состояние вкладки не сбрасывается. Но если, находясь на экране Graphic , я ещё раз нажму на вкладку Dashboard , то после возврата на первый экран в стеке увижу, что его состояние сброшено.
«Не самое лучшее первое впечатление», – подумал я. И начал искать фикс.
У нас есть workaround
Решение этих проблем живёт в специальном репозитории Google-а с примерами работы с Architecture Components , в проекте NavigationAdvancedSample.
Большая часть фикса расположена в файле NavigationExtensions.kt. В самом проекте довольно много кода, поэтому я не буду его разбирать подробно, а вместо этого подсвечу основные моменты, которые относятся к решению проблем.
- Во-первых, для каждой вкладки вводится отдельный, независимый граф навигации
Соответственно, для примера BottomNavigationView с тремя вкладками у нас получится три отдельных файла навигации XML, в которых в качестве startDestination будут указаны первые фрагменты вкладок.
- Во-вторых, для каждой вкладки под капотом создаётся отдельный NavHostFragment , который будет связан с графом навигации этой вкладки
FragmentManager пока что не поддерживает работу с множеством back stack-ов одновременно, поэтому пришлось придумать альтернативное решение, которое позволило ассоциировать с каждым графом свой back stack. Им стало создание отдельного NavHostFragment -а для каждого графа. Из этого следует, что с каждой вкладкой BottomNavigationView у нас будет связан отдельный NavController .
- В-третьих, мы устанавливаем в BottomNavigationView специальный listener , который будет заниматься переключением между back stack-ами фрагментов
В прикреплённом кусочке кода мы видим, как при переключении между вкладками BottomNavigationView выполняется специальная транзакция в FragmentManager -е, которая прикрепляет фрагмент выбранной вкладки и отцепляет все остальные фрагменты. По сути, так мы и переключаемся между различными back stack-ами.
- В итоге метод настройки BottomNavigationView возвращает разработчику специальную LiveData , которая содержит в себе NavController выбранной вкладки. Этот NavController можно использовать, например, для обновления надписи на ActionBar -е
Метод для настройки BottomNavigationView вызывают в onCreate -е, когда Activity создаётся в первый раз, затем в методе onRestoreInstanceState , когда Activity пересоздаётся с помощью сохранённого состояния.
Кроме того, для работы фикса нужно, чтобы идентификаторы элементов меню, которое используется для инициализации табов BottomNavigationView , совпадали с идентификаторами графов навигации.
Опять же, не самая очевидная связь между этими элементами, зато работает.
После применения этого workaround-а первые две проблемы исчезли – теперь состояние вкладки сохраняется между переключениями вкладок.
Первая проблема решилась:
Адаптация workaround-а для фрагментов
Очень здорово, что workaround помог решить основную проблему с сохранением состояний, но этих фиксов мне было недостаточно. Пример из проекта NavigationAdvancedSample использовал в качестве контейнера нижней навигации Activity, мне же нужен был фрагмент.
Посмотрите внимательно на эту схему:
На ней можно увидеть, что пользователь начинает свой путь в приложении со Splash-экрана:
Google говорит, что Splash-экраны – зло, ухудшающее UX приложения. Тем не менее, Splash-экраны – суровая реальность большинства крупных Android-приложений. И если мы хотим использовать в нашем приложении Single Activity -архитектуру, то в качестве контейнера нижней навигации придётся использовать Fragment , а не Activity :
Я добавил вёрстку для фрагмента с нижней навигацией и перенёс настройку BottomNavigationView во фрагмент:
Я добавил в свой пример Splash-экран и дополнительную вкладку для BottomNavigationView . А чтобы пример стал ещё более походить на приложение для соискателей hh.ru, я также убрал из него ActionBar .
Для этого я поменял тему приложения с Theme.MaterialComponents.DayNight.DarkActionBar на Theme.MaterialComponents.DayNight.NoActionBar и убрал код для связки NavController -а с ActionBar -ом:
После всех манипуляций я включил режим Don’t keep activities, запустил свой пример и… получил краш при сворачивании приложения.
На гифке видно, как я запустил приложение, и после Splash-экрана показывается экран с нижней навигацией. После этого мы сворачиваем приложение и получаем краш.
В чём была причина? При вызове onDestroyView активный NavHostFragment пытается отвязаться от NavController -а. Так как мой фрагмент-контейнер с нижней навигацией никак не привязывал к себе NavController , который он получил из LiveData , метод Navigation.findNavController из onDestroyView крашил приложение.
Добавляем привязку NavController -а к фрагменту с нижней навигацией (для этого в Navigation Component-е есть утилитный метод Navigation.setViewNavController ), и проблема исчезает.
Но это ещё не всё. Не выключая режим Don’t keep activities, я попробовал свернуть, а затем развернуть приложение. Оно снова упало, но с другим неприятным исключением – IllegalStateException в FragmentManager – FragmentManager already executing transactions .
На гифке видно, как мы сворачиваем приложение, находясь на экране с нижней навигацией, затем пытаемся развернуть приложение обратно и получаем краш.
Краш происходит в методах, которые прикрепляют NavHostFragment к FragmentManager -у после их создания. Это исключение можно исправить при помощи костыля: обернуть методы attach-detach в Handler.post <> .
После добавления Handler.post приложение заработало, как надо.
Выводы по работе с BottomNavigationView
На этом с BottomNavigationView всё, на следующей неделе расскажу про кейсы с вложенными графами навигации.
Источник
Навигация для Android с использованием Navigation Architecture Component: пошаговое руководство
Попробуем, пользуясь официальным руководством и примерами кода, построить работающую систему навигации для будущего многоэкранного приложения в соответствии со стандартами Navigation Architecture Component. Статья включает следующие разделы:
Часть 1. Подготовительные работы
— 1.1. Создание проекта
— 1.2. Зависимости (Dependencies)
— 1.3. Страницы: создание фрагментов
— 1.4. Адреса: файл ресурсов типа «Navigation»
— 1.5. Фрейм: виджет NavHostFragment
Часть 2. Элементы навигации
— 2.1. Навигация с помощью кнопок
— 2.2. Боковое меню (Drawer)
— 2.3. Панель инструментов: Toolbar вместо ActionBar
— 2.4. Нижнее меню (Bottom Navigation)
— 2.5. Всплывающее меню (Overflow Menu)
Краткие выводы и ссылка на github
Часть 1. Подготовительные работы
1.1. Создание проекта
Нам понадобятся базовые знания Котлина, IDE Android Studio версии не ниже 3.3, смартфон или эмулятор с версией API 14 или выше.
Создадим в Android Studio новый проект под названием «Navigation2019».
IDE создаст файл главной активности «MainActivity.kt» и его макет (шаблон) «activity_main.xml».
1.2. Зависимости (Dependencies)
Откроем файл «build.gradle» модуля (не проекта, а именно модуля) и в блок «dependencies» добавим необходимые зависимости:
Мы использовали библиотеки версии 1.0.0, но в будущем ситуация может измениться. Проверить, какие версии библиотек являются актуальными, можно здесь.
1.3. Страницы: создание фрагментов
Фрагменты — это «страницы» нашего будущего приложения. Кликнув правой кнопкой на каталоге с файлами классов, командой «New -> Fragment -> Fragment(Blank)» создадим «Fragment1».
IDE создаст kt-файл с классом фрагмента и xml-файл с макетом фрагмента. Таким же образом сгенерируем ещё три фрагмента («Fragment2», «Fragment3», «Fragment4»). Мы будем использовать их для создания четырёх разных типов навигации по приложению.
1.4. Адреса: файл ресурсов типа «Navigation»
Кликнув правой кнопкой мыши по папке «res», создадим файл ресурсов типа «Navigation» с названием «routes.xml» («маршруты»).
Откроем созданный файл и с помощью кнопки «New Destination» добавим в навигационную схему наши фрагменты.
«Хватаясь» мышкой за точку в середине правой стороны фрагмента, соединим фрагменты друг с другом так, как они должны быть связаны в приложении.
В соответствии с их названиями, наши фрагменты получат идентификаторы (id) «fragment1», «fragment2», «fragment3», «fragment4». Это «адреса», которые будут использоваться при указании пунктов назначения («destinations») в инструкциях навигационному контроллеру.
Кроме «id», каждый тег «fragment» содержит ещё три параметра: «name», «label» и «layout». Параметры «name» и «layout» нас сейчас не интересуют. Единственное, что стоит отредактировать в файле «routes.xml» — это названия («label») фрагментов. Заменим их на «Фрагмент №1», «Фрагмент №2», «Фрагмент №3» и «Фрагмент №4».
1.5. Фрейм: виджет NavHostFragment
Откроем файл макета «res/layout/activity_main.xml» и удалим текстовый виджет «Hello World!», он нам не понадобится. В палитре (Palette) выберем раздел «Контейнеры» (Containers) и перетащим оттуда на макет активности виджет NavHostFragment (указав наш файл «routes» в качестве источника информации для него). Он выполнит роль фрейма, в котором будут выводиться различные фрагменты приложения.
Изменим id фрагмента на «navFragment». Код макета главной активности будет выглядеть теперь так:
На этом подготовительные работы завершены, теперь можно приступать непосредственно к созданию элементов навигации.
Часть 2. Элементы навигации
2.1. Навигация с помощью кнопок
Откроем макет первого фрагмента («fragment_fragment1.xml»). Удалим ненужный текстовый виджет, изменим тип макета с «FrameLayout» на линейный вертикальный и добавим три кнопки с идентификаторами «button2», «button3», «button4» и соответствующими названиями «Фрагмент 2», «Фрагмент 3», «Фрагмент 4».
В методе «onCreateView» фрагмента получим ссылку на навигационный контроллер (объект «NavController») и запрограммируем поведение при нажатии на кнопки: командой «setOnClickListener» для каждой кнопки создадим слушателя кнопки, который при клике по кнопке будет передавать навигационному контроллеру адрес (id) точки назначения вместе с командой переместиться (navigate) на указанный адрес.
Проверим, как работают наши кнопки.
Одна кнопка — одна строчка кода — и клик по кнопке перемещает нас к указанному фрагменту. Просто, не так ли?
Но без меню не очень удобно, приходится использовать кнопку «Назад» для возвращения на стартовый экран.
2.2. Боковое меню (drawer)
2.2.1. Ресурсный файл меню
В каталоге «res/menu» создадим ресурсный файл меню «drawer_menu.xml». Добавим в него пункты меню, каждый из которых представляет собой тег «item» с параметрами «id» (должен соответствовать таковому в навигационном графе «routes.xml», «title» (заголовок, он может быть другим), «icon» (мы используем одну и ту же картинку для всех пунктов, но, конечно же, они могут быть разными) и др. Наше меню будет выглядеть так:
2.2.2. Шаблон DrawerLayout и виджет NavigationView в макете активности
Откроем файл макета активности «activity_main.xml».
После первого тега (xml version…) добавим начало тега «DrawerLayout».
В конец файла добавим виджет «NavigationView» и окончание тега «DrawerLayout».
2.2.3. Подключение бокового меню в классе активности
Откроем файл «MainActivity.kt» и в методе «onCreate» получим ссылку на «navController» (в активности это выглядит чуть сложнее, чем было во фрагментах).
Затем включим боковое меню:
Код класса теперь выглядит так:
Теперь меню появляется в ответ на свайп от левого края экрана:
Хорошо было бы добавить и кнопку слева-вверху для вызова бокового меню, верно?
2.3. Кнопка и название фрагмента на панели инструментов
Существующий по умолчанию ActionBar, как рекомендует официальное руководство, заменим на Toolbar.
Чтобы отключить существующий ActionBar, в файле «res/values/styles.xml» найдём строку
и заменим «DarkActionBar» на «NoActionBar».
Отлично, ActionBar мы отключили.
Теперь добавим Toolbar. Откроем файл «activity_main.xml», в палитре (Palette) выберем раздел «Контейнеры» (Containers) и перетащим оттуда на макет активности виджет «Toolbar». Панель инструментов добавлена, но она пока пуста.
Переходим в файл активности «MainActivity.kt». Чтобы на Toolbar вывести кнопку и название текущего фрагмента, в метод «onCreate()» добавим следующие строки:
Toolbar теперь выводит название фрагмента и кнопку «Вверх» (Up) для вложенных фрагментов.
Кнопка «вверх» в android’е почему-то обозначается стрелкой «влево»:
Чтобы на стартовом экране приложения выводилась кнопка-гамбургер, нам необходимо в конфигурацию панели инструментов добавить параметр «drawerLayout», который содержит id виджета DrawerLayout из файла «activity_main.xml».
Клик по этой кнопке выводит боковое меню.
2.4. Нижнее меню (Bottom Navigation)
Иногда бывает необходимо акцентировать внимание пользователя на определённых действиях, и эффективно сделать это помогает нижнее меню. Добавим его в третий фрагмент.
Сначала создадим ресурсный файл меню «bottom_nav_menu.xml» с парой ссылок. Затем откроем макет фрагмента №3 (файл «fragment_fragment3.xml») и перетащим на него виджет «BottomNavigationView», попутно согласившись с предложением IDE добавить в dependencies библиотеку «com.android.support:design».
Если бы мы создавали нижнее меню не для одного фрагмента, а сразу для всех, то в метод «onCreate» класса активности (файл «MainActivity.kt») нужно было бы включить следующий код:
Конечно, и виджет «BottomNavigationView» в таком случае надо было бы поместить в макет активности, а не фрагмента.
Но поскольку данное меню нам требуется только во фрагменте №3, то и редактировать мы будем класс фрагмента (файл «Fragment3.kt»), а код будет выглядеть чуть сложнее:
В итоге мы получим нижнее меню, которое будет выводиться только в 3-м фрагменте.
То же самое, но с другими ссылками, сделаем и в 4-м фрагменте.
2.5. Всплывающее меню (Overflow Menu)
Ещё один вид меню — всплывающее меню, которое выводится при клике по кнопке (трём вертикально расположенным точкам) в правом верхнем углу экрана.
Создадим ресурсный файл меню «top_right_menu.xml» с необходимыми ссылками.
В файле «MainActivity» в метод «onCreate» перед «toolBar.setupWithNavController. » добавим строку «setSupportActionBar(toolBar)».
И далее в этом же классе переопределим два метода:
Нажатие на верхнюю правую кнопку теперь отображает всплывающее меню:
Краткие выводы и ссылка на github
Очевидно, что Navigation Architecture Component существенно облегчает труд разработчика. Сложные в прошлом задачи теперь решаются несколькими строчками кода.
Поскольку главной целью данной публикации было осветить базовые алгоритмы создания навигации в android-приложении, многие детали реализации были умышленно оставлены за кадром.
Мой график работы обычно не позволяет поддерживать дискуссии, но Ваши комментарии в любом случае будут полезны для посетителей сайта и потому всегда приветствуются.
Источник