๊นจ์•Œ ๊ฐœ๋… ๐Ÿ“‘/Android

[Android] Navigation - Navigation 2 → 3

interfacer_han 2026. 8. 21. 16:00

๋ณธ ๊ฒŒ์‹œ๊ธ€์—์„œ ๋‹ค๋ฃฌ ์•ฑ์˜ ์ „์ฒด ์†Œ์Šค ์ฝ”๋“œ๋Š” #5์— ์žˆ์Šต๋‹ˆ๋‹ค.

 

#1 ๊ฐœ์š”

๊ธฐ์กด Android Navigation ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” "Jetpack Navigation 2"์˜€๋‹ค. ๋‹ค์Œ ์„ธ๋Œ€์ธ "Jetpack Navigation 3"์ด ์ถœ์‹œ๋˜์—ˆ๋Š”๋ฐ, 2์™€ 3 ๊ฐ๊ฐ์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒ˜ํ”Œ ์•ฑ์„ ํ•˜๋‚˜์”ฉ ๋งŒ๋“ค์–ด๋ณธ๋‹ค. ๊ทธ๋ฆฌ๊ณ  Navigation 3์˜ ๊ตฌ์กฐ๋กœ๋ถ€ํ„ฐ (2 ๋Œ€๋น„) ์–ด๋–ค ๊ธ์ •์  ํšจ๊ณผ๋ฅผ ๊พ€ํ–ˆ๋Š”์ง€ ์‚ดํŽด๋ณธ๋‹ค. ์ƒ˜ํ”Œ ์•ฑ์€ ์ตœ์†Œํ•œ์˜ ์ฝ”๋“œ๋กœ ๊ตฌ์„ฑํ•ด ํ•˜๋‚˜์˜ ํŒŒ์ผ(MainActivity.kt)์— ๋•Œ๋ ค ๋ฐ•์•˜๋‹ค. ๊ณต๋ถ€ ๋ฉด์—์„  ํŒŒ์ผ์„ ๋‚˜๋ˆ„๋Š” ๊ฒƒ๋ณด๋‹ค ์˜คํžˆ๋ ค ๋” ์ง๊ด€์ ์ด๋ผ๋Š” ํŒ๋‹จ์—์„œ๋‹ค.

 

#2 Jetpack Navigation 2

 

ํƒ์ƒ‰ | App architecture | Android Developers

Android Jetpack์˜ ํƒ์ƒ‰ ๊ตฌ์„ฑ์š”์†Œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์•ฑ์—์„œ ํƒ์ƒ‰ ๊ตฌํ˜„

developer.android.com

 

#2-1 ๋„์‹๋„

๋”๋ณด๊ธฐ
```Mermaid %%{init: { "flowchart": { "defaultRenderer": "elk", "curve": "basis", "nodeSpacing": 45, "rankSpacing": 70 }, "themeVariables": { "clusterBkg": "#eaeaea", "clusterBorder": "#f0f1f2", "mainBkg": "#fafafd", "primaryColor": "#fafafd", "textColor": "#202020", "primaryTextColor": "#202020", "tertiaryTextColor": "#202020", "background": "#ffffff", "edgeLabelBackground": "#ffffff" }, "themeCSS": ".cluster-label text { font-size: 20px !important; font-weight: 700 !important; }" }}%% flowchart LR subgraph appGroup["MainActivity.kt (์•ฑ ํ™”๋ฉด)"] direction TB MyApp HomeDestination["HomeDestination"] ArticleDestination["ArticleDestination(id)"] SettingDestination["SettingDestination"] end subgraph frameworkGroup["androidx.navigation (Navigation 2 ํ”„๋ ˆ์ž„์›Œํฌ)"] direction TB NavHost NavController end MyApp -->|"์ƒ์„ฑ ๋ฐ backStack ์ƒํƒœ ๊ตฌ๋…"| NavController MyApp -->|ํ˜ธ์ถœ| NavHost NavHost -->|์ฐธ์กฐ| NavController NavHost -->|๋ชฉ์ ์ง€ ๋“ฑ๋ก| HomeDestination NavHost -->|๋ชฉ์ ์ง€ ๋“ฑ๋ก| ArticleDestination NavHost -->|๋ชฉ์ ์ง€ ๋“ฑ๋ก| SettingDestination HomeDestination -->|ํ˜ธ์ถœ| NavController ArticleDestination -->|ํ˜ธ์ถœ| NavController SettingDestination -->|ํ˜ธ์ถœ| NavController classDef myAppNode stroke:#64748B,stroke-width:3px class MyApp myAppNode classDef navHostNode stroke:#3B82F6,stroke-width:3px class NavHost navHostNode classDef navControllerNode stroke:#A855F7,stroke-width:3px class NavController navControllerNode classDef homeDestNode stroke:#EAB308,stroke-width:3px class HomeDestination homeDestNode classDef articleDestNode stroke:#06B6D4,stroke-width:3px class ArticleDestination articleDestNode classDef settingDestNode stroke:#F97316,stroke-width:3px class SettingDestination settingDestNode linkStyle 0 stroke:#64748B,stroke-width:3.5px linkStyle 1 stroke:#64748B,stroke-width:3.5px linkStyle 2 stroke:#3B82F6,stroke-width:3.5px linkStyle 3 stroke:#3B82F6,stroke-width:3.5px linkStyle 4 stroke:#3B82F6,stroke-width:3.5px linkStyle 5 stroke:#3B82F6,stroke-width:3.5px linkStyle 6 stroke:#EAB308,stroke-width:3.5px linkStyle 7 stroke:#06B6D4,stroke-width:3.5px linkStyle 8 stroke:#F97316,stroke-width:3.5px ```

 

#2-2 MainActivity

package com.example.navigation2basics

...
import androidx.navigation.NavGraph
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
...
import kotlinx.serialization.Serializable

@Serializable
data object Home

@Serializable
data class Article(val id: String)

@Serializable
data object Setting

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            Navigation2BasicsTheme {
                MyApp()
            }
        }
    }
}

@SuppressLint("RestrictedApi")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyApp() {
    val navController = rememberNavController()
    val backStack by navController.currentBackStack.collectAsState() // ๋ฐฑ์Šคํƒ์ด NavHostController ์ž์ฒด์— ๋ฐ•ํ˜€์žˆ๋Š” ๋ชจ์Šต
    val backStackSize = backStack.count {
        it.destination !is NavGraph // "!is NavGraph"๊ฐ€ ์—†์œผ๋ฉด 'Root NavGraph' ๋“ฑ ๋‚ด๊ฐ€ ์‹ ๊ฒฝ์“ฐ์ง€ ์•Š์•„๋„ ๋˜๋Š” ๊ฒƒ๋“ค๊นŒ์ง€ ๋‹ค size๋กœ์„œ ์žกํ˜€๋ฒ„๋ฆฐ๋‹ค
    }

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        topBar = {
            TopAppBar(
                title = { Text("Back stack size: $backStackSize") },
            )
        },
    ) { innerPadding ->
        NavHost(
            navController = navController,
            startDestination = Home,
            modifier = Modifier.padding(innerPadding),
        ) {
            composable<Home> {
                HomeRoute(
                    navigateToArticle = { articleId ->
                        navController.navigate(Article(articleId))
                    },
                    navigateToSetting = {
                        navController.navigate(Setting)
                    }
                )
            }

            composable<Article> { backStackEntry ->
                val article = backStackEntry.toRoute<Article>()

                ArticleRoute(
                    id = article.id,
                    navigateToHome = {
                        navController.navigate(Home)
                    },
                    navigateToSetting = {
                        navController.navigate(Setting)
                    },
                )
            }

            composable<Setting> {
                SettingRoute(
                    navigateToHome = {
                        navController.navigate(Home)
                    },
                    navigateToArticle = { articleId ->
                        navController.navigate(Article(articleId))
                    },
                )
            }
        }
    }
}

@Composable
fun HomeRoute(
    navigateToArticle: (String) -> Unit,
    navigateToSetting: () -> Unit,
) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.SpaceEvenly,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Text("Home")

        Button(
            onClick = {
                navigateToArticle("7")
            }
        ) {
            Text("Go to 7th Article")
        }

        Button(
            onClick = {
                navigateToSetting()
            }
        ) {
            Text("Go to Setting")
        }
    }
}

@Composable
fun ArticleRoute(
    id: String,
    navigateToHome: () -> Unit,
    navigateToSetting: () -> Unit,
) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.SpaceEvenly,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Text("${id}th Article")

        Button(
            onClick = {
                navigateToHome()
            }
        ) {
            Text("Go to Home")
        }

        Button(
            onClick = {
                navigateToSetting()
            }
        ) {
            Text("Go to Setting")
        }
    }
}

@Composable
fun SettingRoute(
    navigateToHome: () -> Unit,
    navigateToArticle: (String) -> Unit,
) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.SpaceEvenly,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Text("Setting")

        Button(
            onClick = {
                navigateToHome()
            }
        ) {
            Text("Go to Home")
        }

        Button(
            onClick = {
                navigateToArticle("8")
            }
        ) {
            Text("Go to 8th Article")
        }
    }
}

 

#2-3 NavHost()

// in NavHost.kt

package androidx.navigation.compose

@Composable public fun NavHost(
    navController: NavHostController,
    startDestination: Any,
    modifier: Modifier = Modifier,
    ...
    builder: NavGraphBuilder.() -> Unit
): Unit

composable์€NavGraphBuilder์˜ ํ™•์žฅ ํ•จ์ˆ˜๋‹ค. ์ฆ‰,

 

NavHost(
    ...
) {
    composable<Home> {
        HomeRoute(
            navigateToArticle = { articleId ->
                navController.navigate(Article(articleId))
            },
            navigateToSetting = {
                navController.navigate(Setting)
            }
        )
    }
    
    composable<Article> {
        ...
    }
    
    ...
}

์—์„œ,

 

 composable()
 = this.composable()
 = navGraphBuilder.composable()

NavGraphBuilder.composable() ํ•จ์ˆ˜๊ฐ€ ์ˆ˜ํ–‰๋˜๋Š” ๋งŒํผ, Navigation์— ๋ชฉ์ ์ง€(= Navigation 2์—์„œ Destination์ด๋ผ๊ณ  ํ‘œํ˜„)๊ฐ€ ์ถ”๊ฐ€๋˜๋Š” ์…ˆ์ด๋‹ค.

 

#2-4 NavController

Jetpack Navigation 2 ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(androidx.navigation)์—์„œ ์ œ๊ณตํ•˜๋Š” Navigation ์ƒํƒœ ๊ด€๋ฆฌ ํด๋ž˜์Šค๋‹ค. "๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ๊ธฐ๋ณธ ์ œ๊ณต"ํ•œ๋‹ค๋Š” ์ ์ด ์ค‘์š”ํ•˜๋‹ค. ์ด๊ฒƒ์ด Navigation 3์™€ ๋Œ€๋น„๋˜๋Š” ์ง€์ ์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. NavController ๋‚ด๋ถ€์—๋Š” ๋ฐฑ์Šคํƒ์ด ์กด์žฌํ•˜๋Š”๋ฐ, (๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์˜ํ•ด) ์ž๋™์œผ๋กœ ๊ด€๋ฆฌ๋œ๋‹ค. ๊ทธ ๋•Œ๋ฌธ์— ๋„ค๋น„๊ฒŒ์ด์…˜ ๋™์ž‘์ด ๊ฝค ๋ณต์žกํ•œ ๊ฒฝ์šฐ๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด ๊ฐœ๋ฐœ์ž๊ฐ€ ๋งŒ์งˆ ํ•„์š”๊ฐ€ ์—†๋‹ค. ๊ฐ€๋ น NavController.navigate() ๋“ฑ์„ ์‚ฌ์šฉ์ž๊ฐ€ ํ˜ธ์ถœํ•˜๋ฉด, NavController๊ฐ€ ์ด ๋ฐฑ์Šคํƒ์— ์•Œ์•„์„œ ํ™”๋ฉด์„ ๋„ฃ๊ณ , '๋’ค๋กœ ๊ฐ€๊ธฐ'๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด ๋ฐ˜๋Œ€๋กœ ์•Œ์•„์„œ pop()์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

 

#2-5 ์ฐธ์กฐ: Now in android์—์„œ์˜ ์“ฐ์ž„

 

Now in Android - Jetpack Navigation 2๊ฐ€ ์ ์šฉ๋œ ๊ตฌ์กฐ ๋ถ„์„

#1 ๊ฐœ์š”#1-1 ๋ถ„์„ ๋Œ€์ƒ Migrate codebase to Navigation 3 by dturner · Pull Request #1902 · android/nowinandroid[WIP] This PR migrates the codebase to using Jetpack Navigation 3. More details to follow...github.comNow in Android์˜ 1902๋ฒˆ PR์ธ, "Mig

kenel.tistory.com

Navigation 2๊ฐ€ ์“ฐ์ธ d438a00 ์ปค๋ฐ‹ ์‹œ์ ์˜ Now in Android๋ฅผ ๋ถ„์„ํ•œ ๊ฒŒ์‹œ๊ธ€์ด๋‹ค.

 

#3 Jetpack Navigation 3

 

ํƒ์ƒ‰ 3 | App architecture | Android Developers

Navigation 3์€ ๋’ค๋กœ ์Šคํƒ, ์ ์‘ํ˜• ๋ ˆ์ด์•„์›ƒ, Android ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์œ„ํ•œ ์œ ์—ฐํ•œ ์•ฑ ํƒ์ƒ‰ ์‹œ์Šคํ…œ์„ ์™„์ „ํžˆ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋Š” ์ƒˆ๋กœ์šด Compose ์ค‘์‹ฌ ํƒ์ƒ‰ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค.

developer.android.com

 

#3-1 ๋„์‹๋„

๋”๋ณด๊ธฐ
```Mermaid %%{init: { "flowchart": {"defaultRenderer": "elk", "curve": "basis", "nodeSpacing": 45, "rankSpacing": 70 }, "themeVariables": { "clusterBkg": "#eaeaea", "clusterBorder": "#f0f1f2", "mainBkg": "#fafafd", "primaryColor": "#fafafd", "textColor": "#202020", "primaryTextColor": "#202020", "tertiaryTextColor": "#202020", "background": "#ffffff", "edgeLabelBackground": "#ffffff" }, "themeCSS": ".cluster-label text { font-size: 20px !important; font-weight: 700 !important; }" }}%% flowchart LR subgraph appGroup["MainActivity.kt (์•ฑ ํ™”๋ฉด)"] direction TB MyApp NavBackStack["NavBackStack (backStack)
์˜ˆ: [Home, Article(7), Setting]"] HomeKey["Home : NavKey"] ArticleKey["Article(id) : NavKey"] SettingKey["Setting : NavKey"] end subgraph frameworkGroup["androidx.navigation3 (Navigation 3 ํ”„๋ ˆ์ž„์›Œํฌ)"] direction TB EntryProvider NavEntry["NavEntry
์˜ˆ: [HomeRoute, ArticleRoute, SettingRoute]"] NavDisplay end MyApp -->|"์ƒ์„ฑ ๋ฐ pop ์ฝœ๋ฐฑ(onBack) ์ •์˜"| NavBackStack MyApp -->|ํ˜ธ์ถœ| NavDisplay NavDisplay -->|์ฐธ์กฐ| NavBackStack NavDisplay -->|์ฐธ์กฐ| EntryProvider HomeKey -->|resolve| EntryProvider ArticleKey -->|resolve| EntryProvider SettingKey -->|resolve| EntryProvider EntryProvider -->|"Key → NavEntry ๋ณ€ํ™˜"| NavEntry NavDisplay -->|์ฐธ์กฐ| NavEntry NavEntry -->|ํ˜ธ์ถœ| NavBackStack classDef myAppNode stroke:#64748B,stroke-width:3px class MyApp myAppNode classDef navDisplayNode stroke:#3B82F6,stroke-width:3px class NavDisplay navDisplayNode classDef backStackNode stroke:#A855F7,stroke-width:3px class NavBackStack backStackNode classDef entryProviderNode stroke:#EAB308,stroke-width:3px class EntryProvider entryProviderNode classDef homeKeyNode stroke:#06B6D4,stroke-width:3px class HomeKey homeKeyNode classDef articleKeyNode stroke:#F97316,stroke-width:3px class ArticleKey articleKeyNode classDef settingKeyNode stroke:#EC4899,stroke-width:3px class SettingKey settingKeyNode classDef navEntryNode stroke:#10B981,stroke-width:3px class NavEntry navEntryNode linkStyle 0 stroke:#64748B,stroke-width:3.5px linkStyle 1 stroke:#64748B,stroke-width:3.5px linkStyle 2 stroke:#3B82F6,stroke-width:3.5px linkStyle 3 stroke:#3B82F6,stroke-width:3.5px linkStyle 4 stroke:#06B6D4,stroke-width:3.5px linkStyle 5 stroke:#F97316,stroke-width:3.5px linkStyle 6 stroke:#EC4899,stroke-width:3.5px linkStyle 7 stroke:#EAB308,stroke-width:3.5px linkStyle 8 stroke:#3B82F6,stroke-width:3.5px linkStyle 9 stroke:#10B981,stroke-width:3.5px ```

NavEntry๋Š” ๋ชฉ์ ์ง€(Destination)๋ฅผ ์˜๋ฏธํ•˜๋ฉฐ, ์‹ค์ œ ํ™”๋ฉด("composable content")์™€ ๋‹ค์–‘ํ•œ ์ •๋ณด("metadata") ๋“ฑ์„ ์ง€๋‹ˆ๋Š” ๊ฐ์ฒด๋‹ค. ๋˜, ๋ชฉ์ ์ง€ ๋ณ„๋กœ Key ๊ฐœ๋…์ด ์ƒ๊ฒผ๋‹ค. ๋ชฉ์ ์ง€๊ฐ€ ํ†ต์งธ๋กœ ๊ด€๋ฆฌ๋˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ Key(์ด๋ฆ„ํ‘œ)๋ฅผ ํ†ตํ•ด ๊ฐ€๋ณ๊ฒŒ ๊ด€๋ฆฌ๋˜๋Š” ๊ฒƒ์ด๋‹ค. ์ด์— ๋”ฐ๋ผ EntryProvider๋ผ๋Š” ํด๋ž˜์Šค๋„ ์ƒ๊ฒผ๋Š”๋ฐ, Key๋ฅผ NavEntry๋กœ ์—ฐ๊ฒฐ(๋ณ€ํ™˜) ์ง“๋Š” ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค. ํ”„๋กœ๊ทธ๋ž˜๋จธ๊ฐ€ Key๋งŒ ์‹ ๊ฒฝ ์จ์ฃผ๋ฉด, EntryProvider์— ์˜ํ•ด UI๊ฐ€ ์•Œ์•„์„œ(์„ ์–ธ์ ์œผ๋กœ) ๊ฐฑ์‹ ๋˜๋Š” ์‹์ด๋‹ค.

NavEntry๊ฐ€ NavBackStack์— ์˜์กดํ•œ๋‹ค๋Š” ๊ฒƒ์€ ์ด๋ก ์ƒ์œผ๋กœ๋Š” ์˜ณ์„ ์ˆ˜ ์žˆ์ง€๋งŒ, ์‚ฌ์‹ค ์‹ค๋ฌด์ ์œผ๋กœ๋Š” ๊ฑฐ์˜ ํ‹€๋ฆฐ ๋ง์ด๋‹ค. ์–ด๋–ค ๋ชฉ์ ์ง€ A์—์„œ B๋กœ ๊ฐ€๋Š” ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•  ๋•Œ, ์ฆ‰, A Entry์—์„œ B Entry๋กœ ๊ฐˆ ๋•Œ NavBackStack์„ A๊ฐ€ ์ง์ ‘ ์กฐ์ž‘ํ•˜๋Š” ๊ฒฝ์šฐ๋Š” ์—†์„ ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์‹ค์ œ ์„œ๋น„์Šค๋˜๋Š” ์•ฑ์ด๋ผ๋ฉด, ๋Œ€๋ถ€๋ถ„ A๋Š” "app" ๋ชจ๋“ˆ๋กœ๋ถ€ํ„ฐ ํ•ด๋‹น ์ด๋ฒคํŠธ๋ฅผ State Hoisting์œผ๋กœ ์ „๋‹ฌ๋ฐ›์•„ ์“ธ ๊ฒƒ์ด๋‹ค.

 

#3-2 MainActivity

package com.example.navigation3basics

...
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
...
import kotlinx.serialization.Serializable

@Serializable
data object Home : NavKey

@Serializable
data class Article(val id: String) : NavKey

@Serializable
data object Setting : NavKey

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        setContent {
            Navigation3BasicsTheme {
                MyApp()
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyApp() {
    val backStack = rememberNavBackStack(Home)

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        topBar = {
            TopAppBar(
                title = {
                    Text("Back stack size: ${backStack.size}")
                },
            )
        },
    ) { innerPadding ->

        NavDisplay(
            backStack = backStack,
            modifier = Modifier.padding(innerPadding),
            onBack = {
                backStack.removeLastOrNull()
            },
            entryProvider = entryProvider {
                entry<Home> {
                    HomeRoute(
                        navigateToArticle = { articleId ->
                            backStack.add(Article(articleId))
                        },
                        navigateToSetting = {
                            backStack.add(Setting)
                        },
                    )
                }

                entry<Article> { article ->
                    ArticleRoute(
                        id = article.id,
                        navigateToHome = {
                            backStack.add(Home)
                        },
                        navigateToSetting = {
                            backStack.add(Setting)
                        },
                    )
                }

                entry<Setting> {
                    SettingRoute(
                        navigateToHome = {
                            backStack.add(Home)
                        },
                        navigateToArticle = { articleId ->
                            backStack.add(Article(articleId))
                        },
                    )
                }
            },
        )
    }
}

@Composable
fun HomeRoute(
    navigateToArticle: (String) -> Unit,
    navigateToSetting: () -> Unit,
) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.SpaceEvenly,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Text("Home")

        Button(
            onClick = {
                navigateToArticle("7")
            }
        ) {
            Text("Go to 7th Article")
        }

        Button(
            onClick = {
                navigateToSetting()
            }
        ) {
            Text("Go to Setting")
        }
    }
}

@Composable
fun ArticleRoute(
    id: String,
    navigateToHome: () -> Unit,
    navigateToSetting: () -> Unit,
) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.SpaceEvenly,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Text("${id}th Article")

        Button(
            onClick = {
                navigateToHome()
            }
        ) {
            Text("Go to Home")
        }

        Button(
            onClick = {
                navigateToSetting()
            }
        ) {
            Text("Go to Setting")
        }
    }
}

@Composable
fun SettingRoute(
    navigateToHome: () -> Unit,
    navigateToArticle: (String) -> Unit,
) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.SpaceEvenly,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Text("Setting")

        Button(
            onClick = {
                navigateToHome()
            }
        ) {
            Text("Go to Home")
        }

        Button(
            onClick = {
                navigateToArticle("8")
            }
        ) {
            Text("Go to 8th Article")
        }
    }
}

์ฝ”๋“œ์—์„œ ๊ฐ€์žฅ ๋ˆˆ์— ๋„๋Š” ์ฐจ์ด๋Š” ๋ฐ”๋กœ NavController๊ฐ€ ์ œ๊ฑฐ๋˜์—ˆ๋‹ค๋Š” ์ ์ด๋‹ค. ๊ทธ๋ฆฌ๊ณ  NavController๊ฐ€ ์•Œ์•„์„œ ๊ด€๋ฆฌํ•ด ์ฃผ๋˜ ๋ฐฑ์Šคํƒ์„ ์ด์ œ ํ”„๋กœ๊ทธ๋ž˜๋จธ์ธ ์šฐ๋ฆฌ๊ฐ€ ์ง์ ‘ ๊ด€๋ฆฌํ•œ๋‹ค. ๊ทธ๋ž˜๋„ ํŽธ์˜๋ฅผ ์œ„ํ•œ rememberNavBackStack()๋ผ๋Š” ํ—ฌํผ ํ•จ์ˆ˜๋Š” ์ œ๊ณตํ•œ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ๋Š” ์ € ํ•จ์ˆ˜๋กœ ๋ฐฑ์Šคํƒ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ๋œ๋‹ค.

 

#3-3 ๋‰˜์•™์Šค์˜ ์ฐจ์ด

๋‹จ์–ด ๋ณ„๋กœ ๋‰˜์•™์Šค์˜ ์ฐจ์ด๋ฅผ ๊ณฑ์”น์–ด๋ณด๋ฉด, 2 → 3 ๋ณ€ํ™”์— ๋Œ€ํ•œ ์ดํ•ด๊ฐ€ ๋” ๊นŠ์–ด์ง„๋‹ค.

NavController → ์ œ๊ฑฐ๋จ
NavController๊ฐ€ back stack ์†Œ์œ  → ์šฐ๋ฆฌ(ํ”„๋กœ๊ทธ๋ž˜๋จธ)๊ฐ€ ์†Œ์œ ใ†๊ด€๋ฆฌํ•˜๊ฒŒ ๋ฐ”๋€œ
NavController.navigate() → (์šฐ๋ฆฌ๊ฐ€ ์ง์ ‘) Key๋ฅผ back stack์— ์ถ”๊ฐ€
Destination → Entry
Navigation Graph → ์ œ๊ฑฐ๋จ
Navigation Graph์— ๋ชฉ์ ์ง€(Destination) ํ•˜๋‚˜ ๋“ฑ๋ก → EntryProvider์—์„œ (Key → Entry) ๊ทœ์น™ ํ•˜๋‚˜ ์ถ”๊ฐ€
startDestination → BackStack์˜ ์ดˆ๊ธฐ Key
NavHost → NavDisplay

Destination → Entry
Destination(๋ชฉ์ ์ง€, ๋„์ฐฉ์ง€)๋Š” Entry(์–ด๋–ค ์ž๋ฃŒ ๊ตฌ์กฐ ์•ˆ์— ๋“ค์–ด ์žˆ๋Š” ํ•ญ๋ชฉ ํ•˜๋‚˜)๋กœ ๋ฐ”๋€Œ์—ˆ๋‹ค. ์ž๋ฃŒ ๊ตฌ์กฐ๋ผ ํ•จ์€ ๋ฐ”๋กœ ๋ฐฑ์Šคํƒ์„ ์•”์‹œํ•œ๋‹ค. Destination์ด๋ผ๋Š” ํ‘œํ˜„๋„ ์—ฌ์ „ํžˆ ์“ฐ๊ธด ํ•œ๋‹ค.

 

Navigation Graph์— ๋ชฉ์ ์ง€(Destination) ํ•˜๋‚˜ ๋“ฑ๋ก → EntryProvider์—์„œ (Key → Entry) ๊ทœ์น™ ํ•˜๋‚˜ ์ถ”๊ฐ€

์‚ฌ์šฉ์ž๋Š” NavHost๋ฅผ ํ†ตํ•ด ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ทธ๋ž˜ํ”„๋ฅผ ๊ทธ๋ ธ๋‹ค. Navigation 3์—์„œ๋Š” ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ทธ๋ž˜ํ”„๋ฅผ ๊ตฌ์„ฑํ–ˆ๋˜ Destination๋“ค์„ EntryProvider๋กœ ์˜ฎ๊ฒจ์•ผ ํ•œ๋‹ค. ํ•˜์ง€๋งŒ, ์ด ๋‘˜์€ ์ค‘์š”ํ•œ ์ฐจ์ด๊ฐ€ ์žˆ๋‹ค. Entry๋Š” Entry ๊ฐ„ ๋ถ€๋ชจ-์ž์‹ ๊ด€๊ณ„๋ฅผ ์ด๋ฃจ์ง€ ์•Š๋Š”๋‹ค๋Š” ์ ์ด๋‹ค. ์ฆ‰ '์ž๋ฃŒ ๊ตฌ์กฐ์ '์œผ๋กœ๋Š” ๊ณ„์ธต์ด ์—†๋Š” ์ˆ˜ํ‰ ๊ด€๊ณ„๋‹ค. ํ•˜์ง€๋งŒ ์‚ฌ์šฉ์ž์˜ ์„ค๊ณ„ํ•˜๊ธฐ์— ๋”ฐ๋ผ ๋…ผ๋ฆฌ์  ๊ณ„์ธต์€ ์–ผ๋งˆ๋“ ์ง€ ์กด์žฌํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ฐ€๋ น, ๋ฐ˜๋“œ์‹œ Home์„ ๊ฑฐ์ณ Article๋กœ ์ „ํ™˜๋˜๋„๋ก ํ”„๋กœ๊ทธ๋ž˜๋จธ๊ฐ€ ๊ฐ•์ œํ•  ์ˆ˜ ์žˆ๋‹ค.

 

NavHost → NavDisplay

Navigation Graph์— ๋”ฐ๋ฅธ ํ˜„์žฌ Destination์˜ UI๋ฅผ ๊ด€๋ฆฌ(Hosting)ํ•˜๋Š” NavHost๋Š”, Navigation Graph๊ฐ€ ์‚ฌ๋ผ์ง์— ๋”ฐ๋ผ ํ•„์š”๊ฐ€ ์—†์–ด์กŒ๋‹ค. NavHost๋Š” ๋น„์Šทํ•œ ์—ญํ• ์˜ NavDisplay๋กœ ๋Œ€์ฒด๋˜์—ˆ๋Š”๋ฐ ์ด ์ด๋ฆ„์€, (ํ”„๋กœ๊ทธ๋ž˜๋จธ๊ฐ€ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฑ์Šคํƒ์— ์ข…์†๋˜์–ด) ๊ทธ์ € ์Šคํƒ ์ตœ์ƒ๋‹จ์— ์žˆ๋Š” ๊ฒƒ(Entry)์„ ํ‘œ์‹œ(Display)ํ•  ๋ฟ์ด๋ผ๋Š” ๋‰˜์•™์Šค๋ฅผ ์ง€๋‹Œ๋‹ค.

 

#3-4 EntryProvider

// in EntryProvider.kt

package androidx.navigation3.runtime

public inline fun <T : Any> entryProvider(
    noinline fallback: (unknownScreen: T) -> NavEntry<T> = {
        throw IllegalStateException("Unknown screen $it")
    },
    builder: EntryProviderScope<T>.() -> Unit,
): (T) -> NavEntry<T> = EntryProviderScope(fallback).apply(builder).build()


↓ ↓ ↓ ์‰ฝ๊ฒŒ ํ’€์–ด์“ด ๋ฒ„์ „ ↓ ↓ ↓


fun <T : Any> entryProvider(
    fallback: (T) -> NavEntry<T>,
    builder: EntryProviderScope<T>.() -> Unit,
): (T) -> NavEntry<T> {

    // 1. Entry ๋“ฑ๋ก ์ •๋ณด๋ฅผ ๋‹ด์„ ๊ฐ์ฒด ์ƒ์„ฑ (≈ ์•„์ดํ…œ์„ ๋‹ด์„ List)
    val scope = EntryProviderScope<T>(fallback)

    // 2. ์‚ฌ์šฉ์ž๊ฐ€ ์ž‘์„ฑํ•œ DSL ์‹คํ–‰
    scope.builder() 

    // 3. ๋“ฑ๋ก ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ, Key -> NavEntry ๋ณ€ํ™˜ ํ•จ์ˆ˜ ์ƒ์„ฑ
    val provider = scope.build()

    // 4. ๊ทธ ํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜
    return provider
}

builder๋ผ๋Š” ๋งค๊ฐœ๋ณ€์ˆ˜ ๋ช…์ด๋‚˜ ์ „์ฒด์ ์ธ ํ๋ฆ„์„ ํ†ตํ•ด, builder ํŒจํ„ด์„ ์“ฐ๊ณ  ์žˆ์Œ์ด ๋ณด์ธ๋‹ค. fallback์€ ๋ชจ๋ฅด๋Š” NavKey๊ฐ€ ๋“ค์–ด์™”์„ ๋•Œ ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ•˜๋Š”์ง€๋ฅผ ๋‹ด๋Š”๋‹ค. ๋ถ„๊ธฐ๋ฌธ์—์„œ์˜ 'else'์™€ ๊ฐ™์€ ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์ด๋‹ค. "์‚ฌ์šฉ์ž๊ฐ€ ์ž‘์„ฑํ•œ DSL ์‹คํ–‰"์€ #3-2์— ์žˆ๋Š”,

 

// ์•„๋ž˜ ํ•จ์ˆ˜๊ฐ€ builder()๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ entryProvider()์— ์ „๋‹ฌ๋œ๋‹ค.
entry<Article> { article ->
    ArticleRoute(
        id = article.id,
        navigateToHome = {
            backStack.add(Home)
        },
        navigateToSetting = {
            backStack.add(Setting)
        },
    )
}

์™€ ๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•จ์„ ์˜๋ฏธํ•œ๋‹ค. ์„ค๋ช…์„ ์œ„ํ•ด entryProvider()๋ฅผ ํ•˜๋“œ ์ฝ”๋”ฉํ•˜์—ฌ ๋ณ€ํ˜•ํ•ด๋ณด๋ฉด,

 

fun <T : Any> entryProvider(
    fallback: (T) -> NavEntry<T>,
    builder: EntryProviderScope<T>.() -> Unit,
): (T) -> NavEntry<T> {

    // 1. Entry ๋“ฑ๋ก ์ •๋ณด๋ฅผ ๋‹ด์„ ๊ฐ์ฒด ์ƒ์„ฑ (≈ ์•„์ดํ…œ์„ ๋‹ด์„ List)
    val scope = EntryProviderScope<T>(fallback)

    // 2. ์‚ฌ์šฉ์ž๊ฐ€ ์ž‘์„ฑํ•œ DSL ์‹คํ–‰
    scope.entry<Home> {
        HomeRoute(
            ...
        )
    }
    
    scope.entry<Article> { article ->
        ArticleRoute(
            ...
        )
    }
    
    scope.entry<Setting> {
        ...
    }

    // 3. ๋“ฑ๋ก ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ, Key -> NavEntry ๋ณ€ํ™˜ ํ•จ์ˆ˜ ์ƒ์„ฑ
    val provider = scope.build()

    // 4. ๊ทธ ํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜
    return provider
}

์™€ ๊ฐ™์€ ์ฝ”๋“œ๊ฐ€ ๋œ๋‹ค.

 

#3-5 ๋ฐฑ์Šคํƒ ๊ด€๋ฆฌ

Entry๊ฐ€ ๊ณ„์ธต(๋ถ€๋ชจ-์ž์‹) ๊ด€๊ณ„๋ฅผ ์ด๋ฃจ์ง€ ์•Š๋Š”๋‹ค๋ฉด, ๋ฐฑ์Šคํƒ ๊ด€๋ฆฌ๋Š” ํ”„๋กœ๊ทธ๋ž˜๋จธ์˜ ๋ชซ์ธ๊ฐ€? ๊ทธ๋ ‡๋‹ค. ๊ฐ„๋‹จํ•œ ์•ฑ์—์„œ๋Š” ๊ด€๋ฆฌ๊ฐ€ ์–ด๋ ต์ง€ ์•Š๊ฒ ์ง€๋งŒ, ์•ฑ์ด ์ปค์ง€๋ฉด ๋ถ€๋‹ด์ด ์ ์  ๋Š˜ ๊ฒƒ์ด๋‹ค. Navigation Graph๋ฅผ ๋งŒ๋“ค์ง€ ์•Š์•„๋„ ๋œ๋‹ค๋Š” ๊ฒƒ์€ ์žฅ์ ์ด์ง€๋งŒ, ์ตœ์†Œํ•œ์˜ ๋ฐฑ์Šคํƒ ์กฐ์ž‘ ์ฝ”๋“œ๊ฐ€ ์š”๊ตฌ๋œ๋‹ค๋Š” ๊ฒƒ์€ ๋‹จ์ ์ผ ์ˆ˜ ์žˆ๊ฒ ๋‹ค. ์•ฑ์ด ์ปค์ง€๋Š” ๊ฒฝ์šฐ์—”, ๋ณ„๋„์˜ ๋ฐฑ์Šคํƒ ๊ด€๋ฆฌ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ์ผ๋ฐ˜์ ์ด๋‹ค. #3-6์— ์žˆ๋Š” Now in Android์ฒ˜๋Ÿผ ๋ง์ด๋‹ค.

 

#3-6 ์ฐธ์กฐ: Now in android์—์„œ์˜ ์“ฐ์ž„

ํ˜„์žฌ ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ ์ค‘

Navigation 3๊ฐ€ ์“ฐ์ธ, 068d8cc ์ปค๋ฐ‹ ์‹œ์ ์˜ Now in Android๋ฅผ ๋ถ„์„ํ•œ ๊ฒŒ์‹œ๊ธ€์ด๋‹ค. ์ด ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋ฐฑ์Šคํƒ์˜ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด NiaNavigator๋ผ๋Š” ์ „์šฉ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด ๋ฐฑ์Šคํƒ ๊ด€๋ฆฌ์˜ ์ฑ…์ž„์„ ๋ชฐ์•„๋„ฃ์—ˆ๋‹ค.

 

#4 ๊ฒฐ๋ก 

Jetpack Navigation 2 → 3๋กœ,

  • ๋ฐฑ์Šคํƒ ์†Œ์œ  ๋ฐ ๊ด€๋ฆฌ์˜ ์ฑ…์ž„์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์•ฑ(ํ”„๋กœ๊ทธ๋ž˜๋จธ)์˜ ์ฑ…์ž„์œผ๋กœ ๋„˜์–ด์™”๋‹ค. ๋”ฐ๋ผ์„œ ์•ฑ์˜ ์ฑ…์ž„๊ณผ ์ž์œ ๋„๊ฐ€ ํ•จ๊ป˜ ์ปค์กŒ๋‹ค. ์•ฑ์˜ ๊ทœ๋ชจ๊ฐ€ ์ž‘๋‹ค๋ฉด Navigation 3๊ฐ€ ์˜คํžˆ๋ ค ๋ถˆ๋ฆฌํ•˜๋‹ค. ํ•˜์ง€๋งŒ ๊ทœ๋ชจ๊ฐ€ ์ปค์งˆ์ˆ˜๋ก, (ํ”„๋กœ๊ทธ๋ž˜๋จธ ์ž…์žฅ์—์„œ) ๋ชฉ์ ์ง€ ์ „ํ™˜ ๋กœ์ง์„ ์งœ๊ธฐ ์‰ฝ๋‹ค๋Š” ์ด์ ์ด ๊ฐ•ํ™”๋œ๋‹ค.
  • Key ๊ฐœ๋…์ด ์ƒ๊ฒผ๋‹ค. Key๋Š” ๋ฐฑ์Šคํƒ์— ๋“ค์–ด๊ฐ„๋‹ค. ์ด๋Š” ์‹ค์ œ ๋ชฉ์ ์ง€๋Š” ์•„๋‹ˆ๊ณ , ๋ชฉ์ ์ง€์— ๋Œ€์‘ํ•˜๋Š” ์ด๋ฆ„ํ‘œ๋‹ค. ์ด๋ ‡๊ฒŒ ๋ถ„๋ฆฌํ•œ ์ด์œ ๋Š” (์›๋ž˜๋Š” NavController๊ฐ€ ์ˆ˜ํ–‰ํ•˜๋˜) ๋ชฉ์ ์ง€ ์ „ํ™˜ ๋กœ์ง๊ณผ ํ™”๋ฉด์— UI๋ฅผ ๊ทธ๋ฆฌ๋Š” ๋กœ์ง์„ ๋ถ„๋ฆฌํ•˜๊ธฐ ์œ„ํ•จ์ด๋‹ค. ์ „์ž๋Š” (๋ฐฑ์Šคํƒ์„ ์กฐ์ž‘ํ•˜๋Š”) ํ”„๋กœ๊ทธ๋ž˜๋จธ๊ฐ€, ํ›„์ž๋Š” NavDisplay๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค. EntryProvider๋Š” ๊ทธ ์ค‘๊ฐ„์—์„œ Key → Entry ๋ณ€ํ™˜์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.
  • Compose์™€ ๋” ๋‹จ์ˆœํ•˜๊ฒŒ ํ†ตํ•ฉ๋œ๋‹ค. ๋ฐฑ์Šคํƒ ์ž์ฒด๊ฐ€ ์•ฑ์ด ์†Œ์œ ํ•˜๋Š” State๊ฐ€ ๋˜๊ณ , NavDisplay๊ฐ€ ์ด๋ฅผ ๊ด€์ฐฐํ•˜์—ฌ UI๋ฅผ ๊ฐฑ์‹ ํ•˜๋Š” ๊ตฌ์กฐ์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
  • ๋ณธ ๊ฒŒ์‹œ๊ธ€์—์„œ ๋‹ค๋ฃจ์ง„ ์•Š์•˜์ง€๋งŒ, ์—ฌ๋Ÿฌ destination์„ ๋™์‹œ์— ์‚ฌ์šฉํ•˜๋Š” adaptive layout์ด ๊ฐ€๋Šฅํ•˜๋‹ค. ๋ฐฑ์Šคํƒ ์ตœ์ƒ๋‹จ์— ์žˆ๋Š” (Key์— ๋Œ€์‘ํ•˜๋Š”) ๋‹จ์ผ Entry ํ‘œ์‹œ๋ฅผ ๋„˜์–ด, 2๊ฐœ ์ด์ƒ์˜ Entry๋ฅผ ํ‘œ์‹œํ•˜๋Š” ์‹์ด๋‹ค.

 

#5 ์†Œ์Šค ์ฝ”๋“œ

#5-1 Navigation 2

 

android-practice/navigation/Navigation2Basics at master · Kanmanemone/android-practice

Contribute to Kanmanemone/android-practice development by creating an account on GitHub.

github.com

 

#5-2 Navigation 3

 

android-practice/navigation/Navigation3Basics at master · Kanmanemone/android-practice

Contribute to Kanmanemone/android-practice development by creating an account on GitHub.

github.com