androidx.navigationは↓で結構前に書いていて、こっそり更新したりしてましたが新しい環境で内容を追加してみたいと思います(令和6年版w)
https://bps-e.com/dev/android-009/
devloperの説明は以下
https://developer.android.com/jetpack/compose/navigation
codelabのサンプル
https://github.com/android/codelab-android-compose/tree/end/NavigationCodelab
依存関係
Project StructuerやGradleなどに直接指定だけではなく、コードからも追加できるようになってました
navigation-commonを追加しなくてもNavGraphBuilder使えるっぽいので
とりあえずnavigation-composeだけ追加しておきます
実装
NavHostControllerの扱い
基底のscreenで持っておけばいいけど、nowinandroidなどgoogleのサンプルなどのようにアプリのステータス管理用のclassを追加してそこに保持しておくのが無難だと思う
※ 内容は以前のものとほぼ同じ
ここでは通常使用するもののみ実装していますが、より複雑な遷移をする場合に必要なbackstackまわりは以下に詳しく説明されていたので参考まで
https://star-zero.medium.com/navigation-compose%E3%81%AEnavoptions-36607e71d2bf
@Stable
class NavState(val navController: NavHostController) {
fun onNavigate(route: String, restore: Boolean = false) {
navController.navigate(route) {
// 同じ画面に遷移した場合同じ画面が再利用される
launchSingleTop = true
if (restore) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// saveState = trueの場合rememberSavable/viewModelの内容を復元(1度)
restoreState = true
}
}
}
fun onBack(route: String = "") {
if (route.isEmpty()) navController.navigateUp()
else navController.popBackStack(route, inclusive = false, saveState = false)
}
@Composable
fun currentBackStackEntry() = navController.currentBackStackEntryAsState().value
}
@Composable
fun rememberNavState(navController: NavHostController = rememberNavController()): NavState =
remember(navController) { NavState(navController) }
Route名の扱い
Googleのsampleなどでも色々な定義の仕方がされていますが、enumならforEachで回せるのでenumで定義でいいんじゃないかなと思います
データ渡しをする場合などで柔軟に拡張したい場合はsealed classの方が便利かもです
昔のサンプルの定義名はNavRouteとかだったけど、最近はxxDestinationがトレンドのようです
例.
enum class NavDestination(@IdRes id: Int) {
Top(R.id.xxx),
Next(R.id.xxx);
// routeに指定する文字列を返すと若干便利
operator fun invoke() = this.name
}
// または
sealed class NavDestination(val route: String, @IdRes val id: Int) {
data object Top: NavDestination("Top", R.id.xxx)
data object Next: NavDestination("Next/{id}", R.id.xxx) {
fun params(id: Int) = "Next/$id"
}
operator fun invoke() = this.route
// operator fun invoke() = this::class.simpleName!!
companion object {
fun values() = listOf(Top, Next)
}
}
// または...
interface NavDestination {
val route: String
val id: Int
operator fun invoke() = this.route
}
object Top : NavDestination {
override val route = "Top"
@IdRes override val id = R.id.xxx
}
NavGraphBuilder
NavGraphBuilderを使わなくても実装できますが、画面遷移に階層がある場合などはNavGraphBuilderでまとめると管理しやすいです
fun NavGraphBuilder.navGraph(onBack: (String) -> Unit, onNavigate: (String) -> Unit) {
NavDestination.entries.forEach { destination ->
composable(route = destination()) {
when(destination) {
NavDestination.Top -> TopScreen(onBack, onNavigate)
NavDestination.Next -> NextScreen(onBack, onNavigate)
}
}
}
}
@Composable
fun TopScreen(onBack: (String) -> Unit, onNavigate: (String) -> Unit) {}
@Composable
fun NextScreen(onBack: (String) -> Unit, onNavigate: (String) -> Unit) {}
NavHost
内容は以前のものとほぼ同じです
@Composable
fun AppNavHost(navController: NavHostController, onBack: (String) -> Unit, onNavigate: (String) -> Unit) {
NavHost(navController = navController, startDestination = NavDestination.Top()) {
navGraph(onBack, onNavigate)
}
}
@Composable
fun AppContent(navState: NavState = rememberNavState()) {
AppNavHost(navState.navController, navState::onBack, navState::onNavigate)
}
遷移アニメーションを変更したい場合は以下のようにNavHostか、画面個別に指定したい場合はcomposable()の引数に指定します
NavHost(
navController = navController,
startDestination = NavDestination.Top(),
enterTransition = { fadeIn(animationSpec = tween(700)) },
exitTransition = { fadeOut(animationSpec = tween(700)) },
popEnterTransition = { fadeIn(animationSpec = tween(700)) },
popExitTransition = { fadeOut(animationSpec = tween(700)) },
) { ... }
画面間でデータを渡す
NavHostController.navigate()のるroute指定の文字列に付加して渡します
省略なしの場合は”/”区切り、省略可能にしたい場合はhtmlのパラメータ渡しの文法で指定します
配列の場合は?list=1&list=2&…のような指定しか現在はサポートされていません
https://issuetracker.google.com/issues/291989170
文字列を渡す場合はurlエンコードして渡します
デコードはbackstack側で行われます
半角スペースも%20にエンコードされるようにしてください
composable(route + "/{text}/{id}",
arguments = listOf(
navArgument("text") { type = NavType.StringType },
navArgument("id") { type = NavType.IntType },
)
) {
val text = it.arguments?.getString("text") ?: ""
val id = it.arguments?.getInt("id") ?: 0
}
composable(route + "?text={text}&id={id}",
arguments = listOf(
navArgument("text") { type = NavType.StringType },
navArgument("id") { type = NavType.IntType },
)
) { ... }
遷移のイベントのハンドリング
NavHostController.addOnDestinationChangedListenerかcurrentBackStackEntryAsStateでできます
val navBackStackEntry by navState.navController.currentBackStackEntryAsState()
LaunchedEffect(navBackStackEntry) {
navBackStackEntry?.apply {
Log.d("entry", "${destination.route}")
// 引数を取得する例
arguments?.apply {
destination.arguments.forEach { entry ->
val value = when (entry.value.type) {
NavType.StringType -> getString(entry.key)
NavType.IntType -> getInt(entry.key)
else -> null
}
Log.d("entry", "${entry.key} = $value")
}
}
}
// 遷移前の情報(初回遷移時はnull)
val previous = appState.navController.previousBackStackEntry
previous?.apply {
Log.d("previous", "${destination.route}")
}
}
戻るキーやジャスチャーをハンドリングしたい場合は対象のScreenでBackHandlerを実装します
BackHandlerを使う場合は戻る処理がフックされますのでBackHandler内で独自の遷移処理を追加してください
※ 戻るキーやジャスチャーのみフックされるのでnavControllerのpopBackはハンドリングできないです
@Composable
fun TopScreen(...) {
// 特にハンドリングのon/offが不要ならtrueを直接指定
var backHandlingEnabled by remember { mutableStateOf(true) }
BackHandler(backHandlingEnabled) {
...
}
}
Android Studio Hedgehog 2023.1.1 built on November 10, 2023