Fragment 8 (495) 900-99-18 Попробовать демо

androidx.fragment.app.Fragment

Собирайте экран из фрагментов, а не из Activity

Практическое руководство по интерфейсу Android-приложения на фрагментах: один Activity, контейнеры FrameLayout, транзакции, поворот экрана и сохранение состояния. Начните прямо здесь — соберите транзакцию и посмотрите, что напишет Logcat.

res/layout/activity_main.xml

FragmentManagerсостояние

  • ListFragment"list" · #frame_containerRESUMED

Транзакция

операции копятся до commit()

getSupportFragmentManager()        .beginTransaction()        // нажмите replace(…) или add(…)
Logcat package:com.example.fragment

    Activity → Fragment

    Почему не Activity на каждый экран

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

    • Стек Activity расходует память

      Каждый новый Activity ложится в стек поверх предыдущих. Система может выгрузить нижние, и пользователь, нажав «Назад», неожиданно окажется за пределами приложения.

    • Жизненный цикл трудно предсказать

      Activity пересоздаётся при повороте экрана, при нехватке памяти, при смене конфигурации. Интерфейс, жёстко привязанный к нему, приходится каждый раз собирать заново.

    • Переходы тяжело анимировать

      Переход между Activity перерисовывает весь экран. Фрагмент меняет только свою часть — анимировать её проще, и она легче для устройства.

    MainActivity
    FrameLayout @id/frame_container
    DetailFragment "detail"
    View TextView ImageView Button
    Фрагмент — прослойка между Activity и View. Он держит в себе элементы интерфейса и получает события жизненного цикла от Activity. Один Activity может показывать сколько угодно фрагментов.

    Fragment lifecycle

    Жизненный цикл: где что делать

    У фрагмента свой жизненный цикл, вложенный в цикл Activity. Колбэки идут парами: что создано на пути вперёд, разбирается на пути назад.

    onAttach · onDetach

    В onAttach фрагмент получает Activity — сохраните ссылку, если нужна связь. В onDetach обнулите её, иначе Activity утечёт из памяти.

    onCreateView

    Здесь появляется View. Раздувайте разметку и ищите элементы здесь, а не в onCreate: до этого момента интерфейса ещё нет.

    onPause

    Приложение ушло в фон. Отключитесь от bound service, остановите анимации и тяжёлую работу.

    Интерфейс — в onCreateView

    Метод получает LayoutInflater и контейнер, в котором будет жить фрагмент. Раздуйте разметку, найдите нужные элементы и верните готовую View — дальше ею управляет FragmentManager.

    AndroidXНастройку элементов удобно вынести в onViewCreated. Колбэк onActivityCreated устарел — не полагайтесь на него.

    DetailFragment.java
    public class DetailFragment extends Fragment {
    
        @Override
        public View onCreateView(@NonNull LayoutInflater inflater,
                                 @Nullable ViewGroup container,
                                 @Nullable Bundle savedInstanceState) {
            View view = inflater.inflate(
                    R.layout.fragment_detail, container, false);
            TextView title = view.findViewById(R.id.title);
            title.setText(R.string.detail_title);
            return view;
        }
    }

    FragmentTransaction

    replace или add: разница видна в Logcat

    FragmentManager уже есть в каждом Activity — его возвращает getSupportFragmentManager(). Изменения описываются транзакцией: beginTransaction(), затем операции, затем один commit(). До вызова commit() экран не меняется.

    replace()

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

    MainActivity.java
    getSupportFragmentManager()
            .beginTransaction()
            .replace(R.id.frame_container,
                    new DetailFragment(), "detail")
            .commit();

    Старый фрагмент: onPause → onStop → … → onDetach

    add()

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

    MainActivity.java
    getSupportFragmentManager()
            .beginTransaction()
            .add(R.id.frame_container,
                    new PlayerFragment(), "player")
            .commit();

    Старый фрагмент: остаётся RESUMED

    Ищите фрагмент по тегу, а не по номеру

    Номер текущего экрана в поле Activity потеряется при повороте: Activity пересоздаётся. Отключать пересоздание через android:configChanges — костыль, который ломается, как только у ориентаций появляются разные разметки. Передайте тег третьим параметром replace() или add() и находите фрагмент по нему.

    MainActivity.java
    Fragment detail = getSupportFragmentManager()
            .findFragmentByTag("detail");
    
    if (detail != null && detail.isVisible()) {
        // пользователь смотрит детали — экран восстановлен
        setTitle(R.string.detail_title);
    }

    Анимация перехода — одна строка

    Смена фрагмента перерисовывает только часть экрана. Встроенных fade_in и fade_out часто достаточно, а свою анимацию можно описать в XML через alpha. Вызовите setCustomAnimations() до replace() или add().

    MainActivity.java
    getSupportFragmentManager()
            .beginTransaction()
            .setCustomAnimations(android.R.anim.fade_in,
                                 android.R.anim.fade_out)
            .replace(R.id.frame_container,
                    new DetailFragment(), "detail")
            .commit();

    res/layout-land

    Поворот экрана — повод показать больше

    Альбомную версию разметки положите в res/layout-land/ под тем же именем. При повороте система сама выберет нужный файл.

    res/
    ├── layout/
    │   └── activity_main.xml   ← один контейнер
    └── layout-land/
        └── activity_main.xml   ← + PreviewFragment

    В альбомной разметке объявите ещё один фрагмент прямо в XML и отдайте ему часть ширины через layout_weight. Он появится только в горизонтальной ориентации, а фрагменты в контейнере FragmentManager вернёт на место сам.

    AndroidXВместо тега <fragment> используйте FragmentContainerView.

    Повернуть экран в демо

    res/layout-land/activity_main.xml
    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">
    
        <FrameLayout
            android:id="@+id/frame_container"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="2" />
    
        <androidx.fragment.app.FragmentContainerView
            android:id="@+id/preview"
            android:name="com.example.fragment.PreviewFragment"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1" />
    
    </LinearLayout>

    setRetainInstance · Bundle

    Удержать — не значит сохранить

    При повороте Activity и его фрагменты пересоздаются. Что переживёт пересоздание, зависит от выбранного механизма — и у каждого своя граница.

    Механизм Что переживёт поворот Ограничение В AndroidX
    setRetainInstance(true) Сам объект фрагмента и его поля. Фоновая задача вернёт результат в тот же экземпляр. View всё равно создаются заново, их содержимое теряется. Метод устарел. Держите данные во ViewModel.
    onSaveInstanceState(Bundle) Всё, что вы положили в outState: строки, числа, Parcelable. Читайте обратно из savedInstanceState. Bundle не рассчитан на большие объёмы. Работает. Для ViewModel есть SavedStateHandle.
    setArguments(Bundle) Стартовые параметры фрагмента — система восстановит их сама. Только то, что нужно для запуска: идентификатор, режим, флаги. Работает так же.
    android:freezesText="true" Текст в TextView и EditText. С собственными View не работает, с некоторыми стандартными — тоже. Работает так же.
    DataFragment.java — удержать объект
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true); // переживёт поворот экрана
    }
    SearchFragment.java — сохранить данные
    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString("query", query);
    }
    
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater,
                             @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        if (savedInstanceState != null) {
            query = savedInstanceState.getString("query");
        }
        return inflater.inflate(
                R.layout.fragment_search, container, false);
    }

    onAttach · onDetach

    Фрагменты общаются через Activity

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

    ListFragment MainActivity DetailFragment
    ListFragment сообщает о выборе статьи через интерфейс. Activity решает, что показать, и передаёт данные в DetailFragment.

    Ссылку на Activity фрагмент получает в onAttach и обязан обнулить её в onDetach — иначе после пересоздания старый Activity останется в памяти.

    AndroidXДля обмена данными между фрагментами есть Fragment Result API и общий ViewModel.

    ListFragment.java
    public class ListFragment extends Fragment {
    
        public interface Listener {
            void onArticleSelected(long articleId);
        }
    
        private Listener listener;
    
        @Override
        public void onAttach(@NonNull Context context) {
            super.onAttach(context);
            listener = (Listener) context;
        }
    
        @Override
        public void onDetach() {
            super.onDetach();
            listener = null; // иначе Activity утечёт из памяти
        }
    }