При написании своего multi-module app pet проекта, у меня возник вопрос, а как же реализовать навигацию?
Часть 1. По понятиям.
Что такое навигация в современном Android-приложении?
В моем понимании, навигация — это один из слоев Android-приложения, обеспечивающий:
1. Переходы между экранами: Activity и Fragments;
2. Работа с различными диалогами: Bottom Sheet, Dialog Fragment и различные пикеры;
3. Сохранение последовательности экранов, с возможностью возвращения на предыдущие экраны, как при нажатии на системную кнопку back, так и принудительно;
Часть 2. Разберемся. По полочкам!
Так как большая часть мобильных приложений имеет некое tab-menu, в Android, я привык называть его BottomNavigationView. С него и начнем! Также приложение будет строиться по принципу Single Activity. Представим себе экран, на котором есть какой-то контент и кнопки.
Посмотрев различные реализации, мне очень понравился комментарий автор библиотеки Cicerone, Константина Цховребова:
Я все время думал, что tab-menu или BottomNavigation является частью навигации или даже ее основой, но нет это не так, я очень ошибался!
По сути, нам необходимо показывать контент в зависимости от нажатого таба. Притом каждый контент — это некий модуль приложения с визуальной составляющей, т.е. с presentation слоем, посмотрим на это.
Тут мы можем видеть, что у каждого tabN есть свой containerN, который в свою очередь, отображает некий presentation слой, находящийся в своем модуле. При введении навигации в multi-module приложении нам необходимо в каждом контейнере показывать разные presentation от разных модулей, перемещаться от одного модуля к другому и обратно, при этом сохраняя состояние каждой вкладки.
На рис. 3.3 как раз можно видеть, как мы перемещаемся по сути вертикально между модулями с их presentation слоями, но так получается, что каждый модуль должен знать друг о друге, и это не правильно, так как будет рушить всю концепцию multi-module, и для того чтобы каждый модуль не знал о происхождении другого модуля, необходимо иметь некий слой навигации, и вот как будет выглядеть.
Можно сделать промежуточный вывод: для того чтобы создать навигацию в многомодульном приложении, необходимо, чтобы каждый модуль был изолирован, и все переходы в другие модули производились через слой навигации. Тем самым каждый модуль не будет знать о других модулях. Даже если у нас есть модуль, который не имеет presentation слоя, мы сможем выполнить переход через слой навигации.
Часть 3. Что по коду?
Для реализации навигации я взял библиотеку Cicerone для упрощения, но я думаю возможно сделать тоже самое и Fragment-API, NavigationGraph.
Создадим enum, который будет указывать необходимое количество табов для BottomNavigationView:
1. idRes — это идентификатор таба, который мы создадим в ресурсах.
2. nameRes — это название таба, которое также будет создано в ресурсах.
3. iconRes — это изображение, которое мы загрузим в ресурсы drawable.
Создадим модуль feature, он будет хранить feature-модули и api модули:
1. TODAY - feature:today, feature:today-api
2. SCHEDULE - feature:schedule, feature:schedule-api
3. ASSIGNMENTS - feature:assignments, feature:assignments-api
4. SETTINGS - feature:settings, feature:settings-api
Как видно, один из модулей будет являться реализацией со всеми слоями, и внутри него уже созданы необходимые фрагменты. С помощью api модуля будем взаимодействовать с основным модулем, по сути скрывая реализацию за интерфейсом.
И создадим модуль core, внутри которого будет модуль nav (:core:nav). И не забудем в app модуле имлементировать все модули, что создали, за исключением тех что api.
В app создадим ContainerFragment — это фрагмент который будет создаваться при создании таба в BottomNavigation и будет привязан к TabItemEntry.
Итак, как видно, наш контейнер наследуется от ContainerNavProvider — это интерфейс, который предоставляет методы getCicerone() и getRouter().
TabItemEntry — это тот самый enum, который мы создали ранее. Дополнительно нам необходимо создать навигатор с использованием библиотеки Cicerone. В методе onCreateView при создании контейнера можно увидеть, как он будет заполняться первым фрагментом. Метод BottomNavScreens.getBottomTabFragment(tabItemEntry) вернёт нам стартовый фрагмент.
Важно обратить внимание на ContainerNavHolder. Если заглянуть внутрь, можно увидеть, что это некий Singleton, который хранит ConcurrentHashMap всех контейнеров. Именно с помощью метода getCicerone мы будем наполнять и возвращать нужный нам router.
Мы создали всё необходимое, и теперь можем перейти к нашей SingleActivity.
Activity наследует интерфейс RootNavProvider, который, в свою очередь, наследует ContainerNavProvider. Это необходимо для методов getCicerone() и getRouter(). Для того чтобы получать нужный Router, прямо из ContainerFragment видимого пользователем, не обращаясь к нему напрямую, а через ContainerNavProvider.
Метод onTabSelected скрывает или открывает нужный нам ContainerFragment, как на рисунке 3.2 при клике на таб, а метод getVisibleFragmentContainer() находит видимый пользователю ContainerFragment.
Создадим дополнительный RootNavHolder, именно этот класс будет хранить в себе RootNavProvider и через него мы будем доставать из ContainerFragment нужный нам Router.
У нас у же есть созданный core:nav внутри мы создали интерфейс Nav, а в app модуле мы создали NavImpl (опять таки, скрываем реализацию за инерфейсом). В NavImpl в конструкторе мы можем обратиться к RootNavHolder
И видим что у нас уже реализованы два метода, а метод Nav предоставляет методы. Который мы сможем использовать в любом месте нашего приложения, и выполнить переход через слой навигации.
Итог.
И как же это будет работать? Представим, что мы находимся на главном экране нашего приложения, и нам необходимо показать пользователю presentation, который находится в другом модуле. Мы обращаемся к core:nav и обращаемся к методу в интерфейсе Nav. Затем в NavImpl скрыто от нас, обращается к RootNavHolder и просит предоставить необходимый Router. Он берется напрямую из видимого пользователю сейчас ContainerFragment, после чего следующий фрагмент из presentation слоя модуля добавляется в стек, и осуществляется переход к нему.
Как видно из этого, мы не взаимодействуем напрямую с другим модулем. По сути, каждый модуль ничего не знает друг о друге, мы можем показывать любой presentation любого модуля из любой точки приложения, достаточно обратиться к интерфейсу Nav.
Это моя первая статья, и я открыт для критики и исправления своих ошибок. Я учусь и стремлюсь узнавать новое, поэтому буду рад, если вы поделитесь своим мнением в комментариях, всем спасибо!
В данном случае я пишу свой pet проект и то, что я описал в статье возможно найти в вот тут https://github.com/pokhodai/DailyCat.
Это первая глава по навигации в многомодульном Android приложении. В дальнейшем будет интересно!)
Список источников:
1. Лицензия на вождение болида, или почему приложения должны быть Single-Activity - https://habr.com/ru/companies/redmadrobot/articles/426617/;
2. Навигация для Android с использованием Navigation Architecture Component: пошаговое руководство https://habr.com/ru/articles/449776/;
3. Презентация - https://assets.ctfassets.net/2grufn031spf/gvqZQu0qR2YqcEA8ckEA4/1df2068cd61c409ed12563fc65806574/Konstantin_Tskhovrebov_Cicerone_Android_Navigation__1_.pdf;
4. Принципы построения многомодульных Android-приложений - https://habr.com/ru/articles/687882/;
5. Navigation Component-дзюцу, vol. 1 — BottomNavigationView - https://habr.com/ru/companies/hh/articles/518332/