프래그먼트 디버그

이 가이드에서는 프래그먼트를 디버그하는 데 사용할 수 있는 도구를 설명합니다.

FragmentManager 로깅

FragmentManager는 다양한 메시지를 Logcat에 내보낼 수 있습니다. 이는 기본적으로 사용 중지되어 있지만 이러한 로그 메시지가 프래그먼트 문제를 해결하는 데 도움이 되는 경우가 있습니다. FragmentManagerDEBUGVERBOSE 로그 수준에서 가장 의미 있는 출력을 내보냅니다.

다음 adb shell 명령어를 사용하여 로깅을 사용 설정할 수 있습니다.

adb shell setprop log.tag.FragmentManager DEBUG

또는 다음과 같이 상세 로깅을 사용 설정할 수 있습니다.

adb shell setprop log.tag.FragmentManager VERBOSE

상세 로깅을 사용 설정하면 Logcat 창에서 로그 수준 필터를 적용할 수 있습니다. 하지만 이렇게 하면 FragmentManager 로그뿐만 아니라 모든 로그가 필터링됩니다. 일반적으로 필요한 로그 수준에서만 FragmentManager 로깅을 사용 설정하는 것이 가장 좋습니다.

DEBUG 로깅

DEBUG 수준에서 일반적으로 FragmentManager는 수명 주기 상태 변경과 관련된 로그 메시지를 내보냅니다. 각 로그 항목에는 FragmenttoString() 덤프가 포함됩니다. 로그 항목은 다음 정보로 구성됩니다.

  • Fragment 인스턴스의 간단한 클래스 이름
  • Fragment 인스턴스의 ID 해시 코드
  • Fragment 인스턴스의 프래그먼트 관리자 고유 ID. 구성 변경 및 프로세스 중단과 재생성 전반에 걸쳐 안정적입니다.
  • Fragment가 추가된 컨테이너의 ID(설정된 경우에만)
  • Fragment 태그(설정된 경우에만)

다음은 샘플 DEBUG 로그 항목입니다.

D/FragmentManager: moveto ATTACHED: NavHostFragment{92d8f1d} (fd92599e-c349-4660-b2d6-0ece9ec72f7b id=0x7f080116)
  • Fragment 클래스가 NavHostFragment
  • ID 해시 코드가 92d8f1d
  • 고유 ID가 fd92599e-c349-4660-b2d6-0ece9ec72f7b
  • 컨테이너 ID가 0x7f080116
  • 설정된 태그가 없어 생략됨. 있는 경우 tag=tag_value 형식으로 ID를 따릅니다.

간결성과 가독성을 위해 UUID가 다음 예와 같이 축약되어 있습니다.

다음은 NavHostFragment를 초기화한 다음 FirstFragment 유형의 startDestination Fragment를 만들고 RESUMED 상태로 전환하는 예입니다.

D/FragmentManager: moveto ATTACHED: NavHostFragment{92d8f1d} (<UUID> id=0x7f080116)
D/FragmentManager:   mName=null mIndex=-1 mCommitted=false
D/FragmentManager:   Operations:
D/FragmentManager:     Op #0: SET_PRIMARY_NAV NavHostFragment{92d8f1d} (<UUID> id=0x7f080116)
D/FragmentManager: moveto CREATED: NavHostFragment{92d8f1d} (<UUID> id=0x7f080116)
D/FragmentManager:   mName=null mIndex=-1 mCommitted=false
D/FragmentManager:   Operations:
D/FragmentManager:     Op #0: REPLACE FirstFragment{ccd2189} (<UUID> id=0x7f080116)
D/FragmentManager:     Op #1: SET_PRIMARY_NAV FirstFragment{ccd2189} (<UUID> id=0x7f080116)
D/FragmentManager: moveto ATTACHED: FirstFragment{ccd2189} (<UUID> id=0x7f080116)
D/FragmentManager: moveto CREATED: FirstFragment{ccd2189} (<UUID> id=0x7f080116)
D/FragmentManager: moveto CREATE_VIEW: NavHostFragment{92d8f1d} (<UUID> id=0x7f080116)
D/FragmentManager: moveto CREATE_VIEW: FirstFragment{ccd2189} (<UUID> id=0x7f080116)
D/FragmentManager: moveto ACTIVITY_CREATED: NavHostFragment{92d8f1d} (<UUID> id=0x7f080116)
D/FragmentManager: moveto RESTORE_VIEW_STATE: NavHostFragment{92d8f1d} (<UUID> id=0x7f080116)
D/FragmentManager: moveto ACTIVITY_CREATED: FirstFragment{ccd2189} (<UUID> id=0x7f080116)
D/FragmentManager: moveto RESTORE_VIEW_STATE: FirstFragment{ccd2189} (<UUID> id=0x7f080116)
D/FragmentManager: moveto STARTED: NavHostFragment{92d8f1d} (<UUID> id=0x7f080116)
D/FragmentManager: moveto STARTED: FirstFragment{ccd2189} (<UUID> id=0x7f080116)
D/FragmentManager: moveto RESUMED: NavHostFragment{92d8f1d} (<UUID> id=0x7f080116)
D/FragmentManager: moveto RESUMED: FirstFragment{ccd2189} (<UUID> id=0x7f080116)

FirstFragment는 사용자 상호작용 후 다양한 수명 주기 상태에서 전환됩니다. 그런 다음 SecondFragment가 인스턴스화되고 RESUMED 상태로 전환됩니다.

D/FragmentManager:   mName=07c8a5e8-54a3-4e21-b2cc-c8efc37c4cf5 mIndex=-1 mCommitted=false
D/FragmentManager:   Operations:
D/FragmentManager:     Op #0: REPLACE SecondFragment{84132db} (<UUID> id=0x7f080116)
D/FragmentManager:     Op #1: SET_PRIMARY_NAV SecondFragment{84132db} (<UUID> id=0x7f080116)
D/FragmentManager: movefrom RESUMED: FirstFragment{ccd2189} (<UUID> id=0x7f080116)
D/FragmentManager: movefrom STARTED: FirstFragment{ccd2189} (<UUID> id=0x7f080116)
D/FragmentManager: movefrom ACTIVITY_CREATED: FirstFragment{ccd2189} (<UUID> id=0x7f080116)
D/FragmentManager: moveto ATTACHED: SecondFragment{84132db} (<UUID> id=0x7f080116)
D/FragmentManager: moveto CREATED: SecondFragment{84132db} (<UUID> id=0x7f080116)
D/FragmentManager: moveto CREATE_VIEW: SecondFragment{84132db} (<UUID> id=0x7f080116)
D/FragmentManager: moveto ACTIVITY_CREATED: SecondFragment{84132db} (<UUID> id=0x7f080116)
D/FragmentManager: moveto RESTORE_VIEW_STATE: SecondFragment{84132db} (<UUID> id=0x7f080116)
D/FragmentManager: moveto STARTED: SecondFragment{84132db} (<UUID> id=0x7f080116)
D/FragmentManager: movefrom CREATE_VIEW: FirstFragment{ccd2189} (<UUID> id=0x7f080116)
D/FragmentManager: moveto RESUMED: SecondFragment{84132db} (<UUID> id=0x7f080116)

동일한 Fragment 클래스의 여러 인스턴스를 추적할 수 있도록 모든 Fragment 인스턴스에 식별자가 접미사로 추가됩니다.

VERBOSE 로깅

VERBOSE 수준에서 일반적으로 FragmentManager는 내부 상태에 관한 로그 메시지를 내보냅니다.

V/FragmentManager: Run: BackStackEntry{f9d3ff3}
V/FragmentManager: add: NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: Added fragment to active set NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 1 for NavHostFragment{86274b0} (<UUID> id=0x7f080130)
D/FragmentManager: moveto ATTACHED: NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: Commit: BackStackEntry{5cfd2ae}
D/FragmentManager:   mName=null mIndex=-1 mCommitted=false
D/FragmentManager:   Operations:
D/FragmentManager:     Op #0: SET_PRIMARY_NAV NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 1 for NavHostFragment{86274b0} (<UUID> id=0x7f080130)
D/FragmentManager: moveto CREATED: NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: Commit: BackStackEntry{e93833f}
D/FragmentManager:   mName=null mIndex=-1 mCommitted=false
D/FragmentManager:   Operations:
D/FragmentManager:     Op #0: REPLACE FirstFragment{886440c} (<UUID> id=0x7f080130)
D/FragmentManager:     Op #1: SET_PRIMARY_NAV FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: Run: BackStackEntry{e93833f}
V/FragmentManager: add: FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: Added fragment to active set FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 1 for FirstFragment{886440c} (<UUID> id=0x7f080130)
D/FragmentManager: moveto ATTACHED: FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 1 for FirstFragment{886440c} (<UUID> id=0x7f080130)
D/FragmentManager: moveto CREATED: FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 1 for FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 1 for FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 1 for FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 1 for FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 1 for NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 1 for NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 1 for NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 4 for NavHostFragment{86274b0} (<UUID> id=0x7f080130)
D/FragmentManager: moveto CREATE_VIEW: NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 2 for FirstFragment{886440c} (<UUID> id=0x7f080130)
D/FragmentManager: moveto CREATE_VIEW: FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 2 for FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 2 for FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 4 for NavHostFragment{86274b0} (<UUID> id=0x7f080130)
D/FragmentManager: moveto ACTIVITY_CREATED: NavHostFragment{86274b0} (<UUID> id=0x7f080130)
D/FragmentManager: moveto RESTORE_VIEW_STATE: NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 4 for FirstFragment{886440c} (<UUID> id=0x7f080130)
D/FragmentManager: moveto ACTIVITY_CREATED: FirstFragment{886440c} (<UUID> id=0x7f080130)
D/FragmentManager: moveto RESTORE_VIEW_STATE: FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 4 for FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: SpecialEffectsController: Enqueuing add operation for fragment FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 4 for FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 4 for FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: SpecialEffectsController: For fragment FirstFragment{886440c} (<UUID> id=0x7f080130) mFinalState = VISIBLE -> VISIBLE.
V/FragmentManager: SpecialEffectsController: Container androidx.fragment.app.FragmentContainerView{7578ffa V.E...... ......I. 0,0-0,0 #7f080130 app:id/nav_host_fragment_content_fragment} is not attached to window. Cancelling pending operation Operation {382a9ab} {mFinalState = VISIBLE} {mLifecycleImpact = ADDING} {mFragment = FirstFragment{886440c} (<UUID> id=0x7f080130)}
V/FragmentManager: SpecialEffectsController: Operation {382a9ab} {mFinalState = VISIBLE} {mLifecycleImpact = ADDING} {mFragment = FirstFragment{886440c} (<UUID> id=0x7f080130)} has called complete.
V/FragmentManager: SpecialEffectsController: Setting view androidx.constraintlayout.widget.ConstraintLayout{3968808 I.E...... ......I. 0,0-0,0} to VISIBLE
V/FragmentManager: computeExpectedState() of 4 for FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 4 for NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: SpecialEffectsController: Enqueuing add operation for fragment NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 4 for NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 4 for NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: SpecialEffectsController: For fragment NavHostFragment{86274b0} (<UUID> id=0x7f080130) mFinalState = VISIBLE -> VISIBLE.
V/FragmentManager: SpecialEffectsController: Container androidx.fragment.app.FragmentContainerView{2ba8ba1 V.E...... ......I. 0,0-0,0 #7f080130 app:id/nav_host_fragment_content_fragment} is not attached to window. Cancelling pending operation Operation {f7eb1c6} {mFinalState = VISIBLE} {mLifecycleImpact = ADDING} {mFragment = NavHostFragment{86274b0} (<UUID> id=0x7f080130)}
V/FragmentManager: SpecialEffectsController: Operation {f7eb1c6} {mFinalState = VISIBLE} {mLifecycleImpact = ADDING} {mFragment = NavHostFragment{86274b0} (<UUID> id=0x7f080130)} has called complete.
V/FragmentManager: SpecialEffectsController: Setting view androidx.fragment.app.FragmentContainerView{7578ffa I.E...... ......I. 0,0-0,0 #7f080130 app:id/nav_host_fragment_content_fragment} to VISIBLE
V/FragmentManager: computeExpectedState() of 4 for NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: Run: BackStackEntry{5cfd2ae}
V/FragmentManager: computeExpectedState() of 4 for NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 4 for NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 4 for NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 5 for NavHostFragment{86274b0} (<UUID> id=0x7f080130)
D/FragmentManager: moveto STARTED: NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 5 for FirstFragment{886440c} (<UUID> id=0x7f080130)
D/FragmentManager: moveto STARTED: FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 5 for FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 5 for FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 5 for NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 5 for NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 7 for NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 7 for NavHostFragment{86274b0} (<UUID> id=0x7f080130)
D/FragmentManager: moveto RESUMED: NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 7 for FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 7 for FirstFragment{886440c} (<UUID> id=0x7f080130)
D/FragmentManager: moveto RESUMED: FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 7 for FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 7 for FirstFragment{886440c} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 7 for NavHostFragment{86274b0} (<UUID> id=0x7f080130)
V/FragmentManager: computeExpectedState() of 7 for NavHostFragment{86274b0} (<UUID> id=0x7f080130)

이 예에서는 FirstFragment의 로드만 다룹니다. SecondFragment로의 전환을 포함하면 로그 항목이 상당히 증가합니다. VERBOSE 수준 로그 메시지 중 상당수는 앱 개발자에게 거의 도움이 되지 않습니다. 그러나 백 스택이 변경되는 시점을 확인하면 일부 문제를 디버그하는 데 도움이 될 수도 있습니다.

프래그먼트의 StrictMode

Jetpack Fragment 라이브러리 버전 1.4.0 이상에는 프래그먼트용 StrictMode가 포함되어 있습니다. 앱의 예기치 않은 작동을 야기할 수 있는 몇 가지 일반적인 문제를 포착할 수 있습니다. StrictMode 작업에 관한 자세한 내용은 StrictMode를 참고하세요.

맞춤 Policy는 감지할 위반을 정의하고 위반이 감지되면 적용할 페널티를 지정합니다.

맞춤 StrictMode 정책을 적용하려면 FragmentManager에 할당합니다. 최대한 빨리 이를 실행합니다. init 블록 또는 Java 생성자에서 할당해야 합니다.

Kotlin

class ExampleActivity : AppCompatActivity() {

    init {
        supportFragmentManager.strictModePolicy =
            FragmentStrictMode.Policy.Builder()
                .penaltyDeath()
                .detectFragmentReuse()
                .allowViolation(FirstFragment::class.java,
                                FragmentReuseViolation::class.java)
                .build()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding = ActivityExampleBinding.inflate(layoutInflater)
        setContentView(binding.root)
        ...
   }
}

Java

class ExampleActivity extends AppCompatActivity() {

    ExampleActivity() {
        getSupportFragmentManager().setStrictModePolicy(
                new FragmentStrictMode.Policy.Builder()
                        .penaltyDeath()
                        .detectFragmentReuse()
                        .allowViolation(FirstFragment.class,
                                        FragmentReuseViolation.class)
                        .build()
        );
    }

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

        ActivityExampleBinding binding =
            ActivityExampleBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        ...
   }
}

불리언 리소스의 값에서와 같이 StrictMode의 사용 설정 여부를 판단하기 위해 Context를 알아야 하는 경우 OnContextAvailableListener를 사용하여 StrictMode 정책을 FragmentManager에 할당하는 작업을 연기할 수 있습니다.

Kotlin

class ExampleActivity : AppCompatActivity() {

    init {
        addOnContextAvailableListener { context ->
            if(context.resources.getBoolean(R.bool.enable_strict_mode)) {
                supportFragmentManager.strictModePolicy = FragmentStrictMode.Policy.Builder()
                    .penaltyDeath()
                    .detectFragmentReuse()
                    .allowViolation(FirstFragment::class.java, FragmentReuseViolation::class.java)
                    .build()
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding = ActivityExampleBinding.inflate(layoutInflater)
        setContentView(binding.root)
        ...
   }
}

Java

class ExampleActivity extends AppCompatActivity() {

    ExampleActivity() {
        addOnContextAvailableListener((context) -> {
            if(context.getResources().getBoolean(R.bool.enable_strict_mode)) {
                getSupportFragmentManager().setStrictModePolicy(
                        new FragmentStrictMode.Policy.Builder()
                                .penaltyDeath()
                                .detectFragmentReuse()
                                .allowViolation(FirstFragment.class, FragmentReuseViolation.class)
                                .build()
                );
            }
        }
    }

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

        ActivityExampleBinding binding = ActivityExampleBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        ...
   }
}

가능한 모든 위반을 포착하도록 StrictMode를 구성할 수 있는 마지막 지점은 super.onCreate()를 호출하기 전 onCreate()에 있습니다.

Kotlin

class ExampleActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        supportFragmentManager.strictModePolicy = FragmentStrictMode.Policy.Builder()
            .penaltyDeath()
            .detectFragmentReuse()
            .allowViolation(FirstFragment::class.java, FragmentReuseViolation::class.java)
            .build()

        super.onCreate(savedInstanceState)

        val binding = ActivityExampleBinding.inflate(layoutInflater)
        setContentView(binding.root)
        ...
   }
}

Java

class ExampleActivity extends AppCompatActivity() {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        getSupportFragmentManager().setStrictModePolicy(
                new FragmentStrictMode.Policy.Builder()
                        .penaltyDeath()
                        .detectFragmentReuse()
                        .allowViolation(FirstFragment.class, FragmentReuseViolation.class)
                        .build()
                );

        super.onCreate(savedInstanceState)

        ActivityExampleBinding binding = ActivityExampleBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        ...
   }
}

이 예에서 사용된 정책은 프래그먼트 재사용 위반만 감지하며 이 위반이 발생할 때마다 앱이 종료됩니다. penaltyDeath()는 위반을 무시할 수 없을 정도로 빠르게 실패하므로 디버그 빌드에서 유용할 수 있습니다.

특정 위반을 선택적으로 허용할 수도 있습니다. 그러나 앞의 예에서 사용된 정책은 다른 모든 프래그먼트 유형에 이 위반을 적용합니다. 이 옵션은 서드 파티 라이브러리 구성요소에 StrictMode 위반이 포함될 수도 있는 경우에 유용합니다.

이러한 경우 라이브러리에서 위반을 수정할 때까지 소유하지 않은 구성요소의 StrictMode 허용 목록에 이 위반을 일시적으로 추가할 수 있습니다.

다른 위반을 구성하는 방법에 관한 자세한 내용은 FragmentStrictMode.Policy.Builder 문서를 참고하세요.

세 가지 페널티 유형이 있습니다.

  • penaltyLog()는 위반 세부정보를 Logcat에 덤프합니다.
  • penaltyDeath()는 위반을 감지한 경우 앱을 종료합니다.
  • penaltyListener()를 사용하면 위반이 감지될 때마다 호출되는 맞춤 리스너를 추가할 수 있습니다.

Policy에서 페널티를 원하는 대로 조합하여 적용할 수 있습니다. 정책에서 페널티를 명시적으로 지정하지 않으면 기본값 penaltyLog()가 적용됩니다. 맞춤 Policy에서 penaltyLog() 외의 페널티를 적용하면 명시적으로 설정하지 않는 한 penaltyLog()가 사용 중지됩니다.

penaltyListener()는 위반을 로깅할 서드 파티 로깅 라이브러리가 있는 경우에 유용합니다. 또는 출시 빌드에서 심각하지 않은 위반 포착을 사용 설정하고 비정상 종료 보고 라이브러리에 로깅하는 것이 좋습니다. 이 전략을 사용하면 놓칠 수 있는 위반을 감지할 수 있습니다.

전역 StrictMode 정책을 설정하려면 FragmentStrictMode.setDefaultPolicy() 메서드를 사용하여 모든 FragmentManager 인스턴스에 적용되는 기본 정책을 설정합니다.

Kotlin

class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        FragmentStrictMode.defaultPolicy =
            FragmentStrictMode.Policy.Builder()
                .detectFragmentReuse()
                .detectFragmentTagUsage()
                .detectRetainInstanceUsage()
                .detectSetUserVisibleHint()
                .detectTargetFragmentUsage()
                .detectWrongFragmentContainer()
                .apply {
                    if (BuildConfig.DEBUG) {
                        // Fail early on DEBUG builds
                        penaltyDeath()
                    } else {
                        // Log to Crashlytics on RELEASE builds
                        penaltyListener {
                            FirebaseCrashlytics.getInstance().recordException(it)
                        }
                    }
                }
                .build()
    }
}

Java

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        FragmentStrictMode.Policy.Builder builder = new FragmentStrictMode.Policy.Builder();
        builder.detectFragmentReuse()
                .detectFragmentTagUsage()
                .detectRetainInstanceUsage()
                .detectSetUserVisibleHint()
                .detectTargetFragmentUsage()
                .detectWrongFragmentContainer();
        if (BuildConfig.DEBUG) {
            // Fail early on DEBUG builds
            builder.penaltyDeath();
        } else {
            // Log to Crashlytics on RELEASE builds
            builder.penaltyListener((exception) ->
                    FirebaseCrashlytics.getInstance().recordException(exception)
            );
        }
        FragmentStrictMode.setDefaultPolicy(builder.build());
    }
}

다음 섹션에서는 위반의 유형과 가능한 해결 방법을 설명합니다.

프래그먼트 재사용

프래그먼트 재사용 위반은 detectFragmentReuse()를 통해 사용 설정되며 FragmentReuseViolation을 발생시킵니다.

이 위반은 Fragment 인스턴스를 FragmentManager에서 삭제한 후에 재사용했음을 나타냅니다. 이러한 재사용으로 인해 Fragment가 이전 사용의 상태를 유지하며 일관되게 동작하지 않을 수도 있기 때문에 문제가 발생할 수 있습니다. 매번 새 인스턴스를 생성하는 경우 인스턴스는 FragmentManager에 추가될 때 항상 초기 상태입니다.

프래그먼트 태그 사용

프래그먼트 태그 사용 위반은 detectFragmentTagUsage()를 통해 사용 설정되며 FragmentTagUsageViolation을 발생시킵니다.

이 위반은 XML 레이아웃에서 <fragment> 태그를 사용하여 Fragment가 확장되었음을 나타냅니다. 이 위반을 해결하려면 <fragment> 태그가 아닌 <androidx.fragment.app.FragmentContainerView> 내부에서 Fragment를 확장합니다. FragmentContainerView를 사용하여 확장된 프래그먼트는 Fragment 트랜잭션과 구성 변경을 안정적으로 처리할 수 있습니다. <fragment> 태그를 대신 사용하면 제대로 작동하지 않을 수도 있습니다.

보관 인스턴스 사용

보관 인스턴스 사용 위반은 detectRetainInstanceUsage()를 통해 사용 설정되며 RetainInstanceUsageViolation을 발생시킵니다.

이 위반은 특히 모두 지원 중단된 setRetainInstance() 또는 getRetainInstance() 호출이 있는 경우 보관된 Fragment가 사용되었음을 나타냅니다.

보관된 Fragment 인스턴스를 직접 관리하기 위해 이러한 메서드를 사용하는 대신 이를 처리해 주는 ViewModel에 상태를 저장합니다.

사용자에게 표시되는 힌트 설정

사용자에게 표시되는 힌트 설정 위반은 detectSetUserVisibleHint()를 통해 사용 설정되며 SetUserVisibleHintViolation을 발생시킵니다.

이 위반은 지원 중단된 setUserVisibleHint()를 호출했음을 나타냅니다.

이 메서드를 수동으로 호출하는 경우에는 대신 setMaxLifecycle()을 호출하세요. 이 메서드를 재정의하면 true를 전달할 때 동작을 onResume()으로 이동하고 false를 전달할 때 동작을 onPause()로 이동합니다.

대상 프래그먼트 사용

대상 프래그먼트 사용 위반은 detectTargetFragmentUsage()를 통해 사용 설정되며 TargetFragmentUsageViolation을 발생시킵니다.

이 위반은 지원 중단된 setTargetFragment(), getTargetFragment(), getTargetRequestCode()를 호출했음을 나타냅니다. 이러한 메서드를 사용하는 대신 FragmentResultListener를 등록하세요. 결과 전달에 관한 자세한 내용은 프래그먼트 간 결과 전달을 참고하세요.

잘못된 프래그먼트 컨테이너

잘못된 프래그먼트 컨테이너 위반은 detectWrongFragmentContainer()를 통해 사용 설정되며 WrongFragmentContainerViolation을 발생시킵니다.

이 위반은 FragmentContainerView 이외의 컨테이너에 Fragment를 추가했음을 나타냅니다. Fragment 태그 사용과 마찬가지로 프래그먼트 트랜잭션은 FragmentContainerView 내에서 호스팅하지 않으면 예상대로 작동하지 않을 수 있습니다. 또한 컨테이너 뷰를 사용하면 종료 애니메이션을 사용하는 프래그먼트가 다른 모든 프래그먼트 위에 그려지도록 하는 View API의 문제를 해결하는 데 도움이 됩니다.