나는 회사에 가기 전 FragmentMannager을 막 썼었다...
그런데 회사에 가서 바텀 프레그먼트간의 취소 버튼등으로 사용자가 원하는 방향으로 움직이는 것에 대해 개발을 하다보니
막쓰면 안된다는 것을 알았고 잘 정리 해보려고 한다.
예시1 (activity 내에서)
supportFragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
supportFragmentManager.beginTransaction()
.replace(R.id.main_fl, fragment, tag)
.addToBackStack(tag)
.commitAllowingStateLoss()
예시2 (activity 내에서)
supportFragmentManager.popBackStackImmediate()
val index = supportFragmentManager.backStackEntryCount - 1
if (index >= 0) {
val backEntry = supportFragmentManager.getBackStackEntryAt(index)
changeBottomNavigationView(backEntry.name)
}
예시3 (activity 내에서)
private fun changeBottomNavigationView(name: String?) {
when (name) {
ApplicationClass.FRAGMENT_HOME_ID -> {
BottomNavigationUtil.setSelect(this, R.id.menu_main_btm_nav_home)
}
...
예시4 (util class 내에서)
fun setSelect(activity: MainActivity, @IdRes elementId: Int) {
CoroutineScope(Main).launch {
val bottomNavigationView =
activity.findViewById<BottomNavigationView>(R.id.main_btm_nav)
bottomNavigationView.menu.findItem(elementId).isChecked = true
}
}
예시5 (adapter class 내에서)
(context as MainActivity).supportFragmentManager.beginTransaction()
.add(R.id.main_fl, UserProfileFragment(item.userNo))
.addToBackStack(ApplicationClass.FRAGMENT_PROFILE_ID)
.commitAllowingStateLoss()
예시6 (fragment 내에서)
childFragmentManager.beginTransaction()
.add(R.id.container, GroupCategoryListFragment(childFragmentManager, this))
.addToBackStack(null)
.commit()
FragmentManager 란?
Note: We strongly recommend using the Navigation library to manage your app's navigation. The framework follows best practices for working with fragments, the back stack, and the fragment manager. For more information about Navigation, see Get started with the Navigation component and Migrate to the Navigation component.
fragment manager 공식문서를 찾아보니, 위와 같은 공지글을 먼저 마주하게 되었다. 해석해보면, 결국 이제 fragment, 백스택, fragment manager 와 관련된 작업을 하려면 jetpack 의 navigation library 를 사용하라는 것이다. jetpack 에 대해서는 차차 알아보자. 그전에 공부할게 많으니까.... 다만, jetpack navigation library 를 사용할 경우, 개발자를 대신해서 fragment manager 를 사용하기 때문에 개발자가 직접 fragment manager 를 만질 일은 없다는 정도로 우선 알아두면 될 것 같다.
FragmentManager 정의
FragmentManager is the class responsible for performing actions on your app's fragments, such as adding, removing, or replacing them, and adding them to the back stack.
FragmentManger 란 앱의 fragment(s) 를 더하고, 삭제하고, 교체하고, 백스택에 더하는 활동 등을 책임을 지는 class 입니다.
FragmentManager 접근하기
activity 에서 접근하기
Every FragmentActivity and subclasses thereof, such as AppCompatActivity, have access to the FragmentManager through the getSupportFragmentManager() method.
모든 FragmentActivity, 그리고 그것의 subclass (AppCompatActivity 와 같은) 는 getSupportFragmentManager로 FragmentManager 에 접근할 수 있습니다.
실제로 코드를 뜯어보니 getSupportFragmentManager가 FragmentManager를 반환하는 FragmentActivity 클래스의 멤버 함수로 구현되어 있는 것을 확인할 수 있었다!!
💡그런데 FragmentActivity와 Activity 의 차이가 무엇일까?
: 이곳에 의하면 FragmentActivity 는 Activity 의 subclass 로, 안드로이드 오래된 버전과의 호환성을 보장하기 위해 추가적으로 몇 가지 메서드를 제공하지만 크게는 별 차이 없다고 한다.
fragment 에서 접근하기
Fragments are also capable of hosting one or more child fragments. Inside a fragment, you can get a reference to the FragmentManager that manages the fragment's children through getChildFragmentManager(). If you need to access its host FragmentManager, you can use getParentFragmentManager().
프래그먼트는 하나 이상의 프래그먼트를 호스팅할 수 있다(담을 수 있다). 프래그먼트 내에서 자식 프래그먼트를 관리하는 FragmentManager 에 접근하기 위해서 getChildFragmentManager()를 사용한다. 그리고 자식 프래그먼트에서 부모의 FragmentManager 에 접근하기 위해서는 getParentFragmentManager 를 사용하면 된다.
example1의 host fragment 는 두 개의 자식 프래그먼트를 호스트하고 있고, example2의 host fragment 는 하나의 자식 프래그먼트를 호스트하고 있습니다. 프래그먼트 내에서 viewpager2를 사용하는 경우를 예로 들 수 있겠다.
각 호스트는 자식 프래그먼트를 관리하는 FragmentManager 를 가지고 있다. 여기에 접근하게 되면, 유저에게 보여지는 프래그먼트를 조작/관리할 수 있겠다.
- 가장 바깥의 host activity (fragment activity)는 자식 프래그먼트인 home 과 profile 프래그먼트를 관리하기 위해 supportFragmentManager 로 fragmentManager 에 접근한다. (예시 1,2,3 에서 supportFragmentManager 를 사용한 이유!!)
- home 또는 profile 프래그먼트인 host fragment 는 자식 프래그먼트를 관리하기 위해 childFragmentManager 메서드를 사용하고, 자신들을 관리하는 FragmentManager 에 접근하기 위해서는 parentFragmentManager 메서드를 사용한다.
결론적으로, FragmentManager property(FragmentManger 를 부르는 메서드)는 이것을 부르는 위치가 어디인지, 그리고 어떤 FragmentManager(자식인지 부모인지)를 부르는지에 따라서 다르다고 할 수 있다.
FragmentManager 사용하기
FragmentManager 는 프래그먼트 백스택을 관리한다. 런타임에 유저의 상호작용에 따라서 프래그먼트를 더하고 제거하는 등의 백스택 오퍼레이션을 수행한다. 각각의 변화(더하거나 제거하는)는 'FragmentTrasaction' 라는 하나의 단위로 commit 된다.
유저가 백버튼을 누르거나, FragmentManager.popBackStack() 를 호출하면 가장 위에 있는 FragmentTrasaction 이 스택에서 pop 된다. 즉, transaction 이 반대로 처리되는 것이다. 예를 들어 bottom navigation A 에서 B로 이동한 다음 백버튼을 눌렀을 때, A->B transaction 이 스택에서 pop 되면서 B->A 로 작동하는 것이다.
만약 더 이상 pop 할 프래그먼트가 없고 자식 프래그먼트가 없다면, activity 로 거슬러 올라간다.
addToBackStack()을 호출할 경우, 해당 transaction이 스택에 포함되는데 그 수는 무한정이 되어도 상관없다. 그리고 스택을 pop 했을 경우 transaction 은 reverse 되어서 처리된다. 그런데 popBackStack 하기 직전에 수행한 transaction 을 addToBackStack 하지 않았을 경우, 해당 transaction은 반대로 처리되지 않는다. 즉, addToBackStack 과 popBackStack 은 짝꿍이라고 볼 수 있다. 해당 transaction 을 pop 하고 싶다면, addToBackStack 을 반드시 수행해야 하는 것이다.
addToBackStack 하지 않았는데 pop 할 경우는 어떻게 될까요?
transaction 수행하기
프래그먼트 컨테이너에 프래그먼트를 보여주려면 FragmentManager 를 사용해서 FragmentTransaction 를 만들면 된다. 이를 사용해서 컨테이너에 프래그먼트를 더하거나 교체해서 보여주면 된다.
간단한 FragmentTransaction 예시이다.
supportFragmentManager.commit {
replace<ExampleFragment>(R.id.fragment_container)
setReorderingAllowed(true)
addToBackStack("name") // name can be null
}
이 예시에서는 ExampleFragment 가 fragment_container 라는 id 를 가진 프래그먼트를 교체(replace)하고 있습니다. replace 메서드에 ExampleFragment 클래스를 전달하면, FragmentManager 가 FragmentFactory 를 이용해서 인스턴스화 한다.
setReorderingAllowed(true) 는 해당 프래그먼트의 상태 변화를 최적화해서 애니메이션과 transaction 이 잘 작동하도록 한다.
addToBackStack() 는 앞서 얘기했던 것 처럼 pop 할 수 있도록 한다. 만약 하나의 transaction 에 여러 프래그먼트를 더하거나 제거하거나 하면, 해당 transaction 이 pop 되었을 때 모두 취소되게 된다!
또한 remove 하는 transaction 을 수행할 때 addToBackStack을 부르지 않으면 해당 프래그먼트는 destroy 되고 유저는 해당 프래그먼트로 돌아갈 수 없다. 하지만 addToBackStack을 부르면, 프래그먼트는 stop 상태가 되고 돌아갈 수 있다. 이때, view 는 destory 된다. (이 부분은 Fragment lifecycle 을 참고.)
addToBackStack에 String 으로 특정 값을 넘기면 pop 할 때도 특정 값을 넣어서 해당 transaction 을 pop 할 수 있다.
popBackStackImmediate VS. popBackStack
1. popBackStackImmediate
- 콜하는 즉시 pop 된다.
- 정확한 타이밍에 pop 되어야 하는 경우에 사용.
- 바로 pop 을 처리하기 때문에 상대적으로 느리고 성능 문제를 야기할 수 있기 때문에 위와 같은 상황이 아니면 popBackStack 사용을 권장한다.
2. popBackStack
- 다음 이벤트 루프에서 pop 된다.
- 따라서 콜한 즉시 백스택에서 FragmentTransaction 이 제거되지 않는다.
- 대부분의 경우 FragmentTransaction 이 바로 지워져야 하지 않기 때문이다.
- 비동기적으로 다른 작업들이 끝난 이후에 작동한다.
- 그런데 매우 빠르게 다음 이벤트 루프에 도달하기 때문에 사용자들이 동작의 차이를 알아차리기 어렵다.
commit VS. commitAllowingStateLoss
아래 글 부터는 Bryan Herbst 의 The many flavors of commit 의 일부를 번역한 것이다.
commit 과 commitAllowingStateLoss 는 구현에 있어서 거의 비슷하다고 볼 수 있습니다. 유일한 차이는 commit이 FragmentManager 가 자신의 state 를 저장했는지 확인한다는 것입니다. 만약 이미 state 를 저장했다면 IllegalStateException 를 던집니다.
그렇다면 commitAllowingStateLoss 는 어떤 state 를 잃는(loss) 다는 것일까요? 정답은 FragmentManager 의 state 그리고 onSaveInstanceState 이후에 더해지거나 제거된 Fragment들의 state 를 잃을 수 있다는 것입니다.
여기서 onSaveInstanceState 과 fragment와 FragmentManager 의 state 는 무엇일까요??
예시를 들어보겠습니다.
1. 당신의 Activity 가 보여지고 있고 Fragment A 를 보이고 있습니다.
2. 당신의 Activity 가 백그라운드(onStop() 과 onSaveInstanceState()가 불렸습니다.) 에 보내졌습니다.
3. 어떤 이벤트에 의해서 당신이 Fragment A 를 Fragment B 로 바꾸었고 commitAllowingStateLoss 를 불렀습니다.
이러한 경우, 유저가 다시 당신의 앱으로 돌아왔을 때 아래의 두 가지 상황 중 하나가 발생할 수 있습니다.
1. 만약 시스템이 다른 앱을 위해 당신의 앱을 죽였다면, 당신의 앱은 2단계에서의 saved state 와 함께 다시 만들어질 것입니다. 따라서 Fragment B 는 보이지 않을 것입니다.
2. 만약 시스템이 당신의 앱을 죽이지 않았다면, (따라서 당신의 앱이 메모리 내에 있다면) 유저가 다시 앱으로 돌아왔을 때 앱은 포그라운드로 불려지고 Fragment B 가 보일 것입니다.
결론적으로 둘 중 어느 것을 선택해서 사용해야 될까요? 이것은 당신이 무엇을 commit 하고, 해당 commit 으로 잃어도 괜찮은지에 달려 있겠지요.
'Android > 기타' 카테고리의 다른 글
채팅방의 스크롤시 버벅임 현상 (0) | 2022.11.23 |
---|---|
layout Inspector (0) | 2022.11.07 |
하단 이슈에 대해 (0) | 2022.11.03 |
생명주기에 대해서 (0) | 2022.11.03 |
다양한 진입 방법에 대해 알아보자 (0) | 2022.10.28 |