Jetpack Navigation 3 is a new Google navigation library that is fundamentally different from previous versions. The main idea of Nav3 is simple: you have a NavBackStack — a regular mutable list where each element represents a screen in your application.Jetpack Navigation 3 is a new Google navigation library that is fundamentally different from previous versions. The main idea of Nav3 is simple: you have a NavBackStack — a regular mutable list where each element represents a screen in your application.

Nav3 Router: Convenient Navigation on Top of Jetpack Navigation 3

2025/09/17 20:00

What is Jetpack Navigation 3?

Jetpack Navigation 3 is a new Google navigation library that is fundamentally different from previous versions. The main idea of Nav3 is simple: you have a NavBackStack — a regular mutable list where each element represents a screen in your application.

\ You add and remove elements from this list, and the UI automatically updates. Each screen is represented as a NavKey — a regular Kotlin class.

\ This gives you full control over navigation, but requires writing quite a lot of boilerplate code for typical operations.

Why Working Directly with NavBackStack is Inconvenient

Let's look at what the code looks like when working directly with 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) } 

Problems begin when you need to trigger navigation from a ViewModel. You'll have to either pass NavBackStack to the ViewModel (which, in my understanding, violates architectural principles, as I believe the ViewModel shouldn't know about Compose-specific things), or create intermediate callbacks for each navigation action.

\ Additionally, when working with the stack directly, it's easy to forget to handle edge cases.

How Nav3 Router Simplifies the Work

Nav3 Router is a thin wrapper over Navigation 3 that provides a familiar API for navigation. Instead of thinking about indices and list operations, you simply say "go to screen X" or "go back."

\ Important point: Nav3 Router doesn't create its own stack. It works with the same NavBackStack that Navigation 3 provides, just making it more convenient to work with. When you call router.push(Screen.Details), the library translates this into the corresponding operation with the original stack.

\ Main advantages:

  • Can be used from ViewModel
  • Navigation commands are buffered if UI is temporarily unavailable (for example, during screen rotation)
  • All stack operations happen atomically
  • Clear API
  • Flexibility in modification and adding custom behavior

Installation

Nav3 Router is available on Maven Central. Add the dependency to your 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") } 

\ The library source code is available on GitHub: github.com/arttttt/Nav3Router

How Nav3 Router is Structured

The library consists of three main parts, each solving its own task:

Router — Developer Interface

Router provides methods like push(), pop(), replace(). When you call these methods, Router creates corresponding commands and sends them down the chain. Router itself knows nothing about how navigation will be executed — this allows using it from anywhere.

CommandQueue — Buffer Between Commands and Their Execution

CommandQueue solves the timing problem. Imagine: the user pressed a button during screen rotation. The UI is being recreated, and the navigator is temporarily unavailable. CommandQueue will save the command and execute it as soon as the navigator is ready again. Without this, the command would simply be lost.

// 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 takes commands and applies them to NavBackStack. Important detail: it first creates a copy of the current stack, applies all commands to it, and only then atomically replaces the original stack with the modified copy. This guarantees that the UI will never see intermediate stack states.

// 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 } 

Getting Started With Nav3 Router

The simplest way — don't even create Router manually. Nav3Host will do it for you:

@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() }                     )                 }             }         )     } } 

For more complex applications, it makes sense to create a Router through DI and pass it to the ViewModel:

\ Define screens

@Serializable sealed interface Screen : NavKey {     @Serializable     data object Home : Screen      @Serializable     data class Product(val id: String) : Screen      @Serializable     data object Cart : Screen } 

\ Pass router to 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 receives Router through constructor.

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         }     } } 

\ In UI, just use ViewModel.

@Composable fun ProductScreen(viewModel: ProductViewModel = koinViewModel()) {     Button(onClick = { viewModel.addToCart(productId) }) {         Text("Add to Cart")     } } 

Examples of Typical Scenarios

Simple Forward-Back Navigation

// 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) 

Working with 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")) 

Checkout Scenario

@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")     } } 

Exiting 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") } 

Bonus: SceneStrategy and Dialogs

So far, we've only talked about simple navigation between screens. But what if you need to show a dialog or bottom sheet? This is where the SceneStrategy concept from Navigation 3 comes to help.

What is SceneStrategy?

SceneStrategy is a mechanism that determines exactly how screens from your stack will be displayed. By default, Navigation 3 uses SinglePaneSceneStrategy, which simply shows the last screen from the stack. But you can create your own strategies for more complex scenarios.

\ Think of SceneStrategy as a director who looks at your stack of screens and decides: "Okay, these three screens we show normally, but this last one — as a modal window on top of the previous ones". This allows representing different UI patterns with the same stack.

Creating a Strategy for ModalBottomSheet

Let's create a strategy that will show certain screens as bottom sheets. First, let's define how we'll mark such screens:

@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 } 

\ Now, let's create the strategy itself. It will check the metadata of the last screen and, if it finds a special marker, show it as a bottom sheet:

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     } } 

Combining Multiple Strategies

In a real application, you might need bottom sheets, dialogs, and regular screens. 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)     } } 

Using in Application

Now, let's put it all together. Here's what using bottom sheet looks like in a real 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()                         }                     )                 }             }         )     } } 

\ What's happening here? When you call router.push(Screen.Filters), the screen is added to the stack as usual. But thanks to metadata and our strategy, the UI understands that this screen needs to be shown as a bottom sheet on top of the previous screen, rather than replacing it completely.

\ When calling router.pop() the bottom sheet will close, and you'll return to the previous screen. From Router's point of view, this is regular back navigation, but visually it looks like closing a modal window.

Advantages of This Approach

Using SceneStrategy provides several important advantages. First, your navigation logic remains simple — you still use push and pop without thinking about how exactly the screen will be shown. Second, navigation state remains consistent — bottom sheet is just another screen in the stack that is properly saved during screen rotation or process kill. And finally, it provides great flexibility — you can easily change how a screen is displayed by just changing its metadata, without touching the navigation logic.

\ This approach is especially useful when the same screen can be shown differently depending on context. For example, a login screen can be a regular screen on first app launch and a modal dialog when attempting to perform an action that requires authorization.

Why You Should Use Nav3 Router

Nav3 Router doesn't try to replace Navigation 3 or add new features. Its task is to make working with navigation convenient and predictable. You get a simple API that can be used from any layer of the application, automatic handling of timing issues, and the ability to easily test navigation logic.

\ At the same time, under the hood, regular Navigation 3 still works with all its capabilities: state saving, animation support, and proper handling of the system "Back" button.

\ If you're already using Navigation 3 or planning to migrate to it, Nav3 Router will help make this experience more pleasant without adding unnecessary complexity to the project.

  • GitHub repository: github.com/arttttt/Nav3Router
  • Usage examples: see sample folder in the repository

\

시장 기회
TOP Network 로고
TOP Network 가격(TOP)
$0.000096
$0.000096$0.000096
0.00%
USD
TOP Network (TOP) 실시간 가격 차트
면책 조항: 본 사이트에 재게시된 글들은 공개 플랫폼에서 가져온 것으로 정보 제공 목적으로만 제공됩니다. 이는 반드시 MEXC의 견해를 반영하는 것은 아닙니다. 모든 권리는 원저자에게 있습니다. 제3자의 권리를 침해하는 콘텐츠가 있다고 판단될 경우, service@support.mexc.com으로 연락하여 삭제 요청을 해주시기 바랍니다. MEXC는 콘텐츠의 정확성, 완전성 또는 시의적절성에 대해 어떠한 보증도 하지 않으며, 제공된 정보에 기반하여 취해진 어떠한 조치에 대해서도 책임을 지지 않습니다. 본 콘텐츠는 금융, 법률 또는 기타 전문적인 조언을 구성하지 않으며, MEXC의 추천이나 보증으로 간주되어서는 안 됩니다.

추천 콘텐츠

Whales keep selling XRP despite ETF success — Data signals deeper weakness

Whales keep selling XRP despite ETF success — Data signals deeper weakness

The post Whales keep selling XRP despite ETF success — Data signals deeper weakness appeared on BitcoinEthereumNews.com. XRP ETFs have crossed $1 billion in assets
공유하기
BitcoinEthereumNews2025/12/20 02:55
Top Solana Treasury Firm Forward Industries Unveils $4 Billion Capital Raise To Buy More SOL ⋆ ZyCrypto

Top Solana Treasury Firm Forward Industries Unveils $4 Billion Capital Raise To Buy More SOL ⋆ ZyCrypto

The post Top Solana Treasury Firm Forward Industries Unveils $4 Billion Capital Raise To Buy More SOL ⋆ ZyCrypto appeared on BitcoinEthereumNews.com. Advertisement &nbsp &nbsp Forward Industries, the largest publicly traded Solana treasury company, has filed a $4 billion at-the-market (ATM) equity offering program with the U.S. SEC  to raise more capital for additional SOL accumulation. Forward Strategies Doubles Down On Solana Strategy In a Wednesday press release, Forward Industries revealed that the 4 billion ATM equity offering program will allow the company to issue and sell common stock via Cantor Fitzgerald under a sales agreement dated Sept. 16, 2025. Forward said proceeds will go toward “general corporate purposes,” including the pursuit of its Solana balance sheet and purchases of income-generating assets. The sales of the shares are covered by an automatic shelf registration statement filed with the US Securities and Exchange Commission that is already effective – meaning the shares will be tradable once they’re sold. An automatic shelf registration allows certain publicly listed companies to raise capital with flexibility swiftly.  Kyle Samani, Forward’s chairman, astutely described the ATM offering as “a flexible and efficient mechanism” to raise and deploy capital for the company’s Solana strategy and bolster its balance sheet.  Advertisement &nbsp Though the maximum amount is listed as $4 billion, the firm indicated that sales may or may not occur depending on existing market conditions. “The ATM Program enhances our ability to continue scaling that position, strengthen our balance sheet, and pursue growth initiatives in alignment with our long-term vision,” Samani said. Forward Industries kicked off its Solana treasury strategy on Sept. 8. The Wednesday S-3 form follows Forward’s $1.65 billion private investment in public equity that closed last week, led by crypto heavyweights like Galaxy Digital, Jump Crypto, and Multicoin Capital. The company started deploying that capital this week, announcing it snatched up 6.8 million SOL for approximately $1.58 billion at an average price of $232…
공유하기
BitcoinEthereumNews2025/09/18 03:42
Cryptos Signal Divergence Ahead of Fed Rate Decision

Cryptos Signal Divergence Ahead of Fed Rate Decision

The post Cryptos Signal Divergence Ahead of Fed Rate Decision appeared on BitcoinEthereumNews.com. Crypto assets send conflicting signals ahead of the Federal Reserve’s September rate decision. On-chain data reveals a clear decrease in Bitcoin and Ethereum flowing into centralized exchanges, but a sharp increase in altcoin inflows. The findings come from a Tuesday report by CryptoQuant, an on-chain data platform. The firm’s data shows a stark divergence in coin volume, which has been observed in movements onto centralized exchanges over the past few weeks. Bitcoin and Ethereum Inflows Drop to Multi-Month Lows Sponsored Sponsored Bitcoin has seen a dramatic drop in exchange inflows, with the 7-day moving average plummeting to 25,000 BTC, its lowest level in over a year. The average deposit per transaction has fallen to 0.57 BTC as of September. This suggests that smaller retail investors, rather than large-scale whales, are responsible for the recent cash-outs. Ethereum is showing a similar trend, with its daily exchange inflows decreasing to a two-month low. CryptoQuant reported that the 7-day moving average for ETH deposits on exchanges is around 783,000 ETH, the lowest in two months. Other Altcoins See Renewed Selling Pressure In contrast, other altcoin deposit activity on exchanges has surged. The number of altcoin deposit transactions on centralized exchanges was quite steady in May and June of this year, maintaining a 7-day moving average of about 20,000 to 30,000. Recently, however, that figure has jumped to 55,000 transactions. Altcoins: Exchange Inflow Transaction Count. Source: CryptoQuant CryptoQuant projects that altcoins, given their increased inflow activity, could face relatively higher selling pressure compared to BTC and ETH. Meanwhile, the balance of stablecoins on exchanges—a key indicator of potential buying pressure—has increased significantly. The report notes that the exchange USDT balance, around $273 million in April, grew to $379 million by August 31, marking a new yearly high. CryptoQuant interprets this surge as a reflection of…
공유하기
BitcoinEthereumNews2025/09/18 01:01