Overview
What
-
Compose와 함께 동작하는 새로운 navigation 라이브러리
-
완벽하게 back stack 관리
-
항목을 목록에서 추가/제거가 매우 간단
→ 유연한 네비게이션 앱 제공 -
back stack 안에 있는 아이템들을 위한 scope를 제공, 그 안에서 state 유지 가능
-
반응형 레이아웃 제공 (e.g. 폴더플 폰)
How
- 탐색할 컨텐츠를 “키”로 정의 Resolve keys to content
- back stack 생성 Create a back stack
- NavDisplay를 통한 back stack 표시. back stack이 변경될 때마다 자동으로 UI 업데이트 Display the back stack
- 반응형 레이아웃을 위해 NavDisplay 전략 수정
Improvements upon Jetpack Navigation
- Compose 통합
- back stack 완벽 관리
- 1개 이상의 컨텐츠를 동시에 back stack에서 읽어올 수 있음 → 반응형 레이아웃 (e.g. 폴더플 폰)
Get started
| Name | What | |
|---|---|---|
| Navigation 3 runtime library | Core (included NavEntry, EntryProvider..) | mandatory |
| Navigation 3 UI library | Display content, including NavDisplay, Scene | mandatory |
| ViewModel Lifecycle for Navigation 3 | ViewModel이 back stack에 scop되도록 허락 | optional |
| Material 3 adaptive layouts for Navigation 3 | 반응형 레이아웃 (SceneStrategies, Scenes, metadata definitions) NavDisplay에서 사용 | optional |
| KotlinX Serialization | navigation key → serialization | optional |
Understand and implement the basics
- Navigation은 앱 안에서 사용자의 이동을 의미
Modeling navigation state
- back stack을 활용하는 것이 편리한 모델링
- 사용자 forward → stack 맨 위 추가
- 사용자 go back → stack 맨 위 제거
그림은 back stack
Create a back stack
- back stack은 컨텐츠를 포함하지 않는다.
- Key를 통한 참조
- 컨텐츠를 포함하지 않는 장점
- key 전달로 간단한 Navigation
- serialization한 키는 storage에 영속 저장 가능
- Navigation 3의 핵심 개념은 back stack을 직접 관리하는 것
- Any key 사용 가능
- NavDisplay를 통해 back stack을 옵저빙하고 UI에 반영
// Define keys that will identify content
data object ProductList
data class ProductDetail(val id: String)
@Composable
fun MyApp() {
// Create a back stack, specifying the key the app should start with
val backStack = remember { mutableStateListOf<Any>(ProductList) }
// Push a key onto the back stack (navigate forward), the navigation library will reflect the change in state
backStack.add(ProductDetail(id = "ABC"))
// Pop a key off the back stack (navigate back), the navigation library will reflect the change in state
backStack.removeLastOrNull()
}Resolve keys to content
- NavEntry로 모델링된 컨텐츠는 @composable func이다. NavEntry는 이동 가능한 single piece of content 이다.
- NavEntry는 컨텐츠에 대한 metadata 포함
- 어떻게 display할 것인가? e.g. 애니메이션
- NavDisplay 같은 NavEntry를 포함할 수 있는 Container에 metadata 제공
- Key ⇒ NavEntry 위해 entryProvider 필요
// entryProvider directly
entryProvider = { key ->
when (key) {
is ProductList -> NavEntry(key) { Text("Product List") }
is ProductDetail -> NavEntry(
key,
metadata = mapOf("extraDataKey" to "extraDataValue")
) { Text("Product ${key.id} ") }
else -> {
NavEntry(Unit) { Text(text = "Invalid Key: $it") }
}
}
}
// entryProvider DSL
entryProvider = entryProvider {
entry<ProductList> { Text("Product List") }
entry<ProductDetail>(
metadata = mapOf("extraDataKey" to "extraDataValue")
) { key -> Text("Product ${key.id} ") }
}Display the back stack
- back stack이 변경되면 UI 새로운 back stack 반영 필요
- NavDisplay을 통해 back stack을 옵저빙하고 UI 업데이트 가능
- NavDisplay의 파라미터 정리
- back stack:
SnapshotStateList<T>로 해당 List를 옵저빙, 그리고 변경되면 NavDisplay 리컴포지션 - entryProvider: Key ⇒ NavEntry로 컨버팅
- onBack: onBack 이벤트 (옵셔널)
- back stack:
val backStack = remember { mutableStateListOf<Any>(Home) }
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider = { key ->
when (key) {
is Home -> NavEntry(key) {
ContentGreen("Welcome to Nav3") {
Button(onClick = {
backStack.add(Product("123"))
}) {
Text("Click to navigate")
}
}
}
is Product -> NavEntry(key) {
ContentBlue("Product ${key.id} ")
}
else -> NavEntry(Unit) { Text("Unknown route") }
}
}
)Putting it all together

- Back stack에서 키 추가, 제거
- NavDisplay에서 옵저빙, single pane layout으로 표시
- Back stack 변경이 있으면 EntryProvider에게 컨텐츠(NavEntry) 요청
- EntryProvider는 해당 Key의 NavEntry 전달
- NavDisplay에 표시
Save and manage navigation state
- back stack을 저장하는 전략
Save your back stack
Use rememberNavBackStack
rememberNavBackStack 는 composable func 으로 데이터 유지 가능 요구 조건
- 모든 Key는
NavKey인터페이스 구현: NavKey는 데이터를 저장할 수 있음을 알리는 인터페이스 - @Serializable 어노테이션 필요
@Serializable
data object Home : NavKey
@Composable
fun NavBackStack() {
val backStack = rememberNavBackStack(Home)
}Alternative: Storing in a ViewModel
ViewModel을 통한 back stack 관리 요구 조건
- 직렬화 가능 확인
- 직렬화/역직렬화를 수동처리하는 것이 개발자 책임
Scoping ViewModels to NavEntrys
- ViewModel은 어떤 ViewModelStoreOwner에 붙어있느냐(scope)“에 따라 언제까지 살아있는지 결정
- NavEntryDecorator를 적용하면 각 NavEntry마다 전용 ViewModelStoreOwner를 제공해주고, 그 결과 NavEntry 내부에서 만든 ViewModel을 “해당 엔트리가 back stack에 있는 동안만” 유지 가능.
NavDisplay(
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator(),
),
backStack = backStackA,
entryProvider = entryProvider {
entry<X> { XScreen(onNext = { backStackA.add(Y) }) }
entry<Y> { YScreen(onBack = { backStackA.removeLast() }) }
entry<Z> { ZScreen(...) }
}
)
class XViewModel : ViewModel() {
var text by mutableStateOf("")
private set
fun onTextChange(v: String) { text = v }
}
@Composable
fun XScreen(onNext: () -> Unit) {
val vm: XViewModel = viewModel() // ✅ "X NavEntry"에 스코프됨
TextField(value = vm.text, onValueChange = vm::onTextChange)
Button(onClick = onNext) { Text("Go Y") }
}Modularize navigation code
- Navigation 코드를 모듈화 가이드
- 모듈 분리 기준은 기능(feature)
Overview
- 모듈화 적요은 Key와 그것의 content를 각각 모듈에 분리하는 과정
- 분리된 책임 제공 요구 조건
- api, impl 모듈로 구분 필요
- api 모듈
- navigation key 정의
- impl 모듈
- entryProviders 정의
- entryProviders를 app 모듈에 di 하는 방식
Separate features into api and implementation submodules
| Module name | Contains |
|---|---|
| api | navigation key |
| impl | NavEntry, EntryProvider |
![]() | |
| e.g. featureA:impl ⇒ featureB:api 모듈 키를 통해 featureB로 이동 가능 |
Separate navigation entries using extension functions & Use dependency injection to add entries to the main app
기능별로 컨텐츠를 나누고 app 모듈에서 사용 가능
// featureA:api
class KeyA : NavKey // Screen A 키
class KeyA2 : NavKey // Screen A2 키
// featureA:impl
fun EntryProviderScope<NavKey>.featureEntryBuilder() {
entry<KeyA> {
// Screen A의 UI (예: Compose 함수 또는 View)
ContentRed("Screen A") {
/* 화면 A의 내용 */
}
}
entry<KeyA2> {
// Screen B의 UI
ContentGreen("Screen A2") {
/* 화면 B의 내용 */
}
}
}
@Module
@InstallIn(ActivityRetainedComponent::class)
object FeatureModule {
@IntoSet
@Provides
fun provideFeatureAEntryBuilder():
EntryProviderScope<NavKey>.() -> Unit = {
featureAEntryBuilder()
}
}
// app
@Module
@InstallIn(ActivityRetainedComponent::class)
object FeatureAappModule {
@IntoSet
@Provides
fun provideFeatureEntryBuilder():
EntryProviderScope<NavKey>.() -> Unit = {
provideFeatureAEntryBuilder()
}
}
class MainActivity : ComponentActivity() {
@Inject
lateinit var entryBuilders: Set<EntryProviderScope<NavKey>.() -> Unit>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NavDisplay(
entryProvider = entryProvider {
// 모든 기능 모듈의 builder 실행
entryBuilders.forEach { builder -> this.builder() }
}
)
}
}
}Create custom layouts using Scenes
TBD
Animate between destinations
- NavDisplay는 빌트인 애니메이션 제공
- NavDisplay나 각 NavEntry metadata를 통해 커스텀 가능
Override default transitions
- NavDisplay의
ContentTransform오버라이딩transitionSpec: 컨텐츠가 추가 될 때 명세서popTransitionSpec: 컨텐츠 제거 될 때 명세서predictivePopTransitionSpec: predictivePop을 통한 컨텐츠 제거 될 때 명세서
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider = entryProvider {
// implement
},
transitionSpec = {
// Slide in from right when navigating forward
slideInHorizontally(initialOffsetX = { it }) togetherWith
slideOutHorizontally(targetOffsetX = { -it })
},
popTransitionSpec = {
// Slide in from left when navigating back
slideInHorizontally(initialOffsetX = { -it }) togetherWith
slideOutHorizontally(targetOffsetX = { it })
},
predictivePopTransitionSpec = {
// Slide in from left when navigating back
slideInHorizontally(initialOffsetX = { -it }) togetherWith
slideOutHorizontally(targetOffsetX = { it })
},
modifier = Modifier.padding(paddingValues)
)Override transitions at the individual NavEntry level
- NavEntry의 metadata를 통한 커스텀
NavDisplay.transitionSpec: 컨텐츠가 추가 될 때 명세서NavDisplay.popTransitionSpec: 컨텐츠 제거 될 때 명세서NavDisplay.predictivePopTransitionSpec: predictivePop을 통한 컨텐츠 제거 될 때 명세서
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider = entryProvider {
entry<ScreenC>(
metadata = NavDisplay.transitionSpec {
// Slide new content up, keeping the old content in place underneath
slideInVertically(
initialOffsetY = { it },
animationSpec = tween(1000)
) togetherWith ExitTransition.KeepUntilTransitionsFinished
} + NavDisplay.popTransitionSpec {
// Slide old content down, revealing the new content in place underneath
EnterTransition.None togetherWith
slideOutVertically(
targetOffsetY = { it },
animationSpec = tween(1000)
)
} + NavDisplay.predictivePopTransitionSpec {
// Slide old content down, revealing the new content in place underneath
EnterTransition.None togetherWith
slideOutVertically(
targetOffsetY = { it },
animationSpec = tween(1000)
)
}
) {
ContentGreen("This is Screen C")
}
},
transitionSpec = {
// Slide in from right when navigating forward
slideInHorizontally(initialOffsetX = { it }) togetherWith
slideOutHorizontally(targetOffsetX = { -it })
},
popTransitionSpec = {
// Slide in from left when navigating back
slideInHorizontally(initialOffsetX = { -it }) togetherWith
slideOutHorizontally(targetOffsetX = { it })
},
predictivePopTransitionSpec = {
// Slide in from left when navigating back
slideInHorizontally(initialOffsetX = { -it }) togetherWith
slideOutHorizontally(targetOffsetX = { it })
},
modifier = Modifier.padding(paddingValues)
)둘다 구현되어 있다면 NavDisplay보다 NavEntry가 우선 (override 생각)
Apply logic or wrappers to destinations
- NavEntryDecorator를 통해 destinations의 동일한 로직을 제공 가능
Create a custom decorator
- decorate: NavEntry가 compose 될 때 호출, e.g. NavEntry에 공통 워터마크 추가
- onPop: NavEntry가 제거될 때 호출, 제거되는 contentKey 반환
// usage 1: 다른 composable로 wrap 할 때
val decorator = NavEntryDecorator<Any> { entry ->
...
MyComposableFunction {
entry.content.invoke(entry.key)
}
}
// usage 2: CompositionLocalProvider를 통해 정보를 제공할 때
val decorator = NavEntryDecorator<Any> { entry ->
...
CompositionLocalProvider(LocalMyStateProvider provides myState) {
entry.content.invoke(entry.key)
}
}
// import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
NavDisplay(
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
remember { CustomNavEntryDecorator() }
),
// ...
)