Overview

What

  • Compose와 함께 동작하는 새로운 navigation 라이브러리

  • 완벽하게 back stack 관리

  • 항목을 목록에서 추가/제거가 매우 간단
    유연한 네비게이션 앱 제공

  • back stack 안에 있는 아이템들을 위한 scope를 제공, 그 안에서 state 유지 가능

  • 반응형 레이아웃 제공 (e.g. 폴더플 폰)

How

  1. 탐색할 컨텐츠를 “키”로 정의 Resolve keys to content
  2. back stack 생성 Create a back stack
  3. NavDisplay를 통한 back stack 표시. back stack이 변경될 때마다 자동으로 UI 업데이트 Display the back stack
    1. 반응형 레이아웃을 위해 NavDisplay 전략 수정

Improvements upon Jetpack Navigation

  • Compose 통합
  • back stack 완벽 관리
  • 1개 이상의 컨텐츠를 동시에 back stack에서 읽어올 수 있음 반응형 레이아웃 (e.g. 폴더플 폰)

Get started

NameWhat
Navigation 3 runtime libraryCore (included NavEntry, EntryProvider..)mandatory
Navigation 3 UI libraryDisplay content, including NavDisplay, Scenemandatory
ViewModel Lifecycle for Navigation 3ViewModel이 back stack에 scop되도록 허락optional
Material 3 adaptive layouts for Navigation 3반응형 레이아웃 (SceneStrategies, Scenes, metadata definitions) NavDisplay에서 사용optional
KotlinX Serializationnavigation key serializationoptional

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 이벤트 (옵셔널)
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

  1. Back stack에서 키 추가, 제거
  2. NavDisplay에서 옵저빙, single pane layout으로 표시
    1. Back stack 변경이 있으면 EntryProvider에게 컨텐츠(NavEntry) 요청
  3. EntryProvider는 해당 Key의 NavEntry 전달
  4. 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 nameContains
apinavigation key
implNavEntry, 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() }
    ),
    // ...
)