Navigation Architecture Component. Вступ

На Google IO було представлено Navigation Architecture Component.

Я детально переглянув вихідні коди цього компонента, щоб точно розуміти, що він робить. Під капотом там ті самі startActivity і FragmentManager. Navigation Architecture Component - це обгортка над цими стандартними механізмами, яка покликана впорядкувати і спростити навігацію в застосунку.

Navigation функціонал може бути вимкнений за замовчуванням. Перевірте налаштування студії Settings > Experimental > Enable Navigation Editor. Перезапустіть студію після ввімкнення цього чекбокса.

dependencies {
    implementation 'androidx.navigation:navigation-fragment-ktx:2.7.4'
    implementation 'androidx.navigation:navigation-ui-ktx:2.7.4'
    implementation 'com.google.android.material:material:1.10.0'
}
plugins {
  // Kotlin serialization plugin for type safe routes and navigation arguments
  kotlin("plugin.serialization") version "2.0.21"
}

dependencies {
  val nav_version = "2.8.5"

  // Jetpack Compose integration
  implementation("androidx.navigation:navigation-compose:$nav_version")

  // Views/Fragments integration
  implementation("androidx.navigation:navigation-fragment:$nav_version")
  implementation("androidx.navigation:navigation-ui:$nav_version")

  // Feature module support for Fragments
  implementation("androidx.navigation:navigation-dynamic-features-fragment:$nav_version")

  // Testing Navigation
  androidTestImplementation("androidx.navigation:navigation-testing:$nav_version")

  // JSON serialization library, works with the Kotlin serialization plugin
  implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
}

Розглянемо простий приклад із MainActivity і трьома фрагментами: Fragment1, Fragment2, Fragment3. MainActivity буде по черзі відображати в собі фрагменти.

Для цього нам знадобляться такі Navigation інструменти:

NavController - цей основний механізм Navigation Component. Саме його ми будемо просити показувати на екрані фрагменти. Але щоб він міг це робити, він повинен мати список фрагментів і контейнер, у якому він буде ці фрагменти відображати.

NavGraph - це список фрагментів, який ми будемо створювати і наповнювати. NavController зможе показувати фрагменти тільки з цього списку. Далі будемо називати його графом.

NavHostFragment - це контейнер. Усередині нього NavController відображатиме фрагменти.

Ще раз, коротко, для розуміння: контролер у контейнері відображає фрагменти з графа.

Почнемо зі створення графа (NavGraph). Це звичайний resource файл із типом Navigation. Після створення він порожній: Давайте додавати фрагменти. У термінології Navigation вони називаються destination. Тиснемо кнопку додавання, студія показує нам фрагменти та Activity, які є в проєкті. Додаємо три фрагменти.

Результат Зліва бачимо список усіх destination у цій графі. Позначкою Start відзначено стартовий destination, який одразу буде відображено під час запуску програми. У нашому випадку це Fragment1.

Посередині відображено ті самі destination, але вже не списком, а в їхньому реальному вигляді, з використанням їхнього layout. Значком будиночка позначено стартовий destination. Для всіх трьох фрагментів я створив однакові layout: назва фрагмента і пара кнопок. Пізніше будемо використовувати ці кнопки для навігації.

Праворуч розташована панель атрибутів для поточного виділеного destination. Про них ми детально поговоримо пізніше. Поки що нас цікавить атрибут ID. Цей ID нам треба буде повідомляти контролеру (NavController), щоб він відобразив відповідний фрагмент. Цей ID до речі відображається в панелі посередині. Над кожним фрагментом можна побачити його ID.

Ок, граф створено. Тепер у MainActivity треба додати контейнер (NavHostFragment), у якому NavController відображатиме фрагменти.

У activity_main додаємо NavHostFragment:

<FrameLayout 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:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">
 
   <fragment
       android:id="@+id/nav_host_fragment"
       android:name="androidx.navigation.fragment.NavHostFragment"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       app:defaultNavHost="true"
       app:navGraph="@navigation/main_graph" />
 
</FrameLayout>

Контейнер готовий. Залишається десь взяти контролер (NavController). Тут нам допоможе контейнер. Він при створенні сам створить контролер і трохи пізніше поділиться ним із нами.

Зверніть увагу, що в атрибуті navGraph ми вказали створений раніше граф main_graph. Контейнер передасть цей граф контролеру.

Переходимо до коду.

Щоб попросити контролер у контейнера, використовуємо метод Navigation.findNavController із зазначенням ID контейнера. Цей метод за ID знайде контейнер NavHostFragment і візьме в нього контролер.

Код в MainActivity.java

NavController navController;
 
@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);
   navController = Navigation.findNavController(this, R.id.nav_host_fragment);
}

Тепер ми можемо використовувати цей контролер для навігації по фрагментах. Для цього є два методи:

  • navigate(int resId) - щоб відкрити будь-який фрагмент із графа, треба в контролера викликати метод navigate і передати йому ID destination. Контролер перегляне граф, визначить якому фрагменту в графі відповідає ID і відобразить цей фрагмент.
  • popBackStack - повернення на крок назад, на попередній фрагмент.

Як ви вже бачили, у кожному з трьох фрагментів є кнопки Back і Next. Після натискання на кнопку Next ми відкриватимемо наступний фрагмент. А після натискання на кнопку Back будемо повертатися на попередній. Я використовував колбеки й обробку натискань на ці кнопки витягнув в Activity. Відповідно в MainActivity у мене 6 методів (3 фрагменти, 2 кнопки в кожному)

У цих методах ми і будемо працювати з контролером.

@Override
public void onFragment1NextClick() {
    navController.navigate(R.id.fragment2);
}
 
@Override
public void onFragment1BackClick() {}
 
 
@Override
public void onFragment2NextClick() {
    navController.navigate(R.id.fragment3);
}
 
@Override
public void onFragment2BackClick() {
    navController.popBackStack();
}
 
 
@Override
public void onFragment3NextClick() {}
 
@Override
public void onFragment3BackClick() {
    navController.popBackStack();
}

За назвою методу зрозуміло для якої кнопки якого фрагмента він є обробником.

Наприклад, після натискання на Next у Fragment1 ми просимо контролер відкрити destination з ID = fragment2. Контролер знайде цей destination у графі, побачить, що це фрагмент Fragment2 і в контейнері відобразить цей фрагмент.

Аналогічно після натискання на Next у Fragment2 ми просимо відкрити destination з ID = fragment3, який у графі відповідає фрагменту Fragment3.

Після натискання на кнопки Back, ми повертаємося на крок назад.

Запускаємо додаток. При старті одразу відобразиться Fragment1, бо він є стартовим у графі.

Виконуємо навігацію.

Системна кнопка Back теж працює і виконує крок назад. Це відбувається завдяки атрибуту

app:defaultNavHost="true"

який ми вказали в контейнері (NavHostFragment). Контейнер перехоплює натискання і показує попередній фрагмент. Якщо встановити його значення в false, то контейнер більше не буде обробляти системну кнопку Back, і Activity буде закриватися.

Action

Під час виклику destination нам може знадобитися задати деякі параметри, наприклад, анімацію, аргументи тощо. Для цього використовується action.

Давайте створимо action, який виконуватиме перехід від fragment1 до fragment2

Для destination fragment1 ми створили action, який веде в destination fragment2.

У action є різні параметри, які ми можемо налаштовувати в редакторі графа. Вони будуть використані під час переходу від destination fragment1 до destination fragment2.

Ми розберемо їх детально в наступних уроках. Поки що нас знову цікавить тільки значення атрибута ID. Ми можемо використовувати його під час виклику методу navigate, щоб викликати action. Давайте зробимо це після натискання на кнопку Next у Fragment1.

@Override
public void onFragment1NextClick() {
   navController.navigate(R.id.action_fragment1_to_fragment2);
}

Контролер зробить таке:

  1. візьме поточний destination (який зараз відображається в контейнері, тобто destination fragment1)
  2. знайде в нього action з ID = action_fragment1_to_fragment2
  3. визначить, що ця дія веде в destination fragment2
  4. визначить, що destination fragment2 - це фрагмент Fragment2
  5. відобразить Fragment2 і при цьому застосує параметри, які були задані в action_fragment1_to_fragment2

Якщо ми спробуємо викликати action, не перебуваючи в destination, якому цей action належить, то буде помилка. Тобто action action_fragment1_to_fragment2 ми можемо викликати тільки перебуваючи в destination fragment1, тому що під час створення action ми малювали його з destination fragment1.

З одного destination можна створити кілька action:

Activity

Як destination ми можемо використовувати не тільки фрагменти, а й Activity.

У цьому ж прикладі я створив SecondActivity і фрагменти Fragment4 і Fragment5. Будемо викликати їх із Fragment3, що знаходиться в MainActivity.

Відкриваємо граф main_graph і додаємо SecondActivity, як новий destination.

destination створено, його ID = secondActivity

При натисканні на кнопку Next у Fragment3 будемо викликати цей destination

@Override
public void onFragment3NextClick() {
   navController.navigate(R.id.secondActivity);
}

Контролер знайде в графі, що destination з таким ID відповідає SecondActivity і запустить його.

На цьому повноваження графа main_graph закінчуються. У новому Activity нам потрібен новий граф.

Створюємо second_graph і додаємо туди Fragment4 і Fragment5

Fragment4 - стартовий, він буде відображений під час відкриття SecondActivity.

У layout activity_second додаємо контейнер NavHostFragment і вказуємо граф second_graph

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".SecondActivity">
 
   <fragment
       android:id="@+id/nav_host_fragment"
       android:name="androidx.navigation.fragment.NavHostFragment"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       app:defaultNavHost="true"
       app:navGraph="@navigation/second_graph" />
 
</FrameLayout>

У SecondActivity знаходимо контролер і в обробниках натискань кнопок фрагментів використовуємо його для навігації.

NavController navController;
 
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_second);
    navController = Navigation.findNavController(this, R.id.nav_host_fragment);
}
 
@Override
public void onFragment4NextClick() {
    navController.navigate(R.id.fragment5);
}
 
@Override
public void onFragment4BackClick() {}
 
@Override
public void onFragment5NextClick() {}
 
@Override
public void onFragment5BackClick() {
    navController.popBackStack();
}

Запускаємо додаток

Із фрагмента Fragment3 переходимо у SecondActivity. Там відразу відкривається Fragment4, тому що він стартовий. З нього переходимо у Fragment5 і назад. А ось повертатися з SecondActivity у MainActivity доводиться за допомогою системної кнопки Back. Контролер у SecondActivity працює тільки в межах цього Activity. Він нічого не знає за його межами. Він не знає, що робити, коли викликається popBackStack у стартовому фрагменті, тобто у Fragment4. Тут уже нам треба самим. Наприклад, можна в onFragment4BackClick викликати метод finish, щоб закрити Activity.

Метод navController.popBackStack повертає boolean. Якщо контролер сам зміг повернутися на крок назад, то він поверне true. Якщо ж він не знає, що робити, то поверне false і в цьому випадку ми самі можемо обробити цю ситуацію.

destination

Наостанок кілька слів про поняття destination у Navigation Component. У грaфі у кожного destination є ID, і ми вказуємо цей ID у методі navigate, коли просимо контролер відкрити destination. При цьому нам не важливо, чим є в графі цей destination - Activity або фрагментом. Це турбота контролера. Він сам це визначить і викличе або startActivity, або працюватиме з FragmentManager.

Наприклад, у нас у додатку є екран конфігурації. Це фрагмент ConfigFragment. У графі у нас цей фрагмент фігурує як destination з ID = configScreen. І ми відкриваємо його викликом методу navController.navigate(R.id.configScreen).

Раптово ми вирішуємо, що треба екран конфігурації винести в окреме ConfigActivity. Створюємо це Activity, переносимо все туди і додаємо його в граф замість ConfigFragment, під тим же ID = configScreen.

При цьому в застосунку взагалі ніяк не змінюється код виклику екрана конфігурації. Це так і залишається виклик методу navigate із зазначенням ID = configScreen. Але тепер контролер відкриватиме не ConfigFragment у поточному контейнері, а запустить нове Activity, тому що ми налаштували це в графі.

А ось такий вигляд має вміст графа main_graph.xml:

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   app:startDestination="@id/fragment1">
   <fragment
       android:id="@+id/fragment1"
       android:name="ru.startandroid.navigation.Fragment1"
       android:label="fragment1"
       tools:layout="@layout/fragment1" >
       <action
           android:id="@+id/action_fragment1_to_fragment2"
           app:destination="@id/fragment2" />
   </fragment>
   <fragment
       android:id="@+id/fragment2"
       android:name="ru.startandroid.navigation.Fragment2"
       android:label="fragment2"
       tools:layout="@layout/fragment2" />
   <fragment
       android:id="@+id/fragment3"
       android:name="ru.startandroid.navigation.Fragment3"
       android:label="fragment3"
       tools:layout="@layout/fragment3" />
   <activity
       android:id="@+id/secondActivity"
       android:name="ru.startandroid.navigation.SecondActivity"
       android:label="activity_second"
       tools:layout="@layout/activity_second" />
</navigation>

Три фрагменти, одне Activity і в першого фрагмента є action, який веде в другий фрагмент.

Table of Contents