Jetpack Navigation-4 백 스택 관리

seong-hwan Kim
shDev
Published in
9 min readJan 22, 2021

Jetpack Navigation-1 기초 및 구성 요소
Jetpack Navigation-2 기초 사용법
Jetpack Navigation-3 고급 사용법
Jetpack Navigation-4 백 스택 관리 ← Here

(21.12.05)
Navigation 2.4.0 부터는 fragment 백스택을 자동으로 저장하고 복구합니다.
https://medium.com/androiddevelopers/multiple-back-stacks-b714d974f134

BottomNavigation의 이슈

BottomNavigationView를 사용할 때 NavigationUI를 적용하면 따로 리스너를 적용하지 않아도 하단 메뉴를 선택할 때 마다 매칭되는 destination으로 이동할 수 있지만 한 가지 이슈가 발생했습니다. 바로 메뉴가 변경될 때 마다 백 스택이 초기화돼서 이전에 선택한 메뉴로 돌아갔을 때 처음부터 다시 이동해야 한다는 점이었습니다.

예를 들어 메뉴 1에서 Fragment1–1, 1–2, 1–3, 순으로 이동하고 메뉴 2를 선택하면, 다시 메뉴 1로 돌아갔을 때 Fragment1–3이 아닌 1–1에서 시작해야 했습니다.

제가 원하는 내비게이션 흐름은 각각의 메뉴 마다 독립적인 내비게이션 그래프 리소스로 분리할 수 있고, 백 스택 또한 각각의 메뉴 마다 구현하여 이전의 내비게이션 흐름을 복구할 수 있도록 구현하고 싶었습니다. 이를 만족는데 있어서 크게 두 가지 방법을 찾을 수 있었습니다.

  1. BottomNavigationViewPager를 연결하여 백 스택 관리를 ViewPager에게 위임한다.
  2. 메뉴가 변경될 때 마다 사용하는 NavController를 변경하며 다른 그래프 리소스를 사용한다.

각각의 방법에 대해 자세한 내용은 아래의 링크에서 확인할 수 있습니다.

저는 두 번째 방법을 사용하여 내비게이션 리소스를 분리하였고, 이 글에선 NavigationUI와 두 번째 방법의 코드를 분석하며 어떻게 적용할 수 있었는지 말하려 합니다.

NavigationUI

먼저 NavigationUI가 어떤 방식으로 Navigation을 수행하는지 분석하여 왜 이슈가 발생하는지 찾아보겠습니다. NavigationUI에서 BottomNavigationViewsetupWithNavController 메서드는 다음과 같습니다.

메서드는 크게 두 가지 작업을 수행합니다.

  1. BottomNavigationView 객체의 setOnNavigationItemSelectedListener를 호출하여 메뉴 아이템을 선택할 때 마다 onNavDestinationSelected를 수행합니다.
  2. NavController 객체의 addOnDestinationChangedListener를 호출하여 destination이 변경될 때 마다 메뉴 아이템의 checked 상태를 변경합니다. 이는 백 버튼으로 startDestination으로 돌아갔을 때 메뉴 아이템의 선택 또한 변경할 목적으로 보입니다.

다음은 onNavigationItemSelected에서 호출하는 onNavDestinationSelected 메서드입니다.

onNavDestinationSelected에서는 NavOptions.Builder를 생성하여 트랜지션 애니메이션을 설정하고, 선택한 메뉴 아이템의 카테고리가 secondary가 아닐 때 빌더에 setPopUpTo 옵션을 추가하여 내비게이션 수행 전 startDestination으로 먼저 복귀할 수 있도록 설정합니다.

이후 메뉴 아이템의 id와 동일한 destination으로 이동합니다.

이슈가 발생하는 부분은 setPopUpTo 옵션이 추가되어 startDestination까지 백 스택이 초기화되기 때문입니다. 이 상황에서 백 스택을 독립적으로 유지하려면 어떻게 해야 할까요?

현재의 BottomNavigaiton은 메뉴가 변경될 때 마다 destination의 변경이 한 NavGraph 상에서 이루어집니다. 그렇다면 메뉴가 변경될 때 다른 NavGraph를 참조하여 컨트롤러를 변경한다면 백 스택을 독립적으로 구현할 수 있지 않을까요?

NavigationAdvancedSample의 코드를 분석하여 예제의 앱은 어떻게 문제를 해결했는지 확인해 보겠습니다.

NavigationAdvancedSample

샘플에선 BottomNavigationViewsetupWithNavController 메서드를 확장하여 설정하고 있습니다. 메서드의 파라미터로는 내비게이션 그래프 리소스 리스트와 FragmentManager, NavHost로 지정할 컨테이너의 id, 딥 링크 설정을 위한 intent가 전달되고 있습니다.

메서드가 LiveData<NavController>를 반환하여 컨트롤러가 변경될 때 마다 앱 바의 설정을 변경할 수 있도록 작성했네요.

다음으로 setupWithNavController 메서드를 분석하겠습니다.

가장 먼저 그래프의 id로 NavHostFragment의 Tag를 저장하는 Map과 반환할 라이브 데이터, 첫 번째 프래그먼트 그래프의 id를 담는 변수들이 초기화되는것을 볼 수 있습니다.

이후 파라미터로 받은 그래프 리소스 리스트를 순회하며 NavHostFragment 들을 초기화하는 작업을 진행합니다. 초기화 과정은 다음 순서로 진행됩니다.

  1. 인덱스로 NavHostFragment를 생성할 때 사용할 Tag를 얻고,
  2. Tag를 사용하여 NavHostFragment를 얻은 후,
  3. 맵에 NavHostFragment와 연결된 그래프 id를 key로 NavHostFragment의 태그를 저장하고,
  4. 현재 선택한 아이템에 해당하는 NavHostFragmentattach 하여 화면에 보여지도록 설정한다.

getFragmentTag 메서드는 인덱스로 구분할 수 있는 문자열을 반환하고, obtainNavHostFragment 메서드는 태그에 해당하는 NavHostFragment가 있다면 해당 프래그먼트를 반환 그렇지 않으면 NavGraph를 사용하여 NavHostFragment를 생성한 후 반환하는 작업을 수행합니다.

이 다음은 BottomNavigationViewNavigationItemSelectedListener를 설정하는 작업을 수행합니다.

BottomNavigationView의 아이템이 변경되면 다음 작업을 수행합니다.

  1. 선택한 아이템에 해당하는 프래그먼트 태그를 가져온다.
  2. 이전에 선택한 아이템과 비교하여 다른 아이템을 선택했을 때, 백 스택을 초기화한다.
  3. 새롭게 선택한 NavHostFragment의 참조를 가져온다.
  4. 선택한 아이템의 Tag와 firstFragmentTag를 비교하여 다른 경우에만 해당 프래그먼트를 attach하고 나머지 프래그먼트를 detach한다.
  5. 백 버튼을 인터셉트할 수 있도록 설정하고 트랜잭션을 백 스택에 추가한다.
  6. 사용한 변수와 라이브 데이터를 업데이트한다.

여기까지 수행하였으면 바텀 내비게이션의 메뉴가 변경될 때 마다 NavHostFragment가 변경되고, MainActivity에서는 라이브 데이터로 전파받은 Controller로 앱 바 설정을 수행할 수 있습니다.

마지막으로 FragmentManagerBackStackChangedListner를 추가하여 백 버튼으로 트랜잭션이 꺼내졌을 때 FirstFragment의 메뉴를 보여주도록 설정합니다.

My Approch

샘플에서 나온 코드를 그대로 적용해도 문제는 없지만 함수의 길이가 길고, 들여쓰기 깊이가 깊어 코드의 흐름을 한번에 이해하기 힘들다는 문제로 저는 조금 변형하여 사용했습니다.

구현한 함수는 7개로, 다음과 같습니다.

setupWithNavController

setupWithNavController를 호출하면 다음 작업을 수행합니다.

  1. 변수의 초기화.
  2. NavHostFragment의 생성 (initNavHosts).
  3. 가장 먼저 보여질 Root Fragmentattach.
  4. 리스너 설정.
  5. 다른 메뉴를 선택한 상황에서 구성 변경 시 처리를 위한 swapFragment 호출

initNavHosts, getNavHost

getNavHost“navHostFragment$index”를 태그로 NavHostFragment를 찾습니다. 아직 프래그먼트가 생성되지 않았다면 NavGraph를 사용하여 NavHostFragment를 생성한 후 반환합니다.

initNavHosts에선 그래프 리스트를 순회하며 먼저 모든 NavHostFragmentdetach하고 맵에 추가합니다. 이후 백 스택을 초기화합니다.

initNavHosts를 수행하면 메인 화면을 보여줄 수 있도록 가장 먼저 보여질 NavHostFragmentattach 합니다.

setupSelectedListener, swapFragment

BottomNavigationView의 메뉴 아이템 변경 시 먼저 백 스택을 초기화합니다. 백 스택이 쌓이기 전 항상 메인 화면이 표시되기 때문에 swapFragment에서 메인 화면을 detach하고 선택한 화면을 attach하여 선택한 화면을 표시합니다.

setupReselectedListener

동일한 메뉴 아이템을 선택 시 초기 화면으로 이동할 수 있도록 NavigatonItemReselectedListener를 설정하였습니다.

setupMainBackStackChangedListener

마지막으로 백 버튼으로 백 스택이 변경될 때 메인 화면을 보여줄 수 있도록 메뉴 아이템의 체크 상태를 변경하고, 백 스택이 변경될 때 마다 라이브 데이터를 업데이트하여 액티비티에 변경을 전파합니다.

--

--