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

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

Шкодим

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

Fragment (Фрагменты). Часть вторая

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

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

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


<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=".MainActivity" >

    <Button
        android:id="@+id/button1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Рыжик" />

    <Button
        android:id="@+id/button2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Барсик" />

    <Button
        android:id="@+id/button3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Мурзик" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Описание кота"
        android:textAppearance="?android:attr/textAppearanceLarge" />

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:scaleType="fitCenter"
        android:src="@drawable/cat_yellow" />

</LinearLayout>

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

Заворачиваем в фрагменты

Фрагмент, как и активность, состоит из разметки и класса. Сначала займёмся разметкой.

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

Создадим две отдельные разметки и скопируем нужные части из общей разметки в разметки для фрагментов. Делаем щелчок правой кнопкой мыши на папке res/layout и выбираем New | Layout Resource File.

Создаём новый файл fragment1.xml и размещаем верхнюю часть кода:


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

    <Button
        android:id="@+id/button1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Рыжик" />

    <Button
        android:id="@+id/button2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Барсик" />

    <Button
        android:id="@+id/button3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Мурзик" />

</LinearLayout>

Также поступаем со вторым фрагментом - создаём новый файл fragment2.xml и в него копируем код из нижней части кода.


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

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Описание кота"
        android:textAppearance="?android:attr/textAppearanceLarge" />

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:scaleType="fitCenter"
        android:src="@drawable/cat" />

</LinearLayout>

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

Пока мы создали разметки для будущих фрагментов. Теперь нужно создать отдельные классы для двух фрагментов. Для начала укажем, что наш класс должен наследоваться от класса Fragment. Не копируйте, а пишите код самостоятельно. Я создаю классы вручную с нуля, можно также воспользоваться готовым шаблоном Fragment (Blank), которым пользовались в первой части.


// Kotlin
import androidx.fragment.app.Fragment

class Fragment1: Fragment(){
	
}

// Java public class Fragment1 extends Fragment { }

Следите, чтобы импортировался класс androidx.fragment.app.Fragment, а не устаревшие классы.

Самостоятельно создайте класс для второго фрагмента Fragment2 по такому же принципу.

Настало время подключить разметки к фрагментам. В активностях мы подключали разметку в методе onCreate() через метод setContentView(). В фрагментах метод onCreate() служит для других задач. А для подключения разметки используется отдельный метод onCreateView().

Чтобы долго не искать нужный нам метод, просто вводите на клавиатуре первую и заглавные буквы метода - ocv. Такой комбинации соответствует только один метод, который нам и нужен. Нажимаем кнопку OK и в код фрагмента будет вставлен следующий шаблон:


// Kotlin
override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    return super.onCreateView(inflater, container, savedInstanceState)
}

// Java @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return super.onCreateView(inflater, container, savedInstanceState); }

У метода используются три параметра. В первом параметре используется объект класса LayoutInflater, который позволяет построить нужный макет, считывая информацию из указанного XML-файла. Удалим строчку, которая возвращает результат и напишем свой вариант.


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

// Java @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { //return super.onCreateView(inflater, container, savedInstanceState); View rootView = inflater.inflate(R.layout.fragment1, container, false); return rootView; }

В Java-варианте код разбит на две части. Сначала мы получаем объект View, а затем уже его возвращаем в методе (не обязательно).

Скопируйте код метода onCreateView() и вставьте его в код класса Fragment2, не забыв указать разметку R.layout.fragment2.

Остальные два параметра container, false используются в связке и указывают на возможность подключения фрагментов в активность через контейнер динамически. Мы обойдёмся без динамики, а создадим собственные блоки для фрагментов, поэтому у нас используется значение false.

Возвращаемся к главной разметке активности. Смело удаляем все элементы с экрана, чтобы остался только корневой элемент LinearLayout.

В старых версиях студии на панели инструментов был готовый компонент <fragment>.

Fragment

Сейчас компонент убрали, поэтому напишем код вручную в режиме Code.


<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=".MainActivity">

    <fragment
        android:id="@+id/fragment1"
        android:name="ru.alexanderklimov.fragment.Fragment1"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        tools:layout="@layout/fragment1" />

    <fragment
        android:id="@+id/fragment2"
        android:name="ru.alexanderklimov.fragment.Fragment2"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        tools:layout="@layout/fragment2" />
</LinearLayout>

Обратите внимание на атрибут tools:layout="@layout/fragmentX" у тегов fragment, они помогут отобразить содержимое фрагментов в режиме дизайна.

Если сейчас запустим приложение, то тоже никаких изменений не увидим. Зачем тогда потратили столько времени на создание фрагментов? Непонятно.

А, я понял. Можно теперь писать в резюме про свои умения: использую фрагменты.

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

Мы знаем, что можно создать отдельную папку res/layout-land (перечитайте урок Ориентация) и разместить там разметку для такого случая.

Небольшой лайфхак: в режиме Design у файла activity_main.xml щёлкните на значке Orientation for Preview и в выпадающем списке выберите пункт Create Tablet variation - файл res/layout-land/activity_main.xml будет создан автоматически.

Скопируем файл activity_main.xml и вставим его в новую папку.

Скорее всего вы не видите созданную папку, но она есть! Переключитесь в режим Project и заново откройте структуру проекта, найдя нужную папку. В дальнейшем оставайтесь в этом режиме для данного урока.

Изменим разметку фрагмента для альбомной ориентации.


<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:baselineAligned="false"
    android:orientation="horizontal"
    tools:context=".MainActivity">

    <fragment
        android:id="@+id/fragment1"
        android:name="ru.alexanderklimov.fragment.Fragment1"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        tools:layout="@layout/fragment1" />

    <fragment
        android:id="@+id/fragment2"
        android:name="ru.alexanderklimov.fragment.Fragment2"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        tools:layout="@layout/fragment2" />

</LinearLayout>

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

Но это мы могли сделать и без фрагментов. Зачем же они нужны? Пока версия с лишней строчкой в резюме остаётся основной - чтобы работодатель уважал за прогрессивный стиль.

Хотя небольшое удобство есть. Благодаря модульности, мы поменяли разметку только у фрагментов, а то, что было внутри фрагментов (кнопки, текстовые блоки и т.д.), мы не трогали.

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

Давайте подключим поддержку планшетов. Создадим новую папку layout-sw600dp и скопируем в него файл из папки layout-land. Идентификатор sw600 говорит о минимальной ширине 600dp, что соответствует 7-дюймовым планшетам в альбомной ориентации. Существуют и другие варианты для планшетов с большими размерами.

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

Мы можем создать одну копию разметки и указать, чтобы её использовали все нужные размеры устройств.

Делаем следующее. В папке layout-land переименовываем файл activity_main.xml в activity_main_wide.xml (Refactor | Rename) и перемещаем файл в папку layout. Пустую папку layout-land можно удалить.

Теперь создайте новую папку res/values-land. В созданной папке создаём новый файл refs.xml (имя не имеет значения, но так принято).


<?xml version="1.0" encoding="utf-8"?>
<resources>

    <item name="activity_main" type="layout">@layout/activity_main_wide
</item>

</resources>

Этот файл говорит, что в альбомной ориентации вместо ресурса activity_main следует подключать ресурс layout/activity_main_wide. Можете запустить приложение и убедиться, что ничего не изменилось.

Если у вас будет поддержка альбомных ориентаций для разных размеров планшетов, то просто копируйте файл refs.xml в папки типа values-720dp_land и др.

Теперь вы можете вносить изменения в одном файле activity_main_wide.xml, а не по отдельности в каждом файле.

В первой части мы узнали, что для создания фрагмента необходимо создать разметку, затем новый класс и в методе onCreateView() указать разметку. Затем в разметке активности указать тег fragment и присвоить ему имя класса фрагмента.

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

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


// Kotlin
class Fragment1: Fragment(), View.OnClickListener {
	...
	
	override fun onClick(v: View?) {

    }
}

// Java public class Fragment1 extends Fragment implements View.OnClickListener { ... @Override public void onClick(View view) { } }

Подключаем кнопки в методе onViewCreated. Код будет похож на код, который мы обычно используем в методе onCreate() у активности, только метод findViewById() будет относиться уже не к классу Activity (обычно, мы опускали это), а к корневому элементу разметки фрагмента, в нашем случае view/rootView. В Java-варианте используется старый пример до появления метода onViewCreated(). Раньше приходилось писать код в onCreateView().


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

    val button1: Button = view.findViewById(R.id.button1)
    val button2: Button = view.findViewById(R.id.button2)
    val button3: Button = view.findViewById(R.id.button3)
    button1.setOnClickListener(this)
    button2.setOnClickListener(this)
    button3.setOnClickListener(this)
}

// Java @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment1, container, false); Button button1 = (Button) rootView.findViewById(R.id.button1); Button button2 = (Button) rootView.findViewById(R.id.button2); Button button3 = (Button) rootView.findViewById(R.id.button3); button1.setOnClickListener(this); button2.setOnClickListener(this); button3.setOnClickListener(this); return rootView; }

Для начала просто выведем сообщение, что кнопка нажата.


// Kotlin
override fun onClick(v: View?) {
    println("Вы нажали на кнопку")
}

// Java @Override public void onClick(View view) { Toast.makeText(getActivity(), "Вы нажали на кнопку", Toast.LENGTH_SHORT).show(); }

Запустите пример и проверьте. Но у нас три кнопки. Надо написать код, который бы получал информацию о нажатой кнопке, чтобы активность могла использовать эту информацию и использовать её для управления вторым фрагментом. Для удобства создадим в классе Fragment1 отдельный метод, который на основании идентификатора кнопки создаст нужный индекс:


// Kotlin
private fun translateIdToIndex(id: Int): Int {
    var index = -1
    when (id) {
        R.id.button1 -> index = 1
        R.id.button2 -> index = 2
        R.id.button3 -> index = 3
    }
    return index
}

// Java int translateIdToIndex(int id) { int index = -1; switch (id) { case R.id.button1: index = 1; break; case R.id.button2: index = 2; break; case R.id.button3: index = 3; break; } return index; }

Каждой кнопке соответствует свой индекс от 1 до 3.

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

Перепишем код для щелчка кнопки, чтобы узнать индекс нажатой кнопки.


// Kotlin
override fun onClick(v: View?) {
	// Временный код для получения индекса нажатой кнопки
    val buttonIndex = translateIdToIndex(v!!.id)
    println("Вы щёлкнули по кнопке $buttonIndex")
}

// Java @Override public void onClick(View view) { // Временный код для получения индекса нажатой кнопки int buttonIndex = translateIdToIndex(view.getId()); Toast.makeText(getActivity(), String.valueOf(buttonIndex), Toast.LENGTH_SHORT).show(); }

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

Для этой цели используются интерфейсы.

Открываем код первого фрагмента Fragment1 и объявляем интерфейс с единственным методом до объявления самого класса Fragment1:


// Kotlin
interface OnSelectedButtonListener {
    fun onButtonSelected(buttonIndex: Int)
}

// Java public interface OnSelectedButtonListener { void onButtonSelected(int buttonIndex); }

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

У нас интерфейс будет использовать класс активности.

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


// Kotlin
class MainActivity : AppCompatActivity(), OnSelectedButtonListener {}

// Java public class MainActivity extends AppCompatActivity implements Fragment1.OnSelectedButtonListener {}

Среда разработки поможет создать заготовку для необходимого метода:


// Kotlin
override fun onButtonSelected(buttonIndex: Int) {
    TODO("Not yet implemented")
}

// Java @Override public void onButtonSelected(int buttonIndex) {}

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

Но сначала подготовим второй фрагмент к работе. Объявим ссылки на компоненты, которые есть в разметке второго фрагмента. А также загрузим массив строк из ресурсов, который будем использовать для описания котов. Не забывайте, что в Java-варианте используется устаревший код.


// Kotlin
package ru.alexanderklimov.fragment

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

class Fragment2 : Fragment() {
    private lateinit var infoTextView: TextView
    private lateinit var catImageView: ImageView
    private lateinit var catDescriptions: Array<String>

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

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

        infoTextView = view.findViewById(R.id.textView)
        catImageView = view.findViewById(R.id.imageView)
        catDescriptions = resources.getStringArray(R.array.cats)
    }
}

// Java public class Fragment2 extends Fragment { private TextView mInfoTextView; private ImageView mCatImageView; private String[] mCatDescriptionArray; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment2, container, false); mInfoTextView = (TextView) rootView.findViewById(R.id.textView); mCatImageView = (ImageView) rootView.findViewById(R.id.imageView); // загружаем массив из ресурсов mCatDescriptionArray = getResources().getStringArray(R.array.cats); return rootView; } }

Массив задаём в ресурсах (файл res/values/strings.xml). Так как первый элемент массива идёт под индексом 0, то добавим нейтральный текст:


<string-array name="cats">
    <item>Просто кот</item>
    <item>Рыжик - рыжий кот</item>
    <item>Барсик болеет за Барселону</item>
    <item>Мурзик выписывает Мурзилку</item>
</string-array>

Подготовим метод, который будет менять содержимое фрагмента в зависимости от индекса нажатой кнопки:


// Kotlin
fun setDescription(buttonIndex: Int) {
    val description: String = catDescriptions[buttonIndex]
    infoTextView.text = description
    when (buttonIndex) {
        1 -> catImageView.setImageResource(R.drawable.cat1)
        2 -> catImageView.setImageResource(R.drawable.cat2)
        3 -> catImageView.setImageResource(R.drawable.cat3)
        else -> {
        }
    }
}

// Java public void setDescription(int buttonIndex) { String catDescription = mCatDescriptionArray[buttonIndex]; mInfoTextView.setText(catDescription); switch (buttonIndex) { case 1: mCatImageView.setImageResource(R.drawable.cat1); break; case 2: mCatImageView.setImageResource(R.drawable.cat2); break; case 3: mCatImageView.setImageResource(R.drawable.cat3); break; default: break; } }

Осталось только получить информацию от активности (не от фрагмента) об индексе.

Опять возвращаемся в активность и напишем код для пустого метода onButtonSelected(), который будет получать от первого фрагмента индекс нажатой кнопки и передавать его второму фрагменту:


// Kotlin
override fun onButtonSelected(buttonIndex: Int) {
    // подключаем FragmentManager
    val fragmentManager = supportFragmentManager

    // Получаем ссылку на второй фрагмент по ID
    val fragment2 = fragmentManager.findFragmentById(R.id.fragment2) as Fragment2?
    fragment2?.setDescription(buttonIndex)
}

// Java @Override public void onButtonSelected(int buttonIndex) { // подключаем FragmentManager FragmentManager fragmentManager = getSupportFragmentManager(); // Получаем ссылку на второй фрагмент по ID Fragment2 fragment2 = (Fragment2) fragmentManager .findFragmentById(R.id.fragment2); // Выводим нужную информацию if (fragment2 != null) fragment2.setDescription(buttonIndex); }

Активность получает доступ к своим фрагментам через специальный менеджер фрагментов (коты называют его манагером). Менеджер есть у любой активности, поэтому мы его не создаём через конструкцию new FragmentManager, а получаем через метод getSupportFragmentManager().

Менеджер фрагментов держит в руках все нити управления над своими фрагментами. Найти нужный фрагмент можно по идентификатору через метод FragmentManager.findFragmentById(), который похож на метод findViewById() для получения идентификатора кнопки, метки и т.д. У менеджера есть ещё один метод для поиска фрагмента по тегу findFragmentByTag().

В созданной заготовке вызываем менеджер фрагментов, получаем ссылку на второй фрагмент через его идентификатор и вызываем его метод setDescription().

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

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

Но теперь в методе onClick() мы можем получить доступ к слушателю активности.


// Kotlin
override fun onClick(v: View?) {

    val buttonIndex = translateIdToIndex(v!!.id)

    val listener = activity as OnSelectedButtonListener?
    listener?.onButtonSelected(buttonIndex)
}

// Java @Override public void onClick(View view) { int buttonIndex = translateIdToIndex(view.getId()); OnSelectedButtonListener listener = (OnSelectedButtonListener) getActivity(); listener.onButtonSelected(buttonIndex); // Можно закомментировать Toast.makeText(getActivity(), String.valueOf(buttonIndex), Toast.LENGTH_SHORT).show(); }

По цепочке мы передаём информацию от первого фрагмента в активность, а затем активность передаёт информацию во второй фрагмент.

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

Запустите проект и проверьте на работоспособность. Для данного случая мы пока не получили никаких преимуществ в использовании фрагментов. Но сейчас главное для вас - понять основные принципы создания и взаимодействия фрагментов.

Спустя несколько лет возможности фрагментов расширились и теперь передавать данные можно проще.

Часть третья. Продолжение

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

Обсуждение статьи на форуме.

Реклама