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

Навигация в многомодульном приложении на Jetpack без магии и DI

Когда вы начинаете создавать приложение, в котором хотя бы несколько экранов, всегда встает вопрос - как лучше реализовать навигацию. Вопрос становится интереснее и сложнее, когда вы собираетесь делать многомодульное приложение. Примерно полтора года назад я рассказывал как можно реализовать навигацию c помощью Jetpack в многомодульном проекте. И вот спустя время, я наткнулся на свою реализацию и понял, что можно на том же Jetpack летать по модулям проще: без магии и DI.


Архитектура проекта

Чтобы покрыть основные кейсы я покажу как реализовать навигацию на многомодульном проекте такой структуры:

Типичная архитектура Android проекта: feature-модули c реализацией экранов зависят от shared-модулей с общей логикой. И app модуль, который зависит от feature и shared.

Сейчас довольно популярен подход Single Activity, поэтому в моем примере будет всего одна Activity с глобальным хостом, в котором будут переключаться фрагменты

Подготовка

От модуля shared:navigation зависят почти все модули проекта не просто так. В этом модуле реализована функция расширения фрагмента для реализации переходов.

fun Fragment.navigate(actionId: Int, hostId: Int? = null, data: Serializable? = null) {val navController = if (hostId == null) {findNavController()} else {Navigation.findNavController(requireActivity(), hostId)}val bundle = Bundle().apply { putSerializable("navigation data", data) }navController.navigate(actionId, bundle)}

У функции есть параметры:

  • actionId - id действия графа навигации

  • hostId - id хоста графа навигации. Если не будет передан, то будет использован текущий хост

  • data - объект с данными типа Serializable

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

val Fragment.navigationData: Serializable?get() = arguments?.getSerializable("navigation data")

Также в этом модуле надо описать id хостов навигации, чтобы к ним был доступ из feature модулей. Для этого в директории ресурсов надо создать файл res/value/ids.xml

<?xml version="1.0" encoding="utf-8"?><resources><item name="host_global" type="id"/><item name="host_main" type="id"/></resources>

Отлично! Подготовка завершена, можно приступать к самой реализации навигации.

Простые переходы в feature-модулях

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

Для начала создади id для этих действий: запишем их в res/value/ids.xml модуля splash

<?xml version="1.0" encoding="utf-8"?><resources><item name="action_splashFragment_to_mainFragment" type="id"/><item name="action_splashFragment_to_onboardingFragment" type="id"/></resources>

Id для действий переходов я рекомендую создавать именно в модулях фич, которые будут использовать эти действия, а не в модуле shared:navigation. Это позволяет модулю знать только о необходимых действиях.

Теперь можно использовать созданные id для выполнения переходов.

import com.example.smmn.shared.navigation.navigateclass SplashFragment : Fragment(R.layout.fragment_splash) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        buttonToOnboarding.setOnClickListener {            navigate(R.id.action_splashFragment_to_onboardingFragment)        }        buttonToMain.setOnClickListener {            navigate(R.id.action_splashFragment_to_mainFragment)        }    }}

Обратите внимание, что для выполнения перехода используется функция расширения из модуля shared:navigation.

Но чтобы этот переход заработал надо настроить глобальный хост и реализовать глобальную навигацию.

Глобальный хост

В нашей архитектуре всего одна Activity. Она будет содержать глобальный хост для фрагментов. Для этого нам ничего не потребуется реализовывать в самом коде Activity.

class MainActivity : AppCompatActivity(R.layout.activity_main)

Хост добавить надо в ее разметке activity_main.xml

<?xml version="1.0" encoding="utf-8"?><androidx.fragment.app.FragmentContainerView xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    xmlns:tools="http://personeltest.ru/away/schemas.android.com/tools"    android:id="@id/host_global"    android:name="androidx.navigation.fragment.NavHostFragment"    android:layout_width="match_parent"    android:layout_height="match_parent"    app:defaultNavHost="true"    app:navGraph="@navigation/navigation_global"    tools:ignore="FragmentTagUsage" />

Глобальная навигация

Это навигация, которая происходит в глобальном хосте. Для ее реализации надо реализовать в модуле app граф навигации res/navigation/navigation_global.xml

<?xml version="1.0" encoding="utf-8"?><navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    android:id="@+id/navigation_global"    app:startDestination="@id/splashFragment">    <fragment        android:id="@+id/splashFragment"        android:name="com.example.smmn.feature.splash.SplashFragment"        android:label="SplashFragment">        <action            android:id="@id/action_splashFragment_to_mainFragment"            app:destination="@id/mainFragment"            app:popUpTo="@id/navigation_global" />        <action            android:id="@id/action_splashFragment_to_onboardingFragment"            app:destination="@id/onboardingFragment"            app:popUpTo="@id/navigation_global" />    </fragment>    <fragment        android:id="@+id/mainFragment"        android:name="com.example.smmn.feature.main.MainFragment"        android:label="MainFragment" >        <action            android:id="@id/action_mainFragment_to_splashFragment"            app:popUpTo="@id/navigation_global"            app:destination="@id/splashFragment" />    </fragment>    <fragment        android:id="@+id/onboardingFragment"        android:name="com.example.smmn.feature.onboarding.OnboardingFragment"        android:label="OnboardingFragment">        <action            android:id="@id/action_onboardingFragment_to_mainFragment"            app:destination="@id/mainFragment"            app:popUpTo="@id/navigation_global" />    </fragment></navigation>

Обратите внимание, что у каждого фрагмента есть набор action (действий) с помощью которых происходит переход между фрагментами. В действии указывается на какой фрагмент будет выполнен переход и как обрабатывать переход назад, например, при нажатии кнопки "Back".

И очень важно отметить, что id действий прописаны без знака +, то есть мы не создаем id в этом графе, а используем id, прописанные в feature модуле.

Прописанные id в модуле splash

<item name="action_splashFragment_to_mainFragment" type="id"/><item name="action_splashFragment_to_onboardingFragment" type="id"/>

Использование их в действиях глобального графа

        <action            android:id="@id/action_splashFragment_to_mainFragment"            app:destination="@id/mainFragment"            app:popUpTo="@id/navigation_global" />        <action            android:id="@id/action_splashFragment_to_onboardingFragment"            app:destination="@id/onboardingFragment"            app:popUpTo="@id/navigation_global" />

Вложенный хост

В Jetpack навигации есть возможность использовать вложенный хост. Это очень полезно, когда мы хотим сделать меню типа BottomNavigation и использовать для этого меню отдельный граф навигации.

В нашем примере во вложенном хосте будут фичи профиля и настроек.

Благодаря библиотеке navigation-ui, реализовать вложенную навигацию довольно просто.

В модуле main создадим меню для BottomNavigation в res/menu/menu_main.xml

<?xml version="1.0" encoding="utf-8"?><menu xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android">    <item        android:id="@+id/profileFragment"        android:icon="@drawable/ic_baseline_account_circle_24"        android:title="@string/main_menu_title_profile" />    <item        android:id="@+id/settingsFragment"        android:icon="@drawable/ic_baseline_settings_24"        android:title="@string/main_menu_title_settings" /></menu>

Создадим граф навигации в res/navigation/navigation_main.xml

<?xml version="1.0" encoding="utf-8"?><navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    android:id="@+id/navigation_main"    app:startDestination="@id/profileFragment">    <fragment        android:id="@+id/profileFragment"        android:name="com.example.smmn.feature.profile.ProfileFragment"        android:label="ProfileFragment">        <action            android:id="@id/action_profileFragment_to_infoFragment"            app:destination="@id/infoFragment" />    </fragment>    <fragment        android:id="@+id/settingsFragment"        android:name="com.example.smmn.feature.settings.SettingsFragment"        android:label="SettingsFragment" />    <fragment        android:id="@+id/infoFragment"        android:name="com.example.smmn.feature.info.InfoFragment"        android:label="InfoFragment" /></navigation>

Здесь важно указать у фрагментов те же id что указаны в файле меню res/menu/menu_main.xml. И не забывать, что id действий брать из модулей фич.

Осталось добавить хост и меню в разметку фрагмента res/layout/fragment_main.xml

<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    android:layout_width="match_parent"    android:layout_height="match_parent">    <fragment        android:id="@id/host_main"        android:name="androidx.navigation.fragment.NavHostFragment"        android:layout_width="match_parent"        android:layout_height="0dp"        app:defaultNavHost="true"        app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintTop_toTopOf="parent"        app:navGraph="@navigation/navigation_main" />    <com.google.android.material.bottomnavigation.BottomNavigationView        android:id="@+id/bottomNavigationView"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:background="@android:color/white"        app:elevation="8dp"        app:layout_constraintBottom_toBottomOf="parent"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintStart_toStartOf="parent"        app:menu="@menu/menu_main" /></androidx.constraintlayout.widget.ConstraintLayout>

И в самом фрагменте настроить bottomNavigationView

class MainFragment : Fragment(R.layout.fragment_main) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        NavigationUI.setupWithNavController(            bottomNavigationView,            Navigation.findNavController(requireActivity(), R.id.host_main)        )    }}

Переходы между фрагментами из разных хостов

Довольно частый случай, когда надо перейти с экрана, который находится внутри вложенного хоста, на экран глобального хоста. Например, у нас есть главный экран c хостом для экранов главных фич: настроек и профиля.

И на экране настроек, который находится внутри хоста главного экрана (не глобальный хост, а глубже) надо выполнить переход на экран сплэша, который находится в глобальном хосте. Например это может понадобиться, если надо разлогинить текущего пользователя.

В этом случае также воспользуемся функцией расширения фрагмента, но укажем id глобального хоста. Мы имеем к нему доступ из фичи, так как он прописан в модуле shared:navigation.

class SettingsFragment : Fragment(R.layout.fragment_settings) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        buttonToSplash.setOnClickListener {            navigate(R.id.action_mainFragment_to_splashFragment, R.id.host_global)        }    }}

Id действия по аналогии с предыдущим переходом прописан в самом модуле фичи res/values/ids.xml

<?xml version="1.0" encoding="utf-8"?><resources><item name="action_mainFragment_to_splashFragment" type="id"/></resources>

Переходы между фрагментами с передачей и получением данных

Чтобы выполнить переход с передачей данных необходимо, чтобы данные можно было положить в bundle. Это могуг быть какие-то примитивные типы или объекты Serializable классов.

Выше я уже реализовал функцию расширения фрагмента для выполнения перехода, в которую можно передать объект Serializable класса. Аналогично вы можете реализовать передачу примитивных типов.

Чтобы передать объект Serializable класса надо чтобы модуль фичи, с которой происходит переход, и модуль фичи, на которую происходит переход, имели доступ к модулю с таким классом. В нашем случае создадим модуль shared:model где будет лежать Serializable класс Info.

data class Info(    val name: String,    val surname: String) : Serializable

Переход будет происходить с экрана profile на экран info. Создадим объект Info и передадим его в функцию расширения фрагмента.

class ProfileFragment : Fragment(R.layout.fragment_profile) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        buttonToInfo.setOnClickListener {            navigate(R.id.action_profileFragment_to_infoFragment, data = Info("name", "surname"))        }    }}

И получим данные используя другую функцию расширения фрагмента, созданную ранее.

class InfoFragment : Fragment(R.layout.fragment_info) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        val info = navigationData as? Info ?: return        textView.text = info.toString()    }}

Так это будет выглядеть в приложении

Заметьте, что мы не указывали в каком хосте выполнить переход, и переход произошел в текущем хосте.

Заключение

Таким несложным способом можно организовать навигацию в вашем многомодульном проекте, используя Jetpack и пару функций расширения. Этот подход функционально не отличается от подхода, который я описывал ранее, но в использовании он намного проще и лаконичнее.

Оставляю ссылку на код примера приложения.

Буду рад обратной связи!

Источник: habr.com
К списку статей
Опубликовано: 22.04.2021 20:09:52
0

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

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

Разработка мобильных приложений

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

Android

Jetpack

Navigation

Graph

Hosts

Action

Androidx

Категории

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

  • Имя: Макс
    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