Життєвий цикл фрагментів

Кожен клас фрагмента успадковується від базового класу Fragment і має свій життєвий цикл, що складається з низки етапів:

Кожен етап життєвого циклу описується однією з констант перерахунку Lifecycle.State:

  • INITIALIZED
  • CREATED
  • STARTED
  • RESUMED
  • DESTROYED

Варто зауважити, що подання фрагмента (його візуальний інтерфейс або View) має окремий життєвий цикл.

Під час створення фрагмент знаходиться в стані INITIALIZED. Щоб фрагмент пройшов усі інші етапи життєвого циклу, фрагмент необхідно передати в об'єкт FragmentManager, який далі визначає стан фрагмента і переводить фрагмент з одного стану в інший.

Методи життєвого циклу фрагмента

  1. onCreate() — відбувається створення фрагмента. У цьому методі ми можемо отримати раніше збережений стан фрагмента через параметр методу Bundle savedInstanceState. (Якщо фрагмент створюється вперше, то цей об'єкт має значення null) Цей метод викликається після виклику відповідного методу onCreate() у activity.
public void onCreate(Bundle savedInstanceState)
  1. onCreateView() — фрагмент створює подання (View або візуальний інтерфейс). У цьому методі ми можемо встановити, який саме візуальний інтерфейс буде використовувати фрагмент. Під час виконання цього методу подання фрагмента переходить у стан INITIALIZED, а сам фрагмент все ще знаходиться в стані CREATED.
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)

Перший параметр — об'єкт LayoutInflater дозволяє отримати вміст ресурсу layout і передати його у фрагмент.

Другий параметр — об'єкт ViewGroup представляє контейнер, у який буде завантажуватись фрагмент.

Третій параметр — об'єкт Bundle представляє стан фрагмента. (Якщо фрагмент завантажується вперше, то дорівнює null)

На виході метод повертає створене за допомогою LayoutInflater подання у вигляді об'єкта View — власне подання фрагмента.

  1. onViewCreated() — викликається після створення подання фрагмента.
public void onViewCreated (View view, Bundle savedInstanceState)

Перший параметр — об'єкт View — подання фрагмента, яке було створене через метод onCreateView.

Другий параметр — об'єкт Bundle представляє стан фрагмента. (Якщо фрагмент завантажується вперше, то дорівнює null)

  1. onViewStateRestored() — отримує стан подання фрагмента. Після виконання цього методу подання фрагмента переходить у стан CREATED.
public void onViewStateRestored (Bundle savedInstanceState)
  1. onStart() — викликається, коли фрагмент стає видимим і разом з поданням переходить у стан STARTED.
public void onStart ()
  1. onResume() — викликається, коли фрагмент стає активним, і користувач може з ним взаємодіяти. При цьому фрагмент і його подання переходять у стан RESUMED.
public void onResume ()
  1. onPause() — фрагмент продовжує залишатися видимим, але вже не активний, і разом з поданням переходить у стан STARTED.
public void onPause ()
  1. onStop() — фрагмент більше не є видимим і разом з поданням переходить у стан CREATED.
public void onStop ()

На цьому етапі життєвого циклу ми можемо зберегти стан фрагмента за допомогою методу onSaveInstanceState(). Однак варто враховувати, що виклик цього методу залежить від версії API. До API 28 onSaveInstanceState() викликається до onStop(), а починаючи з API 28 після onStop().

  1. onDestroyView() — знищується подання фрагмента. Подання переходить у стан DESTROYED.
  2. onDestroy() — остаточне знищення фрагмента — він також переходить у стан DESTROYED.

Додатково для фрагмента визначено два методи зворотного виклику, які пов'язані з приєднанням фрагмента до activity:

  • Коли фрагмент додається в FragmentManager і приєднується до певного класу Activity, у фрагмента викликається метод onAttach(). Цей метод викликається до всіх інших методів життєвого циклу. З цього моменту фрагмент стає активним, і FragmentManager починає керувати його життєвим циклом.
  • Метод onDetach() викликається, коли фрагмент видаляється з FragmentManager і від'єднується від класу Activity. Цей метод викликається після всіх інших методів життєвого циклу.

У коді класу фрагмента ми можемо перевизначити всі або частину з цих методів. Наприклад, нехай у нас буде визначено такий проєкт:

У каталозі res/layout визначено файл layout для фрагмента - fragment_content.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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
    <Button
        android:id="@+id/updateButton"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Обновить"
        app:layout_constraintBottom_toTopOf="@+id/dateTextView"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
 
    <TextView
        android:id="@+id/dateTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:textSize="26sp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/updateButton" />
 
</androidx.constraintlayout.widget.ConstraintLayout>

Клас фрагмента використовує цей файл для встановлення подання, а також визначає методи для управління життєвим циклом:

package com.example.fragmentapp;
 
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
 
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import android.util.Log;
 
import java.util.Date;
 
public class ContentFragment extends Fragment {
 
    private final static String TAG = "ContentFragment";
 
    public ContentFragment(){
        Log.d(TAG, "Constructor");
    }
 
    @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        Log.d(TAG, "onAttach");
    }
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
    }
 
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        Log.d(TAG, "onCreateView");
        return inflater.inflate(R.layout.fragment_content, container, false);
    }
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        Button updateButton = view.findViewById(R.id.updateButton);
        TextView updateBox = view.findViewById(R.id.dateTextView);
 
        updateButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String curDate = new Date().toString();
                updateBox.setText(curDate);
            }
        });
        Log.d(TAG, "onViewCreated");
    }
 
    @Override
    public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
        super.onViewStateRestored(savedInstanceState);
        Log.d(TAG, "onViewStateRestored");
    }
 
    @Override
    public void onStart() {
        super.onStart();
        Log.d(TAG, "onStart");
    }
 
    @Override
    public void onResume() {
        super.onResume();
        Log.d(TAG, "onResume");
    }
 
    @Override
    public void onPause() {
        super.onPause();
        Log.d(TAG, "onPause");
    }
 
    @Override
    public void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
    }
 
    @Override
    public void onDestroyView() {
        super.onDestroyView();
        Log.d(TAG, "onDestroyView");
    }
 
    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
    }
 
    @Override
    public void onDetach() {
        super.onDetach();
        Log.d(TAG, "onDetach");
    }
}

На відміну від попередньої статті, де розглядалося створення фрагмента, тут фрагмент встановлює подання в методі onCreateView. Для цього в метод inflate() об'єкта LayoutInflater передається ідентифікатор ресурсу layout і контейнер — об'єкт ViewGroup, в який буде завантажуватись фрагмент. В результаті метод inflate() повертає створене подання.

return inflater.inflate(R.layout.fragment_content, container, false);

Під час виконання методу onViewCreated() подання вже створене, і воно передається як перший параметр — об'єкт View, через який за допомогою ідентифікаторів ми можемо отримати візуальні елементи — TextView і Button, які визначені в поданні.

Для решти методів життєвого циклу встановлено просте логування за допомогою методу Log.d().

Нехай у файлі activity_main.xml відбувається додавання фрагмента:

<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_container_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:name="com.example.fragmentapp.ContentFragment" />

І клас MainActivity:

package com.example.fragmentapp;

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

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

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

А у вікні Logcat в Android Studio можна буде спостерігати логгування методів життєвого циклу

Lifecycle

Досить часто частина логіки програми зав'язана на життєвий цикл Activity. Ми вмикаємо що-небудь у методах onStart або onResume і вимикаємо в onPause або onStop.

Розглянемо приклад.

У нас є якийсь клас для роботи із сервером. Він має взаємодіяти із сервером, поки Activity відкрито. Відповідно, ми будемо підключати його до сервера під час показу Activity і відключати під час приховування Activity.

public class MyServer {

   public void connect() {
       // ...
   }

   public void disconnect() {
       // ...
   }

}

Метод connect використовується для підключення до сервера, disconnect - для відключення.

Викликаємо ці методи в onStart і onStop в Activity.

@Override
protected void onStart() {
   super.onStart();
   myServer.connect();
}


@Override
protected void onStop() {
   super.onStop();
   myServer.disconnect();
}

Тепер MyServer буде під'єднано, поки Activity мабуть на екрані.

Це цілком класична, часто використовувана схема. І в простому прикладі все виглядає непогано. Але в складних додатках зміст методів onStart, onStop тощо може складатися з декількох десятків рядків і бути досить заплутаним. Гугл рекомендує виносити цю логіку з Activity. Давайте подивимося, як це можна зробити.

У Activity є метод getLifecycle, який повертає об'єкт Lifecycle. На цей об'єкт можна підписати слухачів, які отримуватимуть сповіщення при зміні lifecycle-стану Activity.

Activity і фрагменти в Support Library, починаючи з версії 26.1.0 реалізують інтерфейс LifecycleOwner. Саме цей інтерфейс і додає їм метод getLifecycle.

Тобто у вас має бути такий рядок у build.gradle файлі модуля, у секції dependencies

implementation 'com.android.support:appcompat-v7:26.1.0'

Або використовуйте більш свіжу версію.

У нашому прикладі слухачем буде MyServer. Щоб мати можливість підписатися на Lifecycle, він повинен наслідувати інтерфейс LifecycleObserver.

public class MyServer implements LifecycleObserver {

   @OnLifecycleEvent(Lifecycle.Event.ON_START)
   public void connect() {
       // ...
   }

   @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
   public void disconnect() {
       // ...
   }

}

Зверніть увагу, що інтерфейс LifecycleObserver порожній. У ньому немає купи методів типу onStart, onStop тощо. Ми просто позначаємо в класі MyServer його ж власні методи анотацією OnLifecycleEvent і вказуємо, за якої lifecycle-події метод має бути викликаний.

У нашому прикладі ми вказуємо, що метод connect має викликатися в момент onStart, а метод disconnect - у момент onStop.

Залишилося підписати екземпляр MyServer на Lifecycle.

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);
   // ...
   getLifecycle().addObserver(myServer);
}

В Activity методом getLifecycle отримуємо Lifecycle, і методом addObserver підписуємо myServer.

А методи onStart і onStop в Activity нам більше не потрібні, їх можна видалити.

Тепер, під час переходу Activity зі стану CREATED у стан STARTED, його об'єкт Lifecycle викличе метод myServer.connect. А при переході зі STARTED у CREATED - Lifecycle викличе myServer disconnect.

При цьому в Acivity це вимагало від нас мінімум коду - тільки підписати myServer на Lifecycle. Все інше вирішує сам MyServer.

На схемі нижче ви можете побачити, які стани проходить Activity і які події при цьому викликаються.

Нічого нового тут для нас немає, все це ази Android. Тут ви можете бачити стани і події. Вони пов'язані дуже просто - при переході між станами відбуваються події.

Ці події ми вказували в анотаціях OnLifecycleEvent до методів об'єкта MyServer.

Повний список подій можна подивитися в документації.

Відписатися від Lifecycle можна методом removeObserver (документація).

Any

Ви можете використовувати подію ON_ANY для отримання всіх подій в одному методі

    @OnLifecycleEvent(ON_ANY)
    void onAny(LifecycleOwner source, Lifecycle.Event event) {
        // ...
    }

У цьому випадку всі події будуть викликати цей метод.

Використовуйте вхідний параметр event, щоб визначити, яка саме подія відбулася

Стан

Якщо ви хочете дізнатися поточний стан Activity, то у його об'єкта Lifecycle є метод getCurrentState:

if (getLifecycle().getCurrentState() == Lifecycle.State.RESUMED) {
   // ...
}

Повний список станів можна подивитися в документації.

Також ви можете перевірити, що поточний стан Activity не нижчий за певний стан.

if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
   // ...
}

Метод isAtLeast тут перевіряє, що стан Activity не нижчий, ніж STARTED. Тобто або STARTED, або RESUMED.