lightbulb_outline Help shape the future of the Google Play Console, Android Studio, and Firebase. Start survey

Фрагменты

Фрагмент (класс Fragment) представляет поведение или часть пользовательского интерфейса в операции (класс Activity). Разработчик может объединить несколько фрагментов в одну операцию для построения многопанельного пользовательского интерфейса и повторного использования фрагмента в нескольких операциях. Фрагмент можно рассматривать как модульную часть операции. Такая часть имеет свой жизненный цикл и самостоятельно обрабатывает события ввода. Кроме того, ее можно добавить или удалить непосредственно во время выполнения операции. Это нечто вроде вложенной операции, которую можно многократно использовать в различных операциях.

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

Когда фрагмент добавлен как часть макета операции, он находится в объекте ViewGroup внутри иерархии представлений операции и определяет собственный макет представлений. Разработчик может вставить фрагмент в макет операции двумя способами. Для этого следует объявить фрагмент в файле макета операции как элемент <fragment> или добавить его в существующий объектViewGroup в коде приложения. Впрочем, фрагмент не обязан быть частью макета операции. Можно использовать фрагмент без интерфейса в качестве невидимого рабочего потока операции.

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

Философия проектирования

Фрагменты впервые появились в Android версии 3.0 (API уровня 11), главным образом, для обеспечения большей динамичности и гибкости пользовательских интерфейсов на больших экранах, например, у планшетов. Поскольку экраны планшетов гораздо больше, чем у смартфонов, они предоставляют больше возможностей для объединения и перестановки компонентов пользовательского интерфейса. Фрагменты позволяют делать это, избавляя разработчика от необходимости управлять сложными изменениями в иерархии представлений. Разбивая макет операции на фрагменты, разработчик получает возможность модифицировать внешний вид операции в ходе выполнения и сохранять эти изменения в стеке переходов назад, которым управляет операция.

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

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

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

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

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

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

Рисунок 2. Жизненный цикл фрагмента (во время выполнения операции)

Для создания фрагмента необходимо создать подкласс класса Fragment (или его существующего подкласса). Класс Fragment имеет код, во многом схожий с кодом Activity. Он содержит методы обратного вызова, аналогичные методам операции, такие как onCreate(), onStart(), onPause() и onStop(). На практике, если требуется преобразовать существующее приложение Android так, чтобы в нем использовались фрагменты, достаточно просто переместить код из методов обратного вызова операции в соответствующие методы обратного вызова фрагмента.

Как правило, необходимо реализовать следующие методы жизненного цикла:

onCreate()
Система вызывает этот метод, когда создает фрагмент. В своей реализации разработчик должен инициализировать ключевые компоненты фрагмента, которые требуется сохранить, когда фрагмент находится в состоянии паузы или возобновлен после остановки.
onCreateView()
Система вызывает этот метод при первом отображении пользовательского интерфейса фрагмента на дисплее. Для прорисовки пользовательского интерфейса фрагмента следует возвратить из этого метода объект View, который является корневым в макете фрагмента. Если фрагмент не имеет пользовательского интерфейса, можно возвратить null.
onPause()
Система вызывает этот метод как первое указание того, что пользователь покидает фрагмент (это не всегда означает уничтожение фрагмента). Обычно именно в этот момент необходимо фиксировать все изменения, которые должны быть сохранены за рамками текущего сеанса работы пользователя (поскольку пользователь может не вернуться назад).

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

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

DialogFragment
Отображение перемещаемого диалогового окна. Использование этого класса для создания диалогового окна является хорошей альтернативой вспомогательным методам диалогового окна в классеActivity. Дело в том, что он дает возможность вставить диалоговое окно фрагмента в управляемый операцией стек переходов назад для фрагментов, что позволяет пользователю вернуться к закрытому фрагменту.
ListFragment
Отображение списка элементов, управляемых адаптером (например, SimpleCursorAdapter), аналогично классу ListActivity. Этот класс предоставляет несколько методов для управления списком представлений, например, метод обратного вызова onListItemClick() для обработки нажатий.
PreferenceFragment
Отображение иерархии объектов Preference в виде списка, аналогично классу PreferenceActivity. Этот класс полезен, когда в приложении создается операция «Настройки».

Добавление пользовательского интерфейса

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

Чтобы создать макет для фрагмента, разработчик должен реализовать метод обратного вызова onCreateView(), который система Android вызывает, когда для фрагмента наступает время отобразить свой макет. Реализация этого метода должна возвращать объект View, который является корневым в макете фрагмента.

Примечание. Если фрагмент является подклассом класса ListFragment, реализация по умолчанию возвращает класс ListView из метода onCreateView(), так что реализовывать его нет необходиомости.

Чтобы возвратить макет из метода onCreateView(), можно выполнить его раздувание из ресурса макета, определенного в XML-файле. Для этой цели метод onCreateView() предоставляет объект LayoutInflater.

Например, код подкласса класса Fragment, загружающий макет из файла example_fragment.xml, может выглядеть так:

public static class ExampleFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.example_fragment, container, false);
    }
}

Параметр container, передаваемый методу onCreateView(), является родительским классом ViewGroup (из макета операции), в который будет вставлен макет фрагмента. Параметр savedInstanceState является классом Bundle, который предоставляет данные о предыдущем экземпляре фрагмента во время возобновления фрагмента (восстановление состояния подробно обсуждается в разделе Управление жизненным циклом фрагмента).

Метод inflate() принимает три аргумента:

  • Идентификатор ресурса макета, раздувание которого следует выполнить.
  • Объект класса ViewGroup, который должен стать родительским для макета после раздувания. Передача параметра container необходима для того, чтобы система смогла применить параметры макета к корневому представлению раздутого макета, определяемому родительским представлением, в которое направляется макет.
  • Логическое значение, показывающее, следует ли прикрепить макет к объекту ViewGroup (второй параметр) во время раздувания. (В данном случае это false, потому что система уже вставляет раздутый макет в объект container, ипередача значения true создала бы лишнюю группу представления в окончательном макете).

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

Добавление фрагмента в операцию

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

  • объявив фрагмент в файле макета операции.

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

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <fragment android:name="com.example.news.ArticleListFragment"
                android:id="@+id/list"
                android:layout_weight="1"
                android:layout_width="0dp"
                android:layout_height="match_parent" />
        <fragment android:name="com.example.news.ArticleReaderFragment"
                android:id="@+id/viewer"
                android:layout_weight="2"
                android:layout_width="0dp"
                android:layout_height="match_parent" />
    </LinearLayout>
    

    Атрибут android:name в элементе &lt;fragment&gt; определяет класс Fragment, экземпляр которого создается в макете.

    Когда система создает этот макет операции, она создает экземпляр каждого фрагмента, определенного в макете, и для каждого вызывает метод onCreateView(), чтобы получить макет каждого фрагмента. Система вставляет объект View, возвращенный фрагментом, непосредственно вместо элемента &lt;fragment&gt;.

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

    • указать атрибут android:id с уникальным идентификатором;
    • указать атрибут android:tag с уникальной строкой;
    • ничего не предпринимать, чтобы система использовала идентификатор контейнерного представления.
  • или программным образом, добавив фрагмент в существующий объект ViewGroup.

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

    Для выполнения транзакций с фрагментами внутри операции (таких как добавление, удаление или замена фрагмента) необходимо использовать API-интерфейсы из FragmentTransaction. Экземпляр класса FragmentTransaction можно получить от объекта Activity следующим образом:

    FragmentManager fragmentManager = getFragmentManager()
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
    

    После этого можно добавить фрагмент методом add(), указав добавляемый фрагмент и представление, в которое он должен быть добавлен. Например:

    ExampleFragment fragment = new ExampleFragment();
    fragmentTransaction.add(R.id.fragment_container, fragment);
    fragmentTransaction.commit();
    

    Первый аргумент, передаваемый методу add(), представляет собой контейнерный объект ViewGroup для фрагмента, указанный при помощи идентификатора ресурса. Второй параметр — это фрагмент, который нужно добавить.

    Выполнив изменения с помощью FragmentTransaction, необходимо вызвать метод commit(), чтобы они вступили в силу.

Добавление фрагмента, не имеющего пользовательского интерфейса

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

Чтобы добавить фрагмент без пользовательского интерфейса, добавьте фрагмент из операции, используя метод add(Fragment, String) (передав ему уникальный строковый «тег» для фрагмента вместо идентификатора представления). Фрагмент будет добавлен, но, поскольку он не связан с представлением в макете операции, он не будет принимать вызов метода onCreateView(). Поэтому в реализации этого метода нет необходимости.

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

Пример операции, использующей фрагмент в качестве фонового потока, без пользовательского интерфейса, приведен в образце кода FragmentRetainInstance.java, входящем в число образцов в SDK (и доступном при помощи Android SDK Manager). Путь к нему в системе — <sdk_root>/APIDemos/app/src/main/java/com/example/android/apis/app/FragmentRetainInstance.java.

Управление фрагментами

Для управления фрагментами в операции нужен класс FragmentManager. Чтобы получить его, следует вызвать метод getFragmentManager() из кода операции.

Ниже указаны действия, которые позволяет выполнитьFragmentManager:

  • получать фрагменты, имеющиеся в операции, с помощью метода findFragmentById() (для фрагментов, предоставляющих пользовательский интерфейс в макете операции) или findFragmentByTag() (как для фрагментов, имеющих пользовательский интерфейс, так и для фрагментов без него);
  • снимать фрагменты со стека переходов назад методом popBackStack() (имитируя нажатие кнопки Назад пользователем);
  • регистрировать процесс-слушатель изменений в стеке переходов назад при помощи метода addOnBackStackChangedListener().

Дополнительные сведения об этих и других методах приводятся в документации по классу FragmentManager.

Как было показано в предыдущем разделе, можно использовать класс FragmentManager для открытия FragmentTransaction, что позволяет выполнять транзакции с фрагментами, например, добавление и удаление.

Выполнение транзакций с фрагментами

Большим достоинством использования фрагментов в операции является возможность добавлять, удалять, заменять их и выполнять другие действия с ними в ответ на действия пользователя. Любой набор изменений, вносимых в операцию, называется транзакцией. Ее можно выполнить при помощи API-интерфейсов в FragmentTransaction. Каждую транзакцию можно сохранить в стеке переходов назад, которым управляет операция. Это позволит пользователю перемещаться назад по изменениям во фрагментах (аналогично перемещению назад по операциям).

Экземпляр класса FragmentTransaction можно получить от FragmentManager, например, так:

FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

Каждая транзакция является набором изменений, выполняемых одновременно. Разработчик может указать все изменения, которые ему нужно выполнить в данной транзакции, вызывая методы add(), remove() и replace(). Затем, чтобы применить транзакцию к операции, следует вызвать метод commit().

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

Например, следующий код демонстрирует, как можно заменить один фрагмент другим, сохранив при этом предыдущее состояние в стеке переходов назад:

// Create new fragment and transaction
Fragment newFragment = new ExampleFragment();
FragmentTransaction transaction = getFragmentManager().beginTransaction();

// Replace whatever is in the fragment_container view with this fragment,
// and add the transaction to the back stack
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(null);

// Commit the transaction
transaction.commit();

В этом коде объект newFragment замещает фрагмент (если таковой имеется), находящийся в контейнере макета, на который указывает идентификатор R.id.fragment_container. В результате вызова метода addToBackStack() транзакция замены сохраняется в стеке переходов назад, чтобы пользователь мог обратить транзакцию и вернуть предыдущий фрагмент, нажав кнопку Назад.

Если в транзакцию добавить несколько изменений (например, еще раз вызвать add() или remove()), а затем вызвать addToBackStack(), все изменения, примененные до вызова метода commit(), будут добавлены в стек переходов назад как одна транзакция, и кнопкаНазад обратит их все вместе.

Порядок добавления изменений к объекту FragmentTransaction не играет роли за следующими исключениями:

  • метод commit() должен быть вызван в последнюю очередь;
  • если в один контейнер добавляется несколько фрагментов, то порядок их добавления определяет порядок, в котором они появляются в иерархии видов.

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

Совет. К каждой транзакции с фрагментом можно применить анимацию перехода, вызвав setTransition() до фиксации.

Вызов метода commit() не приводит к немедленному выполнению транзакции. Метод запланирует ее выполнение в потоке пользовательского интерфейса операции (в «главном» потоке), как только у потока появится возможность для этого. Впрочем, при необходимости можно вызвать executePendingTransactions() из потока пользовательского интерфейса, чтобы транзакции, запланированные методом commit() были выполнены немедленно. Как правило, в этом нет необходимости, за исключением случаев, когда транзакция является зависимостью для заданий в других потоках.

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

Взаимодействие с операцией

Хотя Fragment реализован как объект, независимый от класса Activity, и может быть использован внутри нескольких операций, конкретный экземпляр фрагмента напрямую связан с содержащей его операцией.

В частности, фрагмент может обратиться к экземпляру Activity с помощью метода getActivity() и без труда выполнить такие задачи, как поиск представления в макете операции:

View listView = getActivity().findViewById(R.id.list);

Аналогичным образом операция может вызывать методы фрагмента, получив ссылку на объект Fragment от FragmentManager с помощью метода findFragmentById() или findFragmentByTag(). Например:

ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);

Создание обратного вызова события для операции

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

Пусть, например, у новостного приложения имеются два фрагмента в одной операции: один для отображения списка статей (фрагмент A), а другой—для отображения статьи (фрагмент B). Тогда фрагмент A должен сообщать операции о том, что выбран пункт списка, чтобы она могла сообщить фрагменту B о необходимости отобразить статью. В этом случае интерфейс OnArticleSelectedListener объявляется во фрагменте A:

public static class FragmentA extends ListFragment {
    ...
    // Container Activity must implement this interface
    public interface OnArticleSelectedListener {
        public void onArticleSelected(Uri articleUri);
    }
    ...
}

Тогда операция, содержащая этот фрагмент, реализует интерфейс OnArticleSelectedListener и переопределит метод onArticleSelected(), чтобы извещать фрагмент B о событии, исходящем от фрагмента A. Чтобы контейнерная операция наверняка реализовала этот интерфейс, метод обратного вызова onAttach() во фрагменте A (который система вызывает при добавлении фрагмента в операцию) создает экземпляр класса OnArticleSelectedListener, выполнив приведение типа объекта Activity, который передается методу onAttach():

public static class FragmentA extends ListFragment {
    OnArticleSelectedListener mListener;
    ...
    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            mListener = (OnArticleSelectedListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString() + " must implement OnArticleSelectedListener");
        }
    }
    ...
}

Если операция не реализовала интерфейс, фрагмент генерирует исключение ClassCastException. В случае успеха элемент mListener будет содержать ссылку на реализацию интерфейса OnArticleSelectedListener в операции, чтобы фрагмент A мог использовать события совместно с операцией, вызывая методы, определенные интерфейсом OnArticleSelectedListener. Например, если фрагмент A является расширением класса ListFragment, то всякий раз, когда пользователь нажимает элемент списка, система вызывает onListItemClick() во фрагменте. Этот метод, в свою очередь, вызывает метод onArticleSelected(), чтобы использовать событие совместно с операцией:

public static class FragmentA extends ListFragment {
    OnArticleSelectedListener mListener;
    ...
    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        // Append the clicked item's row ID with the content provider Uri
        Uri noteUri = ContentUris.withAppendedId(ArticleColumns.CONTENT_URI, id);
        // Send the event and Uri to the host activity
        mListener.onArticleSelected(noteUri);
    }
    ...
}

Параметр id, передаваемый методу onListItemClick(), — это идентификатор строки с выбранным элементом списка, который операция (или другой фрагмент) использует для получения статьи от объекта ContentProvider приложения.

Дополнительные сведения о работе с поставщиком контента приводятся в документе Поставщики контента.

Добавление элементов в строку действий

Фрагменты могут добавлять пункты меню в Меню вариантов операции (и, следовательно, в Строку действий), реализовав onCreateOptionsMenu(). Однако, чтобы этот метод мог принимать вызовы, необходимо вызывать setHasOptionsMenu() во время выполнения метода onCreate(), чтобы сообщить, что фрагмент намеревается добавить пункты в Меню вариантов (в противном случае фрагмент не примет вызов метода onCreateOptionsMenu()).

Любые пункты, добавляемые фрагментом в Меню вариантов, присоединяются к уже существующим. Кроме того, фрагмент принимает обратные вызовы метода onOptionsItemSelected(), когда пользователь выбирает пункт меню.

Разработчик может также зарегистрировать представление в макете своего фрагмента, чтобы предоставить контекстное меню. Для этого следует вызвать метод registerForContextMenu(). Когда пользователь открывает контекстное меню, фрагмент принимает вызов метода onCreateContextMenu(). Когда пользователь выбирает пункт меню, фрагмент принимает вызов метода onContextItemSelected().

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

Подробные сведения относительно меню см. в руководствах для разработчиков Меню и Строка действий

Управление жизненным циклом фрагмента

Рисунок 3. Влияние жизненного цикла операции на жизненный цикл фрагмента

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

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

Здесь снова просматривается аналогия с операцией: разработчик может сохранить состояние фрагмента с помощью Bundle на случай, если процесс операции будет уничтожен, а разработчику понадобится восстановить состояние фрагмента при повторном создании операции. Состояние можно сохранить во время выполнения метода обратного вызова onSaveInstanceState() во фрагменте и восстановить его во время выполнения onCreate(), onCreateView() или onActivityCreated(). Дополнительные сведения о сохранении состояния приводятся в документе Операции.

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

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

Внимание! Если возникнет необходимость в объекте Context внутри объекта класса Fragment, можно вызвать методgetActivity(). Однако разработчик должен быть внимательным и вызывать метод getActivity() только когда фрагмент прикреплен к операции. Если фрагмент еще не прикреплен или был откреплен в конце его жизненного цикла, метод getActivity() возвратит null.

Согласование с жизненным циклом операции

Жизненый цикл операции, содержащей фрагмент, непосредственным образом влияет на жизненый цикл фрагмента, так что каждый обратный вызов жизненного цикла операции приводит к аналогичному обратного вызову для каждого фрагмента. Например, когда операция принимает вызов onPause(), каждый ее фрагмент принимает onPause().

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

onAttach()
Вызывается, когда фрагмент связывается с операцией (ему передается объект Activity).
onCreateView()
Вызывается для создания иерархии представлений, связанной с фрагментом.
onActivityCreated()
Вызывается, когда метод onCreate(), принадлежащий операции, возвращает управление.
onDestroyView()
Вызывается при удалении иерархии представлений, связанной с фрагментом.
onDetach()
Вызывается при разрыве связи фрагмента с операцией.

Зависимость жизненого цикла фрагмента от содержащей его операции иллюстрируется рисунком 3. На этом рисунке можно видеть, что очередное состояние операции определяет, какие методы обратного вызова может принимать фрагмент. Например, когда операция принимает свой метод обратного вызова onCreate(), фрагмент внутри этой операции принимает всего лишь метод обратного вызова onActivityCreated().

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

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

Пример:

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

Примечание. Полный исходный код этой операции находится в разделе FragmentLayout.java.

Главная операция применяет макет обычным способом, в методе onCreate():

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.fragment_layout);
}

Здесь применяется макет fragment_layout.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent" android:layout_height="match_parent">

    <fragment class="com.example.android.apis.app.FragmentLayout$TitlesFragment"
            android:id="@+id/titles" android:layout_weight="1"
            android:layout_width="0px" android:layout_height="match_parent" />

    <FrameLayout android:id="@+id/details" android:layout_weight="1"
            android:layout_width="0px" android:layout_height="match_parent"
            android:background="?android:attr/detailsElementBackground" />

</LinearLayout>

Пользуясь этим макетом, система создает экземпляр класса TitlesFragment (список пьес), как только операция загрузит макет. При этом объект FrameLayout (в котором будет находиться фрагмент с кратким содержанием) занимает место в правой части экрана, но поначалу остается пустым. Как будет показано ниже, фрагмент не помещается в FrameLayout, пока пользователь не выберет элемент в списке.

Однако не все экраны достаточно широки, чтобы отображать краткое содержание рядом со списком пьес. Поэтому описанный выше макет используется только при альбомной ориентации экрана и хранится в файле res/layout-land/fragment_layout.xml.

Когда же устройство находится в книжной ориентации, система применяет макет, приведенный ниже, который хранится в файлеres/layout/fragment_layout.xml:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent">
    <fragment class="com.example.android.apis.app.FragmentLayout$TitlesFragment"
            android:id="@+id/titles"
            android:layout_width="match_parent" android:layout_height="match_parent" />
</FrameLayout>

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

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

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

public static class TitlesFragment extends ListFragment {
    boolean mDualPane;
    int mCurCheckPosition = 0;

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        // Populate list with our static array of titles.
        setListAdapter(new ArrayAdapter<String>(getActivity(),
                android.R.layout.simple_list_item_activated_1, Shakespeare.TITLES));

        // Check to see if we have a frame in which to embed the details
        // fragment directly in the containing UI.
        View detailsFrame = getActivity().findViewById(R.id.details);
        mDualPane = detailsFrame != null && detailsFrame.getVisibility() == View.VISIBLE;

        if (savedInstanceState != null) {
            // Restore last state for checked position.
            mCurCheckPosition = savedInstanceState.getInt("curChoice", 0);
        }

        if (mDualPane) {
            // In dual-pane mode, the list view highlights the selected item.
            getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
            // Make sure our UI is in the correct state.
            showDetails(mCurCheckPosition);
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt("curChoice", mCurCheckPosition);
    }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        showDetails(position);
    }

    /**
     * Helper function to show the details of a selected item, either by
     * displaying a fragment in-place in the current UI, or starting a
     * whole new activity in which it is displayed.
     */
    void showDetails(int index) {
        mCurCheckPosition = index;

        if (mDualPane) {
            // We can display everything in-place with fragments, so update
            // the list to highlight the selected item and show the data.
            getListView().setItemChecked(index, true);

            // Check what fragment is currently shown, replace if needed.
            DetailsFragment details = (DetailsFragment)
                    getFragmentManager().findFragmentById(R.id.details);
            if (details == null || details.getShownIndex() != index) {
                // Make new fragment to show this selection.
                details = DetailsFragment.newInstance(index);

                // Execute a transaction, replacing any existing fragment
                // with this one inside the frame.
                FragmentTransaction ft = getFragmentManager().beginTransaction();
                if (index == 0) {
                    ft.replace(R.id.details, details);
                } else {
                    ft.replace(R.id.a_item, details);
                }
                ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
                ft.commit();
            }

        } else {
            // Otherwise we need to launch a new activity to display
            // the dialog fragment with selected text.
            Intent intent = new Intent();
            intent.setClass(getActivity(), DetailsActivity.class);
            intent.putExtra("index", index);
            startActivity(intent);
        }
    }
}

Второй фрагмент, DetailsFragment, отображает краткое содержание пьесы, выбранной в списке TitlesFragment:

public static class DetailsFragment extends Fragment {
    /**
     * Create a new instance of DetailsFragment, initialized to
     * show the text at 'index'.
     */
    public static DetailsFragment newInstance(int index) {
        DetailsFragment f = new DetailsFragment();

        // Supply index input as an argument.
        Bundle args = new Bundle();
        args.putInt("index", index);
        f.setArguments(args);

        return f;
    }

    public int getShownIndex() {
        return getArguments().getInt("index", 0);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        if (container == null) {
            // We have different layouts, and in one of them this
            // fragment's containing frame doesn't exist.  The fragment
            // may still be created from its saved state, but there is
            // no reason to try to create its view hierarchy because it
            // won't be displayed.  Note this is not needed -- we could
            // just run the code below, where we would create and return
            // the view hierarchy; it would just never be used.
            return null;
        }

        ScrollView scroller = new ScrollView(getActivity());
        TextView text = new TextView(getActivity());
        int padding = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                4, getActivity().getResources().getDisplayMetrics());
        text.setPadding(padding, padding, padding, padding);
        scroller.addView(text);
        text.setText(Shakespeare.DIALOGUE[getShownIndex()]);
        return scroller;
    }
}

Вспомним код класса TitlesFragment: если пользователь нажимает на пункт списка, а текущий макет не включает в себя представление R.id.details (которому принадлежит фрагмент DetailsFragment), то приложение запускает операцию DetailsActivity для отображения содержимого элемента.

Далее идет код класса DetailsActivity, который всего лишь содержит объект DetailsFragment для отображения краткого содержания выбранной пьесы на экране в книжной ориентации:

public static class DetailsActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (getResources().getConfiguration().orientation
                == Configuration.ORIENTATION_LANDSCAPE) {
            // If the screen is now in landscape mode, we can show the
            // dialog in-line with the list so we don't need this activity.
            finish();
            return;
        }

        if (savedInstanceState == null) {
            // During initial setup, plug in the details fragment.
            DetailsFragment details = new DetailsFragment();
            details.setArguments(getIntent().getExtras());
            getFragmentManager().beginTransaction().add(android.R.id.content, details).commit();
        }
    }
}

Обратите внимание, что в альбомной конфигурации эта операция самостоятельно завершается, чтобы главная операция могла принять управление и отобразить фрагмент DetailsFragment рядом с фрагментомTitlesFragment. Это может произойти, если пользователь запустит операцию DetailsActivity в книжной ориентации экрана, а затем перевернет устройство в альбомную ориентацию (в результате чего текущая операция будет перезапущена).

Дополнительные образцы кода, использующего фрагменты (и файлы с полным исходным кодом этого примера), доступны в приложении-примере API Demos в разделе ApiDemos (которое можно загрузить из компонента Samples SDK).