Jetpack Navigation 3은 무엇입니까? Jetpack Navigation 3는 이전 버전과 근본적으로 다릅니다 새로운 Google 탐색 라이브러리입니다.Nav3의 주요 아이디어는 간단합니다 : 당신은 - 각 요소가 응용 프로그램의 화면을 나타내는 정규 변수 목록. NavBackStack 이 목록에서 요소를 추가하고 제거하면 UI가 자동으로 업데이트됩니다. 일반 코트린 클래스 NavKey 이것은 당신에게 탐색에 대한 완전한 통제권을 제공하지만, 일반적인 작업을 위해 꽤 많은 보일러 플레이트 코드를 작성해야합니다. NavBackStack과 직접 작업하는 것이 불편한 이유 직접 작업할 때 코드가 어떻게 생겼는지 살펴보자. : NavBackStack @Composable fun MyApp() { val backStack = rememberNavBackStack(Screen.Home) // Add a screen backStack.add(Screen.Details("123")) // Go back backStack.removeLastOrNull() // Replace current screen backStack.set(backStack.lastIndex, Screen.Success) } 문제는 ViewModel에서 탐색을 시작해야 할 때 시작됩니다.You will have to either pass ViewModel에 (내 생각에 ViewModel이 Compose 특정 물건에 대해 알 필요가 없기 때문에 건축 원칙을 위반하는) 또는 각 항해 작업에 대해 중간 호출을 만듭니다. NavBackStack 또한, 스택을 직접 작업 할 때, 가장자리 케이스를 처리하는 것을 잊어 버리는 것이 쉽습니다. Nav3 라우터가 작업을 단순화하는 방법 Nav3 Router는 Nav3에 대해 익숙한 API를 제공하는 얇은 포장입니다. 인덱스와 목록 작업에 대해 생각하는 대신, 당신은 단순히 "스크린 X로 이동"또는 "돌아 가십시오"라고 말합니다. 중요한 점 : Nav3 라우터는 자체 스택을 만들지 않습니다. Navigation 3가 제공하는 것, 단지 그것을 더 편리하게 작업 할 수 있습니다. , 도서관은 이것을 원본 스택과 관련된 작업으로 번역합니다. NavBackStack router.push(Screen.Details) 주요 장점 : ViewModel에서 사용할 수 있습니다. UI가 일시적으로 사용할 수 없을 때(예를 들어, 화면 회전 중) 네비게이션 명령어가 버퍼됩니다. 모든 스택 작업은 원자적으로 이루어집니다. 명확한 API 수정 및 사용자 정의 행동 추가에 대한 유연성 설치 Nav3 Router는 Maven Central에서 사용할 수 있습니다. : build.gradle.kts // For shared module in KMP project kotlin { sourceSets { commonMain.dependencies { implementation("io.github.arttttt.nav3router:nav3router:1.0.0") } } } // For Android-only project dependencies { implementation("io.github.arttttt.nav3router:nav3router:1.0.0") } 도서관 소스 코드는 GitHub에서 사용할 수 있습니다 : github.com/arttttt/Nav3Router에 대한 정보 Nav3 Router의 구성 방법 도서관은 세 가지 주요 부분으로 구성되어 있으며 각각 자신의 작업을 해결합니다. Router - 개발자 인터페이스 Router는 이러한 방법을 제공합니다. , , 이러한 방법을 호출하면 라우터가 해당 명령을 생성하고 체인 아래로 전송합니다. 라우터 자체는 탐색이 어떻게 실행되는지 알지 못합니다. push() pop() replace() CommandQueue - 명령과 그 실행 사이의 버퍼 CommandQueue는 타이밍 문제를 해결합니다. 상상해보십시오: 사용자가 화면 회전 중 버튼을 누르고 있습니다. UI가 재구성되고 있으며 네비게이터가 일시적으로 사용할 수 없습니다. CommandQueue는 명령을 저장하고 네비게이터가 다시 준비되면 실행합니다. // Simplified queue logic class CommandQueue<T : Any> { private var navigator: Navigator<T>? = null private val pending = mutableListOf<Command<T>>() fun executeCommand(command: Command<T>) { if (navigator != null) { navigator.apply(command) // Navigator exists - execute immediately } else { pending.add(command) // No - save for later } } } Navigator - Stack과 함께 일하는 사람 Navigator는 명령을 가져오고 그들을 적용합니다. 중요한 세부 사항: 먼저 현재 스택의 사본을 생성하고, 모든 명령을 적용하고, 원본 스택을 수정된 복사본으로 원자적으로 대체합니다.This guarantees that the UI will never see intermediate stack states. NavBackStack // Simplified Navigator logic fun applyCommands(commands: Array<Command>) { val stackCopy = backStack.toMutableList() // Work with a copy for (command in commands) { when (command) { is Push -> stackCopy.add(command.screen) is Pop -> stackCopy.removeLastOrNull() // ... other commands } } backStack.swap(stackCopy) // Atomically apply changes } Nav3 Router로 시작하기 가장 간단한 방법은 수동으로 라우터를 만들지 않는 것입니다. Nav3Host는 당신을 위해 그것을 할 것입니다: @Composable fun App() { val backStack = rememberNavBackStack(Screen.Home) // Nav3Host will create Router automatically Nav3Host(backStack = backStack) { backStack, onBack, router -> NavDisplay( backStack = backStack, onBack = onBack, entryProvider = entryProvider { entry<Screen.Home> { HomeScreen( onOpenDetails = { router.push(Screen.Details) // Use router } ) } entry<Screen.Details> { DetailsScreen( onBack = { router.pop() } ) } } ) } } 더 복잡한 응용 프로그램의 경우 DI를 통해 라우터를 만들고 ViewModel로 전달하는 것이 합리적입니다. 스크린을 정의 @Serializable sealed interface Screen : NavKey { @Serializable data object Home : Screen @Serializable data class Product(val id: String) : Screen @Serializable data object Cart : Screen } 라우터를 Nav3Host로 전송합니다. @Composable fun App() { val backStack = rememberNavBackStack(Screen.Home) val router: Router<Screen> = getSomehowUsingDI() // Pass Router to Nav3Host Nav3Host( backStack = backStack, router = router, ) { backStack, onBack, _ -> NavDisplay( backStack = backStack, onBack = onBack, entryProvider = entryProvider { entry<Screen.Home> { HomeScreen() } entry<Screen.Details> { DetailsScreen() } } ) } } ViewModel은 Constructor를 통해 Router를 수신합니다. class ProductViewModel( private val router: Router<Screen>, private val cartRepository: CartRepository ) : ViewModel() { fun addToCart(productId: String) { viewModelScope.launch { cartRepository.add(productId) router.push(Screen.Cart) // Navigation from ViewModel } } } UI에서는 ViewModel을 사용합니다. @Composable fun ProductScreen(viewModel: ProductViewModel = koinViewModel()) { Button(onClick = { viewModel.addToCart(productId) }) { Text("Add to Cart") } } 전형적인 시나리오의 예 간단한 앞뒤 항해 // Navigate to a new screen router.push(Screen.Details(productId)) // Go back router.pop() // Navigate with replacement of current screen (can't go back) router.replaceCurrent(Screen.Success) Screen Chains 작업 // Open multiple screens at once router.push( Screen.Category("electronics"), Screen.Product("laptop-123"), Screen.Reviews("laptop-123") ) // Return to a specific screen // Will remove all screens above Product from the stack router.popTo(Screen.Product("laptop-123")) 체크 아웃 시나리오 @Composable fun CheckoutScreen(router: Router<Screen>) { Button( onClick = { // After checkout we need to: // 1. Show confirmation screen // 2. Prevent going back to cart router.replaceStack( Screen.Home, Screen.OrderSuccess(orderId) ) // Now only Home and OrderSuccess are in the stack } ) { Text("Place Order") } } 출발 NESTED NAVIGATION // User is deep in settings: // Home -> Settings -> Account -> Privacy -> DataManagement // "Done" button should return to home Button( onClick = { // Will leave only root (Home) router.clearStack() } ) { Text("Done") } // Or if you need to close the app from anywhere Button( onClick = { // Will leave only current screen and trigger system back router.dropStack() } ) { Text("Exit") } 보너스: SceneStrategy and Dialogs 지금까지, 우리는 단지 화면 간 간단한 탐색에 대해 이야기 했습니다.하지만 대화 상자 또는 하단 시트를 보여줄 필요가 있다면 어떨까요? Navigation 3의 SceneStrategy 개념이 도움이되는 곳입니다. 무대 전략이란 무엇인가? SceneStrategy는 스택의 스크린이 어떻게 표시되는지 정확하게 결정하는 메커니즘입니다. 그러나 당신은 더 복잡한 시나리오에 대한 자신의 전략을 만들 수 있습니다. SinglePaneSceneStrategy SceneStrategy를 당신의 스크린 스택을보고 "괜찮아요, 우리가 정상적으로 보여주는이 세 개의 스크린, 그러나이 마지막 - 이전의 창 위에 모드 창으로"라고 결정하는 디렉터로 생각하십시오. ModalBottomSheet에 대한 전략 만들기 먼저, 우리는 이러한 화면을 표시하는 방법을 정의하자 : @Serializable sealed interface Screen : NavKey { @Serializable data object Home : Screen @Serializable data class Product(val id: String) : Screen // This screen will be shown as bottom sheet @Serializable data object Filters : Screen } 이제 전략 자체를 만들자 마지막 화면의 메타데이터를 확인하고, 특별한 마커를 찾으면 하단 시트로 표시합니다. class BottomSheetSceneStrategy<T : Any> : SceneStrategy<T> { companion object { // Metadata key by which we identify bottom sheet private const val BOTTOM_SHEET_KEY = "bottomsheet" // Helper function to create metadata fun bottomSheet(): Map<String, Any> { return mapOf(BOTTOM_SHEET_KEY to true) } } @Composable override fun calculateScene( entries: List<NavEntry<T>>, onBack: (Int) -> Unit ): Scene<T>? { val lastEntry = entries.lastOrNull() ?: return null // Check if the last screen has bottom sheet marker val isBottomSheet = lastEntry.metadata[BOTTOM_SHEET_KEY] as? Boolean if (isBottomSheet == true) { // Return special Scene for bottom sheet return BottomSheetScene( entry = lastEntry, previousEntries = entries.dropLast(1), onBack = onBack ) } // This is not a bottom sheet, let another strategy handle it return null } } 여러 전략을 결합 실제 응용 프로그램에서는 하단 시트, 대화 상자 및 정규 화면이 필요할 수 있습니다.For this, you can create a delegate strategy that will choose the right strategy for each screen: class DelegatedScreenStrategy<T : Any>( private val strategyMap: Map<String, SceneStrategy<T>>, private val fallbackStrategy: SceneStrategy<T> ) : SceneStrategy<T> { @Composable override fun calculateScene( entries: List<NavEntry<T>>, onBack: (Int) -> Unit ): Scene<T>? { val lastEntry = entries.lastOrNull() ?: return null // Check all keys in metadata for (key in lastEntry.metadata.keys) { val strategy = strategyMap[key] if (strategy != null) { // Found suitable strategy return strategy.calculateScene(entries, onBack) } } // Use default strategy return fallbackStrategy.calculateScene(entries, onBack) } } Application에서 사용하기 이제, 모든 것을 함께 넣어 보자.이것은 실제 응용 프로그램에서 하단 시트를 사용하는 것처럼 보이는 것입니다 : @Composable fun ShoppingApp() { val backStack = rememberNavBackStack(Screen.Home) val router = rememberRouter<Screen>() Nav3Host( backStack = backStack, router = router ) { backStack, onBack, router -> NavDisplay( backStack = backStack, onBack = onBack, // Use our combined strategy sceneStrategy = DelegatedScreenStrategy( strategyMap = mapOf( "bottomsheet" to BottomSheetSceneStrategy(), "dialog" to DialogSceneStrategy() // Navigation 3 already has this strategy ), fallbackStrategy = SinglePaneSceneStrategy() // Regular screens ), entryProvider = entryProvider { entry<Screen.Home> { HomeScreen( onOpenFilters = { // Open filters as bottom sheet router.push(Screen.Filters) } ) } entry<Screen.Product> { screen -> ProductScreen(productId = screen.id) } // Specify that Filters should be bottom sheet entry<Screen.Filters>( metadata = BottomSheetSceneStrategy.bottomSheet() ) { FiltersContent( onApply = { filters -> // Apply filters and close bottom sheet applyFilters(filters) router.pop() } ) } } ) } } 여기서 무슨 일이 벌어지고 있는 걸까? - 당신이 전화할 때 그러나 메타데이터와 우리의 전략 덕분에 UI는이 화면이 이전 화면의 상단에있는 하단 시트로 표시되어야한다는 것을 이해합니다. router.push(Screen.Filters) 전화할 때 아래 페이지가 닫히고 이전 화면으로 돌아갑니다.Router의 관점에서 이것은 정기적 인 뒤로 탐색이지만 시각적으로는 모드 창을 닫는 것처럼 보입니다. router.pop() 이 접근법의 장점 SceneStrategy를 사용하면 몇 가지 중요한 장점이 있습니다.첫째, 당신의 탐색 논리는 간단합니다. 그리고 두 번째로, 네비게이션 상태는 일관되게 유지됩니다 - 하단 시트는 스크린 회전 또는 프로세스 킬 중에 적절하게 저장되는 스크린 중 하나입니다.그리고 마지막으로, 그것은 큰 유연성을 제공합니다 - 당신은 쉽게 네비게이션 논리를 만지지 않고 단순히 메타데이터를 변경함으로써 화면이 표시되는 방식을 변경할 수 있습니다. push pop 예를 들어 로그인 화면은 앱을 처음 시작할 때 정규 화면이 될 수 있고 권한을 요구하는 작업을 수행하려고 할 때 모드 대화가 될 수 있습니다.For example, a login screen can be a regular screen at first app launch and a modal dialog when trying to perform an action that requires authorization. Nav3 Router를 사용해야 하는 이유 Nav3 라우터는 Nav3를 대체하거나 새로운 기능을 추가하려고 하지 않습니다. Nav3 라우터의 작업을 편리하고 예측할 수 있도록하는 것입니다. 당신은 응용 프로그램의 모든 층에서 사용할 수있는 간단한 API를 얻고, 타이밍 문제를 자동으로 처리하고, 쉽게 탐색 논리를 테스트 할 수있는 능력을 얻을 수 있습니다. 동시에, 캡 아래, 정규 네비게이션 3는 여전히 모든 기능으로 작동합니다 : 상태 저장, 애니메이션 지원 및 시스템 "뒤로"버튼의 적절한 처리. 이미 Navigation 3을 사용 중이거나 그것으로 마이그레이션 할 계획이라면 Nav3 Router는 프로젝트에 불필요한 복잡성을 추가하지 않고도이 경험을 더 즐겁게 만듭니다. 왼쪽 GitHub 저장소: github.com/arttttt/Nav3Router 사용 예제: 보관소의 샘플 폴더 보기