Fragment에서 뒤로가기 동작 커스텀하기

DoDoBest

·

2024. 5. 4. 21:07

공식 문서 예제

Fragment에서 뒤로가기 동작을 커스텀하려면 Activity에 onBackPressedDispatcher를 등록해주면 됩니다.

 

class MainPageFragment : Fragment() {

	...

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

        requireActivity().onBackPressedDispatcher.addCallback(this) {
            // Handle the back button event
        }
        ...
    }
    ...
}

https://developer.android.com/guide/navigation/navigation-custom-back#implement

 

위 코드가 가진 잠재적인 문제는 무엇이 있을까요?

 

Fragment Lifecycle

dispatcher 함수의 lifecycleOwner로 Fragment를 전달했습니다. 내부 함수를 따라가보면 ON_STOP 또는 ON_DESTROY가 호출되지 않는한  observing이 계속되는 것을 볼 수 있습니다.

 

 

 

FragmentTransaction을 이용해 add로 새로운 Fragment를 호출하면 기존 Fragment는 RESUMED를 유지한 상태로 새로운 Fragment 아래에 살아있습니다.

그래서 새로운 Fragment에서 뒤로가기 버튼을 누르면 기존 Fragment가 뒤로가기 event를 가로챕니다.

 

replace를 이용해서 기존 Fragment를 CREATED 상태로 바꿀 수 있지만, View를 파괴하고 싶지 않은 경우에는 적절한 방법이 아닙니다.

 

해결 방법 1. 새로운 Fragment에서 onBackPressedDispatcher 등록하기

onBackPressedDispatcher는 가장 최근에 등록된 순서대로 호출되고, 먼저 호출된 곳에서 처리를 하지 않고 넘기면 그 다음으로 최근에 등록된 곳이 호출되는 방식으로 동작합니다.

그래서 새로운 Fragment에서 onBackPressedDispatcher를 등록하고, 여기서 popBackStack이 호출되도록 처리합니다.

 

class DetailPageFragment : Fragment() {

    ...

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


        requireActivity().onBackPressedDispatcher.addCallback(this) {
            if (isEnabled) {
                parentFragmentManager.popBackStack()
            }
        }
    }
    
    ...
    
}

 

해결 방법 2. 기존 Fragment의 Lifecycle 변경하기

FragmentTransaction 함수의 setMaxLifecycle 함수를 이용해 기존 Fragment의 Lifecycle을 STARTED로 변경하여 onPause 콜백 함수가 호출되도록 합니다.

onPause 상태에서는 Fragment View가 파괴되지 않은 상태입니다. 새로 생성한 Fragment의 Root Layout의 background 값을 불투명하게 설정하지 않았다면 기존 Fragment가 보이게 됩니다. 기본 값이 투명색이기 때문입니다. 그래서 hide 함수를 이용해 기존 Fragment가 보이지 않도록 설정해줍니다.

 

 parentFragmentManager.commit {
    if (this@MainPageFragment.isVisible) {
        hide(this@MainPageFragment)
        setMaxLifecycle(this@MainPageFragment, Lifecycle.State.STARTED)
    }
    val bundle = Bundle().apply {
        putParcelable(DetailPageFragment.BUNDLE_KEY_FOR_PRODUCT, product)
    }
    val fragment = DetailPageFragment().apply {
        arguments = bundle
    }

    add(R.id.fragment_container_view, fragment, DetailPageFragment.DETAIL_PAGE_FRAGMENT_TAG)
    setReorderingAllowed(true)
    addToBackStack(null)
}

 

 

onBackPressedDispatcher callback 함수에서 enalbed인 경우에만 동작하도록 설정하면 변수를 별도의 property로 저장합니다.

 

class MainPageFragment : Fragment() {

	...

    private lateinit var callback: OnBackPressedCallback

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

        callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
            if (isEnabled) {
                // Handle the back button event
            }
        }
        ...
    }
    ...
}

 

 

onPause callback 함수에서 callback의 enabled 값을 false로 변경해 callback 함수에 등록한 기능이 동작하지 않도록 설정합니다. onResume 함수에서는 callback의 enabled 값을 true로 설정합니다.

override fun onPause() {
    super.onPause()

    callback.isEnabled = false
}

override fun onResume() {
    super.onResume()

    callback.isEnabled = true
}

 

해결 방법 3. 기존 Fragment의 Lifecycle 변경하기 + LifecycleOwner 설정

해결 방법2로도 충분히 동작하지만, Lifecycle에 따라 알아서 OnBackPressedCallback이 observing을 멈추도록 변경하는 방법을 알아보겠습니다.

 

먼저 LifecycleOwner를 구현하는 클래스를 만들어줍니다.

 

class SimpleLifecycleOwner: LifecycleOwner {
    private val _lifecycle = LifecycleRegistry(this)

    override val lifecycle: Lifecycle
        get() = _lifecycle

    fun handleLifecycleEvent(event: Lifecycle.Event) {
        _lifecycle.handleLifecycleEvent(event)
    }
}

 

 

Fragment의 Lifecycle에 따라 원하는 callback State가 호출되었을 때, 위에서 만든 SimpleLifecycleOwner의 lifecycle을 변경해줍니다.

해결 방법2에서 기존 Fragment에서 새로운 Fragment를 add할 때, 기존 Fragment를 hide해주고, ON_PAUSE 상태가 되도록 설정했기 때문에, 아래와 같이 조건문을 작성해줬습니다.

ON_STOP으로 설정한 이유는 OnBackPressedCallback가 observing을 멈추는 시점이 ON_STOP 또는 ON_DESTROY이기 때문입니다.

 

    private val visibleLifecycleOwner: SimpleLifecycleOwner by lazy {
        SimpleLifecycleOwner()
    }

    private fun setLifecycle() {
        viewLifecycleOwner.lifecycle.addObserver(object : LifecycleEventObserver {
            override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
                when (event) {
                    Lifecycle.Event.ON_RESUME, Lifecycle.Event.ON_PAUSE -> {
                        if (isHidden) {
                            visibleLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
                        } else {
                            visibleLifecycleOwner.handleLifecycleEvent(event)
                        }
                    }

                    else -> visibleLifecycleOwner.handleLifecycleEvent(event)
                }
            }

        })
    }

 

마지막으로 onBackPressedDispatcher의 LifecycleOwner로 SimpleLifecycleOwner를 전달해주면 됩니다.

방법2에서 작성한 onPause, onResume에서 enabled를 변경하는 코드는 지워주세요.

 

class MainPageFragment : Fragment() {

	...

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

        requireActivity().onBackPressedDispatcher.addCallback(visibleLifecycleOwner) {
            // Handle the back button event
        }
        ...
    }
    ...
}

 

 

 

 

참고 자료

https://pluu.github.io/blog/android/2023/01/19/fragment_visible_lifecycleowner/

 

Pluu Dev - Fragment의 Show/Hide와 함께 Lifecycle 레벨업

[메모] Actions on Save Posted on 28 Apr 2024 [메모] Compose LazyVerticalGrid 렌더링 프로파일 체크 Posted on 21 Apr 2024 [메모] AndroidX Lifecycle 2.7.0-alpha02부터 변경된 동작 Posted on 10 Apr 2024 [DataBinding] 중복으로 BindingAd

pluu.github.io