Знакомьтесь, компонент Navigation в Android!

Что такое компонент Navigation? 

Всем известно, что навигация между экранами является одним из фундаментальных принципов в приложениях Android. Как правило, мы осуществляем ее при помощи Intents или Event Bus. Но как быть в сложных ситуациях, например с боковым навигационным меню (navigation drawer) или меню, расположенным внизу (bottom navigation), когда необходимо выделить выбранную вкладку, сохранив отображение панели вкладок и синхронно изменить экран на тот, что соответствует вкладке, не говоря уже об управлении бэкстеком. Для реализации этих задач нам явно потребуется что-то получше.

Компонент Navigation — это комплексное решение всех проблем для любого типа навигации в приложениях Android. Он помогает управлять навигацией, транзакциями фрагментов, бэкстеком, анимацией, глубокими ссылками, а также многим другим. Компонент JetPack Navigation представляет собой набор библиотек и инструментов, сопровождается руководством и обеспечивает продуманную структуру навигации внутри приложения. 

Компонент Navigation обеспечивает новый тип навигации в разработке Android, включающей навигационный граф (navigation graph), который позволяет видеть все экраны и пути навигации между ними. 

Давайте рассмотрим три главные части компонента Navigation:  

1. Навигационный граф. Эторесурс XML, содержащий всю навигационную информацию в одном централизованном месте. Он включает в себя все отдельные области контента в приложении, называемые пунктами назначения (destinations), а также возможные пути навигации по приложению, доступные для пользователя. 

2. NavHost. Этопустой контейнер, отображающий пункты назначения из навигационного графа. 

3. NavController. Это объект, управляющий навигацией приложения в NavHost. Он координирует смену контента пунктов назначения в NavHost в процессе перемещения пользователя по приложению. 

Обратим внимание еще на два понятия, которые мы будем часто использовать: 

  • Пункты назначения. Это не что иное, каквсе области контента в приложении, такие какактивности(activities), фрагменты (fragments), диалоговые окна и т. д.
  • Действия (actions). Онииспользуются для создания навигации, имеющей атрибут destination, в котором можно указать id конечной заставки. 

Когда мы осуществляем навигацию по приложению с использованием NavController, то он показывает соответствующий пункт назначения в NavHost. 

Компонент Navigation обеспечивает еще ряд полезных возможностей, среди которых: 

  • упрощенная настройка стандартных шаблонов навигации; 
  • обработка транзакций фрагментов; 
  • безопасность типов при передаче информации в процессе навигации; 
  • обработка анимации переходов; 
  • централизация и визуализация навигации; 
  • корректная обработка действий “верх” (Up) и “назад” (Back) по умолчанию; 
  • предоставление стандартизированных ресурсов для анимации и переходов; 
  • реализация и обработка глубоких ссылок (deep links). 

Итак, мы познакомились с компонентом Navigation и его возможностями. Теперь пора приступить к реализации базового процесса навигации. 

Что же у нас в планах? 

Мы создадим простое приложение с тремя экранами (главная активность (Main Activity) с тремя фрагментами, имеющими по две кнопки) и установим навигацию между ними. 

Приступим…

Требования: 

  • базовые знания Kotlin; 
  • Android Studio 3.2 или более новые версии; 
  • эмулятор или устройство с API 14+.  

После создания базового проекта Android добавьте приведенную ниже зависимость в файл сборки верхнего уровня build.gradle. 

def nav_version = "2.1.0"
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"}

Теперь добавьте ниже указанные зависимости в файл build.gradle модуля app. Как только вы это сделали, сразу щелкните Sync now для синхронизации! 

apply plugin: "androidx.navigation.safeargs.kotlin"

android {
    ...
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8.toString()
    }
}

def nav_version = "2.2.2"
dependencies {
implementation "androidx.navigation:navigation-fragment ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
}

Safe Arguments. Safe Args является плагином Gradle, который используется для передачи данных между пунктами назначения. Главное его преимущество состоит в том, что некоторые проверки, происходящие в среде выполнения, теперь будут осуществляться во время компиляции. 

Подготовительная работа окончена — продолжаем!

Сначала создадим навигационный граф для нашего приложения. 

Как же нам это сделать?

Мы уже выяснили, что навигационный граф — это ресурсный файл со всей навигационной информацией, следовательно создадим этот файл в директории res с ресурсом типа navigation. 

Этапы создания навигационного графа: 

  1. В окне Project щелкните правой кнопкой мыши на директорию res и выберите New > Android Resource File в директорииnavigation. Если вы ее еще не создали, то пора это сделать.
  2. В директории navigation создаем новый файл navigation resource. В нашем примереон получил название navigation, но вы можете назвать его, как пожелаете.
  3. Выбираем Navigation в выпадающем списке Resource type и нажимаем OK
Создание файла навигационного графа 
Навигационный граф на панели проекта 

Теперь мы изменим activity_main.xml и заменим <TextView/>, выставленный по умолчанию,на <fragment/>, приведенный ниже:

<fragment
    android:id="@+id/fragment"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:defaultNavHost="true"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:navGraph="@navigation/navigation" />

Выполнив это действие, мы должны увидеть хост (activity_main), добавленный в навигационный граф (в navigation.xml). 

Навигационный граф с хостом

Теперь можно добавить новый пункт назначения, щелкнув на опцию ‘Click to add a destination’ (Щелкните, чтобы добавить пункт назначения) в центре навигационного графа. Затем выберите опцию ‘Create new destination’ (Создать новый пункт назначения), вслед за этим откроется диалоговое окно Android New Component, где мы можем создать новый фрагмент. 

Для нашего примера создадим два фрагмента, а именно SignupFragment и LoginFragment. После этого щелкнем на один фрагмент и присоединим его к другому. Навигационный граф будет выглядеть следующим образом: 

Навигационный граф с одним действием

Для выше указанного навигационного графа navigation.xml примет следующий вид: 

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/navigation_graph"
    app:startDestination="@id/signupFragment3">
    <fragment
        android:id="@+id/signupFragment3"
        android:name="com.navigation.view.SignupFragment"
        android:label="fragment_signup"
        tools:layout="@layout/fragment_signup" >
        <action
            android:id="@+id/action_signupFragment3_to_loginFragment3"
            app:destination="@id/loginFragment3" />
    </fragment>
    <fragment
        android:id="@+id/loginFragment3"
        android:name="com.navigation.view.LoginFragment"
        android:label="fragment_login"
        tools:layout="@layout/fragment_login" >
        <action
            android:id="@+id/action_loginFragment3_to_signupFragment3"
            app:destination="@id/signupFragment3" />
    </fragment>
</navigation>

Здесь корневым тегом будет navigation с атрибутом startDestination, в котором мы укажем id первоначально загружаемого фрагмента, за которым следуют пункты назначения (фрагменты, диалоговые окна и т. д.) с одним из атрибутов name, значение которого будет названием вашего пункта назначения. 

Вот мы и подошли к навигации. Будем использовать тег action в конкретном пункте назначения, с которого мы намерены начать навигацию. Также есть атрибут, в котором мы указываем id пункта назначения. 

У макета SignupFragment будет две кнопки (Buttons) с id signupBtn и gotoLoginBtn, а также три компонента EditText с id signupUsername, signupPassword и otherInfo

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)gotoLoginBtn.setOnClickListener { onGotoLogin(it)

}

private fun onGotoLogin(v: View) {
    val action = SignupFragmentDirections.actionGoToLogin()
    Navigation.findNavController(v).navigate(action)
}

Типобезопасная зависимость создаст класс в формате name_of_file_Directions (в нашем случае исходный фрагмент SignupFragmentDirections), состоящий из <action></action> в файле навигации (navigation). 

Переходим к созданию навигационного графа с несколькими действиями. Давайте посмотрим, как он будет выглядеть с тремя фрагментами и несколькими действиями. 

Навигационный граф с несколькими действиями

Посмотрим на код XML во вкладке навигации для создания графа по типу указанного выше. 

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/navigation"
    app:startDestination="@id/signupFragment">

<fragment
        android:id="@+id/signupFragment"
        android:name="com.navigation.view.SignupFragment"
        android:label="fragment_signup"
        tools:layout="@layout/fragment_signup">
        <action
            android:id="@+id/actionGoToMain"
            app:destination="@id/mainFragment" />
        <action
            android:id="@+id/actionGoToLogin"
            app:destination="@id/loginFragment" />
    </fragment>
    <fragment
        android:id="@+id/loginFragment"
        android:name="com.navigation.view.LoginFragment"
        android:label="fragment_login"
        tools:layout="@layout/fragment_login">
        <action
            android:id="@+id/actionGoToMain"
            app:destination="@id/mainFragment" />
        <action
            android:id="@+id/actionGoToSignup"
            app:destination="@id/signupFragment" />
    </fragment>
    <fragment
        android:id="@+id/mainFragment"
        android:name="com.navigation.view.MainFragment"
        android:label="fragment_main"
        tools:layout="@layout/fragment_main">
        <action
            android:id="@+id/actionGoToSignup"
            app:destination="@id/signupFragment" />
    </fragment>
</navigation>

Готово: в вашем приложении реализована базовая навигация. Теперь вы сможете переместиться к LoginFragment. 

Освоив базовую реализацию, можем переходить к реализации навигации с аргументами (Navigation With Arguments):

Для этого существуют два возможных способа: 

  1. Использование объектов bundle. 
  2. Использование SafeArgs. 

Ниже я представлю вам два способа реализации и объясню, почему лучше передавать данные, используя SafeArgs вместо Bundle.  

Использование Bundle. Передаем данныеиз исходного фрагмента, получая навигационный контроллер представления как: 

val bundle = bundleOf("username" to signupUsername.text.toString())
Navigation.findNavController(signupUsername).navigate(R.id.actionGoToMain, bundle)

bundleOf() является методом расширенияandroidx. Для этого нам нужно добавить ниже указанную зависимость в файл build.gradle модуля app: 

implementation 'androidx.core:core-ktx:1.3.0-rc01'

Получаем данные во фрагменте назначения как: 

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

signoutBtn.setOnClickListener { onSignout() }
    usernameTV.text = "Hello "+ arguments?.getString("username")

observeViewModel()
}

Вот таким образом мы осуществили передачу данных от одного фрагмента к другому, используя bundle. Однако предчувствую вполне закономерный вопрос с вашей стороны: “Зачем же нам использовать SafeArgs, если мы так легко справились с задачей с помощью выше указанного способа?”.  

Конечно, можно передавать данные, используя bundle. Но бывают ситуации, когда мы пытаемся получить данные в пункте назначения, которые никогда не передавались в качестве дополнительных параметров bundle, что вызывает в среде выполнения NullPointerException. Для избежания подобных проблем архитектурные компоненты предоставляют типобезопасный способ.

Как же использовать Safe Arguments для передачи данных?

Так как в нашем проекте есть активный плагин safe args, посмотрим, как его можно использовать. 

Сначала укажем аргументы, необходимые для пункта назначения в navigation.xml, как показано ниже: 

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/navigation"
    app:startDestination="@id/signupFragment">

<fragment
        android:id="@+id/signupFragment"
        android:name="com.navigation.view.SignupFragment"
        android:label="fragment_signup"
        tools:layout="@layout/fragment_signup">
        <action
            android:id="@+id/actionGoToMain"
            app:destination="@id/mainFragment" />
        <action
            android:id="@+id/actionGoToLogin"
            app:destination="@id/loginFragment" />
    </fragment>
    <fragment
        android:id="@+id/mainFragment"
        android:name="com.navigation.view.MainFragment"
        android:label="fragment_main"
        tools:layout="@layout/fragment_main">
        <argument
           android:name="username"
           app:argType="string"
           android:defaultValue="default"/>
        <action
            android:id="@+id/actionGoToSignup"
            app:destination="@id/signupFragment" />
    </fragment>
</navigation>

Мы отправляем данные также из исходного файла как: 

super.onViewCreated(view, savedInstanceState)
    signupBtn.setOnClickListener { onSignup() }
}

private fun onSignUp(){
val action = SignupFragmentDirections.actionGoToMain(signupUsername.text.toString())
Navigation.findNavController(signupUsername).navigate(action)
}

Получаем данные также во фрагменте назначения как: 

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

signoutBtn.setOnClickListener { onSignout() }
    deleteUserBtn.setOnClickListener { onDelete() }
    usernameTV.text = "Hello "+ MainFragmentArgs.fromBundle(requireArguments()).username

}

Типобезопасная зависимость создаст класс в формате name_of_file_Args (в нашем случае фрагмент назначения MainFragmentArgs), который состоит из <arguments></arguments> в файле навигации

Ура!!! Двигаемся дальше. 

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

Навигация с Deeplink: 

В Android глубокая ссылка означает ссылку, по которой вы переходите непосредственно в конкретный пункт назначения внутри приложения. Для добавления глубокой ссылки щелкните на значок + на панели Argument в секции Deep Link. В диалоговом окнеAdd deep link (Добавить глубокую ссылку) введите URI

На этом всё! Пишите код с удовольствием! 

С полной информацией о проекте вы можете ознакомиться, пройдя по ссылке на GitHub

ashitaasati1608/NavigationComponent
Navigation Component. Contribute to ashitaasati1608/NavigationComponent development by creating an account on GitHub.github.com

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи ashita asati: Android Navigation Component