[Android] Jetpack Navigation — Interaction
안녕하세요 이번글에서는 코드를 사용한 내비게이션 요소들을 활용한 방법과 내비게이션 컴포넌트에서 다루는 상호작용 관련된 내용에 대해서도 다루려고 합니다.
Interact programmatically
이 내용은 Interact programmatically를 번역해서 작성하였습니다.
내비게이션 컴포넌트는 내비게이션의 요소에 대해서 프로그래매틱하게 생성하고 상호작용하는 방법을 제공합니다.
NavBackStackEntry를 사용하여 Destination 참조
내비게이션 2.2.0 버전 이상을 사용한다면 NavController.getBackStackEntry()
를 Destination ID과 함께 호출하면 내비게이션 스택에 있는 어떠한 Destination을 참조할 수 있는 NavBackStackEntry
를 가져올 수 있습니다. 만약 특정 Destination이 백스택에 1개 이상을 포함하고 있다면 getBackStackEntry()
는 스택에서 가장 상위에 있는 인스턴스를 리턴합니다.
전달받은 NavBackStackEntry
객체는 해당 Destination 레벨의 Lifecycle
, ViewModelStore
, SavedStateRegistry
를 제공합니다. 이 객체들은 백스택에 있는 Destination의 라이프타임동안만 유효합니니다. Destination이 백스택에서 꺼내질 때 Lifecycle
은 파괴되는걸 생각해볼 수 있습니다. 상태는 더 이상 저장할 것이 없기 때문에 ViewModel
객체도 클리어가 됩니다.
이 프로퍼티들은 Lifecyle
과 저장을 위한 ViewModel
객체들을 줍니다.
Lifecycle
, ViewModel
객체나 클래스의 저장소와 같이 이 프로퍼티들을 사용하면 Destination의 타입과는 관계없이 상태를 저장할 수 있습니다. 이 기능은 커스텀 Destination처럼 자동으로 라이프사이클과 연결되지않은 Destination 타입들끼리 작업할 때 유용합니다.
예를들어 NavBackStackEntry
의 Lifecycle
을 관찰한다고 가정했을 때 간단하게 Fragment 혹은 Activity의 Lifecycle
을 관찰하게 됩니다. 추가적으로 NavBackStackEntry
는 LifecycleOwner
입니다. 즉, 다음 예시와 같이 LiveData
를 관찰하거나 다른 lifecycle-aware 컴포넌트들과 함께 사용할 수 있다는 것을 의미합니다.
라이프사이클 상태는 navigate()
가 호출될 때 자동으로 업데이트됩니다. 백스택의 맨 위에 있지 않은 Destination에 대한 라이프사이클 상태는 RESUMED
에서 STARTED
로 이동하고 만약 Dialog Destination처럼 FloatingWindow
Destination아래에 보이는 Destination들은 STOPPED
로 이동합니다.
이전 Destination으로 결과를 리턴받기
내비게이션 2.3 이상의 버전을 사용한다면 NavBackStackEntry
는 SavedStateHandle
에 접근할 수 있습니다. SavedStateHandle
은 데이터를 저장하고 되찾을 수 있는 Key-Value로 이루어진 Map 형태입니다. 이 값은 프로세스가 죽거나 구성 변경(Configuration Changes)이 발생해도 사용가능한 동일한 객체로 유지합니다. SavedStateHandle
을 사용한다면 Destination들 간에 데이터를 주고 받을 수 있습니다. 이런 특별한 매커니즘은 Destination이 스택에서 팝되었을 때 데이터를 가져오기 편리합니다.
Destination B에서 Destination A로 백했을 때 데이터를 전달하기 위해서 먼저 DestinationA의 SavedStateHandle
의 결과를 수신해야합니다. getCurrentBackStackEntry()
API를 사용해서 NavBackStackEntry
를 가져오고 SavedStateHandle
이 제공하는 LiveData
에 observe
합니다.
Destination B에서는 getPreviousBackStackEntry()
API를 사용해서 Destination A의 SavedStateHandle
에 결과를 꼭 set
해주어야 합니다.
NavBackStack
은SavedStateHandle
을 사용하므로 모든 결과는 Bundle에 사용할 수 있는 형태여야합니다. Parcelable이나 Serializable이 아닌 커스텀 타입들의 결과를 저장하려면 이전NavBackStackEntry
에서ViewModelStore
를 사용한ViewModel
을 만듭니다.ViewModel
에 자세한 사용법은 ViewModel 가이드를 참조해주세요
만약에 결과값을 한번만 핸들링하길 바란다면 SavedStateHandle
의 remove()
를 호출하여 결과를 삭제해야합니다. 결과값을 지우기를 바라지 않는다면 LiveData
에 접근한 새로운 Observer
인스턴스에 지속적으로 마지막 결과값을 반환해줍니다.
ViewModel을 사용한 Destination간에 UI 관련 데이터 공유
내비게이션 백스택은 각 Destination뿐만 아니라 각 Destination의 부모 내비게이션 그래프에 대해서도 NavBackStackEntry
를 저장합니다. 이렇게하면 범위가 지정된 내비게이션 그래프에서 NavBackStackEntry
를 검색할 수 있습니다. 범위가 지정된 내비게이션 그래프의 NavBackStackEntry
는 범위가 지정된 내비게이션 그래프에서 ViewModel
을 생성하고 그래프의 각 Destination간에 UI관련된 데이터들을 공유할 수 있게 해줍니다. 이렇게 생성된 ViewModel
객체는 연결된 NavHost
및 ViewModelStore
가 지워지거나 백스택에서 탐색그래프가 팝업이 되기전까지 유지됩니다
아래 예제는 내비게이션 그래프에서 범위가 지정된 ViewModel
를 검색하는 방법을 나타냅니다
Navigation 2.2.0 이전 버전을 사용한다면 다음 예제처럼 Saved State with ViewModels를 사용해서 고유의 팩토리를 제공해야합니다.
popUpTo 와 popUpToInclusive
Action을 사용해서 이동할 때 백스택에서 추가 Destination을 Pop 하도록 선택할 수 있습니다. 예를 들어 앱에서 초기 로그인으로 진입하는 그래프나 Destination이 있다면 사용자가 로그인 한 후 뒤로버튼을 누르거나 기존에 로그인 흐름을 지우고 다음 화면으로 넘어가야할 상황에서는 로그인 관련된 Destination들을 Pop해야 하는 상황이 발생합니다.
만약 Destination에서 다른 Destination으로 이동할 때 Destination을 Pop하려면 <action>
엘리먼트에 app:popUpTo
속성을 추가합니다. app:popUpTo
는 navigate()
호출의 일부로 백스택에서 몇 개의 Destination을 팝하도록 내비게이션 라이브러리에게 알려줍니다. 해당 프로퍼티 값(route or id)은 스택애 잔존해야하는 가장 상위의 Destination ID입니다.
또한 app:popUpToInclusive="true"
로 설정하면 app:popUpTo
에 지정된 Destination 까지 백스택에서 삭제됩니다.
popUpTo 예: 순환 로직
앱에서 A,B C 세 개의 Destination과 A->B->C->A 로 연결되는 작업이 있을 때 계속해서 A->B->C->A를 반복한다면 여러 Destination이 중첩되어 사용자 경험이 나빠질 수 있습니다. 이런 반복의 작업을 피하려면 C에서 A로 이동할 때 app:popUpTo
와 app:popUpToInclusive
를 지정해서 사용하면 해결됩니다.
위와 같이 설정하면 Destination C에서 A로 navigate()
호출한다고 가정했을 때, popUpTo
만 설정했을 때 C->A로 내비게이트가 발생하면 A가 나오기 이전의 백스택들은 팝 대상이됩니다. (예를들어 A->B->C->A는 C와 B가 백스택에서 제외되어 A->A가 백스택에 남게됩니다) 만약 popUpToInclusive
까지 true로 설정한다면 백스택에서 제거하는 대상이 A의 이전 스택까지가 아니라 A도 포함하여 팝의 대상이 되기 때문에 위와 같은 시나리오에서는 A만 스택에 남게됩니다 .
Multiple Backstack
내비게이션 컴포넌트는 사용자가 앱에서 이동할 때 안드로이드 OS와 연동하여 백스택을 유지합니다. 사용자가 앞뒤로 이동하는 상황에서는 여러 백스택을 유지하는게 도움이 될 수 있습니다. 예를들어 Bottom Navigation이나 Navigation Drawer를 포함한 경우 백스택 지원을 사용하면 사용자가 앱의 이동하는 흐름에서 자신의 위치를 잃어버리지 않고 자유롭게 전환이 가능합니다.
내비게이션 컴포넌트는 내비게이션 그래프에서 Destination의 상태는 저장하고 복원할 수 있게 멀티플 백스택을 지원하는 API를 제공합니다. NavigationUI 클래스는 이를 자동으로 처리하는 메서드가 포함되지만 기본 API를 이용해서 조금 더 커스터마이징하게 사용할 수 있습니다.
NavigationUI를 사용하여 자동으로 지원
NavigationUI
클래스에는 유저의 메뉴 아이템간의 이동할 때 메뉴의 상태를 자동으로 저장하고 복원하는 API를 포함하고 있습니다. 이런 API는 다음과 같은 사례처럼 자동적으로 멀티플 백스택을 제공합니다.
- setupWithNavController()를 사용하여
NavigationView
혹은BottomNavigationView
의 인스턴스를NavController
인스턴스와 연결하는 경우 - onNavDestinationSelected()를 사용하여
NavController
인스턴스에서 호스팅하는 Destination에 연결된 커스텀 내비게이션 메뉴 UI를 만드는 경우
기본 API를 사용하여 직접 구현
Navgation XML
내비게이션 그래프의 <action>
엘리먼트에 app:popUpToSaveState
속성을 사용하여 app:popUpTo
로 백스택에서 제거한 Destination의 상태를 저장할 수 있습니다. 또한 app:restoreState
속성을 사용하여 app:destination
속성에 정의된 Destination의 이전 저장 상태를 복원할 수 있습니다.
이러한 속성들을 이용해서 멀티플 백스택을 지원하면 됩니다. <action>
엘리먼트에 app:popUpToSaveState
와 app:restoreState
를 true로 설정하면 현재 백스택의 상태를 저장함과 동시에 이전에 저장된 Destination의 백스택 상태가 있는 경우 상태를 복원합니다.
NavOptions
NavOptions 클래스를 사용하면 NavController
를 사용하여 이동할 때 백스택을 저장하고 복원할 수 있습니다. NavOptions
인스턴스를 생성하는 방법은 Kotlin DSL
또는 NavOptions.Builder
를 사용할 수 있습니다.
NavOptions.Builder
NavOptions의 새로운 인스턴스를 생성하기 위한 빌더
- launchSingleTop : 내비게이션 액션이 SingleTop으로 실행되어야 하는지에 대한 여부입니다. (백스택 상단에 최대 1개의 주어진 Destination이 있습니다.) 이 함수는 액티비티에서
android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP
이 동작하는 방식과 유사합니다. - restoreState :
PopUpToBuilder.saveState
또는popUpToSaveState
어트리뷰트를 사용해서 내비게이션 액션이 이전에 저장된 어떠한 상태를 복원할 수 있는지에 대한 여부입니다. 만약 탐색중인 Destination ID에서 이전에 저장된 상태가 없다면 아무런 효과를 발생하지 않습니다.
PopUpToBuilder
NavOptionsBuilder.popUpTo
작업을 커스터마이징 하기 위한 DSL
- inclusive:
popUpTo
의 Destination이 백스택에서 팝되어야 하는지에 대한 여부입니다. - saveState : 현재 Destination과
NavOptionsBuilder.popupTo
ID 사이의 모든 Destination의 상태와 백스택여부입니다. 나중에NavOptionsBuilder.restoreState
혹은restoreState
attribute 를 사용하여 동일한NavOptionsBuilder.popUpTo
아이디를 복구합니다. (여기에 매칭된 ID는inclusive
가 true거나 false과 상관없이 항상 true 입니다.)
마무리
지금까지 내비게이션 컴포넌트들에 여러 요소들에 대해서 알아봤습니다. 안드로이드도 과거의 Activity보다 Fragment를 활용한 SingleActivity, Jetpack Compose로만 화면을 구현했을 때 Navigation 컴포넌트를 활용하면 이점이 많기 때문에 정확한 사용방법을 익히고 여러 요소들을 학습하여 프로덕트에 활용하면 좋겠습니다. 긴 글 읽어주셔서 감사합니다 🙇
목차
- [Android] Jetpack Navigation — Migrate to Compose — Overview
- [Android] Jetpack Navigation — Migrate to Compose — Graph & Destination
- [Android] Jetpack Navigation — Migrate to Compose — DeepLink
- [Android] Jetpack Navigation — Migrate to Compose — Animation
- [Android] Jetpack Navigation — Interaction