프래그먼트 디버그

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

FragmentManager 로깅

FragmentManager는 다양한 메시지를 Logcat에 내보낼 수 있습니다. 이 클래스는 Logcat에 노이즈를 추가하지 않도록 기본적으로 사용 중지되지만 이러한 로그 메시지가 프래그먼트 문제 해결에 도움이 될 수 있는 경우가 있습니다. FragmentManager디버그상세 로그 수준에서 가장 의미 있는 출력을 내보냅니다.

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 인스턴스의 FragmentManager 고유 ID. 구성 변경 및 프로세스 중단과 재생성 전반에 걸쳐 안정적입니다.
  • Fragment가 추가된 컨테이너의 ID(설정된 경우에만)
  • Fragment 태그(설정된 경우에만)
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 클래스의 여러 인스턴스를 추적할 수 있도록 모든 프래그먼트 인스턴스에 식별자가 접미사로 추가됩니다.

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-alpha01 버전은 프래그먼트를 위한 StrictMode를 도입했습니다. 앱의 예기치 않은 작동을 야기할 수 있는 몇 가지 일반적인 문제를 포착할 수 있습니다.

기본적으로 Fragment StrictMode에는 아무것도 포착하지 않는 LAX 정책이 있습니다. 하지만 맞춤 정책을 만들 수 있습니다. 맞춤 Policy는 감지할 위반을 정의하고 위반이 감지되면 적용할 페널티를 지정합니다.

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

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)
        ...
   }
}

자바

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());
        ...
   }
}

엄격 모드를 사용 설정할지 여부를 결정하기 위해 Context를 알아야 하는 경우(예: 부울 리소스의 값에서) OnContextAvailableListener를 사용하여 FragmentManager에 StrictMode 정책을 할당하는 작업을 연기합니다.

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)
        ...
   }
}

자바

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());
        ...
   }
}

가능한 모든 위반을 포착하도록 엄격 모드를 구성해야 하는 마지막 지점은 onCreate()에 있습니다. 하지만 여기서는 super.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)
        ...
   }
}

자바

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()
    }
}

자바

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을 발생시킵니다.

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

보관된 Fragment 인스턴스를 이러한 메서드를 사용해 직접 관리하지 말고 대신 처리할 ViewModel에 상태를 저장해야 합니다.

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

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

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

이 메서드를 직접 호출하는 경우에는 대신 setMaxLifecycle()을 호출해야 합니다. 이 메서드를 재정의하는 경우 true 시 전달 시 동작을 onResume()으로 이동하고 false 전달 시 동작을 onPause()로 이동해야 합니다.

대상 프래그먼트 사용

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

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

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

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

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