바텀 내비게이션과 내비게이션 컨트롤러를 이용한 프래그먼트의 이동을 구현하다 보면 꼬일 때가 있습니다.

위 상황처럼 홈에서 원 프래그먼트로 바로 이동하게 하면 다시 홈으로 갈 수 없는 것을 볼 수 있습니다.
왜 이런 상황이 발생하는지, 해결 방법이 무엇인지 알아봅시다.
내비게이션 백 스택의 구조

일반적인 내비게이션의 백스택입니다. 프래그먼트를 이동할 때마다 이전 프래그먼트가 백스택에 쌓이는 형식입니다.
그렇다면 바텀 내비게이션의 목적지는 어떨까요?

처음 바텀 내비게이션을 정의하면 목적지가 저장됩니다.

위에 보인 예제처럼 홈에서 임의로 액션을 정의하면 바텀 내비게이션의 목적지에 정의한 액션이 들어가게 됩니다. 따라서 위 gif처럼 바텀 내비게이션으로 홈 화면으로 돌아갈 수 없는 것입니다.
일반 내비게이션 백스택에는 HomeFragment가 저장돼 있으므로 onBackPressed이벤트 발생 시 백스택을 불러오도록 하면 HomeFragment로 돌아갈 수 있습니다.
실제 사용 시 내비게이션의 백스택이 어떻게 바뀌는지 직접 보면서 메서드 별 기능 정의를 해보겠습니다.
초기 설정
내비게이션 구현에 관한 것은 이전 게시글을 참고하시면 됩니다.
Navigation으로 Fragment전환을 쉽게 해보자 (feat.BottomNavigation)
Fragment는 생각만 하면 머리가 아파오지만 안드로이드의 필수적인 기능임은 틀림없습니다. 하지만 화면 전환을 할 때만 되면 골치 아파지죠, 특히 프래그먼트간 전환을 할 때입니다. 기존 액티비
huzit.tistory.com
이전 게시글에서 변경된 점이 몇 가지 있습니다.
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
//FragmentContainerView를 NavHostFragment로 가져옴
lateinit var _navHostFragment: NavHostFragment
val navHostFragment
get() = _navHostFragment
//바텀 네비게이션에 들어갈 네비게이션 컨트롤러 정의
lateinit var _navController: NavController
val navController
get() = _navController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initBind()
setBottomNav()
showBackStack()
}
//바인딩
private fun initBind(){
binding = ActivityMainBinding.inflate(layoutInflater)
_navHostFragment = supportFragmentManager.findFragmentById(R.id.fragmentContainer) as NavHostFragment
_navController = navHostFragment.findNavController()
setContentView(binding.root)
}
//바텀 네비 설정
private fun setBottomNav(){
binding.bottomNav.setupWithNavController(navController)
}
fun showBackStack(){
//FragmentContainerView를 NavHostFragment로 가져옴
val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragmentContainer) as NavHostFragment
navHostFragment.childFragmentManager.addOnBackStackChangedListener {
if(navHostFragment.childFragmentManager.backStackEntryCount == 0) {
Toast.makeText(this, "백 스택 수 : ${navHostFragment.childFragmentManager.backStackEntryCount}", Toast.LENGTH_SHORT).show()
Log.d(
"백 스택 없음",
navHostFragment.childFragmentManager.backStackEntryCount.toString()
)
navHostFragment.childFragmentManager.popBackStack()
}
else {
Toast.makeText(this, "백 스택 수 : ${navHostFragment.childFragmentManager.backStackEntryCount}", Toast.LENGTH_SHORT).show()
Log.d(
"백 스택 있음",
navHostFragment.childFragmentManager.backStackEntryCount.toString()
)
}
}
}
var waitTime = 0L
override fun onBackPressed() {
if(!navController.popBackStack()){
if(System.currentTimeMillis() - waitTime >= 1500){
waitTime = System.currentTimeMillis()
Toast.makeText(this, "뒤로가기 한 번 더 하면 앱 종료", Toast.LENGTH_SHORT).show()
} else{
finish()
}
}
}
}
navOption 메서드
메서드들이 무슨 기능을 하는지 하나하나 뜯어볼 차례입니다.
lunchSingleTop

이 내비게이션 동작을 싱글톱(백스택에 있을 경우 인스턴스를 가져옴)으로 시작할지 여부
(백스택 상단에 지정된 대상의 복사본이 하나만 있을 것)
SingleTop이란
액티비티 또는 프래그먼트의 인스턴스가 이미 백스택 맨 위에 있을 때 같은 화면(액티비티 또는 프래그먼트)이 호출될 경우 새 인스턴스를 생성하지 않고 onNewIntent() 메서드를 호출하여 인텐트를 기존 인스턴스로 라우팅 합니다.
즉, 백 스택이 A - B - C (C가 최상단)인 상태에서 standard모드(기본상태)로 C 화면을 다시 부른다면 A - B - C - C(C 최상단)의 형태로 저장됩니다. 하지만 C의 실행 모드가 singTop이라면 A - B - C로 C가 최상단에 있기 때문에 새로 호출하는 것이 아닌 기존의 C인스턴스로 인텐트를 받습니다. 만약 B가 호출된다면 B의 새 인스턴스가 스택에 추가됩니다.
구현 방법은 간단합니다. navOptions에 launchSingleTop 프로퍼티에 true를 할당하면 됩니다.
binding.setNowButton.setOnClickListener {
val action = SubFragmentDirections.actionSubFragmentSelf()
it.findNavController().navigate(action, navOptions {
launchSingleTop = true
})
}


실제 작동 영상을 보면 이해하기 쉽습니다. '지금 화면' 버튼을 누르면 subFragment가 자기 자신을 호출합니다. 그때 singleTop을 true로 설정해 주면 새로 subFragment가 쌓이지 않고 기존 인스턴스로 라우팅 하는 모습을 볼 수 있습니다.
PopUpTo
탐색하기 전에 지정된 목적지까지 팝업을 표시합니다. 이 대상을 찾을 때까지 백업 스택에서 모든 비관측 대상을 팝 합니다.

메서드 오버라이드를 통해 정수값인 아이디 또는 문자열 루트를 받을 수 있습니다.

구현방법은 간단합니다. navOptions에 popUpTo() 메서드를 선언하고 패러미터로 백업 스택에서 찾을 목적지를 넣어줍니다. 바텀 내비게이션의 시작 목적지를 넣어주었습니다.
binding.setOneBt.setOnClickListener {
val action = SubFragmentDirections.actionSubFragmentToOneFragment()
it.findNavController().navigate(action, navOptions {
restoreState = false
popUpTo(findNavController().graph.startDestinationId)
})
}


영상 설명이 조금 복잡할 수도 있지만 간단하게 설명해 보겠습니다.
왼쪽 영상은 popUpTo를 사용하지 않은 영상입니다. 백스택을 채운 후 '원으로 가기' 버튼을 눌러 OneFragment로 넘어가는 흐름입니다.
눈여겨봐야 할 특징은
- '원으로 가기'를 눌렀을 때 바텀 내비게이션을 통해 '서브'메뉴로 넘어갈 수 없는 점
- 백스택의 개수
입니다.
오른쪽 영상은 '원으로 가기' 버튼과 oneFragment의 버튼에 popUpTo를 설정한 상태입니다.
- '원으로 가기'를 눌렀을 때 정상적으로 '서브'메뉴로 넘어갈 수 있고
- 백스택이 쌓인 상태로 '원으로 가기'를 누르면 백스택이 초기화되는 점입니다.
특징을 하나씩 설명하자면
정상적으로 서브 메뉴로 넘어갈 수 없었던 점
이유 : 서브 메뉴의 목적지가 원 메뉴로 가는 목적지로 백스택에 저장되어 있기 때문입니다.
백스택이 초기화되는 이유
이유 : popUpTo메서드를 이용해서 시작지점을 제외한 나머지를 모두 팝 해줬기 때문에 백스택의 개수가 1이 찍히게 되는 것입니다.
간단하게 정리하면 popUpTo의 패러미터로 넘긴 목적지를 제외한 나머지를 모두 백스택에서 털어(pop) 버립니다.
restoreState

이 내비게이션 액션이 이전에 PopUpToBuilder에 의해 저장된 상태를(액션을) 복원해야 하는지 여부
PopUpToBuilder

빌드 클래스입니다.
- saveState : restoreState와 자주 같이 쓰입니다. 페이지(Fragment 또는 Activity)를 넘어갈 때 현재 페이지의 백스택을 저장할지 설정하는 프로퍼티입니다.
- restoreState : 넘어갈 페이지의 백스택을 불러올지 여부를 설정하는 프로퍼티입니다.
- inclusive : popUpTo의 대상을 백스택에서 팝업 할지 여부입니다. 위 영상을 보면 이동시 백스택 개수가 1이 있는 것이 inclusive = false인 상태입니다.
binding.toSubBt.setOnClickListener {
val action = OneFragmentDirections.actionOneFragmentToSubFragment()
it.findNavController().navigate(action, navOptions {
restoreState = true
popUpTo(findNavController().graph.startDestinationId){
saveState = true
inclusive = true
}
})
}
saveState와 restoreState의 케이스별 예제를 먼저 보겠습니다.
saveState = true(SubFragment) / restoreState = true(OneFragment)

아주 정상적으로 작동되는 것을 볼 수 있습니다.
saveState = false(SubFragment) / restoreState = true(OneFragment)

restoreState를 true로 백스택을 복구한다고 해도 백스택을 저장하지 않았기 때문에 백스택 수가 1이 찍히는 것을 볼 수 있습니다.
saveState = true (SubFragment) / restoreState = false(OneFragment)

바텀 내비게이션은 restoreState, saveState가 true로 기본값이 설정돼 있기 때문에 백스택의 개수를 올바르게 출력해 주지만 버튼에는 restoreState를 false로 할당했기 때문에 저장된 백스택을 불러오지 않는 모습을 볼 수 있습니다.
saveState = false (SubFragment) / restoreState = false(OneFragment)

아무것도 안됩니다. 그냥 안 돼요
inclusive
popUpTo의 대상을 Pop후 백스택에서 팝 할지 여부입니다.
binding.oneNextButton.setOnClickListener {
it.findNavController().navigate(MainFragmentDirections.actionMainFragmentToOneFragment(), navOptions {
restoreState = true
popUpTo(findNavController().graph.startDestinationId){
saveState = true
inclusive = false
}
})
}
구현 코드입니다.


HomeFragment에서 OneFragment로 가는 버튼에 inclusive를 설정한 영상들입니다.
두 영상의 차이점은 OneFragment에서 뒤로 가기 버튼을 눌렀을 때 확연히 차이 나는 것을 볼 수 있습니다.
왼쪽 영상에선 뒤로 가기를 눌렀을 시 백 스택의 수가 0이 되면서 HomeFragment로 이동하는 것을 볼 수 있습니다. 이후 뒤로 가기를 누르면 앱이 종료될 수 있다는 메시지가 안내됩니다.
오른쪽 영상에선 뒤로 가기 눌렀을 시 바로 앱이 종료될 수 있다는 메시지가 안내됩니다. 즉, popUpTo의 목적지인 HomeFragment가 popUpTo를 타는 동시에 백스택에서 팝 된 것을 알 수 있습니다.
anim
애니메이션을 설정합니다.


구현입니다. 리소스에서 작업할 수 있지만 이번 시간엔 코드로만 다뤄보겠습니다. (리소스에서 작업하는걸 별로 안 좋아해요. 😱)
binding.oneNextButton.setOnClickListener {
it.findNavController().navigate(MainFragmentDirections.actionMainFragmentToOneFragment(), navOptions {
restoreState = true
popUpTo(findNavController().graph.startDestinationId){
saveState = true
}
anim {
enter = androidx.appcompat.R.anim.abc_slide_in_top
exit = androidx.appcompat.R.anim.abc_slide_in_bottom
popEnter = androidx.appcompat.R.anim.abc_slide_in_top
popExit = androidx.appcompat.R.anim.abc_slide_in_bottom
}
})
}

애니메이션이 잘 적용된 것을 볼 수 있습니다.
popUpToId

builder 가 팝업할 현재 대상의 ID를 반환합니다. popUpTo()에서 파라미터로 ID값을 넘겨줬을 때 값을 가져올 수 있습니다.

로그에 id가 찍히는 것을 볼 수 있습니다.
popUpToRoute

탐색하기 전에 지정된 목적지까지 팝업을 표시합니다. 이 대상을 찾을 때까지 백업 스택에서 모든 비관측 대상을 팝 합니다.
popUpTo에서 파라미터로 String값을 넘겨줬을 때 값을 가져올 수 있습니다.

처음 접하는 개념 + 블로그에 결과 사진, 영상 없이 설명만 있어서 이 참에 직접 영상까지 예제로 다 정리해 봤습니다. 짧게 끝날 것 같았는데 생각보다 오래 결렸네요.
전체 예제는 깃헙에서 확인할 수 있습니다.
GitHub - Huzit/NavigationExample
Contribute to Huzit/NavigationExample development by creating an account on GitHub.
github.com
참고
여러 백 스택 지원 | Android 개발자 | Android Developers
여러 백 스택 지원 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 탐색 구성요소는 사용자가 앱에서 이동할 때 Android 운영체제와 연동하여 백 스택을 유지
developer.android.com
대상으로 이동 | Android 개발자 | Android Developers
대상으로 이동 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 대상으로 이동하는 것은 NavController 객체를 사용하여 실행되며 이 객체는 NavHost 내에서 앱 탐
developer.android.com
android:launchMode, singleTop과 singleTask의 차이
일단 이 내용은 공식적인 기술 문서에 의한 것이 아니라전적으로 개발상의 경험으로 정리한 내용이니 착오 없으시길 바랍니다. 문제의 발단은현재 회사에서 운영 중인 앱의 안드로이드 버전에
mazdah.tistory.com
'안드로이드 > Activity & Fragment' 카테고리의 다른 글
Navigation으로 Fragment전환을 쉽게 해보자 (feat.BottomNavigation) (0) | 2023.03.30 |
---|---|
OnClick 이벤트 설정하는 방법 (0) | 2022.07.15 |
Activity to Fragment, Fragment to Fragment (0) | 2022.06.13 |