프래그먼트 관리자

FragmentManager는 앱 프래그먼트에서 프래그먼트를 추가, 삭제 또는 교체하고 백 스택에 추가하는 등의 작업을 실행하는 클래스입니다.

Jetpack Navigation 라이브러리를 사용하는 경우 FragmentManager와의 직접적인 상호작용은 거의 필요하지 않습니다. 개발자를 대신해 이 라이브러리가 FragmentManager를 사용하기 때문입니다. 그러나 프래그먼트를 사용하는 모든 앱은 어느 정도 FragmentManager를 사용하므로 프래그먼트 관리자가 무엇인지 어떻게 작동하는지 파악하는 것이 중요합니다.

이 페이지에서 다루는 내용은 다음과 같습니다.

  • FragmentManager에 액세스하는 방법
  • 활동 및 프래그먼트와 관련한 FragmentManager의 역할
  • FragmentManager로 백 스택을 관리하는 방법
  • 프래그먼트에 데이터와 종속 항목을 제공하는 방법

FragmentManager에 액세스

FragmentManager에는 활동 또는 프래그먼트에서 액세스할 수 있습니다.

FragmentActivity 및 그 서브클래스(예: AppCompatActivity)는 getSupportFragmentManager() 메서드를 통해 FragmentManager에 액세스할 수 있습니다.

프래그먼트는 하나 이상의 하위 프래그먼트를 호스팅할 수 있습니다. 프래그먼트 내에서 getChildFragmentManager()를 통해 프래그먼트의 하위 요소를 관리하는 FragmentManager 참조를 가져올 수 있습니다. 호스트 FragmentManager에 액세스해야 한다면 getParentFragmentManager()를 사용하면 됩니다.

몇 가지 예를 통해 프래그먼트, 프래그먼트의 호스트, 각각과 연결된 FragmentManager 인스턴스 간의 관계를 확인해 보겠습니다.

프래그먼트와 프래그먼트의 호스트 활동 간 관계를 보여 주는 UI 레이아웃의 두 가지 예
그림 1. 프래그먼트와 프래그먼트의 호스트 활동 간 관계를 보여 주는 UI 레이아웃의 두 가지 예

그림 1에서 보여 주는 두 가지 예에는 각각 단일 활동 호스트가 있습니다. 두 가지 예의 호스트 활동은 호스트 프래그먼트를 앱에서 다양한 화면으로 교체하는 작업을 담당하는 BottomNavigationView로 최상위 탐색을 사용자에게 표시하며 각 화면은 별도의 프래그먼트로 구현됩니다.

예 1의 호스트 프래그먼트는 분할 뷰 화면을 구성하는 하위 프래그먼트 두 개를 호스팅합니다. 예 2의 호스트 프래그먼트는 스와이프 뷰의 디스플레이 프래그먼트를 구성하는 하위 프래그먼트 한 개를 호스팅합니다.

이 설정을 고려해 볼 때 하위 프래그먼트를 관리하는 FragmentManager가 각 호스트에 연결되어 있다고 생각할 수 있습니다. supportFragmentManager, parentFragmentManager, childFragmentManager 사이의 속성 매핑과 함께 그림 2에 나와 있습니다.

각 호스트에는 하위 프래그먼트를 관리하는 자체 FragmentManager가 연결되어 있습니다.
그림 2. 하위 프래그먼트를 관리하는 자체 FragmentManager가 연결되어 있는 각 호스트

참조할 적절한 FragmentManager 속성은 호출 사이트가 프래그먼트 계층 구조에서 어디에 있는지 개발자가 어떤 프래그먼트 관리자에 액세스하려는지에 따라 다릅니다.

FragmentManager 참조가 있으면 이를 사용하여 사용자에게 표시되는 프래그먼트를 조작할 수 있습니다.

하위 프래그먼트

일반적으로 앱은 애플리케이션 프로젝트에서 단일 또는 소수의 활동으로 구성되고 각 활동은 관련 화면 그룹을 나타냅니다. 활동은 최상위 탐색을 배치하는 지점과 ViewModel 객체 및 프래그먼트 간 다른 뷰 상태의 범위 지정 위치를 제공할 수 있습니다. 프래그먼트는 앱의 개별 대상을 나타냅니다.

여러 프래그먼트를 한 번에 표시하려면(예: 분할 뷰 또는 대시보드) 대상 프래그먼트와 그 하위 프래그먼트 관리자에서 관리하는 하위 프래그먼트를 사용할 수 있습니다.

하위 프래그먼트의 다른 사용 사례는 다음과 같습니다.

  • 일련의 하위 프래그먼트 뷰를 관리하기 위해 상위 프래그먼트에 ViewPager2가 있는 화면 슬라이드
  • 일련의 관련 화면 내 하위 탐색
  • 하위 프래그먼트를 개별 대상으로 사용하는 Jetpack Navigation. 활동은 단일 상위 NavHostFragment를 호스팅하고 그 공간을 사용자가 앱을 탐색할 때 다른 하위 대상 프래그먼트로 채웁니다.

FragmentManager 사용

FragmentManager는 프래그먼트 백 스택을 관리합니다. 런타임 시 FragmentManager는 사용자 상호작용에 응답하여 프래그먼트를 추가하거나 삭제하는 등 백 스택 작업을 실행할 수 있습니다. 각 변경사항 집합은 FragmentTransaction이라는 단일 단위로 함께 커밋됩니다. 프래그먼트 트랜잭션에 관한 자세한 내용은 프래그먼트 트랜잭션 가이드를 참고하세요.

사용자가 기기에서 뒤로 버튼을 누르는 경우 또는 개발자가 FragmentManager.popBackStack()을 호출하는 경우 최상위 프래그먼트 트랜잭션이 스택에서 사라집니다. 스택에 더 이상 프래그먼트 트랜잭션이 없고 개발자가 하위 프래그먼트를 사용하지 않는 경우 뒤로 이벤트가 활동까지 채워집니다. 하위 프래그먼트를 사용하는 경우 하위 및 동위 프래그먼트 특별 고려사항을 참고하세요.

트랜잭션에서 addToBackStack()을 호출하면 여러 프래그먼트 추가, 여러 컨테이너의 프래그먼트 교체 등 많은 작업이 트랜잭션에 포함될 수 있습니다.

백 스택이 표시되면 이러한 모든 작업이 단일 원자 작업으로 취소됩니다. 그러나 popBackStack() 호출 전에 추가 트랜잭션을 커밋한 경우 그리고 트랜잭션에 addToBackStack()을 사용하지 않은 경우 이러한 작업은 취소되지 않습니다. 따라서 단일 FragmentTransaction 내에서 백 스택에 영향을 미치는 트랜잭션을 영향을 미치지 않는 트랜잭션과 인터리빙하지 마세요.

트랜잭션 실행

레이아웃 컨테이너 내에 프래그먼트를 표시하려면 FragmentManager를 사용하여 FragmentTransaction을 만듭니다. 그러면 트랜잭션 내 컨테이너에서 add() 또는 replace() 작업을 실행할 수 있습니다.

예를 들어 간단한 FragmentTransaction은 다음과 같습니다.

Kotlin

supportFragmentManager.commit {
   replace<ExampleFragment>(R.id.fragment_container)
   setReorderingAllowed(true)
   addToBackStack("name") // Name can be null
}

Java

FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
    .replace(R.id.fragment_container, ExampleFragment.class, null)
    .setReorderingAllowed(true)
    .addToBackStack("name") // Name can be null
    .commit();

이 예에서 ExampleFragment는 현재 R.id.fragment_container ID로 식별된 레이아웃 컨테이너에 있는 프래그먼트(있는 경우)를 대체합니다. 프래그먼트의 클래스를 replace() 메서드에 제공하면 FragmentManager에서 FragmentFactory를 사용하여 인스턴스화를 처리할 수 있습니다. 자세한 내용은 프래그먼트에 종속 항목 제공 섹션을 참고하세요.

setReorderingAllowed(true)는 애니메이션과 전환이 올바르게 작동하도록 트랜잭션과 관련된 프래그먼트의 상태 변경을 최적화합니다. 애니메이션과 전환으로 탐색하는 방법에 관한 자세한 내용은 프래그먼트 트랜잭션애니메이션을 사용하여 프래그먼트 간 탐색을 참고하세요.

addToBackStack()을 호출하면 트랜잭션이 백 스택에 커밋됩니다. 사용자는 나중에 트랜잭션을 취소하고 뒤로 버튼을 눌러 이전 프래그먼트를 다시 가져올 수 있습니다. 단일 트랜잭션 내에서 여러 프래그먼트를 추가하거나 삭제한 경우 이러한 모든 작업은 백 스택이 표시되면 실행취소됩니다. addToBackStack() 호출에 제공된 선택적 이름을 통해 popBackStack()을 사용하여 특정 트랜잭션으로 다시 돌아갈 수 있습니다.

프래그먼트를 삭제하는 트랜잭션을 실행할 때 addToBackStack()을 호출하지 않으면 삭제된 프래그먼트가 트랜잭션이 커밋될 때 소멸되므로 사용자가 이를 다시 탐색할 수 없습니다. 프래그먼트를 삭제할 때 addToBackStack()을 호출하면 프래그먼트는 STOPPED 상태일 뿐이고 나중에 사용자가 뒤로 탐색할 때 RESUMED 상태가 됩니다. 이 경우 뷰가 소멸됩니다. 자세한 내용은 프래그먼트 수명 주기를 참고하세요.

기존 프래그먼트 찾기

findFragmentById()를 사용하여 레이아웃 컨테이너 내의 현재 프래그먼트 참조를 가져올 수 있습니다. findFragmentById()를 사용하여 프래그먼트를 XML에서 확장할 때 주어진 ID로 또는 FragmentTransaction에 추가할 때 컨테이너 ID로 찾습니다. 예를 들면 다음과 같습니다.

Kotlin

supportFragmentManager.commit {
   replace<ExampleFragment>(R.id.fragment_container)
   setReorderingAllowed(true)
   addToBackStack(null)
}
...
val fragment: ExampleFragment =
        supportFragmentManager.findFragmentById(R.id.fragment_container) as ExampleFragment

Java

FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
    .replace(R.id.fragment_container, ExampleFragment.class, null)
    .setReorderingAllowed(true)
    .addToBackStack(null)
    .commit();
...
ExampleFragment fragment =
        (ExampleFragment) fragmentManager.findFragmentById(R.id.fragment_container);

또는 고유한 태그를 프래그먼트에 할당하고 findFragmentByTag()를 사용하여 참조를 가져올 수 있습니다. 레이아웃 내에서 정의되거나 FragmentTransaction 내에서 add() 또는 replace() 작업 중에 정의된 프래그먼트에서 android:tag XML 속성을 사용하여 태그를 할당할 수 있습니다.

Kotlin

supportFragmentManager.commit {
   replace<ExampleFragment>(R.id.fragment_container, "tag")
   setReorderingAllowed(true)
   addToBackStack(null)
}
...
val fragment: ExampleFragment =
        supportFragmentManager.findFragmentByTag("tag") as ExampleFragment

Java

FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
    .replace(R.id.fragment_container, ExampleFragment.class, null, "tag")
    .setReorderingAllowed(true)
    .addToBackStack(null)
    .commit();
...
ExampleFragment fragment = (ExampleFragment) fragmentManager.findFragmentByTag("tag");

하위 및 동위 프래그먼트 특별 고려사항

하나의 FragmentManager만 프래그먼트 백 스택을 제어할 수 있습니다. 앱에서 여러 동위 프래그먼트를 동시에 화면에 표시하거나 하위 프래그먼트를 사용하는 경우 FragmentManager 하나를 지정하여 앱의 기본 탐색을 처리해야 합니다.

프래그먼트 트랜잭션 내에서 기본 탐색 프래그먼트를 정의하려면 트랜잭션에서 setPrimaryNavigationFragment() 메서드를 호출하여 childFragmentManager에 기본 컨트롤이 있는 프래그먼트의 인스턴스를 전달합니다.

탐색 구조를 일련의 레이어로, 활동을 가장 바깥쪽 레이어로 간주하여 하위 프래그먼트의 각 레이어를 아래에 래핑합니다. 각 레이어에는 기본 탐색 프래그먼트가 하나씩 있습니다.

뒤로 이벤트가 발생하면 가장 안쪽 레이어가 탐색 동작을 제어합니다. 가장 안쪽 레이어에 다시 표시할 프래그먼트 트랜잭션이 더 이상 없으면 컨트롤은 다음 바깥 레이어로 돌아가며 이 프로세스는 활동에 도달할 때까지 반복됩니다.

동시에 프래그먼트가 두 개 이상 표시되면 그중 하나만 기본 탐색 프래그먼트가 됩니다. 프래그먼트를 기본 탐색 프래그먼트로 설정하면 이전 프래그먼트의 지정이 삭제됩니다. 이전 예를 사용하여 세부 프래그먼트를 기본 탐색 프래그먼트로 설정하면 기본 프래그먼트의 지정이 삭제됩니다.

여러 백 스택 지원

앱에서 여러 개의 백 스택을 지원해야 하는 경우가 있습니다. 일반적인 예로 앱에서 하단 탐색 메뉴를 사용하는 경우를 들 수 있습니다. FragmentManager를 사용하면 saveBackStack()restoreBackStack() 메서드로 여러 백 스택을 지원할 수 있습니다. 이러한 메서드는 하나의 백 스택을 저장하고 다른 스택을 복원하여 여러 백 스택 간에 전환할 수 있도록 지원합니다.

saveBackStack()은 선택적 name 매개변수를 사용하여 popBackStack()을 호출하는 것과 비슷하게 작동합니다. 즉, 지정된 트랜잭션과 스택에서 이 트랜잭션 이후에 있는 모든 트랜잭션이 표시됩니다. 차이점은 saveBackStack()은 표시된 트랜잭션에 있는 모든 프래그먼트의 상태를 저장한다는 것입니다.

예를 들어, 아래의 예와 같이 이전에 addToBackStack()을 사용하여 FragmentTransaction을 커밋해서 백 스택에 프래그먼트를 추가했다고 가정해 보겠습니다.

Kotlin

supportFragmentManager.commit {
  replace<ExampleFragment>(R.id.fragment_container)
  setReorderingAllowed(true)
  addToBackStack("replacement")
}

Java

supportFragmentManager.beginTransaction()
  .replace(R.id.fragment_container, ExampleFragment.class, null)
  // setReorderingAllowed(true) and the optional string argument for
  // addToBackStack() are both required if you want to use saveBackStack()
  .setReorderingAllowed(true)
  .addToBackStack("replacement")
  .commit();

이 경우 saveBackStack()를 호출하여 이 프래그먼트 트랜잭션과 ExampleFragment의 상태를 저장할 수 있습니다.

Kotlin

supportFragmentManager.saveBackStack("replacement")

Java

supportFragmentManager.saveBackStack("replacement");

동일한 이름 매개변수를 사용하여 restoreBackStack()을 호출하면 모든 표시된 트랜잭션과 모든 저장된 프래그먼트 상태를 복원할 수 있습니다.

Kotlin

supportFragmentManager.restoreBackStack("replacement")

Java

supportFragmentManager.restoreBackStack("replacement");

프래그먼트에 종속 항목 제공

프래그먼트를 추가할 때 프래그먼트를 수동으로 인스턴스화하여 FragmentTransaction에 추가할 수 있습니다.

Kotlin

fragmentManager.commit {
    // Instantiate a new instance before adding
    val myFragment = ExampleFragment()
    add(R.id.fragment_view_container, myFragment)
    setReorderingAllowed(true)
}

Java

// Instantiate a new instance before adding
ExampleFragment myFragment = new ExampleFragment();
fragmentManager.beginTransaction()
    .add(R.id.fragment_view_container, myFragment)
    .setReorderingAllowed(true)
    .commit();

프래그먼트 트랜잭션을 커밋할 때 개발자가 만든 프래그먼트의 인스턴스가 사용된 인스턴스입니다. 그러나 구성 변경 중에 활동과 활동의 모든 프래그먼트가 소멸되고 가장 적합한 Android 리소스로 다시 만들어집니다. FragmentManager가 이 모든 작업을 처리합니다. 프래그먼트의 인스턴스를 다시 만들고 호스트에 연결하여 백 스택 상태를 다시 만듭니다.

기본적으로 FragmentManager는 프레임워크에서 제공하는 FragmentFactory를 사용하여 프래그먼트의 새 인스턴스를 인스턴스화합니다. 이 기본 팩토리는 리플렉션을 사용하여 프래그먼트의 인수가 없는 생성자를 찾아 호출합니다. 즉, 이 기본 팩토리를 사용하여 프래그먼트에 종속 항목을 제공할 수 없습니다. 또한 처음 프래그먼트를 만드는 데 사용한 모든 맞춤 생성자가 재생성 중에 기본적으로 사용되지 않습니다.

프래그먼트에 종속 항목을 제공하거나 맞춤 생성자를 사용하려면 대신 맞춤 FragmentFactory 서브클래스를 만들고 FragmentFactory.instantiate를 재정의해야 합니다. 그러면 맞춤 팩토리로 FragmentManager의 기본 팩토리를 재정의할 수 있습니다. 맞춤 팩토리는 프래그먼트를 인스턴스화하는 데 사용됩니다.

거주 지역의 인기 디저트를 표시하는 DessertsFragment가 있다고 가정해 보겠습니다. 사용자에게 올바른 UI를 표시하는 데 필요한 정보를 제공하는 DessertsRepository 클래스의 종속 항목이 DessertsFragment에 있다고 가정해 보세요.

생성자에서 DessertsRepository 인스턴스를 요구하도록 DessertsFragment를 정의할 수 있습니다.

Kotlin

class DessertsFragment(val dessertsRepository: DessertsRepository) : Fragment() {
    ...
}

Java

public class DessertsFragment extends Fragment {
    private DessertsRepository dessertsRepository;

    public DessertsFragment(DessertsRepository dessertsRepository) {
        super();
        this.dessertsRepository = dessertsRepository;
    }

    // Getter omitted.

    ...
}

FragmentFactory의 간단한 구현은 다음과 유사합니다.

Kotlin

class MyFragmentFactory(val repository: DessertsRepository) : FragmentFactory() {
    override fun instantiate(classLoader: ClassLoader, className: String): Fragment =
            when (loadFragmentClass(classLoader, className)) {
                DessertsFragment::class.java -> DessertsFragment(repository)
                else -> super.instantiate(classLoader, className)
            }
}

Java

public class MyFragmentFactory extends FragmentFactory {
    private DessertsRepository repository;

    public MyFragmentFactory(DessertsRepository repository) {
        super();
        this.repository = repository;
    }

    @NonNull
    @Override
    public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) {
        Class<? extends Fragment> fragmentClass = loadFragmentClass(classLoader, className);
        if (fragmentClass == DessertsFragment.class) {
            return new DessertsFragment(repository);
        } else {
            return super.instantiate(classLoader, className);
        }
    }
}

이 예에서는 FragmentFactory를 서브클래스로 분류하여 DessertsFragment의 맞춤 프래그먼트 생성 로직을 제공하는 instantiate() 메서드를 재정의합니다. 다른 프래그먼트 클래스는 super.instantiate()를 통해 FragmentFactory의 기본 동작으로 처리됩니다.

그러면 FragmentManager에서 속성을 설정하여 앱의 프래그먼트를 구성할 때 사용할 팩토리로 MyFragmentFactory를 지정할 수 있습니다. 프래그먼트를 다시 만들 때 MyFragmentFactory가 사용되도록 하려면 활동의 super.onCreate() 이전에 이 속성을 설정해야 합니다.

Kotlin

class MealActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        supportFragmentManager.fragmentFactory = MyFragmentFactory(DessertsRepository.getInstance())
        super.onCreate(savedInstanceState)
    }
}

Java

public class MealActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        DessertsRepository repository = DessertsRepository.getInstance();
        getSupportFragmentManager().setFragmentFactory(new MyFragmentFactory(repository));
        super.onCreate(savedInstanceState);
    }
}

활동에서 FragmentFactory를 설정하면 활동의 프래그먼트 계층 구조 전체에 걸쳐 프래그먼트 생성이 재정의됩니다. 즉, 추가하는 모든 하위 프래그먼트의 childFragmentManager가 하위 수준에서 재정의되지 않는 한 여기에서 설정된 맞춤 프래그먼트 팩토리를 사용합니다.

FragmentFactory로 테스트

단일 활동 아키텍처에서는 FragmentScenario 클래스를 사용하여 프래그먼트를 격리된 상태로 테스트해야 합니다. 활동의 맞춤 onCreate 로직에 의존할 수 없으므로 다음 예와 같이 대신 FragmentFactory를 프래그먼트 테스트의 인수로 전달할 수 있습니다.

// Inside your test
val dessertRepository = mock(DessertsRepository::class.java)
launchFragment<DessertsFragment>(factory = MyFragmentFactory(dessertRepository)).onFragment {
    // Test Fragment logic
}

이 테스트 프로세스에 관한 자세한 내용과 전체 예는 프래그먼트 테스트를 참고하세요.