/ ANDROID, FRAGMENTPAGERADAPTER, FRAGMENTSTATEPAGERADAPTER, BEHAVIOR_SET_USER_VISIBLE_HINT

FragmentPagerAdapter, FragmentStatePagerAdapter (생성자 Behavior편)

이름이 워낙 길고, 비슷하기 까지 해서 항상 어떤 녀석을 사용 해야 할지 망설이게 됩니다. 내 앱에서는 딱히 configChanges 나 onPause/onResume에 대한 처리를 딱히 타이트하지 않는데 꼭 “State”가 붙은 것을 사용해야 할지 고민도 되구요. 이 두 클래스에 대해 살펴보겠습니다. 우선 변경된 생성자 이야기부터 시작하겠습니다.

두 클래스 모두 androidx.fragment.app 패키지에 포함되어 있습니다. 이번 문서에서는 공통적인 내용만 다룰 예정이라 FragmentPagerAdapter를 기준으로 설명하고 Fragment(State)PagerAdapter에 대한 지칭은 생략하겠습니다.

급하신 분들은 뒷쪽에 BEHAVIOR_SET_USER_VISIBLE_HINT 과 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 비교를 보시면 됩니다.

FragmentPagerAdapter는 Deprecated된 클래스가 아닙니다. Deprecated된 생성자가 있을 뿐입니다.

기존에 사용하던 FragmentPagerAdapter들이 모두 Deprecated된 상태로 표시되어 오해하시는 분들이 많습니다. 또한 지금 Android developers 페이지를 보면 최종적으로는 RecyclerView를 상속하는 ViewPager2와 FragmentStateAdapter로 전향 할 것을 권고하고 있습니다. 이때문에 오해하시는 경우가 많은데, Deprecated된 생성자가 있을 뿐입니다.

한마디로 정리하면 Fragment.setUserVisibleHint가 Deprecated되었기 때문에 FragmentPagerAdapter(FragmentManager) 생성자는 Deprecated되었습니다.

FragmentPagerAdapter(FragmentManager,@Behavior int)생성자를 추가하였고 Fragment.setUserVisibleHint를 호출하는 BEHAVIOR_SET_USER_VISIBLE_HINT를 그대로 남겨두었고 이것을 Deprecated한것입니다. 그리고 이제 setUserVisibleHint를 사용하지 않고 정밀한 생명주기를 컨트롤하는 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT를 제공합니다.

BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 적용하지않은 Deprecated된 상태 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 적용한 상태

이 생성자의 플래그에 따라 동작이 완전히 달라지가 됩니다.

클래스를 바로 Deprecated로 처리하지않은 구글개발자의 고심이 느껴지는 부분입니다. FragmentPagerAdapter.instantiateItem/setPrimaryItem 두 메서드외에는 달라지는 내용이 없기 때문에 새로운 클래스를 만들지 않고 플래그만 추가하고 기존 로직만 Deprecated 하였습니다.

왜 Deprecated 되었나?

setUserVisibleHint(boolean)이 호출되는 케이스는 두가지 경우가 있습니다.

  1. instantiateItem 에서 아이템이 생성될 때 false 호출.
  2. setPrimaryItem 메서드가 호출 되었을때, 기존/새로운 주아이템에 각각 호출.

setUserVisibleHint 메서드 소스 주석

An app may set this to false to indicate that the fragment’s UI is scrolled out of visibility or is otherwise not directly visible to the user. This may be used by the system to prioritize operations such as fragment lifecycle updates or loader ordering behavior.

setUserVisibleHint 페이지 코멘트를 보면 왜 이 메서드를 포기하고 FragmentTransaction.setMaxLifecycle으로 넘어가게 되었는지 추측 해 볼 수 있습니다.

This method may be called outside of the fragment lifecycle. and thus has no ordering guarantees with regard to fragment lifecycle method calls.

지금까지 많은 개발자들은 setUserVisibleHint를 override하여 페이지 전환에 대한 처리요청을 처리해 왔습니다. 하지만 setUserVisibleHint는 fragment의 생명주기에 따른 동작을 보장 하지 않기 때문에 여러가지 문제를 일으켜 왔습니다.

Fragment에서 setUserVisibleHint의 정의

public void setUserVisibleHint(boolean isVisibleToUser) {
    if (!mUserVisibleHint && isVisibleToUser && mState < STARTED
            && mFragmentManager != null && isAdded() && mIsCreated) {
      mFragmentManager.performPendingDeferredStart(this);
    }
    ...
}

BEHAVIOR_SET_USER_VISIBLE_HINT 과 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 비교

두가지 플래그를 비교하기 위해 아래처럼 로그를 심어서 비교해 보았습니다.

private lateinit var pageViewModel: PageViewModel // onCreated에서 초기화
private var position = -1 // 테스트를 위해 인스턴스를 생성할때 값을 부여했습니다.

override fun setUserVisibleHint(isVisibleToUser: Boolean) {
    super.setUserVisibleHint(isVisibleToUser)
    if (::pageViewModel.isInitialized) {
        Log.e("TEST", "pageViewModel($position) is Initialized - setUserVisibleHint : $isVisibleToUser")
    } else {
        Log.e("TEST", "pageViewModel($position) is not Initialized - setUserVisibleHint : $isVisibleToUser")
    }
}

override fun onResume() {
    super.onResume()
    if (::pageViewModel.isInitialized) {
        Log.e("TEST", "pageViewModel($position) is Initialized - onResume")
    } else {
        Log.e("TEST", "pageViewModel($position) is not Initialized - onResume")
    }
}

override fun onPause() {
    super.onPause()
    if (::pageViewModel.isInitialized) {
        Log.e("TEST", "pageViewModel($position) is Initialized - onPause")
    } else {
        Log.e("TEST", "pageViewModel($position) is not Initialized - onPause")
    }
}

BEHAVIOR_SET_USER_VISIBLE_HINT 적용 LOG

// 첫 실행 (1번 페이지)
E/TEST: pageViewModel(1) is not Initialized - setUserVisibleHint : false
E/TEST: pageViewModel(2) is not Initialized - setUserVisibleHint : false
E/TEST: pageViewModel(1) is not Initialized - setUserVisibleHint : true
E/TEST: pageViewModel(1) is Initialized - onResume
E/TEST: pageViewModel(2) is Initialized - onResume

// 오른쪽으로 스와이프 (2번 페이지로 이동)
E/TEST: pageViewModel(3) is not Initialized - setUserVisibleHint : false
E/TEST: pageViewModel(1) is Initialized - setUserVisibleHint : false
E/TEST: pageViewModel(2) is Initialized - setUserVisibleHint : true
E/TEST: pageViewModel(3) is Initialized - onResume

// 오른쪽으로 스와이프 (3번 페이지로 이동)
E/TEST: pageViewModel(4) is not Initialized - setUserVisibleHint : false
E/TEST: pageViewModel(2) is Initialized - setUserVisibleHint : false
E/TEST: pageViewModel(3) is Initialized - setUserVisibleHint : true
E/TEST: pageViewModel(1) is Initialized - onPause
E/TEST: pageViewModel(4) is Initialized - onResume

// 왼쪽 스와이프 (2번 페이지로 이동)
E/TEST: pageViewModel(1) is Initialized - setUserVisibleHint : false
E/TEST: pageViewModel(3) is Initialized - setUserVisibleHint : false
E/TEST: pageViewModel(2) is Initialized - setUserVisibleHint : true
E/TEST: pageViewModel(4) is Initialized - onPause
E/TEST: pageViewModel(1) is Initialized - onResume

BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 적용 LOG

// 첫 실행 (1번 페이지)
E/TEST: pageViewModel(1) is Initialized - onCreate
E/TEST: pageViewModel(2) is Initialized - onCreate
E/TEST: pageViewModel(1) is Initialized - onResume

// 오른쪽으로 스와이프 (2번 페이지로 이동)
E/TEST: pageViewModel(3) is Initialized - onCreate
E/TEST: pageViewModel(1) is Initialized - onPause
E/TEST: pageViewModel(2) is Initialized - onResume

// 오른쪽으로 스와이프 (3번 페이지로 이동)
E/TEST: pageViewModel(4) is Initialized - onCreate
E/TEST: pageViewModel(2) is Initialized - onPause
E/TEST: pageViewModel(3) is Initialized - onResume

// 왼쪽 스와이프 (2번 페이지로 이동)
E/TEST: pageViewModel(3) is Initialized - onPause
E/TEST: pageViewModel(2) is Initialized - onResume

정리

로그를 보시면 기존에 사용하던 방식인 BEHAVIOR_SET_USER_VISIBLE_HINTsetUserVisibleHint메서드를 오버라이드하였으며, fragment가 onCreate된 후에 setUserVisibleHint이 호출됨를 보장하지 않습니다. 생명주기 상태가 명확하지 않아서 개발자의 많은 개입이 요구됩니다. 또한 사용하지않는 미리 불러놓은 캐쉬된 아이템(현재 활성화된 아이템의 좌우페이지)의 생명주기가 계속 변경되어 더욱 고려할 사항이 많았습니다.

BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT를 사용하면 onResume/onPause 만으로 깔끔하게 bind/unbind 로직을 구현 할 수 있게 되었습니다. 또한 이제 캐쉬된 아이템은 준비단계에서 onCreate만 호출됩니다.

Search

Get more post