Найти тему
khodein

Навигация в muilti-module Android приложении.

Оглавление

При написании своего multi-module app pet проекта, у меня возник вопрос, а как же реализовать навигацию?

Часть 1. По понятиям.

-2

Что такое навигация в современном Android-приложении?

В моем понимании, навигация — это один из слоев Android-приложения, обеспечивающий:

1. Переходы между экранами: Activity и Fragments;

2. Работа с различными диалогами: Bottom Sheet, Dialog Fragment и различные пикеры;

3. Сохранение последовательности экранов, с возможностью возвращения на предыдущие экраны, как при нажатии на системную кнопку back, так и принудительно;

Часть 2. Разберемся. По полочкам!

Так как большая часть мобильных приложений имеет некое tab-menu, в Android, я привык называть его BottomNavigationView. С него и начнем! Также приложение будет строиться по принципу Single Activity. Представим себе экран, на котором есть какой-то контент и кнопки.

Рис. 3.1 - Структурная схема экрана с контентом и BottomNavigationView.
Рис. 3.1 - Структурная схема экрана с контентом и BottomNavigationView.

Посмотрев различные реализации, мне очень понравился комментарий автор библиотеки Cicerone, Константина Цховребова:

-4
Ciceron with bottom navigation · Issue #117 · terrakok/Cicerone

Я все время думал, что tab-menu или BottomNavigation является частью навигации или даже ее основой, но нет это не так, я очень ошибался!

По сути, нам необходимо показывать контент в зависимости от нажатого таба. Притом каждый контент — это некий модуль приложения с визуальной составляющей, т.е. с presentation слоем, посмотрим на это.

Рис. 3.2 - Структурная схема экрана с нажатым Tab1 (зеленым показан видимый контейнер).
Рис. 3.2 - Структурная схема экрана с нажатым Tab1 (зеленым показан видимый контейнер).

Тут мы можем видеть, что у каждого tabN есть свой containerN, который в свою очередь, отображает некий presentation слой, находящийся в своем модуле. При введении навигации в multi-module приложении нам необходимо в каждом контейнере показывать разные presentation от разных модулей, перемещаться от одного модуля к другому и обратно, при этом сохраняя состояние каждой вкладки.

Рис. 3.3 - Структурная схема стека модулей на каждой вкладке.
Рис. 3.3 - Структурная схема стека модулей на каждой вкладке.

На рис. 3.3 как раз можно видеть, как мы перемещаемся по сути вертикально между модулями с их presentation слоями, но так получается, что каждый модуль должен знать друг о друге, и это не правильно, так как будет рушить всю концепцию multi-module, и для того чтобы каждый модуль не знал о происхождении другого модуля, необходимо иметь некий слой навигации, и вот как будет выглядеть.

Рис. 3.4 - Структурная схема навигации в многомодульном приложении.
Рис. 3.4 - Структурная схема навигации в многомодульном приложении.

Можно сделать промежуточный вывод: для того чтобы создать навигацию в многомодульном приложении, необходимо, чтобы каждый модуль был изолирован, и все переходы в другие модули производились через слой навигации. Тем самым каждый модуль не будет знать о других модулях. Даже если у нас есть модуль, который не имеет presentation слоя, мы сможем выполнить переход через слой навигации.

Часть 3. Что по коду?

-8

Для реализации навигации я взял библиотеку Cicerone для упрощения, но я думаю возможно сделать тоже самое и Fragment-API, NavigationGraph.

Создадим enum, который будет указывать необходимое количество табов для BottomNavigationView:

1. idRes — это идентификатор таба, который мы создадим в ресурсах.

2. nameRes — это название таба, которое также будет создано в ресурсах.

3. iconRes — это изображение, которое мы загрузим в ресурсы drawable.

tabitementry.kt

Создадим модуль 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.

-9

В app создадим ContainerFragment — это фрагмент который будет создаваться при создании таба в BottomNavigation и будет привязан к TabItemEntry.

container.kt

Итак, как видно, наш контейнер наследуется от ContainerNavProvider — это интерфейс, который предоставляет методы getCicerone() и getRouter().

TabItemEntry — это тот самый enum, который мы создали ранее. Дополнительно нам необходимо создать навигатор с использованием библиотеки Cicerone. В методе onCreateView при создании контейнера можно увидеть, как он будет заполняться первым фрагментом. Метод BottomNavScreens.getBottomTabFragment(tabItemEntry) вернёт нам стартовый фрагмент.

getbottomtabfragment.kt

Важно обратить внимание на ContainerNavHolder. Если заглянуть внутрь, можно увидеть, что это некий Singleton, который хранит ConcurrentHashMap всех контейнеров. Именно с помощью метода getCicerone мы будем наполнять и возвращать нужный нам router.

containernavholder.kt

Мы создали всё необходимое, и теперь можем перейти к нашей SingleActivity.

activity.kt

Activity наследует интерфейс RootNavProvider, который, в свою очередь, наследует ContainerNavProvider. Это необходимо для методов getCicerone() и getRouter(). Для того чтобы получать нужный Router, прямо из ContainerFragment видимого пользователем, не обращаясь к нему напрямую, а через ContainerNavProvider.

Метод onTabSelected скрывает или открывает нужный нам ContainerFragment, как на рисунке 3.2 при клике на таб, а метод getVisibleFragmentContainer() находит видимый пользователю ContainerFragment.

Создадим дополнительный RootNavHolder, именно этот класс будет хранить в себе RootNavProvider и через него мы будем доставать из ContainerFragment нужный нам Router.

rootnavholder.kt

У нас у же есть созданный core:nav внутри мы создали интерфейс Nav, а в app модуле мы создали NavImpl (опять таки, скрываем реализацию за инерфейсом). В NavImpl в конструкторе мы можем обратиться к RootNavHolder

navimpl2.kt

И видим что у нас уже реализованы два метода, а метод 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/