Освой Android играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

Используем фрагменты. Кошачий переводчик

Кот из фрагментов

Давным-давно, когда только появились фрагменты, я написал статью о них. Сначала они были сложными и неудобными. Программисты критиковали, предлагали свои решения или вообще их не использовали. Потом разработчики Android стали развивать фрагменты, добавляя новые возможности и упрощая где это возможно, их использование. Материала было столько много, что статью пришлось разбить на несколько частей. Со временем, часть материала устарела. Я решил переписать заново статью для первого знакомства с фрагментами. Старый материал пока останется (ссылка на первую статью о фрагментах), но со временем статьи придётся почистить или удалить.

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

Но в реальности это устаревший подход. Хотя было время, когда фрагментов не существовало и создание новых активностей было единственным выходом для сложных приложений. Но любая активность потребляет слишком много ресурсов и современные приложения из трёх и более активностей стали непозволительной роскошью. Сейчас так никто не делает, даже коты.

Мы создадим специальную программу, состоящую из нескольких экранов в рамках одной активности. На первом экране мы разместим картинку, кнопку и поясняющий текст. На втором будет текстовое поле для ввода человеческих слов и кнопка, на третьем экране мы увидим перевод введённого текста на кошачий язык!

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

StartFragment

Фрагмент частично похож на активность и для его создания мы можем воспользоваться мастером шаблонов, который напоминает мастер создания новой активности.

Для этого из контекстного меню пакета проекта выбираем New | Fragment | Fragment (Blank). Заполняем поля для конфигурации фрагмента.

New Fragment

Студия создаст два файла для фрагмента. Первый - это файл StartFragment.kt в папке пакета и файл fragment_start.xml в папке ресурсов res/layout.

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


package ru.alexanderklimov.giraffe

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

/**
 * A simple [Fragment] subclass.
 * Use the [StartFragment.newInstance] factory method to
 * create an instance of this fragment.
 */
class StartFragment : Fragment() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_start, container, false)
    }

    companion object {
        /**
         * Use this factory method to create a new instance of
         * this fragment using the provided parameters.
         *
         * @return A new instance of fragment StartFragment.
         */
        @JvmStatic
        fun newInstance() = StartFragment()
    }
}

Если искать аналогию с активностью, то метод фрагмента onCreateView() очень похож на метод активности onCreate(). И там и там, мы формируем экран за счёт XML-файла из ресурсов layout. Только в фрагментах это происходит через inflate() вместо setContentView(), но суть такая же. Метод onCreateView() вызывается в тот момент, когда система обращается к макету фрагмента. В большинстве случаев вы будете использовать данный метод (если фрагмент используется для экранов).

У метода три параметра. Первый параметр LayoutInflater используется для заполнения макета, о чём было сказано выше. Второй параметр ViewGroup определяет компонент-контейнер в макете активности, используемое для отображения фрагмента. Третий параметр Bundle нужен для сохранения состояния фрагмента (похоже как в активности). Метод возвращает View - заполненный компонент.

Рассмотрим макет фрагмента. Студия по умолчанию создаёт макет из FrameLayout, который содержит TextView. Для нашей задачи такой вариант нам не подходит, поэтому можно смело заменить на любой свой макет. Нам нужна кнопка, картинка и текст.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical"
    android:padding="4dp"
    tools:context=".StartFragment">

    <ImageView
        android:id="@+id/startfragment_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:srcCompat="@drawable/cat_tongue" />

    <TextView
        android:id="@+id/startfragment_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/startfragment_intro"
        android:textAlignment="center"
        android:textSize="24sp" />

    <Button
        android:id="@+id/startfragment_button_start"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Начать" />

</LinearLayout>

StartFragment

Вы можете использовать любой свой макет со своими картинками и текстом.

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

Открываем макет активности activity_main.xml и устанавливаем контейнер FragmentContainerView. Технически можно использовать любой контейнер, даже LinearLayout, чаше использовали FrameLayout, как самый простой и лёгкий вариант. Но затем в библиотеку внедрили новый контейнер, который является родственником FrameLayout. Вам необходимо указать у контейнера идентификатор и имя класса фрагмента.


<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:name="ru.alexanderklimov.giraffe.StartFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" />

В код активности никаких изменений вносить не нужно. Метод setContentView() загрузит макет активности, который содержит в свою очередь макет фрагмента.


package ru.alexanderklimov.giraffe

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

    }
}

Кнопка, которая находится в фрагменте, пока не работает. Её код мы будем писать в классе фрагмента чуть позже.

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


<LinearLayout
    android:orientation="vertical">
    ... />
    
    <androidx.fragment.app.FragmentContainerView
        android:id = "@+id/first_fragment"
        android:name = "..."
        ... />

    <androidx.fragment.app.FragmentContainerView
        android:id = "@+id/second_fragment"
        android:name = "...."
        ... />
</LinearLayout>

Теперь, когда мы поняли, как создать новый фрагмент с нуля, можем повторить предыдущие шаги и создать второй фрагмент MessageFragment, но с другим макетом. Во втором фрагменте у нас будет текстовое поле для ввода текста и кнопка для перехода на третий фрагмент. С помощью мастера студии создаём фрагмент (получим два новых файла: MessageFragment и fragment_message.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:id="@+id/frameLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MessageFragment">

    <EditText
        android:id="@+id/messagefragment_edit"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:hint="Введите текст для перевода на кошачий язык"
        android:inputType="text"
        android:textSize="24sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/messagefragment_translate_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Перевести"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/messagefragment_edit" />
</androidx.constraintlayout.widget.ConstraintLayout>

Navigation

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

Для навигации нужны три вещи: граф, хост и контроллер.

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

Хост навигации - это пустой контейнер, используемый для отображения фрагмента. Он добавляется в макет активности.

Контроллер навигации управляет тем, какой фрагмент отображается в хосте навигации. Контроль осуществляется программным способом.

Navigation подключается отдельно через зависимость в build.gradle.


implementation("androidx.navigation:navigation-fragment-ktx:2.7.3")

Изначально, я использовал старую версию 2.3.5. Потом в новых версиях появились новые возможности, которые вроде в статье не понадобились, но в других проектах может быть критичным.

Можно приступать к созданию графа навигации. Через контекстное меню папки res New | Android Resource File создайте файл nav_graph.xml.

New Android Resource File

Студия предложит визуальный редактор для настройки графа. По нашей задумке пользователь должен переходить от StartFragment к MessageFragment. Добавляем первый фрагмент. В центре визуального редактора есть подсказка - Click значок to add a destination. Сам значок находится на верхней панели.

New Destination

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

Далее следует добавить второй фрагмент, повторив предыдущий шаг. Фрагмент появится в случайном месте редактора. Вы можете его свободно перетаскивать по всей области редактирования. Разместите его рядом с первым фрагментом.

Установим связь между фрагментами. Выделим первый фрагмент. У него сбоку появляется кружок. С помощью мыши протяните стрелку из кружка в сторону второго фрагмента. Тем самым мы задали action (действие). Каждое действие должно иметь свой идентификатор. По умолчанию студия присвоит свой идентификатор, который можно отредактировать, если вам не понравится предложенный вариант. У меня получилось action_startFragment_to_messageFragment. Обратите внимание, что стрелку-действие можно выделить в редакторе наряду с фрагментами. Это отдельный элемент навигации.

Вы можете переключиться в текстовый режим и посмотреть, как выглядит код. Разметка состоит из нескольких блоков. Сначала идёт общий блок navigation с атрибутом startDestination. Внутри общего блока перечислены наши фрагменты. При этом у первого фрагмента указан блок action. Мы подготовили граф навигации и можем двигаться дальше.

Следующий важный шаг - добавление хоста навигации. У компонента Navigation уже есть встроенный хост NavHostFragment (субкласс Fragment с реализацией интерфейса NavHost).

Отредактируем activity_main.xml.


<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView
    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_host_fragment_container"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/container"
    android:name="ru.alexanderklimov.giraffe.StartFragment"
    app:defaultNavHost="true"
    app:navGraph="@navigation/nav_graph"
    tools:context=".MainActivity" />

Заключительный шаг - код для кнопки, чтобы переходить с одного фрагмента на другой. Сначала мы должны получить ссылку на контроллер навигации, а затем выбрать цель. Код для кнопки прописывается в методе onCreateView(), сам метод немного отредактируем для удобства.


override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    val view = inflater.inflate(R.layout.fragment_start, container, false)

    val startButton = view.findViewById<Button>(R.id.startfragment_button_start)
    startButton.setOnClickListener {
        val navController = view.findNavController() // получаем ссылку на контроллер
        // переходим на цель
        navController.navigate(R.id.action_startFragment_to_messageFragment)

        // или без создания переменной
        // view.findNavController().navigate(R.id.action_startFragment_to_messageFragment)
    }

    // Inflate the layout for this fragment
    return view // создадим переменную для удобства
}

Если нужно было бы сделать выбор, на какой фрагмент переходить, то просто добавьте свою логику.


if (isCatChoosen){ // булева переменная
    navController.navigate(R.id.action_startFragment_to_messageFragment)
} else{
    // переходим на другой фрагмент
    navController.navigate(R.id.action_startFragment_to_secondFragment)
}

Можно запустить пример и проверить работоспособность кнопки - при её нажатии приложение покажет нам новый экран от второй активности.

Передача данных между фрагментами

Следующий шаг - перейти со второго фрагмента на третий. Общий принцип мы уловили, поэтому будет легче. При этом появилась новая проблема, мы должны не только перейти, но и передать информацию между фрагментами.

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

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

Для макета фрагмента fragment_converter.xml достаточно одной текстовой метки.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical"
    tools:context=".ConverterFragment">

    <TextView
        android:id="@+id/converterfragment_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20sp" />

</LinearLayout>

Почистим код класса фрагмента.


package ru.alexanderklimov.giraffe

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment

class ConverterFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_converter, container, false)
    }

    companion object {
        @JvmStatic
        fun newInstance() = ConverterFragment()
    }
}

Созданный фрагмент нужно включить в наш граф навигации. Снова открываем визуальный редактор файла nav_graph.xml и добавляем наш фрагмент рядом с двумя предыдущими. Соединим стрелкой фрагмент messageFragment с ConverterFragment - мы получим действие с идентификатором action_messageFragment_to_converterFragment.

Nav Graph

Для перехода со второго фрагмента на третий по щелчку кнопки пока можно скопировать код из щелчка кнопки первого фрагмента.


// MessageFragment

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    val view = inflater.inflate(R.layout.fragment_message, container, false)
    val translateButton = view.findViewById<Button>(R.id.messagefragment_translate_button)
    translateButton.setOnClickListener{
        view.findNavController().navigate(R.id.action_messageFragment_to_converterFragment)
    }

    return view
}

Проверим работоспособность кода. Последовательно перейдём с первого фрагмента на второй и третий.

Если всё работает, то начинаем подключать плагин Safe Args. В файле build.gradle проекта (не модуля!) добавляем новую строку.


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

Также в этом же файле можно прописать и зависимость, но лучше это сделать в файле модуля build.gradle:


plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")

    id("androidx.navigation.safeargs.kotlin")
}

Переходим в визуальный редактор nav_graph.xml, выделяем третий фрагмент и на панели находим раздел Attributes. Щёлкаем на кнопке "+" рядом с разделом Arguments, чтобы вызвать диалоговое окно Add Arguments. В нём указываем имя аргумента (любое) и тип (в нашем случае строковый).

Add Arguments

Если смотреть в код XML, то увидим новый блок argument.


<fragment
    android:id="@+id/converterFragment"
    android:name="ru.alexanderklimov.giraffe.ConverterFragment"
    android:label="fragment_converter"
    tools:layout="@layout/fragment_converter" >
    <argument
        android:name="message"
        app:argType="string" />
</fragment>

Теперь второй фрагмент может передать данные, используя созданный аргумент. Плагин Safe Args создаёт для каждого фрагмента специальный класс Directions. Во втором фрагменте для щелчка кнопки меняем код.


override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    val view = inflater.inflate(R.layout.fragment_message, container, false)
    val messageEditText = view.findViewById<EditText>(R.id.messagefragment_edit)

    val translateButton = view.findViewById<Button>(R.id.messagefragment_translate_button)
    translateButton.setOnClickListener {
        val message = messageEditText.text.toString()
        val action = MessageFragmentDirections
            .actionMessageFragmentToConverterFragment(message)
        view.findNavController().navigate(action)
        //view.findNavController().navigate(R.id.action_messageFragment_to_converterFragment)
    }

    // Inflate the layout for this fragment
    return view
}

Чуть подробнее об изменениях. Мы добавляем ссылку на текстовое поле и переменную message, которая хранит текст из этого поля. Далее заводим переменную action и используем подсказки студии, чтобы быстро набрать длинные названия классов и методов. Эти имена генерируются плагином и вам нужно только правильно выбирать предлагаемые варианты.

В методе navigate() мы теперь передаёт не идентификатор действия, а созданную переменную action, которая содержит строку для передачи.

Тот же плагин Safe Args создаёт в дополнение к классам Directions и классы Args (тоже для каждого фрагмента, который получает аргументы). В нашем случае плагин создаст класс ConverterFragmentArgs. У класса есть метод fromBundle(), который позволяет получить аргументы.

Давайте сначала просто получим текст в готовом виде без изменений, чтобы убедиться в работоспособности кода. У третьего фрагмента пишем следующий код.


override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    val view = inflater.inflate(R.layout.fragment_converter, container, false)

    val message = ConverterFragmentArgs.fromBundle(requireArguments()).message
    
    val translatedText = view.findViewById<TextView>(R.id.converterfragment_text)
    translatedText.text = message

    return view
}

Убеждаемся, что код работает и текст передаётся из одного фрагмента в другой. Осталось сделать главный шаг - сделать перевод. Считаем, сколько символов содержится в сообщении и конвертируем количество в число повторений "мяу".


override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    val view = inflater.inflate(R.layout.fragment_converter, container, false)

    val message = ConverterFragmentArgs.fromBundle(requireArguments()).message
    var meow = ""
    repeat(message.length){
        meow += "мяу "
    }
    val translatedText = view.findViewById<TextView>(R.id.converterfragment_text)
    translatedText.text = meow

    return view
}

Вероятно, вы будете разочарованы. Ожидали, что я действительно поделюсь с вами исходниками кошачьего переводчика? Коты не разрешили мне раскрывать секреты языка. Разговаривайте с ними на нормальном человеческом языке. Помните картинку?

Разговор с котом

Итак, у нас получилось готовое приложение из трёх фрагментов на одной активности.

Cat translator

В завершение проекта изучим возможность менять поведение системной кнопки "Назад". Сейчас по умолчанию, при нажатии этой кнопки на третьем фрагменте мы переместимся на второй фрагмент, а затем на первый (затем приложение закроется). Если нам хочется, чтобы с третьего фрагмента сразу оказаться на первом, минуя второй, то в графе навигации нужно внести изменения.

В очередной раз открываем визуальный редактор файла nav_graph.xml и выделяем стрелку-действие между вторым и третьим фрагментами. На панели в разделе Pop Behavior установите у свойства popUpTo значение startFragment. В XML-коде у второго фрагмента появится новая строка.


<action
    android:id="@+id/action_messageFragment_to_converterFragment"
    app:destination="@id/converterFragment"
    app:popUpTo="@id/startFragment" />

Запустите приложение, дойдите до экрана третьего фрагмента и нажмите кнопку "Назад". Вы сразу же попадётся на экран первого фрагмента, минуя второй фрагмент.

Дополнительное чтение

Реклама