Урок 15. Передача данных между экранами – пунктами назначения. Android Navigation. Bundle vs Safe Args

Продолжаем серию уроков по разработке android-приложений в Android Studio на языке Kotlin.

На прошлом уроке мы выполняли навигацию по условию, авторизован пользователь или нет.

На этом уроке

На этом уроке рассмотрим возможности передачи данных между экранами – пунктами назначения навигации в андроид-приложении. Создадим приложение с двумя экранами. На первом экране будет поле для ввода имени и кнопка отправки, а на втором экране будет отображаться приветствие с именем, которое мы указали. Таким образом, мы передадим данные (имя) с первого экрана на второй.

Передача данных между экранами

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

Bundle или Safe Args?

В этом уроке мы рассмотрим два способа передачи данных между фрагментами: традиционный – с помощью наборов данных Bundle и типобезопасный – при помощи безопасных аргументов SafeArgs. Первый способ относительно прост – создаем набор данных «ключ-значение» типа Bundle и передаем через action в первом фрагменте, и извлекаем во втором фрагменте.
Второй способ потребует немного больше кода. На первый взгляд он может показаться сложнее, поскольку используется кодогенерация – среда разработки создает необходимые классы вместо вас. Но мы попробуем разобраться и вы увидите, что ничего особо сложного там нет. По сути, SafeArgs – просто обертка над Bundle. Тем не менее, разработчики настоятельно рекомендуют применять именно SafeArgs, как типобезопасный способ передачи данных между фрагментами в процессе навигации.

Создаем проект

Откройте среду разработки Android Studio и создайте новый проект с использованием шаблона Empty Activity.

Создаем граф навигации

Далее перейдите в папку res и создайте в ней папку navigation.  Внутри папки navigation создайте Navigation Resource File с именем nav_graph.xml и корневым элементом <navigation>.

Если вы забыли добавить в проект необходимые для поддержки навигации библиотеки – Android Studio предложит это сделать за вас, показав предупреждение.

Добавляем пункты назначения – фрагменты

Добавьте новые пункты назначения. Для этого:

  1. В окне редактора дизайна нажмите кнопку «New destination»
  2. Выберите «Create new destination»
  3. Далее в окне добавления фрагмента выберите Fragment (Blank):

Создайте таким образом два фрагмента:

  • FragmentOne
  • FragmentTwo

 

Чтобы фрагменты из графа навигации отображались на экране, не забудьте добавить в макет главного активити activity_main.xml компонент fragment — хост навигации вместо TextView:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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_graph"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

 

Создаем action

 

Вернемся в nav_graph.xml и добавим action для перехода. Можно просто соединить фрагменты стрелкой  на экране редактора дизайна, и в коде графа навигации добавится секция <action>:

<?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/nav_graph"
    app:startDestination="@id/fragmentOne">

    <fragment
        android:id="@+id/fragmentOne"
        android:name="info.fandroid.myapplication15.FragmentOne"
        android:label="fragment_one"
        tools:layout="@layout/fragment_one" >
        <action
            android:id="@+id/action_fragmentOne_to_fragmentTwo"
            app:destination="@id/fragmentTwo" />
    </fragment>
    <fragment
        android:id="@+id/fragmentTwo"
        android:name="info.fandroid.myapplication15.FragmentTwo"
        android:label="fragment_two"
        tools:layout="@layout/fragment_two" >
    </fragment>
</navigation>

 

 

Добавим аргумент

Для передачи данных нужно использовать элемент <argument>, добавим его в граф навигации. Для этого выделите второй фрагмент в редакторе дизайна и справа в панели атрибутов на вкладке Arguments нажмите плюс. Укажите имя аргумента, например, MyArg и значение по умолчанию. Тип можно не указывать. Я передам в качестве дефолтного значения текст «Hello, Android!». Значение по умолчанию будет отображаться, если мы ничего не передадим из первого фрагмента во второй.

<fragment
    android:id="@+id/fragmentTwo"
    android:name="info.fandroid.myapplication15.FragmentTwo"
    android:label="fragment_two"
    tools:layout="@layout/fragment_two" >
    <argument
        android:name="MyArg"
        android:defaultValue="Hello, Android!" />
</fragment>

Подготовим макеты фрагментов

Измените макет разметки первого фрагмента fragment_one.xml таким образом:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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=".FragmentOne">

    <TextView
        android:id="@+id/tvFragmentOne"
        android:textSize="48sp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/text_what_your_name"
        app:layout_constraintBottom_toTopOf="@+id/editText"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/editText"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.53"
        app:layout_constraintEnd_toStartOf="@+id/imgButton"/>

    <ImageButton
        android:id="@+id/imgButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/baseline_send_black_36"
        app:layout_constraintBottom_toBottomOf="@+id/editText"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toEndOf="@+id/editText"
        app:layout_constraintTop_toTopOf="@+id/editText" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

Здесь мы оставили текстовое поле, которое теперь отображает текст «Ваше имя?». Ниже TextView добавлен EditText для возможности вписать туда имя, и кнопка отправки ImageButton, значок для которой был скачан с сайта https://material.io/resources/icons/
Вы также можете скачать его оттуда, либо взять из исходников этого проекта по ссылке в конце текстовой версии урока.

Введенный на первом экране текст будем передавать во второй фрагмент для отображения в текстовом поле. Откройте макет второго фрагмента fragment_two.xml и сделайте так, чтобы элемент TextView был посредине. Для этого можно заменить корневой FrameLayout, например, на ConstraintLayout. Также добавьте идентификатор для TextView.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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=".FragmentTwo">


    <TextView
        android:id="@+id/tvFragmentTwo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="@string/hello_blank_fragment"
        android:textSize="48sp"/>

</androidx.constraintlayout.widget.ConstraintLayout>

 

 

 Передаем данные через Bundle

В классе FragmentOne переопределим метод onViewCreated и напишем в нем такой код:

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

    val imgButton : ImageButton = view.findViewById(R.id.imgButton)
    val editText : EditText = view.findViewById(R.id.editText)
    val bundle = Bundle()

    imgButton.setOnClickListener {
        val name = editText.text
        val hello = "Привет, $name"
        bundle.putString("MyArg", hello)
        findNavController().navigate(R.id.fragmentTwo, bundle)
    }
 }

 

Обратите внимание – переменные кнопки и поля для ввода текста мы инициализируем через метод findViewById, как мы это делали на первых уроках. Этот способ требует больше повторяющегося кода в отличие от использования ViewBinding и DataBinding для привязки элементов разметки, однако многие программисты пользуются именно этим способом, как самым простым и надежным.
Объявим также переменную для набора данных Bundle.

Далее присваиваем слушатель кнопке, и по ее нажатию производим следующие действия:

  • Инициализируем переменную name и сохраняем в нее текст из поля ввода;
  • Переменная hello уже содержит текст и добавляет в него текст переменной name;
  • В переменную bundle передаем переменную hello с ключем «MyArg»
  • Обращаемся к контроллеру навигации и реализуем переход к FragmentTwo с передачей идентификатора макета фрагмента и bundle в метод navigate контроллера.

 Принимаем данные через Bundle

В коде класса FragmentTwo также переопределяем метод onViewCreated и пишем в него код:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
   
    val tv :TextView = view.findViewById(R.id.tvFragmentTwo)
    val text = arguments?.getString("MyArg")
    tv.text = text

}

 

Здесь мы инициализируем TextView и переменную text. В эту переменную мы сохраняем значение из переданного Bundle, обращаясь к нему через его геттер arguments, и получаем строку с ключом «MyArg». Далее передаем ее для отображения в текстовое поле.

Тестирование приложения

Запустите приложения на эмуляторе или смартфоне. На первом экране с полем ввода и кнопкой введите имя и нажмите кнопку отправки. Откроется второй экран, где будет отображен текст приветствия с введенным именем.

Подключение в проект Safe Args

Несмотря на простоту этого способа, официальные разработчики все же рекомендуют выполнять передачу данных между фрагментами посредством Safe Args, так как этот способ является типобезопасным.

Чтобы добавить поддержку Safe Args в проект, в файле build.gradle верхнего уровня пропишите:

buildscript {
    repositories {
        google()
    }
    dependencies {
        def nav_version = "2.3.3"
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
    }
}

 

Вы также должны применить один из двух доступных плагинов.

Если ваш проект на языке Java, или смешанный, на Java и Kotlin, добавьте эту строку в файл build.gradle модуля app вашего приложения:

apply plugin: "androidx.navigation.safeargs"

 

А если проект только на Kotlin, то добавьте эту:

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

 

Также вы должны для поддержки AndroidX указать android.useAndroidX=true в файле gradle.properties .

Передача данных через Safe Args

Теперь изменим код фрагментов.

Метод onViewCreated класса FragmentOne:

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

    val imgButton : ImageButton = view.findViewById(R.id.imgButton)
    val editText : EditText = view.findViewById(R.id.editText)
    //val bundle = Bundle()

    imgButton.setOnClickListener {
        val name = editText.text
        val hello = "Привет, $name"
       
        /*bundle.putString("MyArg", hello)
        findNavController().navigate(R.id.fragmentTwo, bundle)*/

        val action = FragmentOneDirections.actionFragmentOneToFragmentTwo(hello)
        findNavController().navigate(action)                                                                                                                                                                                                                                                                                                                                                                                                                          
    }
   
}

 

Поскольку мы теперь не работаем с Bundle напрямую, в методе onViewCreated закомментируем строку объявления его переменной.

Далее в слушателе нажатия кнопки вместо сохранения аргумента в bundle и передачи его контроллеру, будем использовать сгенерированный средой разработки класс FragmentOneDirections. В его функцию actionFragmentOneToFragmentTwo передаем строковую переменную hello и затем это все передаем методу navigate контроллера навигации.

А если мы откроем класс FragmentOneDirections, то увидим, что внутри он использует все тот же Bundle.

Теперь изменим код метода onViewCreated класса FragmentTwo:

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

    val tv :TextView = view.findViewById(R.id.tvFragmentTwo)
    //val text = arguments?.getString("MyArg")
    val args: FragmentTwoArgs by navArgs()
    val text = args.MyArg
    tv.text = text

}

 

Вместо получения строки из Bundle инициализируем переменную класса FragmentTwoArgs, который хранит данные, переданные нами из первого фрагмента. Мы можем извлечь данные, обращаясь к свойству экземпляра класса по имени аргумента, которое мы указали при его создании в графе навигации. Здесь мы передаем его в текстовое поле.

Если открыть FragmentTwoArgs, мы увидим, что это дата-класс с публичной переменной, имя которой совпадает с именем нашего аргумента. Эта переменная получает свое значение посредством Bundle, а также имеет значение по умолчанию – это фраза «Hello, Android!» –  та самая, которую мы указали как значение по умолчанию аргумента в графе навигации.

Оба класса – FragmentTwoArgs и FragmentOneDirections – сгенерированы средой разработки, их можно найти в папке build модуля app нашего проекта.

Тестирование приложения

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

Если в коде второго фрагмента ничего не передать в переменную action, то на втором экране будет отображаться значение аргумента по умолчанию.

Исходный код

На этом наш урок закончен, исходный код проекта вы можете скачать по ссылке.
Вопросы задавайте в комментариях. До встречи на следующем уроке.

Урок 16. Android Navigation. Анимация переходов между пунктами назначения. Transition Framework & Animation Framework 

Коментарі: 2
  1. fessn14@gmail.com

    Ладно еще датабиндиг юзаете…
    А почему kotlin synthetic не используете?
    Это же прошлый век так вьюхи искать

    1. admin (автор)

      kotlin synthetic deprecated, если что…

Додати коментар