[Android] 009. androidx.navigation

Hedgehogで確認したこのページの内容から加筆したものは以下です
https://bps-e.com/dev/android-004-003/

1画面のアプリであっても拡張性のためベースにNavigationを適応するようにします

参考
https://developer.android.com/jetpack/compose/navigation?hl=ja

依存関係設定

Project Structuer 設定

以下の最新を追加
androidx.navigation:navigation-compose
androidx.navigation:navigation-common-ktx
navigation-commonはNavGraphBuilderを使う場合に必要

実装

シンプルな実装例

NavHostのstartDestinationに初期画面を指定、composable()に画面UIを実装
navigate()で遷移、navigateUp()で戻る
popBackStack()で戻る場合は端末の戻るボタンと同じ動作になります

@Composable fun AppContent() {
    MaterialTheme {
        var navController = rememberNavController()
        NavHost(navController = navController, startDestination = "top") {
            composable("top") {
                // 画面遷移
                Button(onClick = { navController.navigate("next") }) {
                    Text("top")
                }
            }
            composable("next") {
                // 前の画面に戻る
                Button(onClick = { navController.navigateUp() }) {
                    Text("next")
                }
            }
        }
    }
}

2画面以上で2画面以上戻る場合

popBackStackで遷移先を指定しinclusive/saveStateにfalseを指定します

@Composable fun AppContent() {
    MaterialTheme {
        var navController = rememberNavController()
        NavHost(navController = navController, startDestination = "top") {
            composable("top") {
                Button(onClick = { navController.navigate("next") }) {
                    Text("top")
                }
            }
            composable("next") {
                Button(onClick = { navController.navigate("last") }) {
                    Text("next")
                }
            }
            composable("last") {
                // topに戻る
                Button(onClick = { navController.popBackStack(route = "top", inclusive = false, saveState = false) }) {
                    Text("last")
                }
            }
        }
    }
}

NavGraphBuilderを使用する場合

NavGraphBuilderを拡張して実装します

fun NavGraphBuilder.topGraph() {
    composable(route = "top") {
        ...
    }
}

fun NavGraphBuilder.nextGraph() {
    composable(route = "next") {
        ...
    }
}

@Composable fun App() {
    ...
    NavHost(navController = navController, startDestination = "top") {
        topGraph()
        nextGraph()
    }
}

実際の実装例

navControllerを全体をコンポーザブルに直接渡すとテストや再利用時しにくくなるため、トリガーするナビゲーション アクションを定義するコールバックを使います

また以下のようにパターン化した実装にすることで画面の実装に集中できます
(NavRouteに項目増やしてScreenを実装してNavGrapにセットするだけ)

@Composable fun AppContent(appState: AppState = rememberAppState()) {
    MaterialTheme {
        AppNavHost(appState.navController, appState::onBack, appState::onNavigate)
    }
}

// AppState.kt
@Stable class AppState(val navController: NavHostController) {
    fun onNavigate(route: String) {
        navController.navigate(route)
    }

    fun onBack(route: String = "") {
        if (route.isEmpty()) navController.navigateUp()
        else navController.popBackStack(route, inclusive = false, saveState = false)
    }
}
@Composable fun rememberAppState(navController: NavHostController = rememberNavController()): AppState = remember(navController) { AppState(navController) }

// AppNavHost.kt
enum class NavRoute {
    Top,
    Next;
}

fun NavGraphBuilder.navGraph(route: NavRoute, onBack: (String) -> Unit, onNavigate: (String) -> Unit) {
    composable(route = route.name) {
        when(route) {
            NavRoute.Top -> TopScreen(onNavigate = { onNavigate(it) })
            NavRoute.Next -> NextScreen(onBack = { onBack(it) })
        }
    }
}

@Composable fun AppNavHost(navController: NavHostController, onBack: (String) -> Unit, onNavigate: (String) -> Unit) {
    NavHost(navController = navController, startDestination = NavRoute.Top.name) {
        NavRoute.values().forEach { navGraph(it, onBack, onNavigate) }
    }
}

// TopScreen.kt
@Composable fun TopScreen(onNavigate: (String) -> Unit) {
    Button(onClick = { onNavigate(NavRoute.Next.name) }) {
        Text(NavRoute.Top.name)
    }
}

// NextScreen.kt
@Composable fun NextScreen(onBack: (String) -> Unit) {
    Button(onClick = { onBack("") }) {
        Text(NavRoute.Next.name)
    }
}

データを渡す

navigateの引数に文字列で値を渡します
使いづらい…
この実装の場合はパラメータ無し(空文字)の場合Exceptionが発生します

@OptIn(ExperimentalMaterial3Api::class)
@Composable fun AppContent() {
    MaterialTheme {
        var navController = rememberNavController()
        NavHost(navController = navController, startDestination = "top") {
            composable("top") {
                var text by remember { mutableStateOf("") }

                Column {
                    TextField(value = text, onValueChange = { text = it })
                    Button(onClick = {
                        navController.navigate("next/${text}/1")
                    }) {
                        Text("top")
                    }
                }
            }
            composable("next/{text}/{id}",
                arguments = listOf(
                    navArgument("text") { type = NavType.StringType },
                    navArgument("id") { type = NavType.IntType },
                )
            ) {
                Column {
                    val text = it.arguments?.getString("text") ?: ""
                    val id = it.arguments?.getInt("id") ?: 0
                    Text("${text}_${id}")
                    Button(onClick = { navController.navigateUp() }) {
                        Text("next")
                    }
                }
            }
        }
    }
}

引数省略可能にする場合は以下のようにします

composable("top") {
    ...
    Button(onClick = {
        navController.navigate("next?text=${text}&id=1")
    }) {
        ...
    }
}
composable("next?text={text}&id={id}",
    arguments = listOf(
        navArgument("text") {
            type = NavType.StringType
            defaultValue = ""
        },
        navArgument("id") {
            type = NavType.IntType
            defaultValue = 0
        }
    )
) {
    ...
}

Android Studio Dolphin 2021.3.1 Patch 1 built on September 30, 2022