Jetpack Navigation 에서 백스택에 최하단에 위치한 Fragment 에 접근하는 방법

이지훈·2023년 3월 16일
0

기존의 fragmentManager 를 사용해서 프래그먼트의 백스택을 관리할 때는
getBackStackEntryAt(index: 0) 이라는 함수를 통해 백스택내에 존재하는 최하단에 위치한 프래그먼트의 대한 정보를 얻을 수 있었다.

https://developer.android.com/reference/android/app/FragmentManager

내가 개발하고 있는 프로젝트에서는 Jetpack 의 Navigation을 사용하여 fragmentManager 를 직접 사용하지않으므로 (deprecated 되었기도 했고, 안쓴지 너무 오래되었다.. 다 까먹은거 같은데)

Navigation에서도 같은 역할을 수행하는 메소드가 존재하는지 궁금했다.
그렇게 Navigation 코드를 까보았지만 딱 원하는 그런 함수(백스택의 최하단의 프래그먼트를 반환하는 함수)는 존재하지 않은 듯 했다.
(발견 하지 못한 걸 수도 있으니 있으면 알려주세요)

하지만 최하단의 있는 프래그먼트의 id를 가져올 수 있는 방법은 존재했다.

findNavController().backQueue[1].destination.id

navigation 에서는 의외로 백스택을 'backQueue' 라는 네이밍으로 관리하고 있었다. 충격
Type 은 ArrayDeque Type 이다.

 @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
  public open val backQueue: ArrayDeque<NavBackStackEntry> = ArrayDeque()
  
@set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
  public var destination: NavDestination,

백스택내에 가장 탑에 위치한 프래그먼트를 (정확히는 NavBackStackEntry) 가져오는 코드는 그래서 다음과 같이 구성되어있다.

// The topmost NavBackStackEntry.
// Returns:
// the topmost entry on the back stack or null if the back stack is empty
public open val currentBackStackEntry: NavBackStackEntry?
        get() = backQueue.lastOrNull()
        
        
// The previous visible NavBackStackEntry.
// This skips over any NavBackStackEntry that is associated with a NavGraph.
// Returns:
// the previous visible entry on the back stack or null if the back stack has less than two visible entries
public open val previousBackStackEntry: NavBackStackEntry?
    get() {
        val iterator = backQueue.reversed().iterator()
        // throw the topmost destination away.
        if (iterator.hasNext()) {
            iterator.next()
        }
        return iterator.asSequence().firstOrNull { entry ->
            entry.destination !is NavGraph
        }
    }

SharedFlow 와 StateFlow 를 사용한 코드도 존재하는데 이에 대해선 추후 학습을 통해서 파악해보도록 하겠다.

 private val _currentBackStackEntryFlow: MutableSharedFlow<NavBackStackEntry> =
        MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)

    /**
     * A [Flow] that will emit the currently active [NavBackStackEntry] whenever it changes. If
     * there is no active [NavBackStackEntry], no item will be emitted.
     */
 public val currentBackStackEntryFlow: Flow<NavBackStackEntry> =
        _currentBackStackEntryFlow.asSharedFlow()
        
 private val _visibleEntries: MutableStateFlow<List<NavBackStackEntry>> =
        MutableStateFlow(emptyList())

    /**
     * A [StateFlow] that will emit the currently visible [NavBackStackEntries][NavBackStackEntry]
     * whenever they change. If there is no visible [NavBackStackEntry], this will be set to an
     * empty list.
     *
     * - `CREATED` entries are listed first and include all entries that have been popped from
     * the back stack and are in the process of completing their exit transition
     * - `STARTED` entries on the back stack are next and include all entries that are running
     * their enter transition and entries whose destination is partially covered by a
     * `FloatingWindow` destination
     * - The last entry in the list is the topmost entry in the back stack and is in the `RESUMED`
     * state only if its enter transition has completed. Otherwise it too will be `STARTED`.
     *
     * Note that the `Lifecycle` of any entry cannot be higher than the containing
     * Activity/Fragment - if the Activity is not `RESUMED`, no entry will be `RESUMED`, no matter
     * what the transition state is.
     */
 public val visibleEntries: StateFlow<List<NavBackStackEntry>> =
        _visibleEntries.asStateFlow()

쨌든 다시 본문으로 돌아가서 그러면 왜 index 0 번째가 아닌 1번째일까?
확인을 위해 다음과 같이 로그를 찍어보았다

Timber.d("${findNavController().backQueue[0].destination.id}")
Timber.d("${findNavController().backQueue[0].destination.label}")
Timber.d(findNavController().backQueue[0].destination.navigatorName)
Timber.d(findNavController().backQueue[0].destination.displayName)
Timber.d(findNavController().backQueue[0].destination.route)

Timber.d("${findNavController().backQueue[1].destination.id}")
Timber.d("${findNavController().backQueue[1].destination.label}")
Timber.d(findNavController().backQueue[1].destination.navigatorName)
Timber.d(findNavController().backQueue[1].destination.displayName)
Timber.d(findNavController().backQueue[1].destination.route)

D/LogoutDialogFragment: 2131296698
D/LogoutDialogFragment: null
D/LogoutDialogFragment: navigation
D/LogoutDialogFragment: com.depromeet.sloth:id/nav_main

D/LogoutDialogFragment: 2131296898
D/LogoutDialogFragment: TodayLessonFragment
D/LogoutDialogFragment: fragment
D/LogoutDialogFragment: com.depromeet.sloth:id/today_lesson

10 개의 로그를 찍었는데 8개 밖에 찍히지 않은 것은 마음에 안들지만 파악하는데 무리는 없었다.

TodayLessonFragment 는 navigation의 startDestination으로 설정한 프래그먼트(destination) 이며 이는 BackQueue[1] 번 인덱스에 대한 로그에서 확인할 수 있었다.

BackQueue[0] 번 인덱스에는 로그에서 확인할 수 있는 것 처럼 navGraph 자체가 들어가 있는 듯 하다.

수정)
덧글의 workspace 님께서 알려주시길
navigation 의 최신 버전에서는 backQueue에 직접 접근할 수 없어지기 때문에 navigation version 2.6.x 이상에서는 다음과 같은 방법으로 백스택 최하단의 프래그먼트에 접근할 수 있다

findNavController().currentBackStack.value[1].destination.id

수정전 (2.6.0 버전 미만)

   /**
     * Retrieve the current back stack.
     *
     * @return The current back stack.
     * @hide
     */
    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public open val backQueue: ArrayDeque<NavBackStackEntry> = ArrayDeque()

수정 후 (2.6.0 버전 이상)

    private val backQueue: ArrayDeque<NavBackStackEntry> = ArrayDeque()

    private val _currentBackStack: MutableStateFlow<List<NavBackStackEntry>> =
        MutableStateFlow(emptyList())

    /**
     * Retrieve the current back stack.
     *
     * @return The current back stack.
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public val currentBackStack: StateFlow<List<NavBackStackEntry>> =
        _currentBackStack.asStateFlow()

실제 사용 예시)

    private fun navigateToLogin() {
        val action = NavMainDirections.actionGlobalToLogin()
        val navOptions = NavOptions.Builder()
            //.setPopUpTo(findNavController().backQueue[1].destination.id, true)
            .setPopUpTo(findNavController().currentBackStack.value[1].destination.id, true)
            .build()
        findNavController().navigate(action, navOptions)
    }

참고)
https://velog.io/@ejjjang0414/안드로이드-Fragment-manager

https://android-review.googlesource.com/c/platform/frameworks/support/+/2147462/1/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt

https://velog.io/@changhee09/안드로이드-Fragment-Manager-Fragment-Transaction

profile
실력은 고통의 총합이다. Android Developer

2개의 댓글

comment-user-thumbnail
2023년 3월 21일

navigation 2.6.0 에서 backQueue에 직접 접근이 제한되며, currentBackStack이라는 대체재가 주어집니다. 아래 링크 내용 살펴보세요~~

https://android-review.googlesource.com/c/platform/frameworks/support/+/2147462/1/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt

1개의 답글